feat(core-shared/conformance): manifest coverage schema + vitest helper

First implementation milestone of the agent-first coverage architecture
(ADR-020, PRD 2026-05-13). Lands the keystone — coverage bands as a
typed declaration in feature.manifest.ts plus a helper that derives
vitest threshold shapes from them.

New file packages/core-shared/src/conformance/coverage.ts (self-
contained, no relative imports — loadable at vitest config time):
- CoverageBand / CoverageBands / CoverageManifest / VitestThresholds
  types
- DEFAULT_COVERAGE_BANDS (baseline 80/75/80/80; entities 100/100/100/
  100; use-cases + controllers 100/95/100/100) — matches ADR-011
- DEFAULT_MUTATION_SCORE (80) + DEFAULT_MUTATION_TARGETS (entities +
  use-cases)
- getCoverageBands / getMutationConfig — manifest -> resolved bands,
  with default fallback for missing layers
- vitestThresholdsFromBands / vitestThresholdsFromManifest — convert
  to vitest's coverage.thresholds shape with the layer-to-glob
  mapping

define-feature.ts gains the optional coverage field on FeatureManifest
(imports its type from coverage.ts to avoid a relative-import cycle
at config-load time).

Exposed via two subpaths: @repo/core-shared/conformance (re-exports
for source/test code) and @repo/core-shared/conformance/coverage
(direct subpath safe to load from vitest configs, bypasses the index
re-export chain that Node ESM doesn't auto-extension-resolve).

Auth wired as proof-of-concept:
- packages/auth/src/feature.manifest.ts declares its coverage section
- packages/auth/vitest.config.ts imports the helper +
  DEFAULT_COVERAGE_BANDS and emits thresholds via
  vitestThresholdsFromBands(DEFAULT_COVERAGE_BANDS) — no more
  hand-maintained per-glob thresholds block.

Verified: 175/175 tests pass; 14/14 typechecks clean; auth coverage
green (21 tests, 93.77% overall, all per-layer 100% bands hold).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 13:51:13 +02:00
parent 4dce1df084
commit f7baa8bfd1
7 changed files with 458 additions and 29 deletions

View File

@@ -37,6 +37,25 @@ export const authManifest = defineFeature({
}, },
realtimeChannels: [], realtimeChannels: [],
jobs: [], jobs: [],
coverage: {
bands: {
baseline: { statements: 80, branches: 75, functions: 80, lines: 80 },
entities: { statements: 100, branches: 100, functions: 100, lines: 100 },
"use-cases": {
statements: 100,
branches: 95,
functions: 100,
lines: 100,
},
controllers: {
statements: 100,
branches: 95,
functions: 100,
lines: 100,
},
},
mutationTargets: ["entities", "use-cases"],
},
} as const); } as const);
export type AuthManifest = typeof authManifest; export type AuthManifest = typeof authManifest;

View File

