Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .ai/manifest.adf
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ ADF: 0.1
- governance.adf (Triggers on: validate, drift, audit, trailer, risk, governance)
- classifier.adf (Triggers on: classifier, routing, auto-route, adf add, classify, rule placement)
- analysis.adf (Triggers on: blast, surface, dependency graph, blast radius, route extraction, schema extraction)
- typed-data-access.adf (Triggers on: tenant, user, subscription, quota, credit, mrr, pii, sensitivity, data registry, ontology, disambiguation, DATA_AUTHORITY, raw D1, service boundary, auth_scoped, billing_critical)

💰 BUDGET:
MAX_TOKENS: 4000
Expand Down
49 changes: 49 additions & 0 deletions .ai/typed-data-access.adf
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
ADF: 0.1

🎯 TASK: Typed data access and ontology enforcement policy

📋 CONTEXT:
- Business concepts (tenant, user, subscription, quota, credit, mrr, etc.) are defined in a canonical data registry — the single source of truth for ownership, sensitivity, and access shape across the ecosystem
- Reference registry location: Stackbilt-dev/stackbilt_llc/policies/data-registry.yaml (22+ concepts, 6 sensitivity tiers)
- Each concept declares: owner service, D1 table, sensitivity tier, definition, aliases, rpc_method, mcp_tool
- Consumer services derive their KNOWN_CONCEPTS and alias maps from the registry at build time (see aegis-web/src/lib/data-registry.ts pattern — compiled-const snapshot)
- AEGIS disambiguation firewall halts on undefined concepts rather than guessing
- CodeBeast DATA_AUTHORITY sensitivity class escalates raw D1 access to owned tables
- This charter policy module ties the three mechanisms together: registry → enforcement → disambiguation

🔐 SENSITIVITY TIERS [load-bearing]:
- public — readable from any service, no auth required (e.g., blog_post)
- service_internal — readable/writable only by the owning service, raw D1 access is fine within the owner (e.g., conversation, memory, llm_trace)
- cross_service_rpc — accessible via declared RPC method or Service Binding, never raw D1 from a non-owning service (e.g., tenant, quota, generation)
- pii_scoped — accessible only via owning service + audit_log entry required at the call site (e.g., user)
- billing_critical — writable only by the owning service plus the Stripe webhook handler; never leaves the owning service boundary even via RPC (e.g., subscription, mrr)
- secrets — never leaves the owning service boundary under any circumstance (e.g., api_key)

⚠ CONSTRAINTS [load-bearing]:
- New code referencing a business concept MUST check the canonical registry first; terms not in the registry or its aliases MUST be added before the code lands
- Non-owning services reading or writing `cross_service_rpc` concepts MUST use the declared rpc_method or mcp_tool — raw D1 binding access to another service's table is a DATA_AUTHORITY violation
- `pii_scoped` access requires an audit_log entry at the call site — no silent reads
- `billing_critical` and `secrets` tiers NEVER cross the owning service boundary, even via RPC — treat as in-process only
- When encountering an undefined data concept in requirements, tasks, or user prompts, HALT and ask for clarification rather than guessing shape, ownership, or sensitivity
- Aliases (e.g., "credits" for "quota") are semantically equivalent; prefer the canonical form in new code, accept aliases in user-facing copy
- Registry updates MUST come before consumer code updates — the source of truth leads, consumers follow
- When promoting a concept to a higher sensitivity tier (e.g., service_internal → cross_service_rpc), all existing consumers of raw D1 access to that concept must migrate to RPC in the same change set

📖 ADVISORY:
- Check the registry before reaching for a new type definition: the concept may already exist with a canonical shape
- Use `charter surface --format json` to discover what D1 tables a service currently exposes — cross-reference against registry ownership
- Unregistered-concept warnings from `charter validate` are early signals that ontology drift is beginning; address them immediately
- The disambiguation protocol is load-bearing for autonomous agents (AEGIS, cc-taskrunner, etc.) — these systems cannot safely guess business term semantics, and ambiguity compounds across sessions

