Files
msd-core/docs/features/unreadable-directory-scope-signal.md
Tom Boucher 77dcdda534 enhance(#4014): an unreadable directory must not report as an empty one (#4163)
* 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>
2026-09-02 08:12:57 -04:00

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 the SCOPE discriminator, 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 call findContextMdIn for the scope signal and then still run their own, pre-existing fs.readdirSync on 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.