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:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"generatedAt": "2026-05-18T11:50:20.112Z",
|
"generatedAt": "2026-05-18T15:19:07.787Z",
|
||||||
"commit": "a5ab698",
|
"commit": "81f75f7",
|
||||||
"repo": {
|
"repo": {
|
||||||
"statements": 95.89,
|
"statements": 95.89,
|
||||||
"branches": 89.07,
|
"branches": 89.07,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { ConformanceError } from "@/conformance/conformance-error";
|
|||||||
import { assertFeatureConformance } from "@/conformance/assert-bindings";
|
import { assertFeatureConformance } from "@/conformance/assert-bindings";
|
||||||
import { withSpan } from "@/instrumentation/with-span";
|
import { withSpan } from "@/instrumentation/with-span";
|
||||||
import { withCapture } from "@/instrumentation/with-capture";
|
import { withCapture } from "@/instrumentation/with-capture";
|
||||||
|
import { attachBrand } from "@/conformance/brand-runtime";
|
||||||
import type { ITracer, ISpan } from "@/instrumentation/tracer.interface";
|
import type { ITracer, ISpan } from "@/instrumentation/tracer.interface";
|
||||||
import type { ILogger } from "@/instrumentation/logger.interface";
|
import type { ILogger } from "@/instrumentation/logger.interface";
|
||||||
import type { BindContext } from "@/di/bind-context";
|
import type { BindContext } from "@/di/bind-context";
|
||||||
@@ -40,7 +41,11 @@ describe("assertFeatureConformance", () => {
|
|||||||
const bound = withSpan(
|
const bound = withSpan(
|
||||||
ctx.tracer,
|
ctx.tracer,
|
||||||
{ name: "test.signIn", op: "use-case" },
|
{ 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);
|
container.bind(sym).toConstantValue(bound);
|
||||||
|
|
||||||
@@ -185,9 +190,9 @@ describe("assertFeatureConformance", () => {
|
|||||||
realtimeChannels: [],
|
realtimeChannels: [],
|
||||||
jobs: [],
|
jobs: [],
|
||||||
} as const);
|
} as const);
|
||||||
expect(() => assertFeatureConformance(container, manifest, {}, ctx)).toThrow(
|
expect(() =>
|
||||||
/no symbol provided/,
|
assertFeatureConformance(container, manifest, {}, ctx),
|
||||||
);
|
).toThrow(/no symbol provided/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws when the container cannot resolve the symbol", () => {
|
it("throws when the container cannot resolve the symbol", () => {
|
||||||
@@ -207,4 +212,112 @@ describe("assertFeatureConformance", () => {
|
|||||||
assertFeatureConformance(container, manifest, { signIn: sym }, ctx),
|
assertFeatureConformance(container, manifest, { signIn: sym }, ctx),
|
||||||
).toThrow(ConformanceError);
|
).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 { Container } from "inversify";
|
||||||
import type { FeatureManifest } from "./define-feature";
|
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 { ConformanceError } from "./conformance-error";
|
||||||
import type { BindContext } from "../di/bind-context";
|
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