📊 METRICS:
REGISTRY_PATH: stackbilt_llc/policies/data-registry.yaml
REGISTRY_REPO: Stackbilt-dev/stackbilt_llc
SENSITIVITY_TIERS: 6
DOCUMENTED_CONCEPTS: 22
CONSUMER_PATTERN: web/src/lib/data-registry.ts (compile-from-yaml snapshot)

🔗 REFERENCES:
- Stackbilt-dev/charter#69 — typed data access policy umbrella issue
- codebeast#9 — DATA_AUTHORITY sensitivity class (enforcement side)
- Stackbilt-dev/aegis#344 — disambiguation firewall (runtime halt mechanism)
- Stackbilt-dev/aegis#334 — adversarial reasoning (complementary quality layer)
68 changes: 68 additions & 0 deletions packages/cli/src/__tests__/named-scaffolds.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { describe, it, expect } from 'vitest';
import {
NAMED_MODULE_SCAFFOLDS,
NAMED_MODULE_DEFAULT_TRIGGERS,
TYPED_DATA_ACCESS_SCAFFOLD,
} from '../commands/adf-named-scaffolds';

describe('NAMED_MODULE_SCAFFOLDS registry', () => {
it('contains typed-data-access scaffold entry', () => {
expect(NAMED_MODULE_SCAFFOLDS['typed-data-access']).toBeDefined();
expect(NAMED_MODULE_SCAFFOLDS['typed-data-access']).toBe(TYPED_DATA_ACCESS_SCAFFOLD);
});

it('typed-data-access scaffold is valid ADF 0.1', () => {
const scaffold = NAMED_MODULE_SCAFFOLDS['typed-data-access'];
expect(scaffold).toMatch(/^ADF: 0\.1/);
});

it('typed-data-access scaffold declares the six sensitivity tiers', () => {
const scaffold = NAMED_MODULE_SCAFFOLDS['typed-data-access'];
expect(scaffold).toContain('public');
expect(scaffold).toContain('service_internal');
expect(scaffold).toContain('cross_service_rpc');
expect(scaffold).toContain('pii_scoped');
expect(scaffold).toContain('billing_critical');
expect(scaffold).toContain('secrets');
});

it('typed-data-access scaffold references the canonical registry path', () => {
const scaffold = NAMED_MODULE_SCAFFOLDS['typed-data-access'];
expect(scaffold).toContain('stackbilt_llc/policies/data-registry.yaml');
});

it('typed-data-access scaffold includes load-bearing disambiguation constraint', () => {
const scaffold = NAMED_MODULE_SCAFFOLDS['typed-data-access'];
expect(scaffold).toMatch(/CONSTRAINTS \[load-bearing\]/);
expect(scaffold).toContain('HALT and ask');
});
});

describe('NAMED_MODULE_DEFAULT_TRIGGERS registry', () => {
it('contains typed-data-access trigger keywords', () => {
expect(NAMED_MODULE_DEFAULT_TRIGGERS['typed-data-access']).toBeDefined();
expect(Array.isArray(NAMED_MODULE_DEFAULT_TRIGGERS['typed-data-access'])).toBe(true);
});

it('typed-data-access triggers include canonical business concept names', () => {
const triggers = NAMED_MODULE_DEFAULT_TRIGGERS['typed-data-access'];
expect(triggers).toContain('tenant');
expect(triggers).toContain('user');
expect(triggers).toContain('subscription');
expect(triggers).toContain('quota');
});

it('typed-data-access triggers include sensitivity and policy keywords', () => {
const triggers = NAMED_MODULE_DEFAULT_TRIGGERS['typed-data-access'];
expect(triggers).toContain('sensitivity');
expect(triggers).toContain('DATA_AUTHORITY');
expect(triggers).toContain('disambiguation');
});

it('every named scaffold has default triggers registered', () => {
for (const name of Object.keys(NAMED_MODULE_SCAFFOLDS)) {
expect(NAMED_MODULE_DEFAULT_TRIGGERS[name]).toBeDefined();
expect(NAMED_MODULE_DEFAULT_TRIGGERS[name].length).toBeGreaterThan(0);
}
});
});
113 changes: 113 additions & 0 deletions packages/cli/src/commands/adf-named-scaffolds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* Named-module scaffold registry.
*
* Rich scaffolds for canonical policy modules that consumer repos can adopt
* with `charter adf create <name>`. The generic empty placeholder in
* buildModuleScaffold is the fallback; entries in NAMED_MODULE_SCAFFOLDS
* take precedence.
*
* Each named module also registers default manifest trigger keywords in
* NAMED_MODULE_DEFAULT_TRIGGERS. When `charter adf create <name>` is called
* without an explicit --triggers flag, these auto-populate the ON_DEMAND
* entry so the wiring is a one-command operation.
*
* Adding a new named module:
* 1. Add the scaffold content as an exported const
* 2. Register it in NAMED_MODULE_SCAFFOLDS
* 3. Register default triggers in NAMED_MODULE_DEFAULT_TRIGGERS
* 4. Add tests in __tests__/named-scaffolds.test.ts
*/

