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

@@ -1,5 +1,18 @@
import { describe, it, expectTypeOf } from "vitest";
import { defineFeature, type FeatureManifest } from "@/conformance/define-feature";
import { describe, it, expect, expectTypeOf } from "vitest";
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", () => {
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
expectTypeOf(manifest.name).toEqualTypeOf<"auth">();
// 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.signIn.mutates).toEqualTypeOf<false>();
});
@@ -42,4 +57,213 @@ describe("defineFeature", () => {
} as const);
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,
});
});
});