@@ -1,7 +1,18 @@
import path from "node:path"; import path from "node:path";
import { mergeConfig } from "vitest/config"; import { mergeConfig } from "vitest/config";
import { nodeVitestConfig } from "@repo/core-typescript/vitest.base.node"; import { nodeVitestConfig } from "@repo/core-typescript/vitest.base.node";
import {
DEFAULT_COVERAGE_BANDS,
vitestThresholdsFromBands,
} from "@repo/core-shared/conformance/coverage";
// Coverage thresholds derived from DEFAULT_COVERAGE_BANDS via the shared
// helper — one source of truth for the conventional band shape across all
// features (ADR-020). The feature.manifest.ts `coverage.bands` section also
// declares these for boot-time `assertFeatureConformance` (which reads the
// manifest directly, not the vitest config). For features that need
// non-default bands, override here AND in the manifest, then add a drift
// test in core-shared/conformance/.
export default mergeConfig(nodeVitestConfig, { export default mergeConfig(nodeVitestConfig, {
test: { test: {
coverage: { coverage: {
@@ -18,30 +29,7 @@ export default mergeConfig(nodeVitestConfig, {
// React Query option builders — integration-tested in apps // React Query option builders — integration-tested in apps
"src/ui/**", "src/ui/**",
], ],
thresholds: { thresholds: vitestThresholdsFromBands(DEFAULT_COVERAGE_BANDS),
"src/entities/**": {
statements: 100,
branches: 100,
functions: 100,
lines: 100,
},
"src/application/use-cases/**": {
statements: 100,
branches: 95,
functions: 100,
lines: 100,
},
"src/interface-adapters/controllers/**": {
statements: 100,
branches: 95,
functions: 100,
lines: 100,
},
statements: 80,
branches: 75,
functions: 80,
lines: 80,
},
}, },
}, },
resolve: { resolve: {

View File

@@ -7,6 +7,7 @@
".": "./src/index.ts", ".": "./src/index.ts",
"./audit": "./src/audit/index.ts", "./audit": "./src/audit/index.ts",
"./conformance": "./src/conformance/index.ts", "./conformance": "./src/conformance/index.ts",
"./conformance/coverage": "./src/conformance/coverage.ts",
"./di": "./src/di/index.ts", "./di": "./src/di/index.ts",
"./di/bind-protocols": "./src/di/bind-protocols.ts", "./di/bind-protocols": "./src/di/bind-protocols.ts",
"./di/bind-context": "./src/di/bind-context.ts", "./di/bind-context": "./src/di/bind-context.ts",
@@ -52,8 +53,12 @@
"@sentry/react": "^10.51.0" "@sentry/react": "^10.51.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@sentry/node": { "optional": true }, "@sentry/node": {
"@sentry/react": { "optional": true } "optional": true
},
"@sentry/react": {
"optional": true
}
}, },
"devDependencies": { "devDependencies": {
"@opentelemetry/context-async-hooks": "^1.28.0", "@opentelemetry/context-async-hooks": "^1.28.0",

View File

@@ -0,0 +1,171 @@
/**
* Coverage primitives — types, defaults, and the vitest-threshold helper.
*
* This file is intentionally **self-contained** (no relative imports) so it
* can be loaded at vitest config time, where the Node ESM resolver doesn't
* auto-resolve `.ts` extensions on relative imports. Other files in
* `conformance/` (including `define-feature.ts`) import their coverage types
* from here, not the reverse. See ADR-020.
*/
/**
* A coverage threshold band for a specific path or layer. All four percentages
* are required; partial bands are not supported (matches vitest's v8 coverage
* threshold shape exactly).
*/
export type CoverageBand = {
readonly statements: number;
readonly branches: number;
readonly functions: number;
readonly lines: number;
};
/**
* The named coverage bands a feature declares. Keys are layer aliases
* resolved to glob patterns under `src/`; `baseline` is the fallthrough
* applied to everything not matched by a layer band.
*
* Layer aliases (relative to `packages/<feature>/src/`):
* - "entities" -> entities/**
* - "use-cases" -> application/use-cases/**
* - "controllers" -> interface-adapters/controllers/**
*
* `baseline` is REQUIRED. Layer bands are OPTIONAL.
*/
export type CoverageBands = {
readonly baseline: CoverageBand;
readonly entities?: CoverageBand;
readonly "use-cases"?: CoverageBand;
readonly controllers?: CoverageBand;
};
/**
* The coverage section of a feature manifest.
*/
export type CoverageManifest = {
readonly bands: CoverageBands;
readonly mutationTargets?: readonly ("entities" | "use-cases")[];
readonly mutationScore?: number;
};
/**
* The defaults applied when a manifest omits the `coverage` section.
* Matches the historical per-feature vitest.config.ts thresholds documented
* in ADR-011 (TDD foundation).
*/
export const DEFAULT_COVERAGE_BANDS: CoverageBands = {
baseline: { statements: 80, branches: 75, functions: 80, lines: 80 },
entities: { statements: 100, branches: 100, functions: 100, lines: 100 },
"use-cases": { statements: 100, branches: 95, functions: 100, lines: 100 },
controllers: { statements: 100, branches: 95, functions: 100, lines: 100 },
};
export const DEFAULT_MUTATION_SCORE = 80;
export const DEFAULT_MUTATION_TARGETS = ["entities", "use-cases"] as const;
/**
* Maps a CoverageBands layer alias to the glob pattern vitest matches files
* against. Per ADR-020.
*/
const LAYER_GLOBS = {
entities: "src/entities/**",
"use-cases": "src/application/use-cases/**",
controllers: "src/interface-adapters/controllers/**",
} as const;
type ThresholdNumbers = {
statements: number;
branches: number;
functions: number;
lines: number;
};
/**
* The shape vitest's `coverage.thresholds` config accepts: a baseline plus
* per-glob overrides.
*/
export type VitestThresholds = ThresholdNumbers & {
[glob: string]: number | ThresholdNumbers;
};
/**
* The minimum manifest shape this file consumes. Generic constraint so
* callers can pass any object satisfying the structural minimum (e.g. a
* full `FeatureManifest` with extra properties).
*/
type ManifestWithCoverage = {
readonly coverage?: CoverageManifest;
};
/**
* Resolve a manifest's effective coverage bands, filling in DEFAULT_* for
* missing layers. Always go through this helper rather than reading
* `manifest.coverage` directly.
*/
export function getCoverageBands<M extends ManifestWithCoverage>(
manifest: M,
): CoverageBands {
const declared = manifest.coverage?.bands;
if (!declared) return DEFAULT_COVERAGE_BANDS;
return {
baseline: declared.baseline,
entities: declared.entities ?? DEFAULT_COVERAGE_BANDS.entities,
"use-cases": declared["use-cases"] ?? DEFAULT_COVERAGE_BANDS["use-cases"],
controllers: declared.controllers ?? DEFAULT_COVERAGE_BANDS.controllers,
};
}
/**
* Resolve a manifest's mutation configuration, falling back to defaults.
*/
export function getMutationConfig<M extends ManifestWithCoverage>(
manifest: M,
): {
targets: readonly ("entities" | "use-cases")[];
score: number;
} {
return {
targets: manifest.coverage?.mutationTargets ?? DEFAULT_MUTATION_TARGETS,
score: manifest.coverage?.mutationScore ?? DEFAULT_MUTATION_SCORE,
};
}
/**
* Convert resolved CoverageBands into the shape vitest expects under
* `coverage.thresholds`. Layer aliases become glob keys; baseline becomes
* the top-level keys.
*/
export function vitestThresholdsFromBands(
bands: CoverageBands,
): VitestThresholds {
const result: VitestThresholds = {
statements: bands.baseline.statements,
branches: bands.baseline.branches,
functions: bands.baseline.functions,
lines: bands.baseline.lines,
};
for (const [alias, glob] of Object.entries(LAYER_GLOBS) as Array<
[keyof typeof LAYER_GLOBS, string]
>) {
const band = bands[alias];
if (band) {
result[glob] = {
statements: band.statements,
branches: band.branches,
functions: band.functions,
lines: band.lines,
};
}
}
return result;
}
/**
* Convenience wrapper: resolve a manifest's effective bands then emit the
* vitest shape. Use this from a feature's `vitest.config.ts`.
*/
export function vitestThresholdsFromManifest<M extends ManifestWithCoverage>(
manifest: M,
): VitestThresholds {
return vitestThresholdsFromBands(getCoverageBands(manifest));
}

View File

@@ -1,5 +1,18 @@
import { describe, it, expectTypeOf } from "vitest"; import { describe, it, expect, expectTypeOf } from "vitest";
import { defineFeature, type FeatureManifest } from "@/conformance/define-feature"; import {
defineFeature,
type FeatureManifest,
} from "@/conformance/define-feature";
import {
DEFAULT_COVERAGE_BANDS,
DEFAULT_MUTATION_SCORE,
DEFAULT_MUTATION_TARGETS,
getCoverageBands,
getMutationConfig,
vitestThresholdsFromBands,
vitestThresholdsFromManifest,
type CoverageBand,
} from "@/conformance/coverage";
describe("defineFeature", () => { describe("defineFeature", () => {
it("preserves literal types of a manifest declared with `as const`", () => { it("preserves literal types of a manifest declared with `as const`", () => {
@@ -27,7 +40,9 @@ describe("defineFeature", () => {
// Literal preservation: name is the literal "auth", not string // Literal preservation: name is the literal "auth", not string
expectTypeOf(manifest.name).toEqualTypeOf<"auth">(); expectTypeOf(manifest.name).toEqualTypeOf<"auth">();
// Use-case keys preserved // Use-case keys preserved
expectTypeOf(manifest.useCases.signUp.audits).toEqualTypeOf<readonly ["user.created"]>(); expectTypeOf(manifest.useCases.signUp.audits).toEqualTypeOf<
readonly ["user.created"]
>();
expectTypeOf(manifest.useCases.signUp.mutates).toEqualTypeOf<true>(); expectTypeOf(manifest.useCases.signUp.mutates).toEqualTypeOf<true>();
expectTypeOf(manifest.useCases.signIn.mutates).toEqualTypeOf<false>(); expectTypeOf(manifest.useCases.signIn.mutates).toEqualTypeOf<false>();
}); });
@@ -42,4 +57,213 @@ describe("defineFeature", () => {
} as const); } as const);
expectTypeOf(manifest).toMatchTypeOf<FeatureManifest>(); expectTypeOf(manifest).toMatchTypeOf<FeatureManifest>();
}); });
it("accepts an optional coverage section", () => {
const manifest = defineFeature({
name: "auth",
requiredCores: [],
useCases: {},
realtimeChannels: [],
jobs: [],
coverage: {
bands: {
baseline: { statements: 80, branches: 75, functions: 80, lines: 80 },
entities: {
statements: 100,
branches: 100,
functions: 100,
lines: 100,
},
},
mutationTargets: ["entities"],
mutationScore: 85,
},
} as const);
expectTypeOf(manifest.coverage).toMatchTypeOf<
| undefined
| {
readonly bands: {
readonly baseline: CoverageBand;
readonly entities?: CoverageBand;
readonly "use-cases"?: CoverageBand;
readonly controllers?: CoverageBand;
};
}
>();
expect(manifest.coverage?.mutationScore).toBe(85);
});
});
describe("getCoverageBands", () => {
const blankManifest: FeatureManifest = {
name: "test",
requiredCores: [],
useCases: {},
realtimeChannels: [],
jobs: [],
};
it("returns DEFAULT_COVERAGE_BANDS when manifest has no coverage section", () => {
expect(getCoverageBands(blankManifest)).toEqual(DEFAULT_COVERAGE_BANDS);
});
it("returns DEFAULT_COVERAGE_BANDS when coverage section is undefined", () => {
expect(getCoverageBands({ ...blankManifest, coverage: undefined })).toEqual(
DEFAULT_COVERAGE_BANDS,
);
});
it("uses declared baseline + fills missing layers from defaults", () => {
const result = getCoverageBands({
...blankManifest,
coverage: {
bands: {
baseline: { statements: 90, branches: 85, functions: 90, lines: 90 },
},
},
});
expect(result.baseline).toEqual({
statements: 90,
branches: 85,
functions: 90,
lines: 90,
});
expect(result.entities).toEqual(DEFAULT_COVERAGE_BANDS.entities);
expect(result["use-cases"]).toEqual(DEFAULT_COVERAGE_BANDS["use-cases"]);
expect(result.controllers).toEqual(DEFAULT_COVERAGE_BANDS.controllers);
});
it("uses declared layer bands when present", () => {
const customEntities: CoverageBand = {
statements: 95,
branches: 90,
functions: 95,
lines: 95,
};
const result = getCoverageBands({
...blankManifest,
coverage: {
bands: {
baseline: DEFAULT_COVERAGE_BANDS.baseline,
entities: customEntities,
},
},
});
expect(result.entities).toEqual(customEntities);
});
});
describe("getMutationConfig", () => {
const blankManifest: FeatureManifest = {
name: "test",
requiredCores: [],
useCases: {},
realtimeChannels: [],
jobs: [],
};
it("returns defaults when manifest has no coverage section", () => {
const config = getMutationConfig(blankManifest);
expect(config.targets).toEqual(DEFAULT_MUTATION_TARGETS);
expect(config.score).toBe(DEFAULT_MUTATION_SCORE);
});
it("honors declared mutationTargets + mutationScore", () => {
const config = getMutationConfig({
...blankManifest,
coverage: {
bands: { baseline: DEFAULT_COVERAGE_BANDS.baseline },
mutationTargets: ["entities"],
mutationScore: 90,
},
});
expect(config.targets).toEqual(["entities"]);
expect(config.score).toBe(90);
});
});
describe("DEFAULT_COVERAGE_BANDS", () => {
it("matches the ADR-011 / ADR-020 documented bands", () => {
expect(DEFAULT_COVERAGE_BANDS.baseline).toEqual({
statements: 80,
branches: 75,
functions: 80,
lines: 80,
});
expect(DEFAULT_COVERAGE_BANDS.entities).toEqual({
statements: 100,
branches: 100,
functions: 100,
lines: 100,
});
expect(DEFAULT_COVERAGE_BANDS["use-cases"]).toEqual({
statements: 100,
branches: 95,
functions: 100,
lines: 100,
});
expect(DEFAULT_COVERAGE_BANDS.controllers).toEqual({
statements: 100,
branches: 95,
functions: 100,
lines: 100,
});
});
});
describe("vitestThresholdsFromBands", () => {
it("emits baseline at the top level and each declared layer under its glob", () => {
const result = vitestThresholdsFromBands(DEFAULT_COVERAGE_BANDS);
expect(result.statements).toBe(80);
expect(result.branches).toBe(75);
expect(result["src/entities/**"]).toEqual({
statements: 100,
branches: 100,
functions: 100,
lines: 100,
});
expect(result["src/application/use-cases/**"]).toEqual({
statements: 100,
branches: 95,
functions: 100,
lines: 100,
});
expect(result["src/interface-adapters/controllers/**"]).toEqual({
statements: 100,
branches: 95,
functions: 100,
lines: 100,
});
});
it("omits layer glob when its band is undefined", () => {
const result = vitestThresholdsFromBands({
baseline: DEFAULT_COVERAGE_BANDS.baseline,
entities: DEFAULT_COVERAGE_BANDS.entities,
});
expect(result["src/entities/**"]).toBeDefined();
expect(result["src/application/use-cases/**"]).toBeUndefined();
expect(result["src/interface-adapters/controllers/**"]).toBeUndefined();
});
});
describe("vitestThresholdsFromManifest", () => {
it("uses DEFAULT_COVERAGE_BANDS when the manifest omits coverage", () => {
const manifest: FeatureManifest = {
name: "test",
requiredCores: [],
useCases: {},
realtimeChannels: [],
jobs: [],
};
const result = vitestThresholdsFromManifest(manifest);
expect(result.statements).toBe(80);
expect(result["src/entities/**"]).toEqual({
statements: 100,
branches: 100,
functions: 100,
lines: 100,
});
});
}); });

View File

@@ -1,3 +1,5 @@
import type { CoverageManifest } from "./coverage";
/** /**
* Per-use-case manifest entry. Declares what the use case does at the contract * 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 * level: whether it mutates state, what audit events it emits, what cross-feature
@@ -14,6 +16,10 @@ export type UseCaseManifest = {
/** /**
* The feature-level manifest. One per feature package, conventionally exported * The feature-level manifest. One per feature package, conventionally exported
* as `<featureName>Manifest` from `src/feature.manifest.ts`. * as `<featureName>Manifest` from `src/feature.manifest.ts`.
*
* `coverage` is optional for backward compatibility — features without a
* declared coverage section fall back to `DEFAULT_COVERAGE_BANDS` via
* `getCoverageBands(manifest)` (see `./coverage.ts`).
*/ */
export type FeatureManifest = { export type FeatureManifest = {
readonly name: string; readonly name: string;
@@ -21,6 +27,7 @@ export type FeatureManifest = {
readonly useCases: { readonly [k: string]: UseCaseManifest }; readonly useCases: { readonly [k: string]: UseCaseManifest };
readonly realtimeChannels: readonly string[]; readonly realtimeChannels: readonly string[];
readonly jobs: readonly string[]; readonly jobs: readonly string[];
readonly coverage?: CoverageManifest;
}; };
/** /**

View File

@@ -1,6 +1,21 @@
export type { Instrumented, Captured } from "./brands"; export type { Instrumented, Captured } from "./brands";
export type { FeatureManifest, UseCaseManifest } from "./define-feature"; export type { FeatureManifest, UseCaseManifest } from "./define-feature";
export { defineFeature } from "./define-feature"; export { defineFeature } from "./define-feature";
export type {
CoverageBand,
CoverageBands,
CoverageManifest,
VitestThresholds,
} from "./coverage";
export {
DEFAULT_COVERAGE_BANDS,
DEFAULT_MUTATION_SCORE,
DEFAULT_MUTATION_TARGETS,
getCoverageBands,
getMutationConfig,
vitestThresholdsFromBands,
vitestThresholdsFromManifest,
} from "./coverage";
export type { ProductionUseCase } from "./production-use-case"; export type { ProductionUseCase } from "./production-use-case";
export { export {
attachBrand, attachBrand,