* test(#4014): add failing-first coverage for unreadable-vs-empty directory scope (epic #3473 B4) * fix(#4014): an unreadable directory must not report as an empty one (epic #3473 B4) * test(#4014): update hardcoded generateSlugInternal closing-brace line after import shift src/core-utils.cts's new #4014 import block shifted every subsequent line by 6, moving generateSlugInternal's real closing brace from line 193 to 199. tests/slug-derivation-drift-guard.test.cjs's MAJOR-1 fixture hardcodes that line number to plant a synthetic violation immediately after the function's real body; the guard script itself locates the boundary dynamically via brace-matching and needed no change. * docs(#4014): document the unreadable-directory scope signal and add changeset * docs(#4014): backfill changeset PR number to #4163 * test(#4014): kill pre-existing core-utils.cjs mutation-score gap, unrelated to this issue's diff --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -33,6 +33,10 @@ const { scanPhasePlans } = planScanMod;
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import phaseIdMod = require('./phase-id.cjs');
|
||||
const { scopeToPhase } = phaseIdMod;
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import planningScopeMod = require('./planning-scope.cjs');
|
||||
const { SCOPE } = planningScopeMod;
|
||||
type Scope = planningScopeMod.Scope;
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -73,8 +77,16 @@ interface GapResult {
|
||||
* attempted) or was read successfully. A message naming the phase directory
|
||||
* when it EXISTS but `readdirSync` failed (EACCES/EIO/...) — never
|
||||
* collapsed to the same `[]` an absent directory produces.
|
||||
*
|
||||
* #4014 (epic #3473 B4): kept additive — `phase_dir_scope` below is the
|
||||
* new, typed sibling signal; this field is now derived from it rather than
|
||||
* owning its own readdirSync.
|
||||
*/
|
||||
phase_dir_read_error: string | null;
|
||||
/** #4014 (epic #3473 B4): the SCOPE this phase dir's listing resolved to —
|
||||
* SCOPE.UNREADABLE distinguishes a real read failure from a genuinely
|
||||
* empty/absent phase dir (SCOPE.COMPLETE). */
|
||||
phase_dir_scope: Scope;
|
||||
}
|
||||
|
||||
interface RunGapAnalysisOptions {
|
||||
@@ -347,6 +359,7 @@ function runGapAnalysis(cwd: string, phaseDir: string, options: RunGapAnalysisOp
|
||||
summary: 'workflow.post_planning_gaps disabled — skipping post-planning gap analysis',
|
||||
counts: { total: 0, covered: 0, uncovered: 0 },
|
||||
phase_dir_read_error: null,
|
||||
phase_dir_scope: SCOPE.COMPLETE,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -372,17 +385,20 @@ function runGapAnalysis(cwd: string, phaseDir: string, options: RunGapAnalysisOp
|
||||
|
||||
// Read the phase directory once; reuse the listing for both context detection
|
||||
// and plan-file enumeration (avoids redundant readdirSync calls).
|
||||
let phaseDirFiles: string[] = [];
|
||||
// #3885 (ADR-3473 §8.5): the existsSync guard above already means a catch
|
||||
// here is NEVER "genuinely absent" (ENOENT) — this directory exists, so any
|
||||
// failure to list it is a real read error (EACCES/EIO/...) and must be
|
||||
// named, not folded into the same `[]` an absent directory produces.
|
||||
let phaseDirReadError: string | null = null;
|
||||
try {
|
||||
if (fs.existsSync(absPhaseDir)) phaseDirFiles = fs.readdirSync(absPhaseDir);
|
||||
} catch (err) {
|
||||
phaseDirReadError = `Could not read phase directory ${formatDiagnosticToken(absPhaseDir)}: ${formatDiagnosticToken((err as Error)?.message ?? String(err))}`;
|
||||
}
|
||||
//
|
||||
// #4014 (epic #3473 B4): findContextMdIn's directory-string form now owns
|
||||
// the listing + ENOENT-vs-other discrimination, retiring the local
|
||||
// existsSync-guarded readdirSync try/catch — ENOENT (genuinely absent)
|
||||
// and a successful-but-empty read both resolve to SCOPE.COMPLETE with an
|
||||
// empty listing, exactly like the existsSync guard's short-circuit did.
|
||||
const { files: phaseDirFiles, scope: phaseDirScope } = findContextMdIn(absPhaseDir);
|
||||
// #3885 (ADR-3473 §8.5): `phaseDirReadError` stays additive for the
|
||||
// shipped `phase_dir_read_error` JSON field — derived from `phaseDirScope`
|
||||
// rather than from its own caught error, since findContextMdIn's
|
||||
// directory-string form reports SCOPE, not the raw errno message.
|
||||
const phaseDirReadError = phaseDirScope === SCOPE.UNREADABLE
|
||||
? `Could not read phase directory ${formatDiagnosticToken(absPhaseDir)}`
|
||||
: null;
|
||||
|
||||
// #3511-class: scope the raw listing to this phase dir before the
|
||||
// phase-numbered -CONTEXT.md predicate. `phaseDirFiles` itself stays raw —
|
||||
@@ -450,6 +466,7 @@ function runGapAnalysis(cwd: string, phaseDir: string, options: RunGapAnalysisOp
|
||||
summary: coverageSummary + '; extracted 0 of N — possible format mismatch',
|
||||
counts: { total: rows.length, covered, uncovered },
|
||||
phase_dir_read_error: phaseDirReadError,
|
||||
phase_dir_scope: phaseDirScope,
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -459,6 +476,7 @@ function runGapAnalysis(cwd: string, phaseDir: string, options: RunGapAnalysisOp
|
||||
summary: 'extracted 0 of N — possible format mismatch',
|
||||
counts: { total: 0, covered: 0, uncovered: 0 },
|
||||
phase_dir_read_error: phaseDirReadError,
|
||||
phase_dir_scope: phaseDirScope,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -477,6 +495,7 @@ function runGapAnalysis(cwd: string, phaseDir: string, options: RunGapAnalysisOp
|
||||
summary: 'no requirements or decisions to check',
|
||||
counts: { total: 0, covered: 0, uncovered: 0 },
|
||||
phase_dir_read_error: phaseDirReadError,
|
||||
phase_dir_scope: phaseDirScope,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -498,6 +517,7 @@ function runGapAnalysis(cwd: string, phaseDir: string, options: RunGapAnalysisOp
|
||||
summary,
|
||||
counts: { total: rows.length, covered, uncovered },
|
||||
phase_dir_read_error: phaseDirReadError,
|
||||
phase_dir_scope: phaseDirScope,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user