Files
agentic-dev/docs/superpowers/plans/2026-05-12-conformance-milestone-i.md
Danijel Martinek 31906b9e3e plan(conformance): milestone i — defineFeature + brands + auth.signIn proof
13-task implementation plan for the type-level seam of the conformance system.
Ships brand types (Instrumented/Captured/Audited), defineFeature helper,
ProductionUseCase branded slot, withAudit wrapper, authManifest, and rebinds
auth.signIn through the branded slot — with a negative type test asserting
unwrapped factories are rejected at the slot.

Each task is a single red→green TDD cycle with one commit. Companion
docs/work/conformance-system-v1/ epic + story records are created early in
the plan so subsequent agents have full context when picking up later
stories.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:18:23 +02:00

37 KiB

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<F>, Captured<F>, Audited<F>), a defineFeature manifest helper, and a branded ProductionUseCase<I, O, M> 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<F> only when mutates: true && audits.length > 0. For auth.signIn (read-only) the slot is Instrumented<F> & Captured<F>; 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.tsInstrumented<F> + Captured<F> phantom types
  • packages/core-shared/src/conformance/brands.test.ts — type-level brand assertions
  • packages/core-shared/src/conformance/define-feature.tsdefineFeature 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.tsProductionUseCase<I, O, M> 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.tswithAudit use-case wrapper attaching Audited<F>
  • packages/core-audit/src/with-audit.test.ts — wrapper behavior + brand attachment
  • packages/auth/src/feature.manifest.tsauthManifest declaring signIn, signUp, signOut

Modify

  • packages/core-shared/src/instrumentation/with-span.ts — return Instrumented<F>
  • packages/core-shared/src/instrumentation/with-span.test.ts — assert brand on return type
  • packages/core-shared/src/instrumentation/with-capture.ts — return Captured<F>
  • 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

# 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/<date>-<slug>.prd.md` — source PRDs
- `<epic-slug>/_epic.md` — one folder per epic
- `<epic-slug>/<story-slug>/_story.md` — one folder per story
- `<epic-slug>/<story-slug>/<task-slug>.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
---
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
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

---
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<F>` and `Captured<F>` brand types in `@repo/core-shared/conformance`
- Brand attachment in `withSpan` and `withCapture`
- `Audited<F>` brand and `withAudit` wrapper in `@repo/core-audit`
- `defineFeature` helper + `FeatureManifest` / `UseCaseManifest` types
- `ProductionUseCase<I, O, M>` 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<F>` 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<F>` and `Captured<F>`
- [ ] `withSpan` attaches `Instrumented`
- [ ] `withCapture` attaches `Captured`
- [ ] `defineFeature` helper + manifest types
- [ ] `ProductionUseCase<I, O, M>` 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
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<F> and Captured<F>

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:

import { describe, it, expectTypeOf } from "vitest";
import type { Instrumented, Captured } from "@/conformance/brands";

describe("brand types", () => {
  it("Instrumented<F> is structurally F plus a phantom flag", () => {
    type Fn = (n: number) => Promise<string>;
    expectTypeOf<Instrumented<Fn>>().toBeCallableWith(1);
    expectTypeOf<Instrumented<Fn>>().returns.resolves.toEqualTypeOf<string>();
    // The flag is readonly and required for assignability checks.
    expectTypeOf<Instrumented<Fn>["__instrumented"]>().toEqualTypeOf<true>();
  });

  it("Captured<F> is structurally F plus a phantom flag", () => {
    type Fn = (n: number) => Promise<string>;
    expectTypeOf<Captured<Fn>>().toBeCallableWith(1);
    expectTypeOf<Captured<Fn>["__captured"]>().toEqualTypeOf<true>();
  });

  it("brands compose without conflict", () => {
    type Fn = (n: number) => Promise<string>;
    type Both = Instrumented<Fn> & Captured<Fn>;
    expectTypeOf<Both>().toBeCallableWith(1);
    expectTypeOf<Both["__instrumented"]>().toEqualTypeOf<true>();
    expectTypeOf<Both["__captured"]>().toEqualTypeOf<true>();
  });
});
  • 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:

/**
 * 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> = F & { readonly __instrumented: true };
export type Captured<F> = 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
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<F> and Captured<F> 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:

import { describe, it, expect, vi } from "vitest";

to:

import { describe, it, expect, expectTypeOf, vi } from "vitest";

(b) Add this import line near the other imports at the top of the file:

import type { Instrumented } from "@/conformance/brands";

(c) Append this describe block at the end of the file (after the existing describe("withSpan", …) block):

describe("withSpan — brand", () => {
  it("returns an Instrumented<F>", () => {
    const { tracer } = makeRecordingTracer();
    const fn = async (a: number) => a + 1;
    const wrapped = withSpan(tracer, { name: "test.brand", op: "use-case" }, fn);
    expectTypeOf(wrapped).toMatchTypeOf<Instrumented<typeof fn>>();
  });
});
  • Step 2: Run test to verify it fails

Run: pnpm --filter @repo/core-shared test with-span Expected: FAIL — wrapped is (a: number) => Promise<number>, not assignable to Instrumented<typeof fn>.

  • Step 3: Update with-span.ts to return the branded type

Replace the entire file packages/core-shared/src/instrumentation/with-span.ts:

import type { ITracer, SpanOpts } from "./tracer.interface";
import type { Instrumented } from "../conformance/brands";

export function withSpan<Args extends unknown[], R>(
  tracer: ITracer,
  opts: SpanOpts | ((args: Args) => SpanOpts),
  fn: (...args: Args) => Promise<R>,
): Instrumented<(...args: Args) => Promise<R>> {
  const wrapped: (...args: Args) => Promise<R> = (...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<R>>;
}
  • 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
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<F>"

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:

import { describe, it, expect, vi } from "vitest";

to:

import { describe, it, expect, expectTypeOf, vi } from "vitest";

(b) Add this import line near the other imports at the top of the file:

import type { Captured } from "@/conformance/brands";

(c) Append this describe block at the end of the file:

describe("withCapture — brand", () => {
  it("returns a Captured<F>", () => {
    const logger = makeLogger();
    const fn = async (a: number) => a + 1;
    const wrapped = withCapture(logger, { layer: "use-case" }, fn);
    expectTypeOf(wrapped).toMatchTypeOf<Captured<typeof fn>>();
  });
});
  • 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<typeof fn>.

  • 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):

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<Args extends unknown[], R>(
  logger: ILogger,
  tags: Record<string, string>,
  fn: (...args: Args) => Promise<R>,
): Captured<(...args: Args) => Promise<R>> {
  const wrapped: (...args: Args) => Promise<R> = 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<R>>;
}
  • 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
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<F>"

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:

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<readonly ["user.created"]>();
    expectTypeOf(manifest.useCases.signUp.mutates).toEqualTypeOf<true>();
    expectTypeOf(manifest.useCases.signIn.mutates).toEqualTypeOf<false>();
  });

  it("FeatureManifest type accepts the shape", () => {
    const manifest = defineFeature({
      name: "blog",
      requiredCores: [],
      useCases: {},
      realtimeChannels: [],
      jobs: [],
    } as const);
    expectTypeOf(manifest).toMatchTypeOf<FeatureManifest>();
  });
});
  • 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:

/**
 * 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 `<featureName>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<I, O, M>`) consume the
 * preserved literals to derive binding-slot brand requirements.
 *
 * Usage:
 *
 *   export const authManifest = defineFeature({ name: "auth", ... } as const);
 */
export function defineFeature<const M extends FeatureManifest>(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
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<I, O, M> 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:

import { describe, it, expectTypeOf } from "vitest";
import type { ProductionUseCase } from "@/conformance/production-use-case";
import type { Instrumented, Captured } from "@/conformance/brands";

describe("ProductionUseCase<I, O, M>", () => {
  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<Wrapped>().toMatchTypeOf<Slot>();
  });

  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:

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<I, O, AuthManifest["useCases"]["signIn"]>`.
 *
 * 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<F>` from core-audit satisfies it.
 */
export type ProductionUseCase<I, O, M extends UseCaseManifest> =
  & Instrumented<(input: I) => Promise<O>>
  & Captured<(input: I) => Promise<O>>
  & (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
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<I, O, M> 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:

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"):

"./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:

// 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
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:

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<F>", () => {
    const auditLog = makeAuditLog();
    const fn = async (input: { id: string }) => ({ ok: true });
    const wrapped = withAudit(auditLog, fn);
    expectTypeOf(wrapped).toMatchTypeOf<Audited<typeof 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);
  });
});
  • 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:

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<I, O, M>` when M demands
 * it.
 */
export type Audited<F> = 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<Args extends unknown[], R>(
  // 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<R>,
): Audited<(...args: Args) => Promise<R>> {
  void auditLog;
  return fn as Audited<(...args: Args) => Promise<R>>;
}
  • 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:

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
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<F> brand"

Task 10: authManifest declaration

Files:

  • Create: packages/auth/src/feature.manifest.ts

  • Step 1: Write the manifest

packages/auth/src/feature.manifest.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<F> 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
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:

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:

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:

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
git add packages/auth/src/di/bind-production.ts
git commit -m "feat(auth): bind signIn through ProductionUseCase<I, O, M> 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:

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
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

pnpm typecheck
pnpm test

Expected: PASS, no regressions.

  • Step 2: Run lint to confirm no new violations
pnpm lint

Expected: PASS, no errors.

  • Step 3: Verify the boundaries graph still validates
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-progressstatus: 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

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<F>, Captured<F> brands in @repo/core-shared/conformance
  • Audited<F> brand + withAudit (skeleton) in @repo/core-audit
  • defineFeature helper + FeatureManifest / UseCaseManifest types
  • ProductionUseCase<I, O, M> 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<F>)