feat(core-shared): add Analyzed brand check to assertFeatureConformance
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>
This commit is contained in:
@@ -6,6 +6,7 @@ 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 { attachBrand } from "@/conformance/brand-runtime";
|
||||
import type { ITracer, ISpan } from "@/instrumentation/tracer.interface";
|
||||
import type { ILogger } from "@/instrumentation/logger.interface";
|
||||
import type { BindContext } from "@/di/bind-context";
|
||||
@@ -40,7 +41,11 @@ describe("assertFeatureConformance", () => {
|
||||
const bound = withSpan(
|
||||
ctx.tracer,
|
||||
{ name: "test.signIn", op: "use-case" },
|
||||
withCapture(ctx.logger, { feature: "test", layer: "use-case" }, async (x: number) => x + 1),
|
||||
withCapture(
|
||||
ctx.logger,
|
||||
{ feature: "test", layer: "use-case" },
|
||||
async (x: number) => x + 1,
|
||||
),
|
||||
);
|
||||
container.bind(sym).toConstantValue(bound);
|
||||
|
||||
@@ -185,9 +190,9 @@ describe("assertFeatureConformance", () => {
|
||||
realtimeChannels: [],
|
||||
jobs: [],
|
||||
} as const);
|
||||
expect(() => assertFeatureConformance(container, manifest, {}, ctx)).toThrow(
|
||||
/no symbol provided/,
|
||||
);
|
||||
expect(() =>
|
||||
assertFeatureConformance(container, manifest, {}, ctx),
|
||||
).toThrow(/no symbol provided/);
|
||||
});
|
||||
|
||||
it("throws when the container cannot resolve the symbol", () => {
|
||||
@@ -207,4 +212,112 @@ describe("assertFeatureConformance", () => {
|
||||
assertFeatureConformance(container, manifest, { signIn: sym }, ctx),
|
||||
).toThrow(ConformanceError);
|
||||
});
|
||||
|
||||
it("passes when analyticsEvents declared and binding carries __analyzed brand", () => {
|
||||
const container = new Container();
|
||||
const sym = Symbol("test.trackClick");
|
||||
const ctx = makeCtx();
|
||||
const base = withSpan(
|
||||
ctx.tracer,
|
||||
{ name: "test.trackClick", op: "use-case" },
|
||||
withCapture(
|
||||
ctx.logger,
|
||||
{ feature: "test", layer: "use-case" },
|
||||
async (x: number) => x,
|
||||
),
|
||||
);
|
||||
attachBrand(base, "__analyzed");
|
||||
container.bind(sym).toConstantValue(base);
|
||||
|
||||
const manifest = defineFeature({
|
||||
name: "test",
|
||||
requiredCores: [],
|
||||
useCases: {
|
||||
trackClick: {
|
||||
mutates: false,
|
||||
audits: [],
|
||||
publishes: [],
|
||||
consumes: [],
|
||||
analyticsEvents: ["button.clicked"],
|
||||
},
|
||||
},
|
||||
realtimeChannels: [],
|
||||
jobs: [],
|
||||
} as const);
|
||||
|
||||
expect(() =>
|
||||
assertFeatureConformance(container, manifest, { trackClick: sym }, ctx),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it("throws when analyticsEvents declared but binding is missing __analyzed brand", () => {
|
||||
const container = new Container();
|
||||
const sym = Symbol("test.trackClick");
|
||||
const ctx = makeCtx();
|
||||
const wrappedNoAnalytics = withSpan(
|
||||
ctx.tracer,
|
||||
{ name: "test.trackClick", op: "use-case" },
|
||||
withCapture(ctx.logger, { feature: "test" }, async (x: number) => x),
|
||||
);
|
||||
container.bind(sym).toConstantValue(wrappedNoAnalytics);
|
||||
|
||||
const manifest = defineFeature({
|
||||
name: "test",
|
||||
requiredCores: [],
|
||||
useCases: {
|
||||
trackClick: {
|
||||
mutates: false,
|
||||
audits: [],
|
||||
publishes: [],
|
||||
consumes: [],
|
||||
analyticsEvents: ["button.clicked"],
|
||||
},
|
||||
},
|
||||
realtimeChannels: [],
|
||||
jobs: [],
|
||||
} as const);
|
||||
|
||||
expect(() =>
|
||||
assertFeatureConformance(container, manifest, { trackClick: sym }, ctx),
|
||||
).toThrow(ConformanceError);
|
||||
expect(() =>
|
||||
assertFeatureConformance(container, manifest, { trackClick: sym }, ctx),
|
||||
).toThrow(/Analyzed|__analyzed/);
|
||||
});
|
||||
|
||||
it("passes when analyticsEvents is empty and __analyzed brand is absent", () => {
|
||||
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,
|
||||
),
|
||||
);
|
||||
container.bind(sym).toConstantValue(bound);
|
||||
|
||||
const manifest = defineFeature({
|
||||
name: "test",
|
||||
requiredCores: [],
|
||||
useCases: {
|
||||
signIn: {
|
||||
mutates: false,
|
||||
audits: [],
|
||||
publishes: [],
|
||||
consumes: [],
|
||||
analyticsEvents: [],
|
||||
},
|
||||
},
|
||||
realtimeChannels: [],
|
||||
jobs: [],
|
||||
} as const);
|
||||
|
||||
expect(() =>
|
||||
assertFeatureConformance(container, manifest, { signIn: sym }, ctx),
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { Container } from "inversify";
|
||||
import type { FeatureManifest } from "./define-feature";
|
||||
import { isInstrumented, isCaptured, isAudited } from "./brand-runtime";
|
||||
import {
|
||||
isInstrumented,
|
||||
isCaptured,
|
||||
isAudited,
|
||||
isAnalyzed,
|
||||
} from "./brand-runtime";
|
||||
import { ConformanceError } from "./conformance-error";
|
||||
import type { BindContext } from "../di/bind-context";
|
||||
|
||||
@@ -58,5 +63,12 @@ export function assertFeatureConformance(
|
||||
);
|
||||
}
|
||||
}
|
||||
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?`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user