diff --git a/docs/superpowers/plans/2026-05-12-conformance-milestone-i.md b/docs/superpowers/plans/2026-05-12-conformance-milestone-i.md new file mode 100644 index 0000000..d58c1af --- /dev/null +++ b/docs/superpowers/plans/2026-05-12-conformance-milestone-i.md @@ -0,0 +1,1106 @@ +# Conformance Milestone i — defineFeature + brands + auth.signIn proof + +> **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 type-level seam of the conformance system — branded wrapper types (`Instrumented`, `Captured`, `Audited`), a `defineFeature` manifest helper, and a branded `ProductionUseCase` binding slot — then prove the seam works end-to-end by rebinding `auth.signIn` through the new slot, and demonstrate a TS error fires when an unwrapped factory is bound. + +**Architecture:** Phantom-type brands attached at wrap time (no runtime cost). The manifest is a typed `as const` object; the binding-slot type is derived from the manifest's use-case entry, demanding `Audited` only when `mutates: true && audits.length > 0`. For `auth.signIn` (read-only) the slot is `Instrumented & Captured`; the existing `withSpan ∘ withCapture` composition satisfies it once the brands are attached. + +**Tech Stack:** TypeScript (strict), Vitest, Inversify (existing DI container), `@repo/core-shared/instrumentation` (`withSpan`, `withCapture`), `@repo/core-audit` (audit-log impls). No new dependencies. + +--- + +## File structure + +### Create +- `docs/work/README.md` — what `docs/work/` is, how it's used +- `docs/work/conformance-system-v1/_epic.md` — epic record +- `docs/work/conformance-system-v1/01-define-feature-helper/_story.md` — first story record +- `packages/core-shared/src/conformance/brands.ts` — `Instrumented` + `Captured` phantom types +- `packages/core-shared/src/conformance/brands.test.ts` — type-level brand assertions +- `packages/core-shared/src/conformance/define-feature.ts` — `defineFeature` helper + `FeatureManifest` / `UseCaseManifest` types +- `packages/core-shared/src/conformance/define-feature.test.ts` — round-trip type inference +- `packages/core-shared/src/conformance/production-use-case.ts` — `ProductionUseCase` branded slot type +- `packages/core-shared/src/conformance/production-use-case.test.ts` — assignability tests (positive + `@ts-expect-error` negative) +- `packages/core-shared/src/conformance/index.ts` — barrel re-export +- `packages/core-audit/src/with-audit.ts` — `withAudit` use-case wrapper attaching `Audited` +- `packages/core-audit/src/with-audit.test.ts` — wrapper behavior + brand attachment +- `packages/auth/src/feature.manifest.ts` — `authManifest` declaring `signIn`, `signUp`, `signOut` + +### Modify +- `packages/core-shared/src/instrumentation/with-span.ts` — return `Instrumented` +- `packages/core-shared/src/instrumentation/with-span.test.ts` — assert brand on return type +- `packages/core-shared/src/instrumentation/with-capture.ts` — return `Captured` +- `packages/core-shared/src/instrumentation/with-capture.test.ts` — assert brand on return type +- `packages/core-shared/src/instrumentation/index.ts` — re-export brand types from `../conformance` +- `packages/core-audit/src/index.ts` — re-export `withAudit` and `Audited` +- `packages/auth/src/di/bind-production.ts` — rebind `signIn` through `ProductionUseCase<...>` + +--- + +## Task 1: Scaffold `docs/work/` skeleton and epic record + +This is a documentation-only task. No TDD; no tests. Establishes the address space for everything else in the plan. + +**Files:** +- Create: `docs/work/README.md` +- Create: `docs/work/conformance-system-v1/_epic.md` + +- [ ] **Step 1: Create `docs/work/README.md`** + +```markdown +# docs/work — the local task system + +Filesystem-backed Epic/Story/Task hierarchy used by AI agents and humans alike. +See `docs/architecture/agent-first-workflow-and-conformance.md` for the full +design. Until the `work-system-v1` epic ships orchestration tooling, this +folder is human-driven — agents read the files for context, humans flip +checkboxes. + +## Layout + +- `prds/-.prd.md` — source PRDs +- `/_epic.md` — one folder per epic +- `//_story.md` — one folder per story +- `//.task.md` — one file per task +- `_templates/` — copy-paste templates (added in work-system-v1) +- `_state.json` — derived index (added in work-system-v1) +``` + +- [ ] **Step 2: Create the epic file `docs/work/conformance-system-v1/_epic.md`** + +```markdown +--- +id: conformance-system-v1 +prd: null +title: Conformance system v1 +type: epic +status: in-progress +features: [cross-cutting] +created: 2026-05-12 +--- + +## Goal +Build the feature-conformance enforcement system so AI agents get layered, +sub-second feedback on drift between manifest and code. + +## Why +See `docs/architecture/feature-conformance-explainer.html` and +`docs/architecture/agent-first-workflow-and-conformance.md`. + +## In scope +- defineFeature helper + brand types +- assertConformance + boot wiring +- AST-aware ESLint rules +- CI drift gate +- Generator updates for manifest + contracts + test stubs +- Documentation rewrite (manifest-first workflow) +- Migration of auth feature as the reference + +## Out of scope +- Migration of blog, media, navigation, marketing-pages (Phase 3) +- Sandcastle orchestration (Phase 2: work-system-v1 epic) + +## Stories +- [ ] [01 — defineFeature helper + Instrumented/Captured/Audited brands](01-define-feature-helper/_story.md) +- [ ] 02 — `assertConformance` + boot wiring (later plan) +- [ ] 03 — AST-aware ESLint rules (later plan) +- [ ] 04 — CI drift gate (later plan) +- [ ] 05 — Generator emits manifest + contracts + test stubs (later plan) +- [ ] 06 — Documentation rewrite (later plan) +- [ ] 07 — Migrate auth feature reference (later plan) +``` + +- [ ] **Step 3: Commit** + +```bash +git add docs/work/README.md docs/work/conformance-system-v1/_epic.md +git commit -m "docs(work): scaffold docs/work and conformance-system-v1 epic" +``` + +--- + +## Task 2: First story record — `01-define-feature-helper/_story.md` + +**Files:** +- Create: `docs/work/conformance-system-v1/01-define-feature-helper/_story.md` + +- [ ] **Step 1: Write the story file** + +```markdown +--- +id: 01-define-feature-helper +epic: conformance-system-v1 +title: defineFeature helper + Instrumented/Captured/Audited brands +type: technical-story +status: in-progress +feature: core-shared +depends-on: [] +blocks: [02-boot-assertions] +--- + +## Goal +Manifest helper + brand types enable type-level enforcement that every +use-case binding is wrapped with `withSpan` + `withCapture` +(and `withAudit` when mutating with audits declared). + +## Why +Compile-time feedback is the cheapest layer and the foundation every other +milestone reads. + +## Done when +Compile-time TS2322 fires when an unwrapped factory is bound through +`ProductionUseCase<...>`, and `auth.signIn` is rebound through the new slot. + +## In scope +- `Instrumented` and `Captured` brand types in `@repo/core-shared/conformance` +- Brand attachment in `withSpan` and `withCapture` +- `Audited` brand and `withAudit` wrapper in `@repo/core-audit` +- `defineFeature` helper + `FeatureManifest` / `UseCaseManifest` types +- `ProductionUseCase` branded slot type +- `authManifest` declaring `signIn`, `signUp`, `signOut` +- `auth.signIn` rebound through the branded slot + +## Out of scope +- `auth.signUp` / `auth.signOut` rebinding through branded slots (separate tasks within this story or a follow-up story; signUp requires `Audited` once we declare its `audits`) +- Boot-time `assertConformance` (story 02) +- ESLint rules reading the manifest (story 03) +- Generator emitting manifest stubs (story 05) + +## Tasks +- [ ] Brand types: `Instrumented` and `Captured` +- [ ] `withSpan` attaches `Instrumented` +- [ ] `withCapture` attaches `Captured` +- [ ] `defineFeature` helper + manifest types +- [ ] `ProductionUseCase` slot type +- [ ] `withAudit` wrapper + `Audited` brand +- [ ] `authManifest` declaration +- [ ] `auth.signIn` rebound through branded slot +- [ ] Negative test: unwrapped factory rejected at type level +``` + +- [ ] **Step 2: Commit** + +```bash +git add docs/work/conformance-system-v1/01-define-feature-helper/_story.md +git commit -m "docs(work): story 01 — defineFeature helper + brands" +``` + +--- + +## Task 3: Brand types `Instrumented` and `Captured` + +**Files:** +- Create: `packages/core-shared/src/conformance/brands.ts` +- Create: `packages/core-shared/src/conformance/brands.test.ts` + +- [ ] **Step 1: Write the failing test** + +`packages/core-shared/src/conformance/brands.test.ts`: + +```ts +import { describe, it, expectTypeOf } from "vitest"; +import type { Instrumented, Captured } from "@/conformance/brands"; + +describe("brand types", () => { + it("Instrumented is structurally F plus a phantom flag", () => { + type Fn = (n: number) => Promise; + expectTypeOf>().toBeCallableWith(1); + expectTypeOf>().returns.resolves.toEqualTypeOf(); + // The flag is readonly and required for assignability checks. + expectTypeOf["__instrumented"]>().toEqualTypeOf(); + }); + + it("Captured is structurally F plus a phantom flag", () => { + type Fn = (n: number) => Promise; + expectTypeOf>().toBeCallableWith(1); + expectTypeOf["__captured"]>().toEqualTypeOf(); + }); + + it("brands compose without conflict", () => { + type Fn = (n: number) => Promise; + type Both = Instrumented & Captured; + expectTypeOf().toBeCallableWith(1); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `pnpm --filter @repo/core-shared test conformance/brands` +Expected: FAIL — `Cannot find module '@/conformance/brands'` + +- [ ] **Step 3: Write the brand types** + +`packages/core-shared/src/conformance/brands.ts`: + +```ts +/** + * Phantom-type brands attached at wrap time by `withSpan`, `withCapture`, + * and `withAudit`. Pure type-level — no runtime cost, no proxy, no + * `Object.assign`. The conformance system uses these as the type-level + * seam the binding signature checks; a use-case factory that hasn't been + * wrapped is not assignable to a `ProductionUseCase<...>` slot. + */ +export type Instrumented = F & { readonly __instrumented: true }; +export type Captured = F & { readonly __captured: true }; +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `pnpm --filter @repo/core-shared test conformance/brands` +Expected: PASS, 3 tests. + +- [ ] **Step 5: Commit** + +```bash +git add packages/core-shared/src/conformance/brands.ts packages/core-shared/src/conformance/brands.test.ts +git commit -m "feat(core-shared/conformance): Instrumented and Captured brand types" +``` + +--- + +## Task 4: `withSpan` attaches `Instrumented` brand + +**Files:** +- Modify: `packages/core-shared/src/instrumentation/with-span.ts` +- Modify: `packages/core-shared/src/instrumentation/with-span.test.ts` + +- [ ] **Step 1: Add the failing brand assertion to `with-span.test.ts`** + +Make two changes to `packages/core-shared/src/instrumentation/with-span.test.ts`: + +(a) Extend the existing first import line to include `expectTypeOf`. Change: + +```ts +import { describe, it, expect, vi } from "vitest"; +``` + +to: + +```ts +import { describe, it, expect, expectTypeOf, vi } from "vitest"; +``` + +(b) Add this import line near the other imports at the top of the file: + +```ts +import type { Instrumented } from "@/conformance/brands"; +``` + +(c) Append this `describe` block at the end of the file (after the existing `describe("withSpan", …)` block): + +```ts +describe("withSpan — brand", () => { + it("returns an Instrumented", () => { + const { tracer } = makeRecordingTracer(); + const fn = async (a: number) => a + 1; + const wrapped = withSpan(tracer, { name: "test.brand", op: "use-case" }, fn); + expectTypeOf(wrapped).toMatchTypeOf>(); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `pnpm --filter @repo/core-shared test with-span` +Expected: FAIL — `wrapped` is `(a: number) => Promise`, not assignable to `Instrumented`. + +- [ ] **Step 3: Update `with-span.ts` to return the branded type** + +Replace the entire file `packages/core-shared/src/instrumentation/with-span.ts`: + +```ts +import type { ITracer, SpanOpts } from "./tracer.interface"; +import type { Instrumented } from "../conformance/brands"; + +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)); + }; + // Cast is the only runtime concession — the brand is a phantom type; + // there is no real `__instrumented` property at runtime. + return wrapped as Instrumented<(...args: Args) => Promise>; +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `pnpm --filter @repo/core-shared test with-span` +Expected: PASS, all existing tests + the new 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 returns Instrumented" +``` + +--- + +## Task 5: `withCapture` attaches `Captured` brand + +**Files:** +- Modify: `packages/core-shared/src/instrumentation/with-capture.ts` +- Modify: `packages/core-shared/src/instrumentation/with-capture.test.ts` + +- [ ] **Step 1: Add the failing brand assertion to `with-capture.test.ts`** + +Make two changes to `packages/core-shared/src/instrumentation/with-capture.test.ts`: + +(a) Extend the existing first import line to include `expectTypeOf`. Change: + +```ts +import { describe, it, expect, vi } from "vitest"; +``` + +to: + +```ts +import { describe, it, expect, expectTypeOf, vi } from "vitest"; +``` + +(b) Add this import line near the other imports at the top of the file: + +```ts +import type { Captured } from "@/conformance/brands"; +``` + +(c) Append this `describe` block at the end of the file: + +```ts +describe("withCapture — brand", () => { + it("returns a Captured", () => { + const logger = makeLogger(); + const fn = async (a: number) => a + 1; + const wrapped = withCapture(logger, { layer: "use-case" }, fn); + expectTypeOf(wrapped).toMatchTypeOf>(); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `pnpm --filter @repo/core-shared test with-capture` +Expected: FAIL — wrapped is not assignable to `Captured`. + +- [ ] **Step 3: Update `with-capture.ts` to return the branded type** + +Replace the function in `packages/core-shared/src/instrumentation/with-capture.ts` (keep the docblock as-is; only the function signature + body change): + +```ts +import type { ILogger } from "./logger.interface"; +import type { Captured } from "../conformance/brands"; +import { isReported, markReported } from "./reported-flag"; + +// (keep the existing docblock here) + +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; + } + }; + return wrapped as Captured<(...args: Args) => Promise>; +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `pnpm --filter @repo/core-shared test with-capture` +Expected: PASS, all existing tests + the new brand test. + +- [ ] **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 returns Captured" +``` + +--- + +## Task 6: `defineFeature` helper + manifest types + +**Files:** +- Create: `packages/core-shared/src/conformance/define-feature.ts` +- Create: `packages/core-shared/src/conformance/define-feature.test.ts` + +- [ ] **Step 1: Write the failing test** + +`packages/core-shared/src/conformance/define-feature.test.ts`: + +```ts +import { describe, it, expectTypeOf } from "vitest"; +import { defineFeature, type FeatureManifest } from "@/conformance/define-feature"; + +describe("defineFeature", () => { + it("preserves literal types of a manifest declared with `as const`", () => { + const manifest = defineFeature({ + name: "auth", + requiredCores: ["audit"], + useCases: { + signIn: { + mutates: false, + audits: [], + publishes: [], + consumes: [], + }, + signUp: { + mutates: true, + audits: ["user.created"], + publishes: ["auth.signed-up"], + consumes: [], + }, + }, + realtimeChannels: [], + jobs: [], + } as const); + + // Literal preservation: name is the literal "auth", not string + expectTypeOf(manifest.name).toEqualTypeOf<"auth">(); + // Use-case keys preserved + expectTypeOf(manifest.useCases.signUp.audits).toEqualTypeOf(); + expectTypeOf(manifest.useCases.signUp.mutates).toEqualTypeOf(); + expectTypeOf(manifest.useCases.signIn.mutates).toEqualTypeOf(); + }); + + it("FeatureManifest type accepts the shape", () => { + const manifest = defineFeature({ + name: "blog", + requiredCores: [], + useCases: {}, + realtimeChannels: [], + jobs: [], + } as const); + expectTypeOf(manifest).toMatchTypeOf(); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `pnpm --filter @repo/core-shared test define-feature` +Expected: FAIL — module not found. + +- [ ] **Step 3: Write `define-feature.ts`** + +`packages/core-shared/src/conformance/define-feature.ts`: + +```ts +/** + * Per-use-case manifest entry. Declares what the use case does at the contract + * level: whether it mutates state, what audit events it emits, what cross-feature + * events it publishes or consumes. The conformance system reads these to + * derive binding-slot types and to verify code against manifest declarations. + */ +export type UseCaseManifest = { + readonly mutates: boolean; + readonly audits: readonly string[]; + readonly publishes: readonly string[]; + readonly consumes: readonly string[]; +}; + +/** + * The feature-level manifest. One per feature package, conventionally exported + * as `Manifest` from `src/feature.manifest.ts`. + */ +export type FeatureManifest = { + readonly name: string; + readonly requiredCores: readonly string[]; + readonly useCases: { readonly [k: string]: UseCaseManifest }; + readonly realtimeChannels: readonly string[]; + readonly jobs: readonly string[]; +}; + +/** + * Identity helper that exists purely to widen the input type to satisfy + * `FeatureManifest` while preserving the literal types of the `as const` + * input. Downstream types (`ProductionUseCase`) consume the + * preserved literals to derive binding-slot brand requirements. + * + * Usage: + * + * export const authManifest = defineFeature({ name: "auth", ... } as const); + */ +export function defineFeature(manifest: M): M { + return manifest; +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `pnpm --filter @repo/core-shared test define-feature` +Expected: PASS, 2 tests. + +- [ ] **Step 5: Commit** + +```bash +git add packages/core-shared/src/conformance/define-feature.ts packages/core-shared/src/conformance/define-feature.test.ts +git commit -m "feat(core-shared/conformance): defineFeature helper + manifest types" +``` + +--- + +## Task 7: `ProductionUseCase` branded slot type + +**Files:** +- Create: `packages/core-shared/src/conformance/production-use-case.ts` +- Create: `packages/core-shared/src/conformance/production-use-case.test.ts` + +- [ ] **Step 1: Write the failing test** + +`packages/core-shared/src/conformance/production-use-case.test.ts`: + +```ts +import { describe, it, expectTypeOf } from "vitest"; +import type { ProductionUseCase } from "@/conformance/production-use-case"; +import type { Instrumented, Captured } from "@/conformance/brands"; + +describe("ProductionUseCase", () => { + it("requires Instrumented + Captured for any use case", () => { + type Manifest = { + mutates: false; + audits: readonly []; + publishes: readonly []; + consumes: readonly []; + }; + type Slot = ProductionUseCase<{ x: number }, { y: string }, Manifest>; + type Wrapped = Instrumented<(input: { x: number }) => Promise<{ y: string }>> & + Captured<(input: { x: number }) => Promise<{ y: string }>>; + + expectTypeOf().toMatchTypeOf(); + }); + + it("a plain factory is NOT assignable to the slot", () => { + type Manifest = { + mutates: false; + audits: readonly []; + publishes: readonly []; + consumes: readonly []; + }; + type Slot = ProductionUseCase<{ x: number }, { y: string }, Manifest>; + const factory = async (input: { x: number }) => ({ y: String(input.x) }); + + // @ts-expect-error — factory has no __instrumented / __captured brand + const bad: Slot = factory; + void bad; + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `pnpm --filter @repo/core-shared test production-use-case` +Expected: FAIL — module not found. + +- [ ] **Step 3: Write `production-use-case.ts`** + +`packages/core-shared/src/conformance/production-use-case.ts`: + +```ts +import type { UseCaseManifest } from "./define-feature"; +import type { Instrumented, Captured } from "./brands"; + +/** + * Type-level binding slot for production use cases. Derived from the manifest + * entry: every binding must be Instrumented + Captured; mutating use cases + * that declare audits additionally must be Audited. The Audited brand lives + * in `@repo/core-audit` because the wrap helper that attaches it depends on + * `IAuditLog` — feature packages import the merged slot type implicitly + * by typing their bindings as `ProductionUseCase`. + * + * The Audited requirement is encoded conditionally so this type stays usable + * without depending on core-audit. When `mutates: true` AND `audits` is + * non-empty, the slot demands a marker type with a `__audited` flag; the + * concrete `Audited` from core-audit satisfies it. + */ +export type ProductionUseCase = + & Instrumented<(input: I) => Promise> + & Captured<(input: I) => Promise> + & (M["mutates"] extends true + ? M["audits"]["length"] extends 0 + ? unknown + : { readonly __audited: true } + : unknown); +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `pnpm --filter @repo/core-shared test production-use-case` +Expected: PASS, 2 tests. + +- [ ] **Step 5: Commit** + +```bash +git add packages/core-shared/src/conformance/production-use-case.ts packages/core-shared/src/conformance/production-use-case.test.ts +git commit -m "feat(core-shared/conformance): ProductionUseCase branded slot type" +``` + +--- + +## Task 8: Conformance barrel + package subpath export + +**Files:** +- Create: `packages/core-shared/src/conformance/index.ts` +- Modify: `packages/core-shared/src/instrumentation/index.ts` +- Modify: `packages/core-shared/package.json` + +- [ ] **Step 1: Create the conformance barrel** + +`packages/core-shared/src/conformance/index.ts`: + +```ts +export type { Instrumented, Captured } from "./brands"; +export type { FeatureManifest, UseCaseManifest } from "./define-feature"; +export { defineFeature } from "./define-feature"; +export type { ProductionUseCase } from "./production-use-case"; +``` + +- [ ] **Step 2: Add `./conformance` to `packages/core-shared/package.json` exports** + +Open `packages/core-shared/package.json` and find the `"exports"` object. Add a new line for `./conformance` next to the existing subpath exports (e.g. immediately after `"./audit"`): + +```json +"./conformance": "./src/conformance/index.ts", +``` + +The resulting `exports` map should include `"./conformance"` alongside the existing `"."`, `"./audit"`, `"./di"`, `"./instrumentation"`, etc. + +- [ ] **Step 3: Re-export brand types from instrumentation/index for ergonomics** + +Modify `packages/core-shared/src/instrumentation/index.ts` — append at the bottom: + +```ts +// Re-export brand types alongside the wrappers that attach them, so callers +// can `import { withSpan, type Instrumented } from "@repo/core-shared/instrumentation"`. +export type { Instrumented, Captured } from "../conformance/brands"; +``` + +- [ ] **Step 4: Verify the package builds** + +Run: `pnpm --filter @repo/core-shared typecheck` +Expected: PASS, no errors. + +- [ ] **Step 5: Commit** + +```bash +git add packages/core-shared/src/conformance/index.ts packages/core-shared/src/instrumentation/index.ts packages/core-shared/package.json +git commit -m "feat(core-shared/conformance): barrel + ./conformance subpath export" +``` + +--- + +## Task 9: `withAudit` wrapper + `Audited` brand in `@repo/core-audit` + +**Files:** +- Create: `packages/core-audit/src/with-audit.ts` +- Create: `packages/core-audit/src/with-audit.test.ts` +- Modify: `packages/core-audit/src/index.ts` + +- [ ] **Step 1: Write the failing test** + +`packages/core-audit/src/with-audit.test.ts`: + +```ts +import { describe, it, expect, expectTypeOf, vi } from "vitest"; +import { withAudit, type Audited } from "@/with-audit"; +import type { IAuditLog } from "@/audit-log.interface"; + +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("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); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `pnpm --filter @repo/core-audit test with-audit` +Expected: FAIL — module not found. + +- [ ] **Step 3: Write `with-audit.ts`** + +`packages/core-audit/src/with-audit.ts`: + +```ts +import type { IAuditLog } from "./audit-log.interface"; + +/** + * 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. + */ +export type Audited = F & { readonly __audited: true }; + +/** + * Use-case wrapper applied at DI bind time. In milestone i this is a + * brand-only attachment: it does not yet automatically call `auditLog.record`. + * Use cases continue to call `auditLog.record(...)` in their own bodies; the + * wrapper exists to make "binding was bound through the audit-aware path" + * type-checkable at compile time. + * + * A future story may move auditing logic out of factory bodies and into the + * wrapper itself (driven by manifest declarations) — but that requires the + * manifest's `audits[]` entries to fully specify what gets recorded, which + * is out of scope here. + */ +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>; +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `pnpm --filter @repo/core-audit test with-audit` +Expected: PASS, 3 tests. + +- [ ] **Step 5: Re-export from core-audit barrel** + +Append to `packages/core-audit/src/index.ts`: + +```ts +export { withAudit, type Audited } from "./with-audit"; +``` + +- [ ] **Step 6: Verify the package builds** + +Run: `pnpm --filter @repo/core-audit typecheck` +Expected: PASS. + +- [ ] **Step 7: Commit** + +```bash +git add packages/core-audit/src/with-audit.ts packages/core-audit/src/with-audit.test.ts packages/core-audit/src/index.ts +git commit -m "feat(core-audit): withAudit wrapper + Audited brand" +``` + +--- + +## Task 10: `authManifest` declaration + +**Files:** +- Create: `packages/auth/src/feature.manifest.ts` + +- [ ] **Step 1: Write the manifest** + +`packages/auth/src/feature.manifest.ts`: + +```ts +import { defineFeature } from "@repo/core-shared/conformance"; + +/** + * The auth feature's conformance manifest. Drives binding-slot types in + * `di/bind-production.ts` and is read by ESLint, the boot assertion, and + * the CI drift gate (later milestones). + * + * Conventions: + * - `mutates: true` for any use case that creates, updates, or deletes state + * - `audits` lists every audit event the use case emits (must match calls + * to `auditLog.record(...)` in the factory body — ESLint enforces this + * in a later story) + * - `publishes` / `consumes` cover cross-feature events through `IEventBus` + */ +export const authManifest = defineFeature({ + name: "auth", + requiredCores: [], + useCases: { + signIn: { + mutates: false, + audits: [], + publishes: [], + consumes: [], + }, + signUp: { + mutates: true, + audits: [], + publishes: [], + consumes: [], + }, + signOut: { + mutates: true, + audits: [], + publishes: [], + consumes: [], + }, + }, + realtimeChannels: [], + jobs: [], +} as const); + +export type AuthManifest = typeof authManifest; +``` + +> Note: `signUp` and `signOut` declare `mutates: true` but empty `audits` to keep this milestone scoped to brand checking. Adding audits is its own task in a future story; the `Audited` requirement only fires when `mutates && audits.length > 0`. + +- [ ] **Step 2: Verify the manifest typechecks** + +Run: `pnpm --filter @repo/auth typecheck` +Expected: PASS — manifest infers correctly and `AuthManifest` derives without error. + +- [ ] **Step 3: Commit** + +```bash +git add packages/auth/src/feature.manifest.ts +git commit -m "feat(auth): declare authManifest with signIn/signUp/signOut" +``` + +--- + +## Task 11: Rebind `auth.signIn` through `ProductionUseCase<...>` + +**Files:** +- Modify: `packages/auth/src/di/bind-production.ts` + +- [ ] **Step 1: Locate the existing `signIn` binding** + +Read the existing binding block in `packages/auth/src/di/bind-production.ts`. The current pattern is: + +```ts +const wrappedSignIn = withSpan( + tracer, + { name: "auth.signIn", op: "use-case" }, + withCapture( + logger, + { feature: "auth", layer: "use-case", name: "auth.signIn" }, + signInUseCase(repo, authService), + ), +); +``` + +There is no explicit slot type today. + +- [ ] **Step 2: Add the typed binding for `signIn`** + +Replace the `wrappedSignIn` assignment with an explicitly typed slot: + +```ts +import type { ProductionUseCase } from "@repo/core-shared/conformance"; +import { authManifest, type AuthManifest } from "../feature.manifest"; +import type { SignInInput, SignInOutput } from "../application/use-cases/sign-in.use-case"; + +// ... existing imports ... + +// Inside bindProductionAuth, replacing the existing wrappedSignIn assignment: +const wrappedSignIn: ProductionUseCase< + SignInInput, + SignInOutput, + AuthManifest["useCases"]["signIn"] +> = withSpan( + tracer, + { name: "auth.signIn", op: "use-case" }, + withCapture( + logger, + { feature: "auth", layer: "use-case", name: "auth.signIn" }, + signInUseCase(repo, authService), + ), +); +``` + +Place the new imports next to the existing imports at the top of the file. Keep the `void authManifest` no-op or use it (see Step 3) so TypeScript doesn't warn about the unused import — but since `AuthManifest` is referenced in the slot type, only `authManifest` may be unused. If unused, drop the value import and keep only the type: + +```ts +import type { AuthManifest } from "../feature.manifest"; +``` + +- [ ] **Step 3: Run typecheck and the auth test suite** + +Run: `pnpm --filter @repo/auth typecheck` +Expected: PASS. + +Run: `pnpm --filter @repo/auth test` +Expected: PASS, no test regressions. + +- [ ] **Step 4: Commit** + +```bash +git add packages/auth/src/di/bind-production.ts +git commit -m "feat(auth): bind signIn through ProductionUseCase branded slot" +``` + +--- + +## Task 12: Negative test — unwrapped factory rejected at the type level + +**Files:** +- Create: `packages/auth/src/di/bind-production.types.test.ts` + +This is a type-only test file that asserts the *failure* of an unwrapped factory to satisfy the branded slot. The `@ts-expect-error` annotation makes the file pass `tsc` only when the suppressed line actually has an error. + +- [ ] **Step 1: Write the type test** + +`packages/auth/src/di/bind-production.types.test.ts`: + +```ts +import { describe, it } from "vitest"; +import type { ProductionUseCase } from "@repo/core-shared/conformance"; +import type { AuthManifest } from "../feature.manifest"; +import type { + SignInInput, + SignInOutput, +} from "../application/use-cases/sign-in.use-case"; +import { signInUseCase } from "../application/use-cases/sign-in.use-case"; + +describe("auth.signIn binding slot (type-level)", () => { + it("rejects an unwrapped factory", () => { + type Slot = ProductionUseCase< + SignInInput, + SignInOutput, + AuthManifest["useCases"]["signIn"] + >; + + // Build the unwrapped factory exactly as it would be at the use-case file. + // It returns a function with no brand attached — must not be assignable. + const fakeRepo = {} as never; + const fakeAuth = {} as never; + const unwrapped = signInUseCase(fakeRepo, fakeAuth); + + // @ts-expect-error — unwrapped factory has no __instrumented / __captured brand + const _bad: Slot = unwrapped; + void _bad; + }); +}); +``` + +- [ ] **Step 2: Run typecheck — the `@ts-expect-error` must consume a real error** + +Run: `pnpm --filter @repo/auth typecheck` +Expected: PASS. If the line under `@ts-expect-error` does NOT error, tsc will report "Unused '@ts-expect-error' directive." That signals a hole in the brand check. + +- [ ] **Step 3: Run the test (no runtime assertions; it just needs to load)** + +Run: `pnpm --filter @repo/auth test bind-production.types` +Expected: PASS — the test body has no runtime assertions, so the test passes as long as the module loads. + +- [ ] **Step 4: Commit** + +```bash +git add packages/auth/src/di/bind-production.types.test.ts +git commit -m "test(auth): assert unwrapped factory rejected at branded slot" +``` + +--- + +## Task 13: Final verification + story checkbox + +**Files:** +- Modify: `docs/work/conformance-system-v1/01-define-feature-helper/_story.md` +- Modify: `docs/work/conformance-system-v1/_epic.md` + +- [ ] **Step 1: Run the full type-check and test suite** + +```bash +pnpm typecheck +pnpm test +``` + +Expected: PASS, no regressions. + +- [ ] **Step 2: Run lint to confirm no new violations** + +```bash +pnpm lint +``` + +Expected: PASS, no errors. + +- [ ] **Step 3: Verify the boundaries graph still validates** + +```bash +pnpm turbo boundaries +``` + +Expected: PASS. + +- [ ] **Step 4: Tick the story's task checkboxes and flip the story status** + +Edit `docs/work/conformance-system-v1/01-define-feature-helper/_story.md`: +- Change frontmatter `status: in-progress` → `status: done` +- Change all `- [ ]` task checkboxes in the Tasks section to `- [x]` + +- [ ] **Step 5: Tick the story in the epic** + +Edit `docs/work/conformance-system-v1/_epic.md`: +- Change `- [ ] [01 — defineFeature helper + Instrumented/Captured/Audited brands](01-define-feature-helper/_story.md)` to `- [x] [01 — defineFeature helper + Instrumented/Captured/Audited brands](01-define-feature-helper/_story.md)` + +- [ ] **Step 6: Commit the milestone close** + +```bash +git add docs/work/conformance-system-v1/01-define-feature-helper/_story.md docs/work/conformance-system-v1/_epic.md +git commit -m "docs(work): close story 01 — defineFeature + brands + auth.signIn proof" +``` + +--- + +## Done — what this leaves behind + +- `Instrumented`, `Captured` brands in `@repo/core-shared/conformance` +- `Audited` brand + `withAudit` (skeleton) in `@repo/core-audit` +- `defineFeature` helper + `FeatureManifest` / `UseCaseManifest` types +- `ProductionUseCase` branded slot type +- `authManifest` declared +- `auth.signIn` bound through the typed slot +- Negative test confirms an unwrapped factory does NOT type-check at the slot +- All existing tests pass; no behavioral changes; pure type-system seam added + +## What comes next (separate plans) + +- Milestone ii: `assertConformance` boot-time runtime check + wire into all three apps' `bindAll` +- Milestone iii: AST-aware ESLint rules (`manifest-usecase-signature-matches`, `no-undeclared-event-publish`, `no-undeclared-audit`, `required-cores-installed`) +- Milestone iv: `pnpm conformance` CI script (event closure, scaffold drift, required-cores) +- Generator updates: `turbo gen feature` emits `feature.manifest.ts` + contracts + test stubs +- Documentation rewrite: `CLAUDE.md`, `AGENTS.md`, `docs/guides/tdd-workflow.md`, new agent-workflow guide +- Auth migration polish: bind `signUp` / `signOut` through the slot (and add audits, exercising `Audited`)