diff --git a/packages/core-shared/src/conformance/define-feature.test.ts b/packages/core-shared/src/conformance/define-feature.test.ts new file mode 100644 index 0000000..3510c41 --- /dev/null +++ b/packages/core-shared/src/conformance/define-feature.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expectTypeOf } from "vitest"; +import { defineFeature, type FeatureManifest } from "@/conformance/define-feature"; + +describe("defineFeature", () => { + it("preserves literal types of a manifest declared with `as const`", () => { + const manifest = defineFeature({ + name: "auth", + requiredCores: ["audit"], + useCases: { + signIn: { + mutates: false, + audits: [], + publishes: [], + consumes: [], + }, + signUp: { + mutates: true, + audits: ["user.created"], + publishes: ["auth.signed-up"], + consumes: [], + }, + }, + realtimeChannels: [], + jobs: [], + } as const); + + // Literal preservation: name is the literal "auth", not string + expectTypeOf(manifest.name).toEqualTypeOf<"auth">(); + // Use-case keys preserved + expectTypeOf(manifest.useCases.signUp.audits).toEqualTypeOf(); + expectTypeOf(manifest.useCases.signUp.mutates).toEqualTypeOf(); + expectTypeOf(manifest.useCases.signIn.mutates).toEqualTypeOf(); + }); + + it("FeatureManifest type accepts the shape", () => { + const manifest = defineFeature({ + name: "blog", + requiredCores: [], + useCases: {}, + realtimeChannels: [], + jobs: [], + } as const); + expectTypeOf(manifest).toMatchTypeOf(); + }); +}); diff --git a/packages/core-shared/src/conformance/define-feature.ts b/packages/core-shared/src/conformance/define-feature.ts new file mode 100644 index 0000000..91ee653 --- /dev/null +++ b/packages/core-shared/src/conformance/define-feature.ts @@ -0,0 +1,38 @@ +/** + * Per-use-case manifest entry. Declares what the use case does at the contract + * level: whether it mutates state, what audit events it emits, what cross-feature + * events it publishes or consumes. The conformance system reads these to + * derive binding-slot types and to verify code against manifest declarations. + */ +export type UseCaseManifest = { + readonly mutates: boolean; + readonly audits: readonly string[]; + readonly publishes: readonly string[]; + readonly consumes: readonly string[]; +}; + +/** + * The feature-level manifest. One per feature package, conventionally exported + * as `Manifest` from `src/feature.manifest.ts`. + */ +export type FeatureManifest = { + readonly name: string; + readonly requiredCores: readonly string[]; + readonly useCases: { readonly [k: string]: UseCaseManifest }; + readonly realtimeChannels: readonly string[]; + readonly jobs: readonly string[]; +}; + +/** + * Identity helper that exists purely to widen the input type to satisfy + * `FeatureManifest` while preserving the literal types of the `as const` + * input. Downstream types (`ProductionUseCase`) consume the + * preserved literals to derive binding-slot brand requirements. + * + * Usage: + * + * export const authManifest = defineFeature({ name: "auth", ... } as const); + */ +export function defineFeature(manifest: M): M { + return manifest; +}