* 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:
@@ -40,6 +40,12 @@ import phaseIdModule = require('./phase-id.cjs');
|
||||
import planningWorkspace = require('./planning-workspace.cjs');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import shellCommandProjection = require('./shell-command-projection.cjs');
|
||||
// planning-scope.cjs is a leaf module (no imports of its own — see its file
|
||||
// header), so importing it here directly cannot introduce a cycle.
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import planningScopeMod = require('./planning-scope.cjs');
|
||||
const { SCOPE } = planningScopeMod;
|
||||
type Scope = planningScopeMod.Scope;
|
||||
|
||||
// ─── Line-ending normalization ─────────────────────────────────────────────────
|
||||
|
||||
@@ -250,14 +256,14 @@ interface PhaseFileStats {
|
||||
hasContext: boolean;
|
||||
hasVerification: boolean;
|
||||
hasReviews: boolean;
|
||||
scope: string;
|
||||
scope: Scope;
|
||||
}
|
||||
|
||||
// Minimal shape this module needs from plan-scan.cjs's scanPhasePlans result.
|
||||
interface PlanScanResultShape {
|
||||
planFiles: string[];
|
||||
summaryFiles: string[];
|
||||
scope: string;
|
||||
scope: Scope;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -291,16 +297,40 @@ interface PlanScanResultShape {
|
||||
* Degrades on an unreadable directory instead of throwing: empty arrays,
|
||||
* every flag false, scope UNREADABLE (mirroring scanPhasePlans's own
|
||||
* degrade path).
|
||||
*
|
||||
* #4014 (epic #3473 B4-unreadable): this function's OWN readdirSync used to
|
||||
* catch its own failure and then return `scope: scan.scope` — the scope of
|
||||
* the UNRELATED, already-successful scanPhasePlans call — silently dropping
|
||||
* that THIS read failed. The own-readdirSync is now routed through
|
||||
* findContextMdIn's directory-string form (which never throws and reports
|
||||
* its own SCOPE.UNREADABLE), and the two independently-scoped answers are
|
||||
* combined into the worse of the two below — never letting a successful
|
||||
* scan.scope mask this function's own failed read.
|
||||
*
|
||||
* The combination is written out as a two-value comparison rather than
|
||||
* calling planning-snapshot.cts's `worstScope` directly: that module
|
||||
* requires core-utils.cjs (for `findOrphanSummaries`) at its own top level,
|
||||
* so importing it back here would create a tight, direct
|
||||
* core-utils.cjs <-> planning-snapshot.cjs cycle — a materially different
|
||||
* (and riskier) shape than this file's existing, documented lazy-access
|
||||
* cyclic partners (planning-workspace.cjs, phase-id.cjs), which
|
||||
* planning-snapshot.cts sits *above* in the dependency graph, not beside.
|
||||
* `findContextMdIn`'s directory-string form can only ever report
|
||||
* SCOPE.COMPLETE or SCOPE.UNREADABLE (a single readdirSync is binary — see
|
||||
* its own doc comment), so per planning-snapshot.cts's SCOPE_SEVERITY
|
||||
* ordering (UNREADABLE is maximal), `worstScope(scan.scope, ownScope)` is
|
||||
* exactly `ownScope === UNREADABLE ? UNREADABLE : scan.scope` — the
|
||||
* expression below is that identity, not a re-derivation of the ordering.
|
||||
*/
|
||||
function getPhaseFileStats(phaseDir: string): PhaseFileStats {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
||||
const scanPhasePlans: (dir: string) => PlanScanResultShape = require('./plan-scan.cjs');
|
||||
const scan = scanPhasePlans(phaseDir);
|
||||
|
||||
let files: string[];
|
||||
try {
|
||||
files = fs.readdirSync(phaseDir);
|
||||
} catch {
|
||||
const { files, scope: ownScope } = planningWorkspace.findContextMdIn(phaseDir);
|
||||
const scope: Scope = ownScope === SCOPE.UNREADABLE ? SCOPE.UNREADABLE : scan.scope;
|
||||
|
||||
if (ownScope === SCOPE.UNREADABLE) {
|
||||
return {
|
||||
plans: scan.planFiles,
|
||||
summaries: scan.summaryFiles,
|
||||
@@ -308,7 +338,7 @@ function getPhaseFileStats(phaseDir: string): PhaseFileStats {
|
||||
hasContext: false,
|
||||
hasVerification: false,
|
||||
hasReviews: false,
|
||||
scope: scan.scope,
|
||||
scope,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -321,7 +351,7 @@ function getPhaseFileStats(phaseDir: string): PhaseFileStats {
|
||||
hasContext: planningWorkspace.findContextMdIn(scopedFiles) !== null,
|
||||
hasVerification: scopedFiles.some(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md'),
|
||||
hasReviews: scopedFiles.some(f => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md'),
|
||||
scope: scan.scope,
|
||||
scope,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user