feat(core-shared): extend wireUseCase with analytics arg and Analyzed brand propagation
Add AnalyticsProtocol to bind-protocols, extend WireUseCaseOptions with optional analytics field, and compose the __analyzed brand inline in wireUseCase (innermost, before withAudit) when analytics is provided. Propagate __analyzed through withCapture and withSpan PROPAGATED_BRANDS so the outermost container binding carries the brand for boot-time assertion checks (Story 05). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
|||||||
isInstrumented,
|
isInstrumented,
|
||||||
isCaptured,
|
isCaptured,
|
||||||
isAudited,
|
isAudited,
|
||||||
|
isAnalyzed,
|
||||||
} from "@/conformance/brand-runtime";
|
} from "@/conformance/brand-runtime";
|
||||||
import type {
|
import type {
|
||||||
ITracer,
|
ITracer,
|
||||||
@@ -13,7 +14,7 @@ import type {
|
|||||||
SpanOpts,
|
SpanOpts,
|
||||||
} from "@/instrumentation/tracer.interface";
|
} from "@/instrumentation/tracer.interface";
|
||||||
import type { ILogger } from "@/instrumentation/logger.interface";
|
import type { ILogger } from "@/instrumentation/logger.interface";
|
||||||
import type { AuditLogProtocol } from "@/di/bind-protocols";
|
import type { AuditLogProtocol, AnalyticsProtocol } from "@/di/bind-protocols";
|
||||||
|
|
||||||
function makeTracer() {
|
function makeTracer() {
|
||||||
const calls: SpanOpts[] = [];
|
const calls: SpanOpts[] = [];
|
||||||
@@ -42,6 +43,10 @@ function makeAuditLog(): AuditLogProtocol {
|
|||||||
return { record: vi.fn() };
|
return { record: vi.fn() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeAnalytics(): AnalyticsProtocol {
|
||||||
|
return { track: vi.fn() };
|
||||||
|
}
|
||||||
|
|
||||||
const doubleFactory = () => async (x: number) => x * 2;
|
const doubleFactory = () => async (x: number) => x * 2;
|
||||||
|
|
||||||
describe("wireUseCase — no-audit path", () => {
|
describe("wireUseCase — no-audit path", () => {
|
||||||
@@ -241,6 +246,107 @@ describe("wireUseCase — container binding", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("wireUseCase — analytics path", () => {
|
||||||
|
it("attaches Analyzed brand when analytics is provided", () => {
|
||||||
|
const { tracer } = makeTracer();
|
||||||
|
const logger = makeLogger();
|
||||||
|
const container = new Container();
|
||||||
|
const sym = Symbol("test.double");
|
||||||
|
const analytics = makeAnalytics();
|
||||||
|
|
||||||
|
const wired = wireUseCase({
|
||||||
|
container,
|
||||||
|
symbol: sym,
|
||||||
|
factory: doubleFactory,
|
||||||
|
deps: [],
|
||||||
|
feature: "test",
|
||||||
|
layer: "use-case",
|
||||||
|
name: "double",
|
||||||
|
tracer,
|
||||||
|
logger,
|
||||||
|
analytics,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(isAnalyzed(wired)).toBe(true);
|
||||||
|
expect(isInstrumented(wired)).toBe(true);
|
||||||
|
expect(isCaptured(wired)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("attaches Analyzed + Audited brands when both analytics and auditLog are provided", () => {
|
||||||
|
const { tracer } = makeTracer();
|
||||||
|
const logger = makeLogger();
|
||||||
|
const container = new Container();
|
||||||
|
const sym = Symbol("test.double");
|
||||||
|
const analytics = makeAnalytics();
|
||||||
|
const auditLog = makeAuditLog();
|
||||||
|
|
||||||
|
const wired = wireUseCase({
|
||||||
|
container,
|
||||||
|
symbol: sym,
|
||||||
|
factory: doubleFactory,
|
||||||
|
deps: [],
|
||||||
|
feature: "test",
|
||||||
|
layer: "use-case",
|
||||||
|
name: "double",
|
||||||
|
tracer,
|
||||||
|
logger,
|
||||||
|
analytics,
|
||||||
|
auditLog,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(isAnalyzed(wired)).toBe(true);
|
||||||
|
expect(isAudited(wired)).toBe(true);
|
||||||
|
expect(isInstrumented(wired)).toBe(true);
|
||||||
|
expect(isCaptured(wired)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("executes the factory result on invocation (analytics path)", async () => {
|
||||||
|
const { tracer } = makeTracer();
|
||||||
|
const logger = makeLogger();
|
||||||
|
const container = new Container();
|
||||||
|
const sym = Symbol("test.double");
|
||||||
|
const analytics = makeAnalytics();
|
||||||
|
|
||||||
|
const wired = wireUseCase({
|
||||||
|
container,
|
||||||
|
symbol: sym,
|
||||||
|
factory: doubleFactory,
|
||||||
|
deps: [],
|
||||||
|
feature: "test",
|
||||||
|
layer: "use-case",
|
||||||
|
name: "double",
|
||||||
|
tracer,
|
||||||
|
logger,
|
||||||
|
analytics,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(wired(7)).resolves.toBe(14);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("wireUseCase — no-analytics path", () => {
|
||||||
|
it("does not attach Analyzed brand when analytics is absent", () => {
|
||||||
|
const { tracer } = makeTracer();
|
||||||
|
const logger = makeLogger();
|
||||||
|
const container = new Container();
|
||||||
|
const sym = Symbol("test.double");
|
||||||
|
|
||||||
|
const wired = wireUseCase({
|
||||||
|
container,
|
||||||
|
symbol: sym,
|
||||||
|
factory: doubleFactory,
|
||||||
|
deps: [],
|
||||||
|
feature: "test",
|
||||||
|
layer: "use-case",
|
||||||
|
name: "double",
|
||||||
|
tracer,
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(isAnalyzed(wired)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("wireUseCase — idempotent re-bind", () => {
|
describe("wireUseCase — idempotent re-bind", () => {
|
||||||
it("unbinds the existing binding and replaces it when the symbol is already bound", () => {
|
it("unbinds the existing binding and replaces it when the symbol is already bound", () => {
|
||||||
const { tracer } = makeTracer();
|
const { tracer } = makeTracer();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Container } from "inversify";
|
import type { Container } from "inversify";
|
||||||
import type { ITracer } from "../instrumentation/tracer.interface";
|
import type { ITracer } from "../instrumentation/tracer.interface";
|
||||||
import type { ILogger } from "../instrumentation/logger.interface";
|
import type { ILogger } from "../instrumentation/logger.interface";
|
||||||
import type { AuditLogProtocol } from "../di/bind-protocols";
|
import type { AuditLogProtocol, AnalyticsProtocol } from "../di/bind-protocols";
|
||||||
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 "./brand-runtime";
|
import { attachBrand } from "./brand-runtime";
|
||||||
@@ -20,6 +20,7 @@ export type WireUseCaseOptions<
|
|||||||
name: string;
|
name: string;
|
||||||
tracer: ITracer;
|
tracer: ITracer;
|
||||||
logger: ILogger;
|
logger: ILogger;
|
||||||
|
analytics?: AnalyticsProtocol;
|
||||||
auditLog?: AuditLogProtocol;
|
auditLog?: AuditLogProtocol;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -50,6 +51,7 @@ export function wireUseCase<
|
|||||||
name,
|
name,
|
||||||
tracer,
|
tracer,
|
||||||
logger,
|
logger,
|
||||||
|
analytics,
|
||||||
auditLog,
|
auditLog,
|
||||||
} = opts;
|
} = opts;
|
||||||
|
|
||||||
@@ -59,14 +61,30 @@ export function wireUseCase<
|
|||||||
const raw = factory(...deps);
|
const raw = factory(...deps);
|
||||||
|
|
||||||
let toWrap: (...args: FnArgs) => Promise<R>;
|
let toWrap: (...args: FnArgs) => Promise<R>;
|
||||||
if (auditLog !== undefined) {
|
// analytics is innermost — wraps raw before audit. withAnalytics lives in
|
||||||
void auditLog; // reserved for future automated audit recording from manifest declarations
|
// @repo/core-analytics which depends on core-shared (not vice versa), so we
|
||||||
const audited: (...args: FnArgs) => Promise<R> = (...args) => raw(...args);
|
// replicate the forwarding-wrapper semantics inline to avoid a circular dep.
|
||||||
attachBrand(audited, "__audited");
|
if (analytics !== undefined) {
|
||||||
toWrap = audited;
|
void analytics; // reserved for future automated event recording from manifest declarations
|
||||||
|
const analyzed: (...args: FnArgs) => Promise<R> = (...args) => raw(...args);
|
||||||
|
attachBrand(analyzed, "__analyzed");
|
||||||
|
toWrap = analyzed;
|
||||||
} else {
|
} else {
|
||||||
toWrap = raw;
|
toWrap = raw;
|
||||||
}
|
}
|
||||||
|
if (auditLog !== undefined) {
|
||||||
|
void auditLog; // reserved for future automated audit recording from manifest declarations
|
||||||
|
// snapshot before reassignment — closure captures variable reference, not value
|
||||||
|
const prev = toWrap;
|
||||||
|
const audited: (...args: FnArgs) => Promise<R> = (...args) => prev(...args);
|
||||||
|
attachBrand(audited, "__audited");
|
||||||
|
// propagate __analyzed from the analytics layer below so withCapture/withSpan
|
||||||
|
// can see it on the outermost binding
|
||||||
|
if ((prev as unknown as Record<string, unknown>)["__analyzed"] === true) {
|
||||||
|
attachBrand(audited, "__analyzed");
|
||||||
|
}
|
||||||
|
toWrap = audited;
|
||||||
|
}
|
||||||
|
|
||||||
const wired = withSpan(
|
const wired = withSpan(
|
||||||
tracer,
|
tracer,
|
||||||
|
|||||||
@@ -68,3 +68,15 @@ export type MetricsProtocol = {
|
|||||||
export type AuditLogProtocol = {
|
export type AuditLogProtocol = {
|
||||||
record(entry: AuditEntry): Promise<void>;
|
record(entry: AuditEntry): Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal analytics protocol surface. `IAnalytics` (in `@repo/core-analytics`)
|
||||||
|
* extends this — typechecks fail if narrowed below. Feature binders that
|
||||||
|
* receive `ctx.analytics` see only this protocol type.
|
||||||
|
*/
|
||||||
|
export type AnalyticsProtocol = {
|
||||||
|
track(
|
||||||
|
event: string,
|
||||||
|
attributes?: Record<string, string | number | boolean>,
|
||||||
|
): void;
|
||||||
|
};
|
||||||
|
|||||||
@@ -28,7 +28,11 @@ export function withCapture<Args extends unknown[], R>(
|
|||||||
tags: Record<string, string>,
|
tags: Record<string, string>,
|
||||||
fn: (...args: Args) => Promise<R>,
|
fn: (...args: Args) => Promise<R>,
|
||||||
): Captured<(...args: Args) => Promise<R>> {
|
): Captured<(...args: Args) => Promise<R>> {
|
||||||
const PROPAGATED_BRANDS = ["__instrumented", "__audited"] as const;
|
const PROPAGATED_BRANDS = [
|
||||||
|
"__instrumented",
|
||||||
|
"__audited",
|
||||||
|
"__analyzed",
|
||||||
|
] as const;
|
||||||
|
|
||||||
const wrapped: (...args: Args) => Promise<R> = async (...args) => {
|
const wrapped: (...args: Args) => Promise<R> = async (...args) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ 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;
|
const PROPAGATED_BRANDS = ["__captured", "__audited", "__analyzed"] 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,
|
||||||
|
|||||||
Reference in New Issue
Block a user