scripts/ci-test-scope.cjs's classify() previously set full_matrix=true for ANY changed tests/**/*.test.cjs file, unconditionally (restored by #4421 after #962's narrowing let a real macOS-only regression, PR #4384, land undetected). This replaces that blanket rule with a reachability check against real data instead of a path prefix: - A changed test file forces full_matrix only when it is present in Phase 2's committed CONFORMANCE_TIER_FILES list (scripts/lib/platform- conformance-tier.generated.cjs) -- direct membership, not a graph walk. - A changed src/ file forces full_matrix when its own content carries a genuine platform-conditional signal, reusing gen-platform-conformance- tier.cjs's classifyContent with a narrowed, source-code-safe signal subset (excludes two categories -- hardcoded-path-vs-path-call and symlink-keyword -- empirically found to flag 100/235 src/ files when applied verbatim, versus 28/235 with the narrow subset, all verified to carry genuine platform branches). New export: NOISY_FOR_SOURCE_REACHABILITY. - A change to the classification mechanism's own definition files (gen-platform-conformance-tier.cjs, the generated tier list, or suite-detection.cjs) always forces full_matrix -- the mechanism being changed cannot presume its own new output is safe. - Any computation error (a require/read failure, a malformed module) fails safe to full_matrix=true, per the issue's explicit requirement. The existing RULES array entries with their own fullMatrix:true (workflow automation, installer/package layout, hooks, environment/dependency gates, test harness) are deliberately left untouched -- they are curated, narrowly-scoped triggers for "this diff changes the CI/installer/hooks mechanism itself," a different and still-valid reason than "product code might reach a platform branch." Disclosed in .gsd/phase/.../40-design.md as a scope decision, since the issue's "Done when" wording read broader than its "Proposed work" bullets. Two design assumptions were caught and corrected before any code was written (rubber-duck pass, documented in 40-design.md): (1) reusing Phase 2's classifyContent verbatim against src/ was far too noisy; (2) a single hardcoded seam file (src/shell-command-projection.cts only, per CLAUDE.md's "single platform seam" framing) would have silently missed genuine, independent platform branches in src/runtime-hooks-surface.cts, src/capability-lock.cts, src/capability-ledger.cts, and src/surface.cts -- reintroducing the #4421 failure shape inside src/ instead of tests/. An isolated code-review pass found and fixed one real defect (a dead, untested branch that would have survived Stryker mutation testing) and one design-doc completeness gap (2 of 8 "narrow" signal categories were left implicitly rather than explicitly audited). An isolated security-review pass found no qualifying findings. tests/ci-test-scope.test.cjs gains the full #4592 boundary-case matrix (.gsd/phase/.../50-test-matrix.md), including a named #4421 regression case proving tests/state-todos-render.test.cjs still forces full_matrix, now for the documented reason instead of the removed blanket rule. Two pre-existing tests were corrected: one used a nonexistent fixture path (src/semver.cts -> src/semver-compare.cts, a real file); one (A3) asserted the exact old blanket-rule behavior this issue removes, updated to the new, verified- correct expectation. Co-authored-by: sim <sim@local> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,9 +3,10 @@
|
||||
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
const { existsSync, readdirSync, appendFileSync } = require('fs');
|
||||
const { existsSync, readdirSync, appendFileSync, readFileSync } = require('fs');
|
||||
|
||||
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
||||
const { classifyContent, NOISY_FOR_SOURCE_REACHABILITY } = require('./gen-platform-conformance-tier.cjs');
|
||||
|
||||
// Workflow files that are purely administrative / policy bots. Changes to these
|
||||
// files do NOT require the cross-platform test matrix — only a lightweight
|
||||
@@ -487,7 +488,55 @@ function addAll(set, values) {
|
||||
const WINDOWS_HINTS = ['windows', 'win32', 'shell', 'path'];
|
||||
const isWindowsHint = s => WINDOWS_HINTS.some(k => s.toLowerCase().includes(k));
|
||||
|
||||
function classify(files) {
|
||||
// A change to the classification mechanism itself cannot be presumed safe by
|
||||
// the very mechanism being changed (#4592).
|
||||
const CLASSIFIER_DEFINITION_FILES = new Set([
|
||||
'scripts/gen-platform-conformance-tier.cjs',
|
||||
'scripts/lib/platform-conformance-tier.generated.cjs',
|
||||
'scripts/lib/suite-detection.cjs',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Does `file`'s blast radius reach (a) Phase 2's conformance-tier test-file
|
||||
* list or (b) a live platform-conditional signal in src/? Fail-safe: any
|
||||
* thrown error (a require failure, a readFileSync failure, a malformed
|
||||
* generated module, etc.) is treated as uncertainty and returns true — per
|
||||
* #4592's explicit "fail-safe to full_matrix=true on any reachability-
|
||||
* computation error or uncertainty" requirement.
|
||||
* `loadConformanceTier` is injectable (defaults to the real generated module)
|
||||
* solely so tests can simulate a load failure without touching the real,
|
||||
* committed generated file.
|
||||
* @param {string} file
|
||||
* @param {{loadConformanceTier?: () => {CONFORMANCE_TIER_FILES: string[]}}} [deps]
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function reachesConformanceTierOrSeam(file, deps = {}) {
|
||||
const loadConformanceTier =
|
||||
deps.loadConformanceTier || (() => require('./lib/platform-conformance-tier.generated.cjs'));
|
||||
try {
|
||||
if (file.startsWith('tests/') && file.endsWith('.test.cjs')) {
|
||||
const { CONFORMANCE_TIER_FILES } = loadConformanceTier();
|
||||
return CONFORMANCE_TIER_FILES.includes(file);
|
||||
}
|
||||
|
||||
if (file.startsWith('src/')) {
|
||||
const content = readFileSync(file, 'utf8');
|
||||
const { signals } = classifyContent(content);
|
||||
const narrowSignals = signals.filter(signal => !NOISY_FOR_SOURCE_REACHABILITY.has(signal));
|
||||
return narrowSignals.length > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// `reachabilityDeps` is injectable (defaults to {}, which makes
|
||||
// reachesConformanceTierOrSeam use the real generated module) solely so
|
||||
// tests can simulate a reachability-computation failure without touching
|
||||
// the real, committed generated file.
|
||||
function classify(files, reachabilityDeps = {}) {
|
||||
const targeted = new Set();
|
||||
const windows = new Set();
|
||||
const reasons = [];
|
||||
@@ -532,8 +581,30 @@ function classify(files) {
|
||||
// lane already covered them. Rescinded per #4421: PR #4384 landed a
|
||||
// macOS-only regression on 2026-09-06 that stayed invisible pre-merge
|
||||
// precisely because this carve-out suppressed the only macOS signal.
|
||||
// The ~25-runner-minute cost on test-touching PRs is accepted.
|
||||
// #4592: the blanket rule is replaced with a reachability check — only
|
||||
// a changed test file that actually reaches Phase 2's conformance-tier
|
||||
// list (or is the classification mechanism itself) forces full_matrix.
|
||||
if (reachesConformanceTierOrSeam(file, reachabilityDeps)) {
|
||||
fullMatrix = true;
|
||||
reasons.push(`${file}: conformance-tier reachability`);
|
||||
}
|
||||
}
|
||||
|
||||
// #4592: a src/-only diff (no test file touched) must still be able to
|
||||
// set full_matrix=true when it carries a live platform-conditional
|
||||
// signal — this is independent of the tests/ branch above.
|
||||
if (file.startsWith('src/') && reachesConformanceTierOrSeam(file, reachabilityDeps)) {
|
||||
fullMatrix = true;
|
||||
reasons.push(`${file}: platform seam reachability`);
|
||||
}
|
||||
|
||||
// #4592: a changed file that IS the classification mechanism itself
|
||||
// (neither under tests/ nor src/, so neither branch above reaches it)
|
||||
// must also force full_matrix — a change to the mechanism cannot be
|
||||
// presumed safe by the very mechanism being changed.
|
||||
if (CLASSIFIER_DEFINITION_FILES.has(file)) {
|
||||
fullMatrix = true;
|
||||
reasons.push(`${file}: reachability classifier definition changed`);
|
||||
}
|
||||
|
||||
for (const rule of RULES) {
|
||||
@@ -629,4 +700,12 @@ if (require.main === module) {
|
||||
runMain(main);
|
||||
}
|
||||
|
||||
module.exports = { RULES, missingRuleTestFiles, PROTECTED_WORKFLOWS, INERT_WORKFLOWS, missingProtectedWorkflows };
|
||||
module.exports = {
|
||||
RULES,
|
||||
missingRuleTestFiles,
|
||||
PROTECTED_WORKFLOWS,
|
||||
INERT_WORKFLOWS,
|
||||
missingProtectedWorkflows,
|
||||
classify,
|
||||
reachesConformanceTierOrSeam,
|
||||
};
|
||||
|
||||
@@ -150,6 +150,14 @@ const CATEGORIES = [
|
||||
},
|
||||
];
|
||||
|
||||
// Two CATEGORIES entries precise enough for TEST-file classification (this
|
||||
// module's own purpose) but far too broad for SOURCE-file reachability
|
||||
// (scripts/ci-test-scope.cjs's #4592 use). Empirically verified: applying
|
||||
// classifyContent to every file under src/ (235 files) flags 100 of them,
|
||||
// driven almost entirely by these two categories; excluding them narrows it
|
||||
// to 28 files, all verified to carry a genuine platform-conditional branch.
|
||||
const NOISY_FOR_SOURCE_REACHABILITY = new Set(['hardcoded-path-vs-path-call', 'symlink-keyword']);
|
||||
|
||||
/**
|
||||
* Pure classifier: given a test file's raw string content, returns which
|
||||
* platform-conformance categories matched and whether the file needs real-OS
|
||||
@@ -333,4 +341,4 @@ if (require.main === module) {
|
||||
runMain(main);
|
||||
}
|
||||
|
||||
module.exports = { classifyContent, CATEGORIES, walkTestFiles, classifyTree, renderGeneratedFile };
|
||||
module.exports = { classifyContent, CATEGORIES, NOISY_FOR_SOURCE_REACHABILITY, walkTestFiles, classifyTree, renderGeneratedFile };
|
||||
|
||||
Reference in New Issue
Block a user