refactor: align coverage counter provenance
This commit is contained in:
@@ -0,0 +1,133 @@
|
|||||||
|
# Counter-bearing Coverage Provenance Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Align repository coverage provenance names and static classification with the counters that Vitest/V8 actually emits, without making claims about JavaScript runtime executability.
|
||||||
|
|
||||||
|
**Architecture:** The inventory parser classifies source files only by whether their top-level AST contains statements known to receive V8 counters. Coverage evaluation requires exact agreement between that static counter-bearing/counterless partition and producer rows, while policy-sensitive modules must remain counter-bearing. The JSON artifact exposes the same terminology as the inventory and diagnostics.
|
||||||
|
|
||||||
|
**Tech Stack:** TypeScript 7, Node.js 24, `@babel/eslint-parser`, Vitest 4, V8 coverage.
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- `runtime.ts` declarations/initializers and direct execution statements are counter-bearing.
|
||||||
|
- Type-only modules, `import type {}`, `import {}`, bare side-effect imports, value imports, named value re-exports, and star value re-exports are counterless under the observed Vitest/V8 producer.
|
||||||
|
- Counterless does not mean non-executable; code, artifacts, diagnostics, tests, and documentation must not make that claim.
|
||||||
|
- Exact all-zero rows are accepted only for statically counterless modules.
|
||||||
|
- Critical and high-risk policy modules cannot be counterless.
|
||||||
|
- All source edits use `apply_patch` and behavior changes follow RED-GREEN TDD.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Lock the Vitest/V8 classifier contract with RED tests
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `tests/unit/risk-coverage.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `buildProductionModuleInventory()` and `evaluateRiskCoverage()`.
|
||||||
|
- Produces: expectations for `counterBearingModules`, `counterlessModules`, `counterBearingTotal`, `instrumentedCounterBearingTotal`, `counterlessTotal`, and `counterlessModules`.
|
||||||
|
|
||||||
|
- [x] **Step 1: Rename the test inventory helper and artifact assertions to the desired API.**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function inventory(
|
||||||
|
files: readonly string[],
|
||||||
|
generatedExclusions: readonly string[] = [],
|
||||||
|
counterlessModules: readonly string[] = [],
|
||||||
|
): ProductionModuleInventory {
|
||||||
|
return {
|
||||||
|
files,
|
||||||
|
preExclusionTotal: files.length + generatedExclusions.length,
|
||||||
|
generatedExclusions,
|
||||||
|
counterBearingModules: files.filter((file) => !counterlessModules.includes(file)),
|
||||||
|
counterlessModules,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **Step 2: Add a real-source inventory regression table.**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const counterlessSources = {
|
||||||
|
"import-type-empty.ts": "import type {} from './a.ts';\n",
|
||||||
|
"import-value-empty.ts": "import {} from './a.ts';\n",
|
||||||
|
"import-side-effect.ts": "import './a.ts';\n",
|
||||||
|
"import-value.ts": "import { a } from './a.ts';\n",
|
||||||
|
"reexport-named.ts": "export { a } from './a.ts';\n",
|
||||||
|
"reexport-star.ts": "export * from './a.ts';\n",
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Assert every key appears in `counterlessModules`, while `export const runtimeValue = 1` and `void globalThis` appear in `counterBearingModules`.
|
||||||
|
|
||||||
|
- [x] **Step 3: Run the focused test and verify RED.**
|
||||||
|
|
||||||
|
Run: `./node_modules/.bin/vitest run tests/unit/risk-coverage.test.ts --reporter=dot`
|
||||||
|
|
||||||
|
Expected: TypeScript/test failures because the counter-bearing API fields do not exist and the current bare import classifier is executable-labelled.
|
||||||
|
|
||||||
|
### Task 2: Rename and align static coverage provenance
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `scripts/lib/risk-coverage.ts`
|
||||||
|
- Modify: `tests/unit/risk-coverage.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: Babel `Program.body` nodes and parsed Istanbul/V8 counters.
|
||||||
|
- Produces: `hasCoverageCounterBearingStatements(source, relativePath)`, a complete `counterBearingModules`/`counterlessModules` partition, and consistently named `RiskCoverageResult` fields.
|
||||||
|
|
||||||
|
- [x] **Step 1: Implement the minimal classifier needed by the RED cases.**
|
||||||
|
|
||||||
|
`ImportDeclaration`, `ExportAllDeclaration`, and export declarations without a local declaration return `false`; `importKind === "type"` therefore remains counterless even with an empty specifier list. Runtime declarations/initializers and direct statements return `true`.
|
||||||
|
|
||||||
|
- [x] **Step 2: Rename inventory, evaluator sets, totals, diagnostics, and policy guards.**
|
||||||
|
|
||||||
|
Use these exact artifact fields: `counterBearingTotal`, `instrumentedCounterBearingTotal`, `counterlessTotal`, `counterlessModules`. Use diagnostics containing `counter-bearing`, `counterless`, and `policy-sensitive module cannot be counterless`; remove executable/non-executable terminology from the risk-coverage implementation and tests.
|
||||||
|
|
||||||
|
- [x] **Step 3: Run focused GREEN verification.**
|
||||||
|
|
||||||
|
Run: `./node_modules/.bin/vitest run tests/unit/risk-coverage.test.ts tests/unit/risk-coverage-files.test.ts tests/unit/bounded-body-reader.test.ts --reporter=dot`
|
||||||
|
|
||||||
|
Expected: all focused tests pass and both static partition directions remain fail-closed.
|
||||||
|
|
||||||
|
### Task 3: Refresh documentation, repository evidence, and removal evidence
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `docs/testing/frontend-platform-testing-strategy.md`
|
||||||
|
- Modify: `.superpowers/sdd/2026-08-01-quality-architecture-remediation/task-1-report.md`
|
||||||
|
- Modify: `.superpowers/sdd/2026-08-01-quality-architecture-remediation/progress.md`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: root and sample-removal checker output after Task 2.
|
||||||
|
- Produces: documented V8 counter-bearing semantics and current 285/285 plus 268/268 evidence.
|
||||||
|
|
||||||
|
- [x] **Step 1: Document that counterless imports/re-exports may execute at runtime but receive no file counters in the observed producer.**
|
||||||
|
|
||||||
|
- [x] **Step 2: Run relevant verification.**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./node_modules/.bin/tsc --noEmit -p tsconfig.node.json
|
||||||
|
./node_modules/.bin/tsc --noEmit -p tsconfig.test.json
|
||||||
|
./node_modules/.bin/eslint scripts/lib/risk-coverage.ts tests/unit/risk-coverage.test.ts --max-warnings=0
|
||||||
|
node scripts/check-risk-coverage.ts
|
||||||
|
corepack pnpm test:sample-removal
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected root checker: `Risk coverage: PASS (285/285 production modules, 80 thresholds)`.
|
||||||
|
|
||||||
|
Expected removal checker: `Risk coverage: PASS (268/268 production modules, 76 thresholds)`; the already-known dependency-cruiser architecture diagnostic may remain the sole removal failure.
|
||||||
|
|
||||||
|
- [x] **Step 3: Commit the independently verified follow-up.**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git add docs/superpowers/plans/2026-08-02-counter-bearing-coverage-provenance.md docs/testing/frontend-platform-testing-strategy.md scripts/lib/risk-coverage.ts tests/unit/risk-coverage.test.ts
|
||||||
|
git commit -m "refactor: align coverage counter provenance"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Self-review
|
||||||
|
|
||||||
|
- Spec coverage: terminology, import/re-export edge cases, policy diagnostics, artifact fields, root/removal evidence, report, and ledger are each assigned above.
|
||||||
|
- Placeholder scan: no deferred implementation or unspecified test step remains.
|
||||||
|
- Type consistency: inventory and result names use `counterBearing*`/`counterless*` throughout; the classifier is `hasCoverageCounterBearingStatements`.
|
||||||
@@ -133,6 +133,17 @@ operation/query-mutation/registry compatibility 12개 high-risk module에 52개
|
|||||||
scoped threshold를 적용한다. critical module 누락 또는 threshold 미달 fixture는
|
scoped threshold를 적용한다. critical module 누락 또는 threshold 미달 fixture는
|
||||||
merge gate를 실패시킨다.
|
merge gate를 실패시킨다.
|
||||||
|
|
||||||
|
Repository inventory의 정적 provenance는 runtime 실행 가능 여부가 아니라 현재
|
||||||
|
Vitest/V8 producer가 file counter를 생성하는 문장의 존재 여부를 나타낸다. 따라서
|
||||||
|
artifact는 `counterBearingTotal`, `instrumentedCounterBearingTotal`,
|
||||||
|
`counterlessTotal`, `counterlessModules`를 기록한다. 실제 microfixture에서
|
||||||
|
`import type {}`, `import {}`, bare side-effect import, value import, named/star value
|
||||||
|
re-export만 있는 모듈은 모두 exact all-zero row였고, 선언/초기화 또는 직접 실행문이
|
||||||
|
있는 모듈은 counter-bearing이었다. Counterless import/re-export도 module evaluation
|
||||||
|
과 side effect를 유발할 수 있으므로 이를 non-executable로 해석하지 않는다. Gate는
|
||||||
|
static counterless 집합과 exact all-zero row 집합의 일치를 요구하고 critical/high-risk
|
||||||
|
policy module이 counterless이면 실패시킨다.
|
||||||
|
|
||||||
남은 범위는 실제 device/browser farm, cloud visual approval, 외부 인증·telemetry
|
남은 범위는 실제 device/browser farm, cloud visual approval, 외부 인증·telemetry
|
||||||
provider와 production field data다. 이 증거가 없을 때 저장소 내부 test를
|
provider와 production field data다. 이 증거가 없을 때 저장소 내부 test를
|
||||||
`PRODUCTION_READY`의 대체물로 사용하지 않는다.
|
`PRODUCTION_READY`의 대체물로 사용하지 않는다.
|
||||||
|
|||||||
@@ -63,18 +63,18 @@ export type ProductionModuleInventory = Readonly<{
|
|||||||
files: readonly string[];
|
files: readonly string[];
|
||||||
preExclusionTotal: number;
|
preExclusionTotal: number;
|
||||||
generatedExclusions: readonly string[];
|
generatedExclusions: readonly string[];
|
||||||
executableModules: readonly string[];
|
counterBearingModules: readonly string[];
|
||||||
nonExecutableModules: readonly string[];
|
counterlessModules: readonly string[];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type RiskCoverageResult = Readonly<{
|
export type RiskCoverageResult = Readonly<{
|
||||||
status: "PASS" | "FAIL";
|
status: "PASS" | "FAIL";
|
||||||
selectedTotal: number;
|
selectedTotal: number;
|
||||||
repositoryTotal: number;
|
repositoryTotal: number;
|
||||||
executableTotal: number;
|
counterBearingTotal: number;
|
||||||
instrumentedExecutableTotal: number;
|
instrumentedCounterBearingTotal: number;
|
||||||
nonExecutableTotal: number;
|
counterlessTotal: number;
|
||||||
nonExecutableModules: readonly string[];
|
counterlessModules: readonly string[];
|
||||||
preExclusionTotal: number;
|
preExclusionTotal: number;
|
||||||
generatedExclusionCount: number;
|
generatedExclusionCount: number;
|
||||||
generatedExclusions: readonly string[];
|
generatedExclusions: readonly string[];
|
||||||
@@ -370,13 +370,12 @@ type TypeScriptAstNode = Readonly<{
|
|||||||
type?: unknown;
|
type?: unknown;
|
||||||
body?: unknown;
|
body?: unknown;
|
||||||
declaration?: unknown;
|
declaration?: unknown;
|
||||||
specifiers?: unknown;
|
|
||||||
declare?: unknown;
|
declare?: unknown;
|
||||||
const?: unknown;
|
const?: unknown;
|
||||||
importKind?: unknown;
|
importKind?: unknown;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
function statementIsExecutable(value: unknown): boolean {
|
function statementIsCoverageCounterBearing(value: unknown): boolean {
|
||||||
if (!isRecord(value) || typeof value.type !== "string") {
|
if (!isRecord(value) || typeof value.type !== "string") {
|
||||||
throw new TypeError("TypeScript parser returned an invalid statement");
|
throw new TypeError("TypeScript parser returned an invalid statement");
|
||||||
}
|
}
|
||||||
@@ -393,7 +392,7 @@ function statementIsExecutable(value: unknown): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (statement.type === "ImportDeclaration") {
|
if (statement.type === "ImportDeclaration") {
|
||||||
return Array.isArray(statement.specifiers) && statement.specifiers.length === 0;
|
return false;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
statement.type === "ExportAllDeclaration" ||
|
statement.type === "ExportAllDeclaration" ||
|
||||||
@@ -406,7 +405,7 @@ function statementIsExecutable(value: unknown): boolean {
|
|||||||
statement.type === "ExportDefaultDeclaration"
|
statement.type === "ExportDefaultDeclaration"
|
||||||
) {
|
) {
|
||||||
return statement.declaration !== null && statement.declaration !== undefined
|
return statement.declaration !== null && statement.declaration !== undefined
|
||||||
? statementIsExecutable(statement.declaration)
|
? statementIsCoverageCounterBearing(statement.declaration)
|
||||||
: false;
|
: false;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -427,7 +426,7 @@ function statementIsExecutable(value: unknown): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasExecutableTypeScriptStatements(
|
export function hasCoverageCounterBearingStatements(
|
||||||
source: string,
|
source: string,
|
||||||
relativePath: string,
|
relativePath: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
@@ -454,7 +453,7 @@ export function hasExecutableTypeScriptStatements(
|
|||||||
if (!isRecord(parsed) || !Array.isArray(parsed.body)) {
|
if (!isRecord(parsed) || !Array.isArray(parsed.body)) {
|
||||||
throw new TypeError("TypeScript parser returned an invalid program");
|
throw new TypeError("TypeScript parser returned an invalid program");
|
||||||
}
|
}
|
||||||
return parsed.body.some(statementIsExecutable);
|
return parsed.body.some(statementIsCoverageCounterBearing);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function buildProductionModuleInventory(
|
export async function buildProductionModuleInventory(
|
||||||
@@ -481,8 +480,8 @@ export async function buildProductionModuleInventory(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const allModules: string[] = [];
|
const allModules: string[] = [];
|
||||||
const executableModules: string[] = [];
|
const counterBearingModules: string[] = [];
|
||||||
const nonExecutableModules: string[] = [];
|
const counterlessModules: string[] = [];
|
||||||
async function visit(relativeDirectory: string): Promise<void> {
|
async function visit(relativeDirectory: string): Promise<void> {
|
||||||
const absoluteDirectory = path.join(repositoryRoot, relativeDirectory);
|
const absoluteDirectory = path.join(repositoryRoot, relativeDirectory);
|
||||||
const entries = await readDirectory(absoluteDirectory);
|
const entries = await readDirectory(absoluteDirectory);
|
||||||
@@ -543,10 +542,10 @@ export async function buildProductionModuleInventory(
|
|||||||
await handle?.close();
|
await handle?.close();
|
||||||
}
|
}
|
||||||
allModules.push(relativeTarget);
|
allModules.push(relativeTarget);
|
||||||
if (hasExecutableTypeScriptStatements(source, relativeTarget)) {
|
if (hasCoverageCounterBearingStatements(source, relativeTarget)) {
|
||||||
executableModules.push(relativeTarget);
|
counterBearingModules.push(relativeTarget);
|
||||||
} else {
|
} else {
|
||||||
nonExecutableModules.push(relativeTarget);
|
counterlessModules.push(relativeTarget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -565,11 +564,11 @@ export async function buildProductionModuleInventory(
|
|||||||
files: Object.freeze(inventory),
|
files: Object.freeze(inventory),
|
||||||
preExclusionTotal: allModules.length,
|
preExclusionTotal: allModules.length,
|
||||||
generatedExclusions: Object.freeze([...generatedPaths].sort()),
|
generatedExclusions: Object.freeze([...generatedPaths].sort()),
|
||||||
executableModules: Object.freeze(
|
counterBearingModules: Object.freeze(
|
||||||
executableModules.filter((file) => !generated.has(file)).sort(),
|
counterBearingModules.filter((file) => !generated.has(file)).sort(),
|
||||||
),
|
),
|
||||||
nonExecutableModules: Object.freeze(
|
counterlessModules: Object.freeze(
|
||||||
nonExecutableModules.filter((file) => !generated.has(file)).sort(),
|
counterlessModules.filter((file) => !generated.has(file)).sort(),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -718,22 +717,22 @@ export function evaluateRiskCoverage(input: Readonly<{
|
|||||||
if (new Set(inventory).size !== inventory.length) {
|
if (new Set(inventory).size !== inventory.length) {
|
||||||
throw new TypeError("production module inventory contains a duplicate path");
|
throw new TypeError("production module inventory contains a duplicate path");
|
||||||
}
|
}
|
||||||
const executableModules = input.inventory.executableModules.map((file) =>
|
const counterBearingModules = input.inventory.counterBearingModules.map((file) =>
|
||||||
exactSourcePath(file, "executable inventory path"),
|
exactSourcePath(file, "counter-bearing inventory path"),
|
||||||
);
|
);
|
||||||
const nonExecutableModules = input.inventory.nonExecutableModules.map((file) =>
|
const counterlessModules = input.inventory.counterlessModules.map((file) =>
|
||||||
exactSourcePath(file, "non-executable inventory path"),
|
exactSourcePath(file, "counterless inventory path"),
|
||||||
);
|
);
|
||||||
const executableSet = new Set(executableModules);
|
const counterBearingSet = new Set(counterBearingModules);
|
||||||
const nonExecutableSet = new Set(nonExecutableModules);
|
const counterlessSet = new Set(counterlessModules);
|
||||||
const partition = [...executableModules, ...nonExecutableModules].sort();
|
const partition = [...counterBearingModules, ...counterlessModules].sort();
|
||||||
if (
|
if (
|
||||||
executableSet.size !== executableModules.length ||
|
counterBearingSet.size !== counterBearingModules.length ||
|
||||||
nonExecutableSet.size !== nonExecutableModules.length ||
|
counterlessSet.size !== counterlessModules.length ||
|
||||||
executableModules.some((file) => nonExecutableSet.has(file)) ||
|
counterBearingModules.some((file) => counterlessSet.has(file)) ||
|
||||||
partition.join("\n") !== [...inventory].sort().join("\n")
|
partition.join("\n") !== [...inventory].sort().join("\n")
|
||||||
) {
|
) {
|
||||||
throw new TypeError("production inventory executable provenance is inconsistent");
|
throw new TypeError("production inventory counter-bearing provenance is inconsistent");
|
||||||
}
|
}
|
||||||
const generatedExclusions = input.inventory.generatedExclusions.map((file) =>
|
const generatedExclusions = input.inventory.generatedExclusions.map((file) =>
|
||||||
exactSourcePath(file, "generated exclusion"),
|
exactSourcePath(file, "generated exclusion"),
|
||||||
@@ -780,14 +779,14 @@ export function evaluateRiskCoverage(input: Readonly<{
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
const zeroCoverageSet = new Set(zeroCoverageModules);
|
const zeroCoverageSet = new Set(zeroCoverageModules);
|
||||||
const zeroExecutableModules = zeroCoverageModules.filter((file) =>
|
const zeroCounterBearingModules = zeroCoverageModules.filter((file) =>
|
||||||
executableSet.has(file),
|
counterBearingSet.has(file),
|
||||||
);
|
);
|
||||||
const nonExecutableWithCounters = nonExecutableModules.filter((file) => {
|
const counterlessWithCounters = counterlessModules.filter((file) => {
|
||||||
const metrics = selected.get(file);
|
const metrics = selected.get(file);
|
||||||
return metrics !== undefined && !zeroCoverageSet.has(file);
|
return metrics !== undefined && !zeroCoverageSet.has(file);
|
||||||
});
|
});
|
||||||
const instrumentedExecutableTotal = executableModules.filter((file) => {
|
const instrumentedCounterBearingTotal = counterBearingModules.filter((file) => {
|
||||||
const metrics = selected.get(file);
|
const metrics = selected.get(file);
|
||||||
return metrics !== undefined && !zeroCoverageSet.has(file);
|
return metrics !== undefined && !zeroCoverageSet.has(file);
|
||||||
}).length;
|
}).length;
|
||||||
@@ -827,18 +826,18 @@ export function evaluateRiskCoverage(input: Readonly<{
|
|||||||
.filter(
|
.filter(
|
||||||
(file) =>
|
(file) =>
|
||||||
!selected.has(file) ||
|
!selected.has(file) ||
|
||||||
(executableSet.has(file) && zeroCoverageSet.has(file)),
|
(counterBearingSet.has(file) && zeroCoverageSet.has(file)),
|
||||||
)
|
)
|
||||||
.sort();
|
.sort();
|
||||||
failures.push(
|
failures.push(
|
||||||
...inventory
|
...inventory
|
||||||
.filter((file) => !selected.has(file))
|
.filter((file) => !selected.has(file))
|
||||||
.map((file) => `production module missing from coverage: ${file}`),
|
.map((file) => `production module missing from coverage: ${file}`),
|
||||||
...zeroExecutableModules.map(
|
...zeroCounterBearingModules.map(
|
||||||
(file) => `production module has zero coverage totals: ${file}`,
|
(file) => `counter-bearing module has zero coverage totals: ${file}`,
|
||||||
),
|
),
|
||||||
...nonExecutableWithCounters.map(
|
...counterlessWithCounters.map(
|
||||||
(file) => `non-executable module has coverage counters: ${file}`,
|
(file) => `counterless module has coverage counters: ${file}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
for (const modulePolicy of input.policy.criticalModules) {
|
for (const modulePolicy of input.policy.criticalModules) {
|
||||||
@@ -846,8 +845,10 @@ export function evaluateRiskCoverage(input: Readonly<{
|
|||||||
failures.push(`critical module is outside production inventory: ${modulePolicy.path}`);
|
failures.push(`critical module is outside production inventory: ${modulePolicy.path}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (nonExecutableSet.has(modulePolicy.path)) {
|
if (counterlessSet.has(modulePolicy.path)) {
|
||||||
failures.push(`critical module cannot be non-executable: ${modulePolicy.path}`);
|
failures.push(
|
||||||
|
`critical policy-sensitive module cannot be counterless: ${modulePolicy.path}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const actual = selected.get(modulePolicy.path);
|
const actual = selected.get(modulePolicy.path);
|
||||||
if (!actual) {
|
if (!actual) {
|
||||||
@@ -863,8 +864,10 @@ export function evaluateRiskCoverage(input: Readonly<{
|
|||||||
if (!inventorySet.has(highRiskPath)) {
|
if (!inventorySet.has(highRiskPath)) {
|
||||||
failures.push(`high-risk module is outside production inventory: ${highRiskPath}`);
|
failures.push(`high-risk module is outside production inventory: ${highRiskPath}`);
|
||||||
}
|
}
|
||||||
if (nonExecutableSet.has(highRiskPath)) {
|
if (counterlessSet.has(highRiskPath)) {
|
||||||
failures.push(`high-risk module cannot be non-executable: ${highRiskPath}`);
|
failures.push(
|
||||||
|
`high-risk policy-sensitive module cannot be counterless: ${highRiskPath}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const waiver of input.policy.waivers) {
|
for (const waiver of input.policy.waivers) {
|
||||||
@@ -881,10 +884,10 @@ export function evaluateRiskCoverage(input: Readonly<{
|
|||||||
status: failures.length === 0 ? "PASS" : "FAIL",
|
status: failures.length === 0 ? "PASS" : "FAIL",
|
||||||
selectedTotal: inventory.length - uncoveredModules.length,
|
selectedTotal: inventory.length - uncoveredModules.length,
|
||||||
repositoryTotal: inventory.length,
|
repositoryTotal: inventory.length,
|
||||||
executableTotal: executableModules.length,
|
counterBearingTotal: counterBearingModules.length,
|
||||||
instrumentedExecutableTotal,
|
instrumentedCounterBearingTotal,
|
||||||
nonExecutableTotal: nonExecutableModules.length,
|
counterlessTotal: counterlessModules.length,
|
||||||
nonExecutableModules: Object.freeze([...nonExecutableModules].sort()),
|
counterlessModules: Object.freeze([...counterlessModules].sort()),
|
||||||
preExclusionTotal: input.inventory.preExclusionTotal,
|
preExclusionTotal: input.inventory.preExclusionTotal,
|
||||||
generatedExclusionCount: generatedExclusions.length,
|
generatedExclusionCount: generatedExclusions.length,
|
||||||
generatedExclusions: Object.freeze([...generatedExclusions].sort()),
|
generatedExclusions: Object.freeze([...generatedExclusions].sort()),
|
||||||
|
|||||||
@@ -48,16 +48,16 @@ const fullMetrics = metrics();
|
|||||||
function inventory(
|
function inventory(
|
||||||
files: readonly string[],
|
files: readonly string[],
|
||||||
generatedExclusions: readonly string[] = [],
|
generatedExclusions: readonly string[] = [],
|
||||||
nonExecutableModules: readonly string[] = [],
|
counterlessModules: readonly string[] = [],
|
||||||
): ProductionModuleInventory {
|
): ProductionModuleInventory {
|
||||||
return {
|
return {
|
||||||
files,
|
files,
|
||||||
preExclusionTotal: files.length + generatedExclusions.length,
|
preExclusionTotal: files.length + generatedExclusions.length,
|
||||||
generatedExclusions,
|
generatedExclusions,
|
||||||
executableModules: files.filter(
|
counterBearingModules: files.filter(
|
||||||
(file) => !nonExecutableModules.includes(file),
|
(file) => !counterlessModules.includes(file),
|
||||||
),
|
),
|
||||||
nonExecutableModules,
|
counterlessModules,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,8 +160,8 @@ describe("repository-aware risk coverage", () => {
|
|||||||
files: ["src/a.ts", "src/nested/b.tsx"],
|
files: ["src/a.ts", "src/nested/b.tsx"],
|
||||||
preExclusionTotal: 3,
|
preExclusionTotal: 3,
|
||||||
generatedExclusions: ["src/generated.ts"],
|
generatedExclusions: ["src/generated.ts"],
|
||||||
executableModules: ["src/a.ts", "src/nested/b.tsx"],
|
counterBearingModules: ["src/a.ts", "src/nested/b.tsx"],
|
||||||
nonExecutableModules: [],
|
counterlessModules: [],
|
||||||
});
|
});
|
||||||
expect(result).toMatchObject({
|
expect(result).toMatchObject({
|
||||||
status: "FAIL",
|
status: "FAIL",
|
||||||
@@ -347,7 +347,7 @@ describe("repository-aware risk coverage", () => {
|
|||||||
uncoveredModules: ["src/a.ts"],
|
uncoveredModules: ["src/a.ts"],
|
||||||
});
|
});
|
||||||
expect(result.failures).toContain(
|
expect(result.failures).toContain(
|
||||||
"production module has zero coverage totals: src/a.ts",
|
"counter-bearing module has zero coverage totals: src/a.ts",
|
||||||
);
|
);
|
||||||
expect(result.failures).toEqual(
|
expect(result.failures).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
@@ -359,7 +359,7 @@ describe("repository-aware risk coverage", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("accepts exact all-zero rows only for statically non-executable modules", () => {
|
it("accepts exact all-zero rows only for statically counterless modules", () => {
|
||||||
const parsedPolicy = parseRiskCoveragePolicy(
|
const parsedPolicy = parseRiskCoveragePolicy(
|
||||||
policy({ repositoryBaseline: 2, generatedPaths: [] }),
|
policy({ repositoryBaseline: 2, generatedPaths: [] }),
|
||||||
{ now },
|
{ now },
|
||||||
@@ -384,10 +384,10 @@ describe("repository-aware risk coverage", () => {
|
|||||||
status: "PASS",
|
status: "PASS",
|
||||||
selectedTotal: 2,
|
selectedTotal: 2,
|
||||||
repositoryTotal: 2,
|
repositoryTotal: 2,
|
||||||
executableTotal: 1,
|
counterBearingTotal: 1,
|
||||||
instrumentedExecutableTotal: 1,
|
instrumentedCounterBearingTotal: 1,
|
||||||
nonExecutableTotal: 1,
|
counterlessTotal: 1,
|
||||||
nonExecutableModules: ["src/type-only.ts"],
|
counterlessModules: ["src/type-only.ts"],
|
||||||
uncoveredModules: [],
|
uncoveredModules: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -402,11 +402,11 @@ describe("repository-aware risk coverage", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(mismatch.failures).toContain(
|
expect(mismatch.failures).toContain(
|
||||||
"non-executable module has coverage counters: src/type-only.ts",
|
"counterless module has coverage counters: src/type-only.ts",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("forbids critical and high-risk modules from being non-executable", () => {
|
it("forbids policy-sensitive modules from being counterless", () => {
|
||||||
const parsedPolicy = parseRiskCoveragePolicy(
|
const parsedPolicy = parseRiskCoveragePolicy(
|
||||||
policy({ repositoryBaseline: 1, generatedPaths: [] }),
|
policy({ repositoryBaseline: 1, generatedPaths: [] }),
|
||||||
{ now },
|
{ now },
|
||||||
@@ -420,8 +420,8 @@ describe("repository-aware risk coverage", () => {
|
|||||||
|
|
||||||
expect(result.failures).toEqual(
|
expect(result.failures).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
"critical module cannot be non-executable: src/a.ts",
|
"critical policy-sensitive module cannot be counterless: src/a.ts",
|
||||||
"high-risk module cannot be non-executable: src/a.ts",
|
"high-risk policy-sensitive module cannot be counterless: src/a.ts",
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -564,7 +564,7 @@ describe("repository-aware risk coverage", () => {
|
|||||||
).rejects.toThrow(/stable file identity unavailable/u);
|
).rejects.toThrow(/stable file identity unavailable/u);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("classifies type-only and barrel modules separately from runtime statements", async () => {
|
it("matches the observed V8 counterless import and re-export syntax", async () => {
|
||||||
const repositoryRoot = await repositoryFixture();
|
const repositoryRoot = await repositoryFixture();
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
writeFile(
|
writeFile(
|
||||||
@@ -584,9 +584,29 @@ describe("repository-aware risk coverage", () => {
|
|||||||
"void globalThis;\n",
|
"void globalThis;\n",
|
||||||
),
|
),
|
||||||
writeFile(
|
writeFile(
|
||||||
path.join(repositoryRoot, "src/side-effect-import.ts"),
|
path.join(repositoryRoot, "src/import-type-empty.ts"),
|
||||||
|
"import type {} from './a.ts';\n",
|
||||||
|
),
|
||||||
|
writeFile(
|
||||||
|
path.join(repositoryRoot, "src/import-value-empty.ts"),
|
||||||
|
"import {} from './a.ts';\n",
|
||||||
|
),
|
||||||
|
writeFile(
|
||||||
|
path.join(repositoryRoot, "src/import-side-effect.ts"),
|
||||||
"import './a.ts';\n",
|
"import './a.ts';\n",
|
||||||
),
|
),
|
||||||
|
writeFile(
|
||||||
|
path.join(repositoryRoot, "src/import-value.ts"),
|
||||||
|
"import { a } from './a.ts';\n",
|
||||||
|
),
|
||||||
|
writeFile(
|
||||||
|
path.join(repositoryRoot, "src/reexport-named.ts"),
|
||||||
|
"export { a } from './a.ts';\n",
|
||||||
|
),
|
||||||
|
writeFile(
|
||||||
|
path.join(repositoryRoot, "src/reexport-star.ts"),
|
||||||
|
"export * from './a.ts';\n",
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const productionInventory = await buildProductionModuleInventory({
|
const productionInventory = await buildProductionModuleInventory({
|
||||||
@@ -594,19 +614,22 @@ describe("repository-aware risk coverage", () => {
|
|||||||
generatedPaths: ["src/generated.ts"],
|
generatedPaths: ["src/generated.ts"],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(productionInventory.nonExecutableModules).toEqual([
|
expect(productionInventory.counterlessModules).toEqual([
|
||||||
"src/barrel.ts",
|
"src/barrel.ts",
|
||||||
|
"src/import-side-effect.ts",
|
||||||
|
"src/import-type-empty.ts",
|
||||||
|
"src/import-value-empty.ts",
|
||||||
|
"src/import-value.ts",
|
||||||
|
"src/reexport-named.ts",
|
||||||
|
"src/reexport-star.ts",
|
||||||
"src/type-only.ts",
|
"src/type-only.ts",
|
||||||
]);
|
]);
|
||||||
expect(productionInventory.executableModules).toEqual(
|
expect(productionInventory.counterBearingModules).toEqual([
|
||||||
expect.arrayContaining([
|
"src/a.ts",
|
||||||
"src/a.ts",
|
"src/nested/b.tsx",
|
||||||
"src/nested/b.tsx",
|
"src/runtime-export.ts",
|
||||||
"src/runtime-export.ts",
|
"src/side-effect.ts",
|
||||||
"src/side-effect-import.ts",
|
]);
|
||||||
"src/side-effect.ts",
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects a post-lstat file identity swap even without relying on O_NOFOLLOW", async () => {
|
it("rejects a post-lstat file identity swap even without relying on O_NOFOLLOW", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user