diff --git a/packages/core-shared/src/conformance/production-use-case.test.ts b/packages/core-shared/src/conformance/production-use-case.test.ts new file mode 100644 index 0000000..cd482b6 --- /dev/null +++ b/packages/core-shared/src/conformance/production-use-case.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expectTypeOf } from "vitest"; +import type { ProductionUseCase } from "@/conformance/production-use-case"; +import type { Instrumented, Captured } from "@/conformance/brands"; + +describe("ProductionUseCase", () => { + it("requires Instrumented + Captured for any use case", () => { + type Manifest = { + mutates: false; + audits: readonly []; + publishes: readonly []; + consumes: readonly []; + }; + type Slot = ProductionUseCase<{ x: number }, { y: string }, Manifest>; + type Wrapped = Instrumented<(input: { x: number }) => Promise<{ y: string }>> & + Captured<(input: { x: number }) => Promise<{ y: string }>>; + + expectTypeOf().toMatchTypeOf(); + }); + + it("a plain factory is NOT assignable to the slot", () => { + type Manifest = { + mutates: false; + audits: readonly []; + publishes: readonly []; + consumes: readonly []; + }; + type Slot = ProductionUseCase<{ x: number }, { y: string }, Manifest>; + const factory = async (input: { x: number }) => ({ y: String(input.x) }); + + // @ts-expect-error — factory has no __instrumented / __captured brand + const bad: Slot = factory; + void bad; + }); +}); diff --git a/packages/core-shared/src/conformance/production-use-case.ts b/packages/core-shared/src/conformance/production-use-case.ts new file mode 100644 index 0000000..26079f5 --- /dev/null +++ b/packages/core-shared/src/conformance/production-use-case.ts @@ -0,0 +1,24 @@ +import type { UseCaseManifest } from "./define-feature"; +import type { Instrumented, Captured } from "./brands"; + +/** + * Type-level binding slot for production use cases. Derived from the manifest + * entry: every binding must be Instrumented + Captured; mutating use cases + * that declare audits additionally must be Audited. The Audited brand lives + * in `@repo/core-audit` because the wrap helper that attaches it depends on + * `IAuditLog` — feature packages import the merged slot type implicitly + * by typing their bindings as `ProductionUseCase`. + * + * The Audited requirement is encoded conditionally so this type stays usable + * without depending on core-audit. When `mutates: true` AND `audits` is + * non-empty, the slot demands a marker type with a `__audited` flag; the + * concrete `Audited` from core-audit satisfies it. + */ +export type ProductionUseCase = + & Instrumented<(input: I) => Promise> + & Captured<(input: I) => Promise> + & (M["mutates"] extends true + ? M["audits"]["length"] extends 0 + ? unknown + : { readonly __audited: true } + : unknown);