Mirrors the existing Audited check: when a use case declares analyticsEvents.length > 0 and the bound function lacks the __analyzed brand, assertFeatureConformance throws ConformanceError at boot time. Adds three synthetic conformance tests: passes when brand present + events declared, throws naming Analyzed when events declared + brand missing, passes when events empty + brand absent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
75 lines
2.6 KiB
TypeScript
75 lines
2.6 KiB
TypeScript
import type { Container } from "inversify";
|
|
import type { FeatureManifest } from "./define-feature";
|
|
import {
|
|
isInstrumented,
|
|
isCaptured,
|
|
isAudited,
|
|
isAnalyzed,
|
|
} 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?`,
|
|
);
|
|
}
|
|
}
|
|
if ((useCase.analyticsEvents?.length ?? 0) > 0) {
|
|
if (!isAnalyzed(bound)) {
|
|
throw new ConformanceError(
|
|
`${manifest.name}.${name}: declares analyticsEvents but binding is missing __analyzed brand — was withAnalytics applied at bind time?`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|