12-task implementation plan for the runtime boot-time check of the conformance system. Reifies the phantom-type brands from milestone i as non-enumerable runtime markers via Object.defineProperty, and ships assertFeatureConformance(container, manifest, symbols, ctx) which walks the manifest, resolves each use case from the feature's container, and throws ConformanceError synchronously when a binding is missing a required brand. Per-feature self-assertion: each bindProductionX(ctx) calls assertFeatureConformance at its tail, so the boot check runs once per feature and is forward-compatible with future apps wiring those binders. Also bundles the two forward-looking items the milestone-i reviewer flagged (re-export authManifest from auth root barrel; TODO breadcrumb in withAudit for future automation). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1293 lines
46 KiB
Markdown
1293 lines
46 KiB
Markdown
# 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<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>>;
|
|
}
|
|
```
|
|
|
|
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<Args extends unknown[], R>(
|
|
// 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<R>,
|
|
): Audited<(...args: Args) => Promise<R>> {
|
|
void auditLog;
|
|
return fn as Audited<(...args: Args) => Promise<R>>;
|
|
}
|
|
```
|
|
|
|
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<F extends object>(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<string, unknown>)[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<Args extends unknown[], R, Extra extends object>(
|
|
tracer: ITracer,
|
|
opts: SpanOpts | ((args: Args) => SpanOpts),
|
|
fn: ((...args: Args) => Promise<R>) & Extra,
|
|
): Instrumented<((...args: Args) => Promise<R>) & Extra>;
|
|
export function withSpan<Args extends unknown[], R>(
|
|
tracer: ITracer,
|
|
opts: SpanOpts | ((args: Args) => SpanOpts),
|
|
fn: (...args: Args) => Promise<R>,
|
|
): Instrumented<(...args: Args) => Promise<R>>;
|
|
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));
|
|
};
|
|
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<R>>;
|
|
}
|
|
```
|
|
|
|
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<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;
|
|
}
|
|
};
|
|
attachBrand(wrapped, "__captured");
|
|
return wrapped as Captured<(...args: Args) => Promise<R>>;
|
|
}
|
|
```
|
|
|
|
- [ ] **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<F>", () => {
|
|
const auditLog = makeAuditLog();
|
|
const fn = async (_input: { id: string }) => ({ ok: true });
|
|
const wrapped = withAudit(auditLog, fn);
|
|
expectTypeOf(wrapped).toMatchTypeOf<Audited<typeof fn>>();
|
|
});
|
|
|
|
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<I, O, M>` 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> = 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<Args extends unknown[], R>(
|
|
// 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<R>,
|
|
): Audited<(...args: Args) => Promise<R>> {
|
|
void auditLog;
|
|
const wrapped: (...args: Args) => Promise<R> = (...args) => fn(...args);
|
|
attachBrand(wrapped, "__audited");
|
|
return wrapped as Audited<(...args: Args) => Promise<R>>;
|
|
}
|
|
```
|
|
|
|
`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<unknown>) => {
|
|
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<string, symbol>,
|
|
_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 `// <gen:event-handlers>` / `// <gen:jobs>` / `// <gen:realtime-handlers>` 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<I, O, M>` (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`).
|