Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions dashboard/public/data/criteria-docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,8 @@
"what": "Static test-coverage readiness: whether source files have tests, and whether CI enforces a coverage bar. A discipline floor, not a quality guarantee — coverage is not correctness.",
"why": "Without a safety net of tests, neither an agent nor an engineer can refactor or ship with confidence.",
"how": {
"tools": "Static test↔source mapping, and coverage enforcement read from GitHub Actions workflows (minimum_coverage gates). The coverage library itself is a secondary audit label. No suite is run.",
"anchors": "Two facets, mean of 2. Breadth: the fraction of source files with a paired test file, pooled across stacks. Discipline: coverage enforced in CI, graded on Google's 60/75/90 tiers, mean of the applicable stacks.",
"tools": "Static test↔source mapping, with the code graph (graphify) for JavaScript and TypeScript, and coverage enforcement read from GitHub Actions workflows (minimum_coverage gates). The coverage library itself is a secondary audit label. No suite is run.",
"anchors": "Two facets, mean of 2. Breadth: the fraction of source files with a paired test, pooled across stacks. Discipline: coverage enforced in CI, graded on Google's 60/75/90 tiers, mean of the applicable stacks.",
"scoring": "Breadth is linear — 5 × the fraction of source files with a paired test. Discipline is a rung per stack: none 0, tool configured but no threshold 2.0, enforced at a threshold graded on the Google tiers (60 → 3.5, 75 → 4.25, 90 → 5.0). The criterion is the mean of the two facets. No hard-cap."
},
"nuances": [
Expand All @@ -144,8 +144,8 @@
"detail": "A source file counts as tested when a test file pairs with it. Actual line coverage needs a compile, a database and secrets, none of which this scan has. The proxy reads generously and the report says so wherever it is quoted."
},
{
"title": "The pairing ignores paths, so it flatters",
"detail": "Breadth pairs a source file to a test by basename stem, ignoring directory, so one user_test.exs marks every */user.ex as tested. A precise per-unit map needs the Clear Domain Boundaries code graph and comes with it."
"title": "How a test is paired with a source file",
"detail": "For JavaScript and TypeScript, a source file counts as tested only when a test named for it imports it, as read from the code graph. A same-named test elsewhere that never touches the file does not count. Every other stack, and any repo where the graph sees the import for fewer than half of its same-named test pairs, falls back to the name alone, which ignores directory and so flatters: one user_test.exs marks every */user.ex as tested. The report names the method used for each stack and, on a fallback, the share the graph resolved."
},
{
"title": "minimum_coverage: 0 counts as configured, not enforced",
Expand All @@ -160,7 +160,7 @@
"detail": "Which paths matter most — the intersection of the code graph, churn, complexity and traffic — is not part of this criterion. Until it is, a high breadth number says tests exist, not that the right things are tested."
}
],
"last_reviewed": "2026-09-14"
"last_reviewed": "2026-10-13"
},
{
"id": "7",
Expand Down
41 changes: 36 additions & 5 deletions dist/assemble-scores.js
Original file line number Diff line number Diff line change
Expand Up @@ -997,7 +997,23 @@ function boundaryGraphDisposition(stack) {
return stack === 'elixir' ? 'unreliable' : 'ok';
}

module.exports = { documentedApisDisposition, boundaryGraphDisposition };
// C6 test breadth: how a source file is paired with its test.
//
// 'imports' — a test counts only when it is named for the file AND the code
// graph shows it importing the file.
// 'filename' — a same-named test anywhere in the repo counts (the original
// file-presence proxy).
//
// 'imports' only for stacks whose test imports the graph has been shown to
// resolve; the scan still falls back per repo when it does not (choosePairing).
// Elixir under-resolves (see boundaryGraphDisposition) and Python src-layout
// imports are not resolved, so both keep the filename rule.
const IMPORT_PAIRED_STACKS = new Set(['frontend']);
function testPairingDisposition(stack) {
return IMPORT_PAIRED_STACKS.has(stack) ? 'imports' : 'filename';
}

module.exports = { documentedApisDisposition, boundaryGraphDisposition, testPairingDisposition };


/***/ }),
Expand Down Expand Up @@ -2968,6 +2984,11 @@ function scoreTestCoverage(report, { assessedAt } = {}) {
totalSource += Number(s.source_files) || 0;
totalTested += Number(s.tested_files) || 0;
byStackBreadth[k] = { source: Number(s.source_files) || 0, tested: Number(s.tested_files) || 0 };
if (s.pairing === 'imports') {
byStackBreadth[k].pairing = 'imports';
byStackBreadth[k].credited_by_name_only = Number(s.credited_by_name_only) || 0;
}
if (s.pairing_fallback) byStackBreadth[k].pairing_fallback = s.pairing_fallback;
}
if (totalSource === 0) return null;
const breadthCoverage = totalTested / totalSource;
Expand All @@ -2993,8 +3014,13 @@ function scoreTestCoverage(report, { assessedAt } = {}) {
const pct = Math.round(breadthCoverage * 100);
const untestedTotal = totalSource - totalTested;

const findings = [
`Test breadth: ${pct}% (${totalTested} of ${totalSource} source files have a paired test) — file-presence proxy, not line coverage`,
const importPaired = keys.filter((k) => stacks[k].pairing === 'imports');
const pairingNote = keys
.map((k) => `${STACK_LABEL[k] || k}: ${importPaired.includes(k) ? 'a same-named test that imports the file' : 'a same-named test'}`)
.join('; ');
const findings = [importPaired.length
? `Test breadth: ${pct}% (${totalTested} of ${totalSource} source files have a paired test — ${pairingNote}) — not line coverage`
: `Test breadth: ${pct}% (${totalTested} of ${totalSource} source files have a paired test) — file-presence proxy, not line coverage`,
];

// Discipline finding: summarise the enforcement picture across stacks.
Expand All @@ -3009,7 +3035,10 @@ function scoreTestCoverage(report, { assessedAt } = {}) {

const actions = [];
if (untestedTotal > 0) {
actions.push(`Add tests for untested source files — ${untestedTotal} source file${untestedTotal === 1 ? ' has' : 's have'} no paired test file (file-presence proxy; a paired test does not guarantee line coverage).`);
const files = `${untestedTotal} source file${untestedTotal === 1 ? ' has' : 's have'}`;
actions.push(importPaired.length
? `Add tests for untested source files — ${files} no paired test (${pairingNote}; a paired test does not guarantee line coverage).`
: `Add tests for untested source files — ${files} no paired test file (file-presence proxy; a paired test does not guarantee line coverage).`);
}
if (noneBits.length) {
actions.push(`Adopt a coverage tool and enforce a minimum threshold in CI for: ${noneBits.join(', ')} (Google acceptable floor is 60%).`);
Expand All @@ -3024,7 +3053,9 @@ function scoreTestCoverage(report, { assessedAt } = {}) {
critical: false,
assessed: true,
assessed_at: assessedAt,
source: 'Test-coverage readiness — breadth (test↔source file-presence proxy, pooled full-stack) + discipline (CI-enforced coverage, Google 60/75/90 tiers). Static/headless; actual coverage % deferred to Monitor.',
source: importPaired.length
? 'Test-coverage readiness — breadth (a same-named test that imports the file where the code graph is trusted, else a same-named test; pooled full-stack) + discipline (CI-enforced coverage, Google 60/75/90 tiers). Static/headless; actual coverage % deferred to Monitor.'
: 'Test-coverage readiness — breadth (test↔source file-presence proxy, pooled full-stack) + discipline (CI-enforced coverage, Google 60/75/90 tiers). Static/headless; actual coverage % deferred to Monitor.',
sub: {
test_breadth: {
score: breadthScore, colour: colourFor(breadthScore), pct, tested: totalTested, total: totalSource,
Expand Down
86 changes: 55 additions & 31 deletions dist/scan-boundaries.js
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,52 @@ function isManifestPath(p) {
return MANIFESTS.includes(base);
}

// One graphify link judged against every rule below: EXTRACTED confidence, a
// dependency relation, both ends source code, no build output, same language
// family, and no stdlib name collision on an import-shaped relation.
// `forTestPairing` reads a test's own imports: it accepts .vue components as
// JavaScript and keeps files whose names match a builtin (constants.ts), since
// a test importing its own subject is never the stdlib misresolution.
function classifyLink(l, byId, { forTestPairing = false } = {}) {
const rel = l.relation;
if (l.confidence !== 'EXTRACTED') return { skip: 'not-extracted' };
if (!isDependencyRelation(rel)) return { skip: 'non-dependency' };
const s = byId.get(l.source);
const t = byId.get(l.target);
if (!s || !t) return { skip: 'dangling' };
const sf = s.source_file || '';
const tf = t.source_file || '';
if (!sf || !tf) return { skip: 'no-file' };
if (isManifestPath(sf)) return { skip: 'manifest', node: l.source };
if (isManifestPath(tf)) return { skip: 'manifest', node: l.target };
// Config/asset files appear as targets when graphify traces configuration
// imports; they are not runtime module dependencies.
const isVue = (f) => forTestPairing && f.endsWith('.vue');
if (!(SOURCE_EXT.test(sf) || isVue(sf)) || !(SOURCE_EXT.test(tf) || isVue(tf))) return { skip: 'non-source-ext' };
// Test paths are deliberately NOT excluded: a test importing across a module
// boundary is a real code dependency.
if (isNonSourcePath(sf) || isNonSourcePath(tf)) return { skip: 'non-source-path' };
const sFam = isVue(sf) ? 'js' : languageFamily(sf);
const tFam = isVue(tf) ? 'js' : languageFamily(tf);
if (!sFam || !tFam || sFam !== tFam) return { skip: 'cross-language' };
const targetCollides = isStdlibCollidingFile(tf);
if (!forTestPairing && IMPORT_SHAPED_RELATIONS.has(rel) && targetCollides) return { skip: 'stdlib-collision' };
return { skip: null, sf, tf, targetCollides };
}

// File -> set of files it depends on, under the same rules as the module graph.
function fileDependencies(graph, options = {}) {
const byId = new Map((graph.nodes || []).map((n) => [n.id, n]));
const deps = new Map();
for (const l of graph.links || []) {
const link = classifyLink(l, byId, options);
if (link.skip || link.sf === link.tf) continue;
if (!deps.has(link.sf)) deps.set(link.sf, new Set());
deps.get(link.sf).add(link.tf);
}
return deps;
}

function buildModuleGraph(graph, modules) {
const byId = new Map((graph.nodes || []).map((n) => [n.id, n]));
const edges = [];
Expand Down Expand Up @@ -827,37 +873,14 @@ function buildModuleGraph(graph, modules) {
for (const l of graph.links || []) {
const rel = l.relation;
bump(rel, 'total');
if (l.confidence !== 'EXTRACTED') continue;
bump(rel, 'extracted');
if (!isDependencyRelation(rel)) continue;
const s = byId.get(l.source);
const t = byId.get(l.target);
if (!s || !t) continue;
const sf = s.source_file || '';
const tf = t.source_file || '';
if (!sf || !tf) continue;
if (isManifestPath(sf)) { skippedManifestNodes.add(l.source); continue; }
if (isManifestPath(tf)) { skippedManifestNodes.add(l.target); continue; }
// Require both ends to be source-code files. Config/asset files (e.g. .json,
// .yaml, .oxlintrc.json, babel.config.json) appear as edge targets when
// graphify's static analyser traces configuration imports; they are not runtime
// module dependencies and create spurious cross-module cycles.
if (!SOURCE_EXT.test(sf) || !SOURCE_EXT.test(tf)) continue;
// Build output, vendored trees and minified bundles are not repo source.
// Test paths are deliberately NOT excluded here: a test importing across a
// module boundary is a real code dependency, and dropping it would risk a
// false green on cycles.
if (isNonSourcePath(sf) || isNonSourcePath(tf)) { skippedNonSourcePathEdges += 1; continue; }
// Cross-language links are name collisions by construction (see above).
const sFam = languageFamily(sf);
const tFam = languageFamily(tf);
if (!sFam || !tFam || sFam !== tFam) { skippedCrossLanguageEdges += 1; continue; }
// Stdlib misresolution defence (see above).
const targetCollides = isStdlibCollidingFile(tf);
if (IMPORT_SHAPED_RELATIONS.has(rel) && targetCollides) {
skippedStdlibCollisionEdges += 1;
continue;
}
if (l.confidence === 'EXTRACTED') bump(rel, 'extracted');
const link = classifyLink(l, byId);
if (link.skip === 'manifest') { skippedManifestNodes.add(link.node); continue; }
if (link.skip === 'non-source-path') { skippedNonSourcePathEdges += 1; continue; }
if (link.skip === 'cross-language') { skippedCrossLanguageEdges += 1; continue; }
if (link.skip === 'stdlib-collision') { skippedStdlibCollisionEdges += 1; continue; }
if (link.skip) continue;
const { sf, tf, targetCollides } = link;
bump(rel, 'considered');
const from = moduleForFile(sf, modules);
const to = moduleForFile(tf, modules);
Expand Down Expand Up @@ -1227,6 +1250,7 @@ function computeFanOut(edges, modules, externalByModule = {}) {
}

module.exports = {
classifyLink, fileDependencies,
discoverModules, isLayerShaped, LAYER_VOCABULARY, MANIFESTS, SOURCE_EXT,
DEPENDENCY_RELATIONS, NON_DEPENDENCY_RELATIONS, isDependencyRelation,
IMPORT_SHAPED_RELATIONS, languageFamily, isStdlibCollidingFile,
Expand Down
18 changes: 17 additions & 1 deletion dist/scan-documented-apis.js
Original file line number Diff line number Diff line change
Expand Up @@ -4388,7 +4388,23 @@ function boundaryGraphDisposition(stack) {
return stack === 'elixir' ? 'unreliable' : 'ok';
}

module.exports = { documentedApisDisposition, boundaryGraphDisposition };
// C6 test breadth: how a source file is paired with its test.
//
// 'imports' — a test counts only when it is named for the file AND the code
// graph shows it importing the file.
// 'filename' — a same-named test anywhere in the repo counts (the original
// file-presence proxy).
//
// 'imports' only for stacks whose test imports the graph has been shown to
// resolve; the scan still falls back per repo when it does not (choosePairing).
// Elixir under-resolves (see boundaryGraphDisposition) and Python src-layout
// imports are not resolved, so both keep the filename rule.
const IMPORT_PAIRED_STACKS = new Set(['frontend']);
function testPairingDisposition(stack) {
return IMPORT_PAIRED_STACKS.has(stack) ? 'imports' : 'filename';
}

module.exports = { documentedApisDisposition, boundaryGraphDisposition, testPairingDisposition };


/***/ }),
Expand Down
Loading
Loading