From 1d8b30045abb382e89d767501b650e99b27d8334 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Tue, 12 May 2026 22:42:16 +0200 Subject: [PATCH] feat(core-audit): withAudit wraps and attaches runtime __audited marker --- packages/core-audit/src/with-audit.test.ts | 17 +++++++++++++++ packages/core-audit/src/with-audit.ts | 25 +++++++++++----------- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/packages/core-audit/src/with-audit.test.ts b/packages/core-audit/src/with-audit.test.ts index 0ab2e8b..731be28 100644 --- a/packages/core-audit/src/with-audit.test.ts +++ b/packages/core-audit/src/with-audit.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, expectTypeOf, vi } from "vitest"; import { withAudit, type Audited } from "@/with-audit"; import type { IAuditLog } from "@/audit-log.interface"; +import { isAudited } from "@repo/core-shared/conformance"; function makeAuditLog(): IAuditLog { return { @@ -17,6 +18,22 @@ describe("withAudit", () => { expectTypeOf(wrapped).toMatchTypeOf>(); }); + it("attaches __audited as a non-enumerable property on the wrapped function", () => { + const auditLog = makeAuditLog(); + const fn = async () => ({ ok: true }); + const wrapped = withAudit(auditLog, fn); + expect(isAudited(wrapped)).toBe(true); + expect(Object.keys(wrapped)).not.toContain("__audited"); + }); + + it("does NOT pollute the original input function with the brand", () => { + const auditLog = makeAuditLog(); + const fn = async () => ({ ok: true }); + const wrapped = withAudit(auditLog, fn); + expect(isAudited(fn)).toBe(false); + expect(wrapped).not.toBe(fn); + }); + it("passes input and output through unchanged", async () => { const auditLog = makeAuditLog(); const fn = async (input: { id: string }) => ({ ok: true, id: input.id }); diff --git a/packages/core-audit/src/with-audit.ts b/packages/core-audit/src/with-audit.ts index 93a0e07..89d0af7 100644 --- a/packages/core-audit/src/with-audit.ts +++ b/packages/core-audit/src/with-audit.ts @@ -1,25 +1,24 @@ import type { IAuditLog } from "./audit-log.interface"; +import { attachBrand } from "@repo/core-shared/conformance"; /** * Phantom-type brand attached at wrap time by `withAudit`. The conformance * system uses this as the type-level seam for mutating use cases that * declare `audits: [...]` in their manifest — without `__audited`, the * binding is not assignable to `ProductionUseCase` when M demands - * it. + * it. At runtime the brand is a non-enumerable property attached by + * `attachBrand` from `@repo/core-shared/conformance`, so the boot-time + * assertion can verify the binding went through the audit-aware path. */ export type Audited = F & { readonly __audited: true }; /** - * Use-case wrapper applied at DI bind time. In milestone i this is a - * brand-only attachment: it does not yet automatically call `auditLog.record`. - * Use cases continue to call `auditLog.record(...)` in their own bodies; the - * wrapper exists to make "binding was bound through the audit-aware path" - * type-checkable at compile time. - * - * A future story may move auditing logic out of factory bodies and into the - * wrapper itself (driven by manifest declarations) — but that requires the - * manifest's `audits[]` entries to fully specify what gets recorded, which - * is out of scope here. + * Use-case wrapper applied at DI bind time. The wrapper is a thin closure + * that forwards to `fn` unchanged and carries the `__audited` brand. The + * forward closure (instead of returning `fn` directly) keeps the brand on + * a fresh function so the caller's original `fn` is not mutated — important + * when the same factory output is used elsewhere unwrapped (dev-seed paths, + * tests). */ export function withAudit( // TODO(conformance milestone iii+): wire automated recording from manifest @@ -31,5 +30,7 @@ export function withAudit( fn: (...args: Args) => Promise, ): Audited<(...args: Args) => Promise> { void auditLog; - return fn as Audited<(...args: Args) => Promise>; + const wrapped: (...args: Args) => Promise = (...args) => fn(...args); + attachBrand(wrapped, "__audited"); + return wrapped as Audited<(...args: Args) => Promise>; }