/**
* Typed data access and ontology enforcement policy (Stackbilt-dev/charter#69).
*
* Codifies the cross-repo policy for how services reference business concepts
* (tenant, user, subscription, quota, etc.) — derived from the canonical data
* registry at Stackbilt-dev/stackbilt_llc/policies/data-registry.yaml.
*
* Declares six sensitivity tiers, the disambiguation protocol, and RPC
* boundary rules. Consumed by charter validate / codebeast DATA_AUTHORITY /
* AEGIS disambiguation firewall as the single source of truth for data
* access policy across the ecosystem.
*/
export const TYPED_DATA_ACCESS_SCAFFOLD = `ADF: 0.1

\u{1F3AF} TASK: Typed data access and ontology enforcement policy

\u{1F4CB} CONTEXT:
- Business concepts (tenant, user, subscription, quota, credit, mrr, etc.) are defined in a canonical data registry — the single source of truth for ownership, sensitivity, and access shape across the ecosystem
- Reference registry location: Stackbilt-dev/stackbilt_llc/policies/data-registry.yaml (22+ concepts, 6 sensitivity tiers)
- Each concept declares: owner service, D1 table, sensitivity tier, definition, aliases, rpc_method, mcp_tool
- Consumer services derive their KNOWN_CONCEPTS and alias maps from the registry at build time (compiled-const snapshot)
- Disambiguation protocol halts on undefined concepts rather than guessing
- CodeBeast DATA_AUTHORITY sensitivity class escalates raw D1 access to owned tables

\u{1F510} SENSITIVITY TIERS [load-bearing]:
- public \u2014 readable from any service, no auth required (e.g., blog_post)
- service_internal \u2014 readable/writable only by the owning service, raw D1 access is fine within the owner
- cross_service_rpc \u2014 accessible via declared rpc_method or Service Binding, never raw D1 from a non-owning service
- pii_scoped \u2014 accessible only via owning service + audit_log entry required at the call site
- billing_critical \u2014 writable only by the owning service plus the Stripe webhook handler; never leaves the owning service boundary even via RPC
- secrets \u2014 never leaves the owning service boundary under any circumstance

\u26A0\uFE0F CONSTRAINTS [load-bearing]:
- New code referencing a business concept MUST check the canonical registry first; terms not in the registry or its aliases MUST be added before the code lands
- Non-owning services reading or writing cross_service_rpc concepts MUST use the declared rpc_method or mcp_tool \u2014 raw D1 access to another service's table is a DATA_AUTHORITY violation
- pii_scoped access requires an audit_log entry at the call site \u2014 no silent reads
- billing_critical and secrets tiers NEVER cross the owning service boundary, even via RPC
- When encountering an undefined data concept in requirements, tasks, or user prompts, HALT and ask for clarification rather than guessing shape, ownership, or sensitivity
- Registry updates MUST come before consumer code updates \u2014 the source of truth leads, consumers follow
- When promoting a concept to a higher sensitivity tier, all existing consumers of raw D1 access must migrate to RPC in the same change set

\u{1F4D6} ADVISORY:
- Check the registry before reaching for a new type definition \u2014 the concept may already exist with a canonical shape
- Use charter surface --format json to discover what D1 tables a service currently exposes; cross-reference against registry ownership
- Aliases (e.g., "credits" for "quota") are semantically equivalent; prefer the canonical form in new code, accept aliases in user-facing copy
- The disambiguation protocol is load-bearing for autonomous agents \u2014 these systems cannot safely guess business term semantics

\u{1F4CA} METRICS:
REGISTRY_PATH: stackbilt_llc/policies/data-registry.yaml
REGISTRY_REPO: Stackbilt-dev/stackbilt_llc
SENSITIVITY_TIERS: 6
DOCUMENTED_CONCEPTS: 22

\u{1F517} REFERENCES:
- Stackbilt-dev/charter#69 \u2014 typed data access policy umbrella issue
- codebeast#9 \u2014 DATA_AUTHORITY sensitivity class (enforcement side)
- Stackbilt-dev/aegis#344 \u2014 disambiguation firewall (runtime halt mechanism)
`;

