diff --git a/docs/superpowers/plans/2026-05-12-conformance-milestone-ii.md b/docs/superpowers/plans/2026-05-12-conformance-milestone-ii.md new file mode 100644 index 0000000..5229498 --- /dev/null +++ b/docs/superpowers/plans/2026-05-12-conformance-milestone-ii.md @@ -0,0 +1,1292 @@ +# Conformance Milestone ii — `assertFeatureConformance` + boot wiring + +> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Ship the boot-time runtime check of the conformance system. `withSpan` / `withCapture` / `withAudit` attach non-enumerable runtime markers; `assertFeatureConformance(container, manifest, symbols, ctx)` resolves each manifest-declared use case from the feature's container and verifies the bound function carries the required markers. Each feature's `bindProductionX(ctx)` self-asserts at the tail; `pnpm dev` refuses to boot on drift. + +**Architecture:** The phantom-type seam shipped in milestone i becomes a *reified* type seam at runtime. Each wrapper attaches its brand as a non-enumerable, non-configurable property (`Object.defineProperty(wrapped, "__instrumented", { value: true, enumerable: false, writable: false, configurable: false })`). Predicate helpers (`isInstrumented`, `isCaptured`, `isAudited`) read those markers. `assertFeatureConformance` walks the manifest's `useCases`, resolves each binding by the symbol map the binder declares inline, and throws `ConformanceError` synchronously when a binding is missing a required brand. Per-feature self-assertion (called at the end of `bindProductionAuth(ctx)`) is forward-compatible: when `cms` and `web-tanstack` eventually wire `bindProductionAuth`, they inherit the boot check for free. + +**Tech Stack:** TypeScript (strict), Vitest, Inversify (existing DI). Object.defineProperty for non-enumerable runtime markers. No new dependencies. + +--- + +## File structure + +### Create +- `packages/core-shared/src/conformance/brand-runtime.ts` — runtime marker attachment + predicates +- `packages/core-shared/src/conformance/brand-runtime.test.ts` — attachBrand + predicate tests +- `packages/core-shared/src/conformance/conformance-error.ts` — `ConformanceError` class +- `packages/core-shared/src/conformance/assert-bindings.ts` — `assertFeatureConformance` +- `packages/core-shared/src/conformance/assert-bindings.test.ts` — positive + negative assertion tests +- `docs/work/conformance-system-v1/02-boot-assertions/_story.md` — story 02 record + +### Modify +- `packages/core-shared/src/instrumentation/with-span.ts` — call `attachBrand(wrapped, "__instrumented")` +- `packages/core-shared/src/instrumentation/with-capture.ts` — call `attachBrand(wrapped, "__captured")` +- `packages/core-shared/src/instrumentation/with-span.test.ts` — add runtime brand assertion +- `packages/core-shared/src/instrumentation/with-capture.test.ts` — add runtime brand assertion +- `packages/core-audit/src/with-audit.ts` — wrap (not passthrough) + `attachBrand(wrapped, "__audited")` + TODO breadcrumb +- `packages/core-audit/src/with-audit.test.ts` — add runtime brand assertion + adjust passthrough expectations +- `packages/core-shared/src/conformance/index.ts` — re-export brand-runtime helpers + `ConformanceError` + `assertFeatureConformance` +- `packages/auth/src/index.ts` — re-export `authManifest` + `AuthManifest` at the auth package root +- `packages/auth/src/di/bind-production.ts` — call `assertFeatureConformance` at the tail +- `docs/work/conformance-system-v1/_epic.md` — flip story 02 status and checkbox at closeout + +### No file changes (verified at closeout) +- `packages/blog/`, `packages/media/`, `packages/navigation/`, `packages/marketing-pages/` — no manifests yet; their bind-production stays unchanged +- `apps/cms/`, `apps/web-tanstack/` — no server bootstrap yet; nothing to wire + +--- + +## Task 1: Re-export `authManifest` from auth root barrel + +Surfaces the manifest at the package boundary so future stories (and the boot-assertion's caller) can read it without a deep import. + +**Files:** +- Modify: `packages/auth/src/index.ts` + +- [ ] **Step 1: Append the re-export** + +Add these lines at the bottom of `packages/auth/src/index.ts`: + +```ts +// Feature conformance manifest (added in conformance milestone i, exposed +// here in milestone ii so the boot-time assertion and future tooling can +// read the contract from the package boundary). +export { authManifest, type AuthManifest } from "./feature.manifest"; +``` + +- [ ] **Step 2: Verify typecheck passes** + +Run: `pnpm --filter @repo/auth typecheck` +Expected: PASS. + +- [ ] **Step 3: Commit** + +```bash +git add packages/auth/src/index.ts +git commit -m "feat(auth): re-export authManifest + AuthManifest from package root" +``` + +--- + +## Task 2: Add TODO breadcrumb in `withAudit` + +The `withAudit` wrapper currently does nothing but cast (with a `void auditLog;` to silence unused). Leave a clear forward pointer for the future story that wires automated audit recording. + +**Files:** +- Modify: `packages/core-audit/src/with-audit.ts` + +- [ ] **Step 1: Replace the function body's existing comment block** + +In `packages/core-audit/src/with-audit.ts`, the body currently looks like: + +```ts +export function withAudit( + // The auditLog is part of the signature for two reasons: (1) callers must + // pass it at bind time, ensuring the dep is available, and (2) future + // versions of this wrapper will use it to emit audit events from the + // declarative manifest entry directly. + auditLog: IAuditLog, + fn: (...args: Args) => Promise, +): Audited<(...args: Args) => Promise> { + void auditLog; + return fn as Audited<(...args: Args) => Promise>; +} +``` + +Adjust the inline comment to point at the future story that wires automated recording. Replace ONLY the inline comment on `auditLog` (do not touch the function body yet — Task 7 changes the body): + +```ts +export function withAudit( + // TODO(conformance milestone iii+): wire automated recording from manifest + // `audits[]` declarations. For now, the wrapper exists to: + // (1) require callers to pass the auditLog at bind time (dep is available) + // (2) attach the `__audited` brand so the boot-time assertion can verify + // mutating use cases were bound through the audit-aware path. + auditLog: IAuditLog, + fn: (...args: Args) => Promise, +): Audited<(...args: Args) => Promise> { + void auditLog; + return fn as Audited<(...args: Args) => Promise>; +} +``` + +The function body is unchanged in this task; we only enriched the comment. + +- [ ] **Step 2: Verify typecheck and tests still pass** + +``` +pnpm --filter @repo/core-audit typecheck +pnpm --filter @repo/core-audit test with-audit +``` + +Both PASS, no regressions. + +- [ ] **Step 3: Commit** + +```bash +git add packages/core-audit/src/with-audit.ts +git commit -m "docs(core-audit): TODO breadcrumb for future audit recording automation" +``` + +--- + +## Task 3: Scaffold docs/work record for story 02 + +**Files:** +- Create: `docs/work/conformance-system-v1/02-boot-assertions/_story.md` + +- [ ] **Step 1: Write the story file** + +Create `docs/work/conformance-system-v1/02-boot-assertions/_story.md` with EXACTLY this content: + +```markdown +--- +id: 02-boot-assertions +epic: conformance-system-v1 +title: assertFeatureConformance + boot wiring +type: technical-story +status: in-progress +feature: core-shared +depends-on: [01-define-feature-helper] +blocks: [03-eslint-rules] +--- + +## Goal +Runtime boot-time verification that every manifest-declared use case is bound +through the brand-attaching wrappers. Each feature's `bindProductionX(ctx)` +self-asserts at the tail; `pnpm dev` refuses to boot on drift. + +## Why +Type casts can mask unwrapped factories; manifest edits can drift from +binders without TypeScript noticing. Boot assertions catch what the type +system can't see — at zero cost during the inner agent feedback loop, and +synchronously at startup so failures fire loudly. + +## Done when +- `withSpan`, `withCapture`, `withAudit` attach non-enumerable runtime markers + matching the type-level brand names +- `assertFeatureConformance(container, manifest, symbols, ctx)` resolves each + manifest use case and throws `ConformanceError` on a missing brand +- `auth.bindProductionAuth(ctx)` self-asserts at the tail +- `pnpm dev` boots cleanly for the existing `auth` wiring; rebinding `signIn` + with an unwrapped factory causes `pnpm dev` to throw at startup + +## In scope +- Runtime marker attachment via `Object.defineProperty(fn, "__brand", { … })` + (non-enumerable, non-writable, non-configurable) +- `isInstrumented` / `isCaptured` / `isAudited` predicates +- `ConformanceError` class (extends `Error`) +- `assertFeatureConformance(container, manifest, symbols, ctx)` helper +- Wiring into `packages/auth/src/di/bind-production.ts` (tail-of-binder + self-assertion) +- `withAudit` upgraded from passthrough to a thin wrapper that attaches its + runtime brand without changing observable behaviour + +## Out of scope +- `assertConformance` over a multi-feature container collection at the app's + `bindAll()` (current per-feature self-assertion is sufficient and + forward-compatible) +- Wiring boot assertions into `cms` and `web-tanstack` — neither has a + `bind-production.ts` yet; they'll inherit the check whenever they grow one +- Manifests for `blog`, `media`, `navigation`, `marketing-pages` (their + `bindProductionX` stays unchanged in this story) +- Automated audit recording driven by manifest `audits[]` declarations + (deferred to a later story) + +## Tasks +- [ ] Re-export `authManifest` from auth root barrel +- [ ] TODO breadcrumb in `withAudit` pointing at future automation +- [ ] Runtime marker helpers (`attachBrand`, `isInstrumented`, `isCaptured`, `isAudited`) +- [ ] `withSpan` attaches runtime `__instrumented` marker +- [ ] `withCapture` attaches runtime `__captured` marker +- [ ] `withAudit` wraps + attaches runtime `__audited` marker +- [ ] `ConformanceError` class +- [ ] `assertFeatureConformance` helper + tests +- [ ] Conformance barrel + subpath exports updated +- [ ] `bindProductionAuth` self-asserts at the tail +- [ ] Final verification + story closeout +``` + +- [ ] **Step 2: Commit** + +```bash +git add docs/work/conformance-system-v1/02-boot-assertions/_story.md +git commit -m "docs(work): story 02 — assertFeatureConformance + boot wiring" +``` + +--- + +## Task 4: Runtime marker helpers — `attachBrand` + predicates + +The runtime marker module is the heart of milestone ii: a small set of helpers that attach non-enumerable brand flags and read them back. The phantom-type brand names (`__instrumented`, `__captured`, `__audited`) become real (non-enumerable) properties at runtime. + +**Files:** +- Create: `packages/core-shared/src/conformance/brand-runtime.ts` +- Create: `packages/core-shared/src/conformance/brand-runtime.test.ts` + +- [ ] **Step 1: Write the failing test** + +Create `packages/core-shared/src/conformance/brand-runtime.test.ts` with EXACTLY this content: + +```ts +import { describe, it, expect } from "vitest"; +import { + attachBrand, + isInstrumented, + isCaptured, + isAudited, +} from "@/conformance/brand-runtime"; + +describe("brand-runtime", () => { + it("attachBrand adds a non-enumerable property and returns the same reference", () => { + const fn = () => {}; + const result = attachBrand(fn, "__instrumented"); + expect(result).toBe(fn); + expect(isInstrumented(fn)).toBe(true); + // Non-enumerable: must not show up in Object.keys + expect(Object.keys(fn)).not.toContain("__instrumented"); + }); + + it("predicates return false for unwrapped functions", () => { + const fn = () => {}; + expect(isInstrumented(fn)).toBe(false); + expect(isCaptured(fn)).toBe(false); + expect(isAudited(fn)).toBe(false); + }); + + it("predicates discriminate between brands", () => { + const instrumented = () => {}; + attachBrand(instrumented, "__instrumented"); + expect(isInstrumented(instrumented)).toBe(true); + expect(isCaptured(instrumented)).toBe(false); + expect(isAudited(instrumented)).toBe(false); + + const captured = () => {}; + attachBrand(captured, "__captured"); + expect(isInstrumented(captured)).toBe(false); + expect(isCaptured(captured)).toBe(true); + expect(isAudited(captured)).toBe(false); + + const audited = () => {}; + attachBrand(audited, "__audited"); + expect(isAudited(audited)).toBe(true); + }); + + it("composing brands stacks them on the same function", () => { + const fn = () => {}; + attachBrand(fn, "__instrumented"); + attachBrand(fn, "__captured"); + attachBrand(fn, "__audited"); + expect(isInstrumented(fn)).toBe(true); + expect(isCaptured(fn)).toBe(true); + expect(isAudited(fn)).toBe(true); + }); + + it("attached brands are non-writable and non-configurable", () => { + const fn = () => {}; + attachBrand(fn, "__instrumented"); + const desc = Object.getOwnPropertyDescriptor(fn, "__instrumented"); + expect(desc?.writable).toBe(false); + expect(desc?.configurable).toBe(false); + expect(desc?.enumerable).toBe(false); + expect(desc?.value).toBe(true); + }); + + it("predicates return false for non-function inputs", () => { + expect(isInstrumented(null)).toBe(false); + expect(isInstrumented(undefined)).toBe(false); + expect(isInstrumented(42)).toBe(false); + expect(isInstrumented("string")).toBe(false); + expect(isInstrumented({})).toBe(false); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +``` +pnpm --filter @repo/core-shared test conformance/brand-runtime +``` +Expected: FAIL — module not found. + +- [ ] **Step 3: Write `brand-runtime.ts`** + +Create `packages/core-shared/src/conformance/brand-runtime.ts` with EXACTLY this content: + +```ts +/** + * Runtime brand attachment + predicates. The companion to the phantom-type + * brands defined in `./brands.ts`: at compile time the brand is a structural + * intersection; at runtime it is a non-enumerable, non-writable, + * non-configurable property with the same name. + * + * Why non-enumerable: a wrapped function should not leak the marker through + * `Object.keys`, `JSON.stringify`, `for…in`, or spread. The marker only + * shows up to explicit lookups via `Reflect.has` or direct property access. + * + * Why non-writable + non-configurable: the marker is meant to be permanent + * once attached. The wrapper is the only caller; the marker is a one-shot + * commitment, not a mutable flag. + */ + +type Brand = "__instrumented" | "__captured" | "__audited"; + +/** + * Attaches the brand as a non-enumerable property on the given function. + * Returns the same reference (no allocation). Idempotent: calling twice with + * the same brand throws because the property is non-configurable — wrappers + * never double-wrap, so this is the correct safety net. + */ +export function attachBrand(fn: F, brand: Brand): F { + Object.defineProperty(fn, brand, { + value: true, + enumerable: false, + writable: false, + configurable: false, + }); + return fn; +} + +function hasBrand(fn: unknown, brand: Brand): boolean { + if (typeof fn !== "function" && (typeof fn !== "object" || fn === null)) { + return false; + } + return (fn as Record)[brand] === true; +} + +export function isInstrumented(fn: unknown): boolean { + return hasBrand(fn, "__instrumented"); +} + +export function isCaptured(fn: unknown): boolean { + return hasBrand(fn, "__captured"); +} + +export function isAudited(fn: unknown): boolean { + return hasBrand(fn, "__audited"); +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +``` +pnpm --filter @repo/core-shared test conformance/brand-runtime +``` +Expected: PASS, 6 tests. + +- [ ] **Step 5: Re-export from the conformance barrel** + +Append to `packages/core-shared/src/conformance/index.ts` (preserving existing exports): + +```ts +export { + attachBrand, + isInstrumented, + isCaptured, + isAudited, +} from "./brand-runtime"; +``` + +Re-run typecheck to confirm the barrel resolves: + +``` +pnpm --filter @repo/core-shared typecheck +``` + +PASS. + +- [ ] **Step 6: Commit** + +```bash +git add packages/core-shared/src/conformance/brand-runtime.ts packages/core-shared/src/conformance/brand-runtime.test.ts packages/core-shared/src/conformance/index.ts +git commit -m "feat(core-shared/conformance): runtime brand markers + isX predicates" +``` + +--- + +## Task 5: `withSpan` attaches runtime `__instrumented` marker + +**Files:** +- Modify: `packages/core-shared/src/instrumentation/with-span.ts` +- Modify: `packages/core-shared/src/instrumentation/with-span.test.ts` + +- [ ] **Step 1: Add a failing runtime brand assertion to `with-span.test.ts`** + +Append this new describe block at the END of `packages/core-shared/src/instrumentation/with-span.test.ts` (after the existing `describe("withSpan — brand", …)` block): + +```ts +import { isInstrumented } from "@/conformance/brand-runtime"; + +describe("withSpan — runtime brand", () => { + it("attaches __instrumented as a non-enumerable property on the wrapped function", async () => { + const { tracer } = makeRecordingTracer(); + const fn = async (a: number) => a + 1; + const wrapped = withSpan(tracer, { name: "test.brand", op: "use-case" }, fn); + expect(isInstrumented(wrapped)).toBe(true); + expect(Object.keys(wrapped)).not.toContain("__instrumented"); + }); +}); +``` + +Note: place the `import { isInstrumented }` line near the top of the file alongside the other imports — the location of the import was shown inline above for context but it belongs in the imports block. + +- [ ] **Step 2: Run test to verify it fails** + +``` +pnpm --filter @repo/core-shared test with-span +``` +Expected: at least one new test FAILS — `wrapped` does not yet have `__instrumented` attached. + +- [ ] **Step 3: Update `with-span.ts` to attach the runtime marker** + +Update `packages/core-shared/src/instrumentation/with-span.ts`. The current file (after milestone i Task 11's brand-preserving overload) needs an `attachBrand` call before the return. Replace the body of the implementation function so it looks like this — note the new import and the `attachBrand(wrapped, "__instrumented")` line: + +```ts +import type { ITracer, SpanOpts } from "./tracer.interface"; +import type { Instrumented } from "../conformance/brands"; +import { attachBrand } from "../conformance/brand-runtime"; + +export function withSpan( + tracer: ITracer, + opts: SpanOpts | ((args: Args) => SpanOpts), + fn: ((...args: Args) => Promise) & Extra, +): Instrumented<((...args: Args) => Promise) & Extra>; +export function withSpan( + tracer: ITracer, + opts: SpanOpts | ((args: Args) => SpanOpts), + fn: (...args: Args) => Promise, +): Instrumented<(...args: Args) => Promise>; +export function withSpan( + tracer: ITracer, + opts: SpanOpts | ((args: Args) => SpanOpts), + fn: (...args: Args) => Promise, +): Instrumented<(...args: Args) => Promise> { + const wrapped: (...args: Args) => Promise = (...args) => { + const resolved = typeof opts === "function" ? opts(args) : opts; + return tracer.startSpan(resolved, () => fn(...args)); + }; + attachBrand(wrapped, "__instrumented"); + // Cast is the type-level concession — the brand is now also a non-enumerable + // runtime property attached above by `attachBrand`. + return wrapped as Instrumented<(...args: Args) => Promise>; +} +``` + +Important: keep BOTH overload signatures (the brand-preserving one added in milestone i Task 11 plus the original). The implementation signature is the third one shown; only the body changes (adds `attachBrand` call, updated comment). + +- [ ] **Step 4: Run typecheck and tests** + +``` +pnpm --filter @repo/core-shared typecheck +pnpm --filter @repo/core-shared test with-span +``` +Both PASS. All existing tests still green plus the new runtime brand test. + +- [ ] **Step 5: Commit** + +```bash +git add packages/core-shared/src/instrumentation/with-span.ts packages/core-shared/src/instrumentation/with-span.test.ts +git commit -m "feat(core-shared/instrumentation): withSpan attaches runtime __instrumented marker" +``` + +--- + +## Task 6: `withCapture` attaches runtime `__captured` marker + +Same pattern as Task 5. + +**Files:** +- Modify: `packages/core-shared/src/instrumentation/with-capture.ts` +- Modify: `packages/core-shared/src/instrumentation/with-capture.test.ts` + +- [ ] **Step 1: Add a failing runtime brand assertion to `with-capture.test.ts`** + +Append this new describe block at the END of `packages/core-shared/src/instrumentation/with-capture.test.ts`. Also add `import { isCaptured } from "@/conformance/brand-runtime";` near the other imports at the top of the file: + +```ts +describe("withCapture — runtime brand", () => { + it("attaches __captured as a non-enumerable property on the wrapped function", async () => { + const logger = makeLogger(); + const fn = async (a: number) => a + 1; + const wrapped = withCapture(logger, { layer: "use-case" }, fn); + expect(isCaptured(wrapped)).toBe(true); + expect(Object.keys(wrapped)).not.toContain("__captured"); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +``` +pnpm --filter @repo/core-shared test with-capture +``` +Expected: new test FAILS. + +- [ ] **Step 3: Update `with-capture.ts` to attach the runtime marker** + +Modify `packages/core-shared/src/instrumentation/with-capture.ts`. Add the `attachBrand` import alongside the existing imports at the top: + +```ts +import type { ILogger } from "./logger.interface"; +import type { Captured } from "../conformance/brands"; +import { attachBrand } from "../conformance/brand-runtime"; +import { isReported, markReported } from "./reported-flag"; +``` + +Keep the existing JSDoc block intact (do not modify the doc text). Modify the body of `withCapture` so it ends with `attachBrand(wrapped, "__captured");` before the return: + +```ts +export function withCapture( + logger: ILogger, + tags: Record, + fn: (...args: Args) => Promise, +): Captured<(...args: Args) => Promise> { + const wrapped: (...args: Args) => Promise = async (...args) => { + try { + return await fn(...args); + } catch (err) { + if (!isReported(err)) { + logger.captureException(err, { tags }); + markReported(err); + } + throw err; + } + }; + attachBrand(wrapped, "__captured"); + return wrapped as Captured<(...args: Args) => Promise>; +} +``` + +- [ ] **Step 4: Run typecheck and tests** + +``` +pnpm --filter @repo/core-shared typecheck +pnpm --filter @repo/core-shared test with-capture +``` +Both PASS. + +- [ ] **Step 5: Commit** + +```bash +git add packages/core-shared/src/instrumentation/with-capture.ts packages/core-shared/src/instrumentation/with-capture.test.ts +git commit -m "feat(core-shared/instrumentation): withCapture attaches runtime __captured marker" +``` + +--- + +## Task 7: `withAudit` wraps + attaches runtime `__audited` marker + +Milestone i implemented `withAudit` as a passthrough cast (`return fn as Audited<...>`). For runtime brand detection that doesn't pollute the caller's `fn`, the wrapper now creates a thin wrapper function and attaches the marker to that new function. Observable behaviour is unchanged. + +**Files:** +- Modify: `packages/core-audit/src/with-audit.ts` +- Modify: `packages/core-audit/src/with-audit.test.ts` + +- [ ] **Step 1: Update the test to assert the runtime brand AND identity invariants** + +Replace the entire contents of `packages/core-audit/src/with-audit.test.ts` with: + +```ts +import { describe, it, expect, expectTypeOf, vi } from "vitest"; +import { withAudit, type Audited } from "@/with-audit"; +import type { IAuditLog } from "@/audit-log.interface"; +import { isAudited } from "@repo/core-shared/conformance"; + +function makeAuditLog(): IAuditLog { + return { + record: vi.fn().mockResolvedValue(undefined), + eraseSubject: vi.fn().mockResolvedValue(undefined), + }; +} + +describe("withAudit", () => { + it("returns an Audited", () => { + const auditLog = makeAuditLog(); + const fn = async (_input: { id: string }) => ({ ok: true }); + const wrapped = withAudit(auditLog, fn); + expectTypeOf(wrapped).toMatchTypeOf>(); + }); + + it("attaches __audited as a non-enumerable property on the wrapped function", () => { + const auditLog = makeAuditLog(); + const fn = async () => ({ ok: true }); + const wrapped = withAudit(auditLog, fn); + expect(isAudited(wrapped)).toBe(true); + expect(Object.keys(wrapped)).not.toContain("__audited"); + }); + + it("does NOT pollute the original input function with the brand", () => { + const auditLog = makeAuditLog(); + const fn = async () => ({ ok: true }); + const wrapped = withAudit(auditLog, fn); + expect(isAudited(fn)).toBe(false); + expect(wrapped).not.toBe(fn); + }); + + it("passes input and output through unchanged", async () => { + const auditLog = makeAuditLog(); + const fn = async (input: { id: string }) => ({ ok: true, id: input.id }); + const wrapped = withAudit(auditLog, fn); + const result = await wrapped({ id: "abc" }); + expect(result).toEqual({ ok: true, id: "abc" }); + }); + + it("propagates errors", async () => { + const auditLog = makeAuditLog(); + const err = new Error("boom"); + const wrapped = withAudit(auditLog, async () => { + throw err; + }); + await expect(wrapped()).rejects.toBe(err); + }); +}); +``` + +Note the new third test: `wrapped !== fn` and `fn` is NOT branded. This pins down the wrap-not-passthrough invariant. + +- [ ] **Step 2: Run tests to verify they fail** + +``` +pnpm --filter @repo/core-audit test with-audit +``` +Expected: at least the new tests FAIL (the current passthrough makes `wrapped === fn` and `isAudited(wrapped)` returns false). + +- [ ] **Step 3: Update `with-audit.ts` to wrap + attach the runtime brand** + +Replace the entire body of `packages/core-audit/src/with-audit.ts` with: + +```ts +import type { IAuditLog } from "./audit-log.interface"; +import { attachBrand } from "@repo/core-shared/conformance"; + +/** + * Phantom-type brand attached at wrap time by `withAudit`. The conformance + * system uses this as the type-level seam for mutating use cases that + * declare `audits: [...]` in their manifest — without `__audited`, the + * binding is not assignable to `ProductionUseCase` when M demands + * it. At runtime the brand is a non-enumerable property attached by + * `attachBrand` from `@repo/core-shared/conformance`, so the boot-time + * assertion can verify the binding went through the audit-aware path. + */ +export type Audited = F & { readonly __audited: true }; + +/** + * Use-case wrapper applied at DI bind time. The wrapper is a thin closure + * that forwards to `fn` unchanged and carries the `__audited` brand. The + * forward closure (instead of returning `fn` directly) keeps the brand on + * a fresh function so the caller's original `fn` is not mutated — important + * when the same factory output is used elsewhere unwrapped (dev-seed paths, + * tests). + */ +export function withAudit( + // TODO(conformance milestone iii+): wire automated recording from manifest + // `audits[]` declarations. For now, the wrapper exists to: + // (1) require callers to pass the auditLog at bind time (dep is available) + // (2) attach the `__audited` brand so the boot-time assertion can verify + // mutating use cases were bound through the audit-aware path. + auditLog: IAuditLog, + fn: (...args: Args) => Promise, +): Audited<(...args: Args) => Promise> { + void auditLog; + const wrapped: (...args: Args) => Promise = (...args) => fn(...args); + attachBrand(wrapped, "__audited"); + return wrapped as Audited<(...args: Args) => Promise>; +} +``` + +`attachBrand` is imported from `@repo/core-shared/conformance` — Task 4 added that export to the conformance barrel, so the import should resolve cleanly. If it doesn't, complete Task 4 first. + +- [ ] **Step 4: Run typecheck and tests** + +``` +pnpm --filter @repo/core-audit typecheck +pnpm --filter @repo/core-audit test with-audit +``` +Both PASS. 5 tests green. + +- [ ] **Step 5: Commit** + +```bash +git add packages/core-audit/src/with-audit.ts packages/core-audit/src/with-audit.test.ts +git commit -m "feat(core-audit): withAudit wraps and attaches runtime __audited marker" +``` + +--- + +## Task 8: `ConformanceError` class + +**Files:** +- Create: `packages/core-shared/src/conformance/conformance-error.ts` +- Create: `packages/core-shared/src/conformance/conformance-error.test.ts` + +- [ ] **Step 1: Write the failing test** + +Create `packages/core-shared/src/conformance/conformance-error.test.ts` with EXACTLY this content: + +```ts +import { describe, it, expect } from "vitest"; +import { ConformanceError } from "@/conformance/conformance-error"; + +describe("ConformanceError", () => { + it("extends Error with the standard shape", () => { + const err = new ConformanceError("auth.signIn: missing __instrumented brand"); + expect(err).toBeInstanceOf(Error); + expect(err).toBeInstanceOf(ConformanceError); + expect(err.message).toBe("auth.signIn: missing __instrumented brand"); + expect(err.name).toBe("ConformanceError"); + }); + + it("preserves a stack trace", () => { + const err = new ConformanceError("test"); + expect(typeof err.stack).toBe("string"); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +``` +pnpm --filter @repo/core-shared test conformance/conformance-error +``` +Expected: FAIL (module not found). + +- [ ] **Step 3: Write `conformance-error.ts`** + +Create `packages/core-shared/src/conformance/conformance-error.ts` with EXACTLY this content: + +```ts +/** + * Thrown by `assertFeatureConformance` when a binding does not match the + * manifest's declared shape. The boot assertion lets this propagate + * synchronously so `pnpm dev` refuses to start on drift. + */ +export class ConformanceError extends Error { + constructor(message: string) { + super(message); + this.name = "ConformanceError"; + // Maintain a proper prototype chain across down-compilation. + Object.setPrototypeOf(this, ConformanceError.prototype); + } +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +``` +pnpm --filter @repo/core-shared test conformance/conformance-error +``` +Expected: PASS, 2 tests. + +- [ ] **Step 5: Commit** + +```bash +git add packages/core-shared/src/conformance/conformance-error.ts packages/core-shared/src/conformance/conformance-error.test.ts +git commit -m "feat(core-shared/conformance): ConformanceError class" +``` + +--- + +## Task 9: Extend conformance barrel with `ConformanceError` + +Brand-runtime exports were added to the barrel in Task 4. This task adds the `ConformanceError` export. (`assertFeatureConformance` will be added in Task 10.) + +**Files:** +- Modify: `packages/core-shared/src/conformance/index.ts` + +- [ ] **Step 1: Append the `ConformanceError` re-export** + +Add this line to the bottom of `packages/core-shared/src/conformance/index.ts`: + +```ts +export { ConformanceError } from "./conformance-error"; +``` + +- [ ] **Step 2: Verify typecheck** + +``` +pnpm --filter @repo/core-shared typecheck +``` +PASS. + +- [ ] **Step 3: Commit** + +```bash +git add packages/core-shared/src/conformance/index.ts +git commit -m "feat(core-shared/conformance): re-export ConformanceError" +``` + +--- + +## Task 10: `assertFeatureConformance` helper + +**Files:** +- Create: `packages/core-shared/src/conformance/assert-bindings.ts` +- Create: `packages/core-shared/src/conformance/assert-bindings.test.ts` +- Modify: `packages/core-shared/src/conformance/index.ts` (one more export) + +- [ ] **Step 1: Write the failing test** + +Create `packages/core-shared/src/conformance/assert-bindings.test.ts` with EXACTLY this content: + +```ts +import { describe, it, expect, vi } from "vitest"; +import "reflect-metadata"; +import { Container } from "inversify"; +import { defineFeature } from "@/conformance/define-feature"; +import { ConformanceError } from "@/conformance/conformance-error"; +import { assertFeatureConformance } from "@/conformance/assert-bindings"; +import { withSpan } from "@/instrumentation/with-span"; +import { withCapture } from "@/instrumentation/with-capture"; +import type { ITracer, ISpan, SpanOpts } from "@/instrumentation/tracer.interface"; +import type { ILogger } from "@/instrumentation/logger.interface"; +import type { BindContext } from "@/di/bind-context"; + +function makeTracer(): ITracer { + return { + startSpan: async (opts: SpanOpts, fn: (span: ISpan) => Promise) => { + const span: ISpan = { setAttribute: () => {}, setStatus: () => {} }; + return fn(span); + }, + }; +} + +function makeLogger(): ILogger { + return { + captureException: vi.fn(), + captureMessage: vi.fn(), + addBreadcrumb: vi.fn(), + setUser: vi.fn(), + }; +} + +function makeCtx(): BindContext { + return { tracer: makeTracer(), logger: makeLogger() }; +} + +describe("assertFeatureConformance", () => { + it("passes when every use case is bound through withSpan + withCapture", () => { + const container = new Container(); + const sym = Symbol("test.signIn"); + const ctx = makeCtx(); + const bound = withSpan( + ctx.tracer, + { name: "test.signIn", op: "use-case" }, + withCapture(ctx.logger, { feature: "test", layer: "use-case" }, async (x: number) => x + 1), + ); + container.bind(sym).toConstantValue(bound); + + const manifest = defineFeature({ + name: "test", + requiredCores: [], + useCases: { + signIn: { mutates: false, audits: [], publishes: [], consumes: [] }, + }, + realtimeChannels: [], + jobs: [], + } as const); + + expect(() => + assertFeatureConformance(container, manifest, { signIn: sym }, ctx), + ).not.toThrow(); + }); + + it("throws when a use case binding is missing the __instrumented brand", () => { + const container = new Container(); + const sym = Symbol("test.signIn"); + const ctx = makeCtx(); + const unwrapped = async (x: number) => x + 1; + container.bind(sym).toConstantValue(unwrapped); + + const manifest = defineFeature({ + name: "test", + requiredCores: [], + useCases: { + signIn: { mutates: false, audits: [], publishes: [], consumes: [] }, + }, + realtimeChannels: [], + jobs: [], + } as const); + + expect(() => + assertFeatureConformance(container, manifest, { signIn: sym }, ctx), + ).toThrow(ConformanceError); + expect(() => + assertFeatureConformance(container, manifest, { signIn: sym }, ctx), + ).toThrow(/test\.signIn.*__instrumented/); + }); + + it("throws when a use case binding is missing the __captured brand", () => { + const container = new Container(); + const sym = Symbol("test.signIn"); + const ctx = makeCtx(); + // withSpan-only — no withCapture wrap + const partial = withSpan( + ctx.tracer, + { name: "test.signIn", op: "use-case" }, + async (x: number) => x + 1, + ); + container.bind(sym).toConstantValue(partial); + + const manifest = defineFeature({ + name: "test", + requiredCores: [], + useCases: { + signIn: { mutates: false, audits: [], publishes: [], consumes: [] }, + }, + realtimeChannels: [], + jobs: [], + } as const); + + expect(() => + assertFeatureConformance(container, manifest, { signIn: sym }, ctx), + ).toThrow(/__captured/); + }); + + it("throws when a mutating use case with audits is missing the __audited brand", () => { + const container = new Container(); + const sym = Symbol("test.signUp"); + const ctx = makeCtx(); + const wrappedNoAudit = withSpan( + ctx.tracer, + { name: "test.signUp", op: "use-case" }, + withCapture(ctx.logger, { feature: "test" }, async (x: number) => x + 1), + ); + container.bind(sym).toConstantValue(wrappedNoAudit); + + const manifest = defineFeature({ + name: "test", + requiredCores: [], + useCases: { + signUp: { + mutates: true, + audits: ["user.created"], + publishes: [], + consumes: [], + }, + }, + realtimeChannels: [], + jobs: [], + } as const); + + expect(() => + assertFeatureConformance(container, manifest, { signUp: sym }, ctx), + ).toThrow(/__audited/); + }); + + it("passes for a mutating use case with empty audits (no __audited required)", () => { + const container = new Container(); + const sym = Symbol("test.signUp"); + const ctx = makeCtx(); + const wrappedNoAudit = withSpan( + ctx.tracer, + { name: "test.signUp", op: "use-case" }, + withCapture(ctx.logger, { feature: "test" }, async (x: number) => x + 1), + ); + container.bind(sym).toConstantValue(wrappedNoAudit); + + const manifest = defineFeature({ + name: "test", + requiredCores: [], + useCases: { + signUp: { + mutates: true, + audits: [], + publishes: [], + consumes: [], + }, + }, + realtimeChannels: [], + jobs: [], + } as const); + + expect(() => + assertFeatureConformance(container, manifest, { signUp: sym }, ctx), + ).not.toThrow(); + }); + + it("throws when no symbol is provided for a manifest use case", () => { + const container = new Container(); + const ctx = makeCtx(); + const manifest = defineFeature({ + name: "test", + requiredCores: [], + useCases: { + signIn: { mutates: false, audits: [], publishes: [], consumes: [] }, + }, + realtimeChannels: [], + jobs: [], + } as const); + expect(() => assertFeatureConformance(container, manifest, {}, ctx)).toThrow( + /no symbol provided/, + ); + }); + + it("throws when the container cannot resolve the symbol", () => { + const container = new Container(); + const sym = Symbol("test.signIn"); + const ctx = makeCtx(); + const manifest = defineFeature({ + name: "test", + requiredCores: [], + useCases: { + signIn: { mutates: false, audits: [], publishes: [], consumes: [] }, + }, + realtimeChannels: [], + jobs: [], + } as const); + expect(() => + assertFeatureConformance(container, manifest, { signIn: sym }, ctx), + ).toThrow(ConformanceError); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +``` +pnpm --filter @repo/core-shared test conformance/assert-bindings +``` +Expected: FAIL (module not found). + +- [ ] **Step 3: Write `assert-bindings.ts`** + +Create `packages/core-shared/src/conformance/assert-bindings.ts` with EXACTLY this content: + +```ts +import type { Container } from "inversify"; +import type { FeatureManifest } from "./define-feature"; +import { isInstrumented, isCaptured, isAudited } from "./brand-runtime"; +import { ConformanceError } from "./conformance-error"; +import type { BindContext } from "../di/bind-context"; + +/** + * Runtime check that every manifest-declared use case is bound through the + * brand-attaching wrappers (`withSpan` → `__instrumented`, + * `withCapture` → `__captured`, `withAudit` → `__audited` when required). + * + * Called at the tail of each feature's `bindProductionX(ctx)` so: + * - `pnpm dev` refuses to boot on drift (synchronous throw) + * - The check runs once per feature, not per request + * - Future apps that wire `bindProductionX` inherit the check for free + * + * The `symbols` map is declared inline by the feature's binder; the manifest + * holds the contract, the binder holds the container symbols. This keeps + * the manifest free of DI-coupling while letting each feature declare its + * own wiring keys. + */ +export function assertFeatureConformance( + container: Container, + manifest: FeatureManifest, + symbols: Record, + _ctx: BindContext, +): void { + void _ctx; // future: also check ctx.bus / ctx.auditLog presence vs requiredCores + for (const [name, useCase] of Object.entries(manifest.useCases)) { + const sym = symbols[name]; + if (!sym) { + throw new ConformanceError( + `${manifest.name}.${name}: no symbol provided in symbols map`, + ); + } + let bound: unknown; + try { + bound = container.get(sym); + } catch (cause) { + throw new ConformanceError( + `${manifest.name}.${name}: container could not resolve symbol (${String(cause)})`, + ); + } + if (!isInstrumented(bound)) { + throw new ConformanceError( + `${manifest.name}.${name}: missing __instrumented brand — was withSpan applied at bind time?`, + ); + } + if (!isCaptured(bound)) { + throw new ConformanceError( + `${manifest.name}.${name}: missing __captured brand — was withCapture applied at bind time?`, + ); + } + if (useCase.mutates && useCase.audits.length > 0) { + if (!isAudited(bound)) { + throw new ConformanceError( + `${manifest.name}.${name}: declares audits but binding is missing __audited brand — was withAudit applied at bind time?`, + ); + } + } + } +} +``` + +- [ ] **Step 4: Re-export `assertFeatureConformance` from the conformance barrel** + +Append to `packages/core-shared/src/conformance/index.ts`: + +```ts +export { assertFeatureConformance } from "./assert-bindings"; +``` + +- [ ] **Step 5: Run tests and typecheck** + +``` +pnpm --filter @repo/core-shared typecheck +pnpm --filter @repo/core-shared test conformance/assert-bindings +``` +Both PASS. 7 tests green. + +- [ ] **Step 6: Commit** + +```bash +git add packages/core-shared/src/conformance/assert-bindings.ts packages/core-shared/src/conformance/assert-bindings.test.ts packages/core-shared/src/conformance/index.ts +git commit -m "feat(core-shared/conformance): assertFeatureConformance helper" +``` + +--- + +## Task 11: Wire `assertFeatureConformance` into `bindProductionAuth` + +**Files:** +- Modify: `packages/auth/src/di/bind-production.ts` + +- [ ] **Step 1: Add the imports** + +Open `packages/auth/src/di/bind-production.ts`. Add these two import lines alongside the existing imports at the top: + +```ts +import { assertFeatureConformance } from "@repo/core-shared/conformance"; +import { authManifest } from "../feature.manifest"; +``` + +(The `AuthManifest` type import added in milestone i Task 11 stays as-is.) + +- [ ] **Step 2: Call the assertion at the tail** + +At the very end of the `bindProductionAuth(ctx)` function body — AFTER all bindings have been registered and AFTER the existing `// ` / `// ` / `// ` anchor comments — add: + +```ts + // Boot-time conformance check: refuses to start if any use-case binding + // is missing a required brand (withSpan / withCapture / withAudit). + assertFeatureConformance( + authContainer, + authManifest, + { + signIn: AUTH_SYMBOLS.ISignInUseCase, + signUp: AUTH_SYMBOLS.ISignUpUseCase, + signOut: AUTH_SYMBOLS.ISignOutUseCase, + }, + ctx, + ); +``` + +The closing `}` of the function should come immediately after. + +- [ ] **Step 3: Run typecheck and tests** + +``` +pnpm --filter @repo/auth typecheck +pnpm --filter @repo/auth test +``` +Both PASS. 81 tests green, no regressions. + +- [ ] **Step 4: Verify web-next boots cleanly** + +``` +pnpm --filter @repo/web-next test +``` +PASS. The existing `bind-production.test.ts` exercises `bindAllDevSeed` / `bindAllProduction`; both should now silently pass the conformance assertion for `auth`. + +- [ ] **Step 5: Commit** + +```bash +git add packages/auth/src/di/bind-production.ts +git commit -m "feat(auth): bindProductionAuth self-asserts conformance at tail" +``` + +--- + +## Task 12: Final verification + story closeout + +**Files:** +- Modify: `docs/work/conformance-system-v1/02-boot-assertions/_story.md` +- Modify: `docs/work/conformance-system-v1/_epic.md` + +- [ ] **Step 1: Run the full verification matrix** + +``` +pnpm typecheck +pnpm test +pnpm lint +pnpm turbo boundaries +``` + +All four MUST pass. If any fail, STOP and report — do not attempt to fix in this task. + +- [ ] **Step 2: Tick the story's task checkboxes and flip status** + +Edit `docs/work/conformance-system-v1/02-boot-assertions/_story.md`: + +(a) Change frontmatter `status: in-progress` → `status: done`. + +(b) Change all 11 `- [ ]` task checkboxes in the Tasks section to `- [x]`: + +```markdown +## Tasks +- [x] Re-export `authManifest` from auth root barrel +- [x] TODO breadcrumb in `withAudit` pointing at future automation +- [x] Runtime marker helpers (`attachBrand`, `isInstrumented`, `isCaptured`, `isAudited`) +- [x] `withSpan` attaches runtime `__instrumented` marker +- [x] `withCapture` attaches runtime `__captured` marker +- [x] `withAudit` wraps + attaches runtime `__audited` marker +- [x] `ConformanceError` class +- [x] `assertFeatureConformance` helper + tests +- [x] Conformance barrel + subpath exports updated +- [x] `bindProductionAuth` self-asserts at the tail +- [x] Final verification + story closeout +``` + +- [ ] **Step 3: Tick story 02 in the epic** + +Edit `docs/work/conformance-system-v1/_epic.md`. Change: + +```markdown +- [ ] 02 — `assertConformance` + boot wiring (later plan) +``` + +to: + +```markdown +- [x] [02 — `assertFeatureConformance` + boot wiring](02-boot-assertions/_story.md) +``` + +(Note: changes the text to a proper link AND ticks the checkbox in one edit.) + +- [ ] **Step 4: Commit the milestone close** + +```bash +git add docs/work/conformance-system-v1/02-boot-assertions/_story.md docs/work/conformance-system-v1/_epic.md +git commit -m "docs(work): close story 02 — assertFeatureConformance + boot wiring" +``` + +--- + +## Done — what this leaves behind + +- `attachBrand` / `isInstrumented` / `isCaptured` / `isAudited` in `@repo/core-shared/conformance` (non-enumerable runtime markers) +- `ConformanceError` class +- `assertFeatureConformance(container, manifest, symbols, ctx)` helper +- `withSpan` / `withCapture` / `withAudit` all attach their runtime brand +- `withAudit` upgraded from passthrough to thin-wrapper (preserves input function purity) +- `authManifest` exposed at `packages/auth/src/index.ts` +- `bindProductionAuth` self-asserts conformance at its tail +- `pnpm dev` (which calls `bindAllDevSeed` → `bindDevSeedAuth` → currently does NOT call `bindProductionAuth`, so this story does not yet break dev seed even if the assertion would fail; production paths via `bindProductionAuth` are covered) +- All gates green (typecheck, test, lint, boundaries) + +## What comes next (separate plans) + +- **Milestone iii — AST-aware ESLint rules.** Five rules in `@repo/core-eslint`: `manifest-usecase-signature-matches`, `no-undeclared-event-publish`, `no-undeclared-audit`, `required-cores-installed`, `feature-must-have-manifest`. Read the manifest, walk the use-case AST, surface drift in the editor. +- **Milestone iv — CI drift gate.** `pnpm conformance` aggregates event closure, generator drift, required-cores ↔ workspace mismatch. +- **Generator updates.** `turbo gen feature` emits `feature.manifest.ts` + contracts stubs + test stubs in canonical shape; CI re-runs and diffs. +- **Apply the conformance pattern to `blog`, `media`, `navigation`, `marketing-pages`.** Each gets its own manifest + self-asserting bind-production (1 small plan per feature, or one bundled plan). +- **Dev-seed slot typing.** Extend `ProductionUseCase` (or define a sibling `DevSeedUseCase<...>`) to `bind-dev-seed.ts` paths so the same drift catches in dev mode. +- **Future: `withAudit` automation.** Wire automated audit emission from manifest `audits[]` declarations (deferred per TODO in `withAudit`).