Merge branch 'worktree-conformance-milestone-ii': conformance milestone ii — assertFeatureConformance + boot wiring
This commit is contained in:
65
docs/work/conformance-system-v1/02-boot-assertions/_story.md
Normal file
65
docs/work/conformance-system-v1/02-boot-assertions/_story.md
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
id: 02-boot-assertions
|
||||
epic: conformance-system-v1
|
||||
title: assertFeatureConformance + boot wiring
|
||||
type: technical-story
|
||||
status: done
|
||||
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
|
||||
- [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
|
||||
@@ -31,7 +31,7 @@ See `docs/architecture/feature-conformance-explainer.html` and
|
||||
|
||||
## Stories
|
||||
- [x] [01 — defineFeature helper + Instrumented/Captured/Audited brands](01-define-feature-helper/_story.md)
|
||||
- [ ] 02 — `assertConformance` + boot wiring (later plan)
|
||||
- [x] [02 — `assertFeatureConformance` + boot wiring](02-boot-assertions/_story.md)
|
||||
- [ ] 03 — AST-aware ESLint rules (later plan)
|
||||
- [ ] 04 — CI drift gate (later plan)
|
||||
- [ ] 05 — Generator emits manifest + contracts + test stubs (later plan)
|
||||
|
||||
@@ -6,7 +6,9 @@ import {
|
||||
type ILogger,
|
||||
} from "@repo/core-shared/instrumentation";
|
||||
import type { BindProductionContext } from "@repo/core-shared/di";
|
||||
import { assertFeatureConformance } from "@repo/core-shared/conformance";
|
||||
import type { ProductionUseCase } from "@repo/core-shared/conformance";
|
||||
import { authManifest } from "../feature.manifest";
|
||||
import type { AuthManifest } from "../feature.manifest";
|
||||
import type {
|
||||
SignInInput,
|
||||
@@ -158,4 +160,17 @@ export function bindProductionAuth(ctx: BindProductionContext): void {
|
||||
// <gen:event-handlers>
|
||||
// <gen:jobs>
|
||||
// <gen:realtime-handlers>
|
||||
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,3 +43,8 @@ export {
|
||||
type UserSignedUpEvent,
|
||||
} from "./events/user-signed-up.event";
|
||||
// <gen:realtime-channels>
|
||||
|
||||
// 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";
|
||||
|
||||
76
packages/core-audit/src/with-audit.chain.test.ts
Normal file
76
packages/core-audit/src/with-audit.chain.test.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { withSpan } from "@repo/core-shared/instrumentation";
|
||||
import { withCapture } from "@repo/core-shared/instrumentation";
|
||||
import { isInstrumented, isCaptured, isAudited } from "@repo/core-shared/conformance";
|
||||
import { withAudit } from "@/with-audit";
|
||||
import type { IAuditLog } from "@/audit-log.interface";
|
||||
import type { ITracer, ISpan } from "@repo/core-shared/instrumentation";
|
||||
import type { ILogger } from "@repo/core-shared/instrumentation";
|
||||
|
||||
function makeTracer(): ITracer {
|
||||
return {
|
||||
startSpan: vi.fn(async (_opts, fn) => {
|
||||
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 makeAuditLog(): IAuditLog {
|
||||
return {
|
||||
record: vi.fn().mockResolvedValue(undefined),
|
||||
eraseSubject: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
}
|
||||
|
||||
describe("brand propagation across the full wrapper chain", () => {
|
||||
it("withSpan(withCapture(withAudit(fn))) carries all three brands on the outer wrapper", () => {
|
||||
const tracer = makeTracer();
|
||||
const logger = makeLogger();
|
||||
const auditLog = makeAuditLog();
|
||||
const fn = async (input: { id: string }) => ({ ok: true, id: input.id });
|
||||
|
||||
const wrapped = withSpan(
|
||||
tracer,
|
||||
{ name: "test.chain", op: "use-case" },
|
||||
withCapture(
|
||||
logger,
|
||||
{ feature: "test", layer: "use-case" },
|
||||
withAudit(auditLog, fn),
|
||||
),
|
||||
);
|
||||
|
||||
expect(isInstrumented(wrapped)).toBe(true);
|
||||
expect(isCaptured(wrapped)).toBe(true);
|
||||
expect(isAudited(wrapped)).toBe(true);
|
||||
});
|
||||
|
||||
it("the full chain preserves input/output behaviour end-to-end", async () => {
|
||||
const tracer = makeTracer();
|
||||
const logger = makeLogger();
|
||||
const auditLog = makeAuditLog();
|
||||
const fn = async (input: { id: string }) => ({ ok: true, id: input.id });
|
||||
|
||||
const wrapped = withSpan(
|
||||
tracer,
|
||||
{ name: "test.chain", op: "use-case" },
|
||||
withCapture(
|
||||
logger,
|
||||
{ feature: "test", layer: "use-case" },
|
||||
withAudit(auditLog, fn),
|
||||
),
|
||||
);
|
||||
|
||||
const result = await wrapped({ id: "abc" });
|
||||
expect(result).toEqual({ ok: true, id: "abc" });
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
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 {
|
||||
@@ -17,6 +18,22 @@ describe("withAudit", () => {
|
||||
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 });
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
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.
|
||||
* 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. 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.
|
||||
* 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>(
|
||||
// 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.
|
||||
// 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>>;
|
||||
const wrapped: (...args: Args) => Promise<R> = (...args) => fn(...args);
|
||||
attachBrand(wrapped, "__audited");
|
||||
return wrapped as Audited<(...args: Args) => Promise<R>>;
|
||||
}
|
||||
|
||||
210
packages/core-shared/src/conformance/assert-bindings.test.ts
Normal file
210
packages/core-shared/src/conformance/assert-bindings.test.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
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 } from "@/instrumentation/tracer.interface";
|
||||
import type { ILogger } from "@/instrumentation/logger.interface";
|
||||
import type { BindContext } from "@/di/bind-context";
|
||||
|
||||
function makeTracer(): ITracer {
|
||||
return {
|
||||
startSpan: vi.fn(async (_opts, fn) => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
62
packages/core-shared/src/conformance/assert-bindings.ts
Normal file
62
packages/core-shared/src/conformance/assert-bindings.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
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?`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
71
packages/core-shared/src/conformance/brand-runtime.test.ts
Normal file
71
packages/core-shared/src/conformance/brand-runtime.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
53
packages/core-shared/src/conformance/brand-runtime.ts
Normal file
53
packages/core-shared/src/conformance/brand-runtime.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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). Calling twice with the same
|
||||
* brand on the same fn is a no-op (matching descriptors) — the engine silently
|
||||
* accepts a redundant `defineProperty` call when every descriptor attribute
|
||||
* matches the existing one. Redefining with different attributes (e.g. flipping
|
||||
* `configurable`) would throw, but wrappers never do that.
|
||||
*/
|
||||
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");
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
/**
|
||||
* 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.
|
||||
* and `withAudit`. The brand has a type-level form (this file) and a
|
||||
* non-enumerable runtime counterpart (see `./brand-runtime.ts`). The runtime
|
||||
* cost is one `Object.defineProperty` call per wrap. 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 };
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
13
packages/core-shared/src/conformance/conformance-error.ts
Normal file
13
packages/core-shared/src/conformance/conformance-error.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -2,3 +2,11 @@ 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";
|
||||
export {
|
||||
attachBrand,
|
||||
isInstrumented,
|
||||
isCaptured,
|
||||
isAudited,
|
||||
} from "./brand-runtime";
|
||||
export { ConformanceError } from "./conformance-error";
|
||||
export { assertFeatureConformance } from "./assert-bindings";
|
||||
|
||||
@@ -3,6 +3,7 @@ import { withCapture } from "@/instrumentation/with-capture";
|
||||
import type { ILogger } from "@/instrumentation/logger.interface";
|
||||
import { isReported } from "@/instrumentation/reported-flag";
|
||||
import type { Captured } from "@/conformance/brands";
|
||||
import { isCaptured } from "@/conformance/brand-runtime";
|
||||
|
||||
function makeLogger(): ILogger & { captureException: ReturnType<typeof vi.fn> } {
|
||||
return {
|
||||
@@ -70,3 +71,13 @@ describe("withCapture — brand", () => {
|
||||
expectTypeOf(wrapped).toMatchTypeOf<Captured<typeof fn>>();
|
||||
});
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ILogger } from "./logger.interface";
|
||||
import type { Captured } from "../conformance/brands";
|
||||
import { attachBrand } from "../conformance/brand-runtime";
|
||||
import { isReported, markReported } from "./reported-flag";
|
||||
|
||||
/**
|
||||
@@ -27,6 +28,8 @@ export function withCapture<Args extends unknown[], R>(
|
||||
tags: Record<string, string>,
|
||||
fn: (...args: Args) => Promise<R>,
|
||||
): Captured<(...args: Args) => Promise<R>> {
|
||||
const PROPAGATED_BRANDS = ["__instrumented", "__audited"] as const;
|
||||
|
||||
const wrapped: (...args: Args) => Promise<R> = async (...args) => {
|
||||
try {
|
||||
return await fn(...args);
|
||||
@@ -38,5 +41,14 @@ export function withCapture<Args extends unknown[], R>(
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
// Propagate brands from the inner function (e.g. __audited from withAudit,
|
||||
// __instrumented if already spanned) so the outermost binding carries all brands.
|
||||
// __captured is omitted here because it is attached explicitly below.
|
||||
for (const brand of PROPAGATED_BRANDS) {
|
||||
if ((fn as unknown as Record<string, unknown>)[brand] === true) {
|
||||
attachBrand(wrapped, brand);
|
||||
}
|
||||
}
|
||||
attachBrand(wrapped, "__captured");
|
||||
return wrapped as Captured<(...args: Args) => Promise<R>>;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, it, expect, expectTypeOf, vi } from "vitest";
|
||||
import { withSpan } from "@/instrumentation/with-span";
|
||||
import type { ITracer, ISpan, SpanOpts } from "@/instrumentation/tracer.interface";
|
||||
import type { Instrumented } from "@/conformance/brands";
|
||||
import { isInstrumented } from "@/conformance/brand-runtime";
|
||||
|
||||
function makeRecordingTracer() {
|
||||
const calls: SpanOpts[] = [];
|
||||
@@ -70,3 +71,13 @@ describe("withSpan — brand", () => {
|
||||
expectTypeOf(wrapped).toMatchTypeOf<Instrumented<typeof fn>>();
|
||||
});
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { ITracer, SpanOpts } from "./tracer.interface";
|
||||
import type { Instrumented } from "../conformance/brands";
|
||||
import { attachBrand } from "../conformance/brand-runtime";
|
||||
|
||||
const PROPAGATED_BRANDS = ["__captured", "__audited"] as const;
|
||||
|
||||
export function withSpan<Args extends unknown[], R, Extra extends object>(
|
||||
tracer: ITracer,
|
||||
@@ -20,7 +23,17 @@ export function withSpan<Args extends unknown[], R>(
|
||||
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.
|
||||
attachBrand(wrapped, "__instrumented");
|
||||
// Propagate brands from the inner function (e.g. __captured from withCapture,
|
||||
// __audited from withAudit) so the outermost binding carries all brands.
|
||||
// withSpan is always outermost — the assertFeatureConformance check reads the
|
||||
// container-resolved value (the withSpan result), so brands must be visible here.
|
||||
for (const brand of PROPAGATED_BRANDS) {
|
||||
if ((fn as unknown as Record<string, unknown>)[brand] === true) {
|
||||
attachBrand(wrapped, brand);
|
||||
}
|
||||
}
|
||||
// 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>>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user