diff --git a/.ai/manifest.adf b/.ai/manifest.adf index 84d9a97..67e0010 100644 --- a/.ai/manifest.adf +++ b/.ai/manifest.adf @@ -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 diff --git a/.ai/typed-data-access.adf b/.ai/typed-data-access.adf new file mode 100644 index 0000000..e610144 --- /dev/null +++ b/.ai/typed-data-access.adf @@ -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) diff --git a/packages/cli/src/__tests__/named-scaffolds.test.ts b/packages/cli/src/__tests__/named-scaffolds.test.ts new file mode 100644 index 0000000..db1dea4 --- /dev/null +++ b/packages/cli/src/__tests__/named-scaffolds.test.ts @@ -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); + } + }); +}); diff --git a/packages/cli/src/commands/adf-named-scaffolds.ts b/packages/cli/src/commands/adf-named-scaffolds.ts new file mode 100644 index 0000000..8307eaf --- /dev/null +++ b/packages/cli/src/commands/adf-named-scaffolds.ts @@ -0,0 +1,113 @@ +/** + * Named-module scaffold registry. + * + * Rich scaffolds for canonical policy modules that consumer repos can adopt + * with `charter adf create `. 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 ` 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 ` + * 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 = { + 'typed-data-access': TYPED_DATA_ACCESS_SCAFFOLD, +}; + +/** + * Default manifest trigger keywords for named modules. Used when + * `charter adf create ` matches a known module and no explicit + * --triggers flag is provided. + */ +export const NAMED_MODULE_DEFAULT_TRIGGERS: Record = { + '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', + ], +}; diff --git a/packages/cli/src/commands/adf.ts b/packages/cli/src/commands/adf.ts index 3c014b7..d453275 100644 --- a/packages/cli/src/commands/adf.ts +++ b/packages/cli/src/commands/adf.ts @@ -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 @@ -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; @@ -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