feat(core-shared/conformance): assertFeatureConformance helper
Adds boot-time check that every manifest-declared use case is bound through withSpan (__instrumented) + withCapture (__captured), with withAudit (__audited) enforced when audits[] is non-empty. Propagates inner brands through withSpan so the outermost container-resolved binding carries all brand markers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
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?`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,3 +9,4 @@ export {
|
|||||||
isAudited,
|
isAudited,
|
||||||
} from "./brand-runtime";
|
} from "./brand-runtime";
|
||||||
export { ConformanceError } from "./conformance-error";
|
export { ConformanceError } from "./conformance-error";
|
||||||
|
export { assertFeatureConformance } from "./assert-bindings";
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import type { ITracer, SpanOpts } from "./tracer.interface";
|
|||||||
import type { Instrumented } from "../conformance/brands";
|
import type { Instrumented } from "../conformance/brands";
|
||||||
import { attachBrand } from "../conformance/brand-runtime";
|
import { attachBrand } from "../conformance/brand-runtime";
|
||||||
|
|
||||||
|
const PROPAGATED_BRANDS = ["__captured", "__audited"] as const;
|
||||||
|
|
||||||
export function withSpan<Args extends unknown[], R, Extra extends object>(
|
export function withSpan<Args extends unknown[], R, Extra extends object>(
|
||||||
tracer: ITracer,
|
tracer: ITracer,
|
||||||
opts: SpanOpts | ((args: Args) => SpanOpts),
|
opts: SpanOpts | ((args: Args) => SpanOpts),
|
||||||
@@ -22,6 +24,15 @@ export function withSpan<Args extends unknown[], R>(
|
|||||||
return tracer.startSpan(resolved, () => fn(...args));
|
return tracer.startSpan(resolved, () => fn(...args));
|
||||||
};
|
};
|
||||||
attachBrand(wrapped, "__instrumented");
|
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
|
// Cast is the type-level concession — the brand is now also a non-enumerable
|
||||||
// runtime property attached above by `attachBrand`.
|
// runtime property attached above by `attachBrand`.
|
||||||
return wrapped as Instrumented<(...args: Args) => Promise<R>>;
|
return wrapped as Instrumented<(...args: Args) => Promise<R>>;
|
||||||
|
|||||||
Reference in New Issue
Block a user