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:
@@ -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;
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
171
packages/core-shared/src/conformance/coverage.ts
Normal file
171
packages/core-shared/src/conformance/coverage.ts
Normal 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));
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user