/**
* Registry of rich named-module scaffolds. When `charter adf create <name>`
* matches a name in this map, the corresponding scaffold is written instead
* of the generic empty placeholder from buildModuleScaffold's fallback.
*/
export const NAMED_MODULE_SCAFFOLDS: Record<string, string> = {
'typed-data-access': TYPED_DATA_ACCESS_SCAFFOLD,
};

/**
* Default manifest trigger keywords for named modules. Used when
* `charter adf create <name>` matches a known module and no explicit
* --triggers flag is provided.
*/
export const NAMED_MODULE_DEFAULT_TRIGGERS: Record<string, string[]> = {
'typed-data-access': [
'tenant',
'user',
'subscription',
'quota',
'credit',
'mrr',
'pii',
'sensitivity',
'data registry',
'ontology',
'disambiguation',
'DATA_AUTHORITY',
'raw D1',
'service boundary',
'auth_scoped',
'billing_critical',
],
};
26 changes: 25 additions & 1 deletion packages/cli/src/commands/adf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ import { adfMetricsCommand } from './adf-metrics';
import { adfTidyCommand } from './adf-tidy';
import { adfPopulateCommand } from './adf-populate';
import { adfContextCommand } from './adf-context';
import {
NAMED_MODULE_SCAFFOLDS,
NAMED_MODULE_DEFAULT_TRIGGERS,
} from './adf-named-scaffolds';

// Re-export named-scaffold registry for programmatic consumers and tests.
export {
TYPED_DATA_ACCESS_SCAFFOLD,
NAMED_MODULE_SCAFFOLDS,
NAMED_MODULE_DEFAULT_TRIGGERS,
} from './adf-named-scaffolds';

// ============================================================================
// Scaffold Content
Expand Down Expand Up @@ -602,7 +613,12 @@ function adfCreate(options: CLIOptions, args: string[]): number {

const manifestDoc = parseAdf(fs.readFileSync(manifestPath, 'utf-8'));
const sectionKey = load === 'default' ? 'DEFAULT_LOAD' : 'ON_DEMAND';
const triggers = parseTriggers(getFlag(args, '--triggers'));
const explicitTriggers = parseTriggers(getFlag(args, '--triggers'));
// Named-module modules can ship default triggers; explicit --triggers wins.
const moduleName = path.basename(moduleRelPath, '.adf');
const triggers = explicitTriggers.length > 0
? explicitTriggers
: (NAMED_MODULE_DEFAULT_TRIGGERS[moduleName] ?? []);
const manifestEntry = load === 'on-demand' && triggers.length > 0
? `${moduleRelPath} (Triggers on: ${triggers.join(', ')})`
: moduleRelPath;
Expand Down Expand Up @@ -762,6 +778,14 @@ function parseModulePathFromEntry(entry: string): string {

function buildModuleScaffold(modulePath: string): string {
const name = path.basename(modulePath, '.adf');

// Named-module registry: rich scaffolds for canonical policy modules.
// Falls back to the generic placeholder below for user-defined modules.
const named = NAMED_MODULE_SCAFFOLDS[name];
if (named) {
return named;
}

return `ADF: 0.1
\u{1F3AF} TASK: ${name} module

Expand Down
Loading