* 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>
2.6 KiB
id, title, group
| id | title | group |
|---|---|---|
| 4014 | Unreadable-Directory Scope Signal | v1.7.0 Features |
Purpose: ADR-3473 §8.4 ("failure is a value") applies to filesystem
listings, not only command argv. #3885 (B5) gave roadmap analyze,
gap-checker, and init's JSON bundles a context_read_error /
phase_dir_read_error string naming an unreadable phase directory — but the
underlying has_context / hasContext boolean stayed false either way,
so a consumer branching on that boolean alone still cannot tell "genuinely
no context file" from "could not read the directory at all." This closes
that gap with a typed signal, reusing ADR-3180's existing frozen SCOPE
enum rather than a new vocabulary.
findContextMdIn (src/planning-workspace.cts) now reports its own
scope. Called with a directory path, it returns { file, files, scope }
instead of a bare filename-or-null, and never throws — an unreadable
directory reports scope: 'unreadable' (previously it threw, forcing every
caller to hand-roll its own try/catch); a genuinely absent directory
(ENOENT) reports scope: 'complete', the same "real empty" answer as
today. The array-input call form (an already-read listing) is unchanged.
Five downstream call sites gain an additive scope field, none renamed
or removed: roadmap analyze's AnalyzePhase.context_scope,
gap-checker's phase_dir_scope, and init's context_scope on all three
JSON bundles (init plan-phase, init phase-op, init manager) —
including cmdInitManager, whose own read failure previously vanished into
a bare empty catch {} with no signal of any kind. getPhaseFileStats
(src/core-utils.cts) — the shared listing owner behind roadmap analyze
and init's has_context — no longer lets its own failed read get masked
by an unrelated, already-successful scanPhasePlans scope on the same
phase directory.
Known limits:
context_read_error/phase_dir_read_error's message text is now a fixed "Could not read phase directory<path>" rather than embedding the underlying OS errno text —findContextMdIn's directory-string form reports only theSCOPEdiscriminator, not the raw caught error. The field's presence and type are unchanged; only its message detail is coarser than before #4014.init.cts's three call sites callfindContextMdInfor the scope signal and then still run their own, pre-existingfs.readdirSyncon the same path for the rest of their output — an intentional, additive-only choice to avoid altering already-complex failure control-flow at those sites, not a performance optimization.