feat(core-shared/conformance): ProductionUseCase<I, O, M> branded slot type
This commit is contained in:
@@ -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<I, O, M>", () => {
|
||||||
|
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<Wrapped>().toMatchTypeOf<Slot>();
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
});
|
||||||
24
packages/core-shared/src/conformance/production-use-case.ts
Normal file
24
packages/core-shared/src/conformance/production-use-case.ts
Normal file
@@ -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<I, O, AuthManifest["useCases"]["signIn"]>`.
|
||||||
|
*
|
||||||
|
* 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<F>` from core-audit satisfies it.
|
||||||
|
*/
|
||||||
|
export type ProductionUseCase<I, O, M extends UseCaseManifest> =
|
||||||
|
& Instrumented<(input: I) => Promise<O>>
|
||||||
|
& Captured<(input: I) => Promise<O>>
|
||||||
|
& (M["mutates"] extends true
|
||||||
|
? M["audits"]["length"] extends 0
|
||||||
|
? unknown
|
||||||
|
: { readonly __audited: true }
|
||||||
|
: unknown);
|
||||||
Reference in New Issue
Block a user