Skip to content

fix(prd): keep a PRD it cannot read so its number is not reissued - #266

Merged
ralyodio merged 1 commit into
moshcoder:mainfrom
clawedassistant26:fix/prd-unreadable-file-dropped
Aug 3, 2026
Merged

fix(prd): keep a PRD it cannot read so its number is not reissued#266
ralyodio merged 1 commit into
moshcoder:mainfrom
clawedassistant26:fix/prd-unreadable-file-dropped

Conversation

@clawedassistant26

Copy link
Copy Markdown
Contributor

The bug

listPrds dropped any PRD whose readFileSync threw:

} catch { continue; }

That contradicts what the rest of the same loop does. A PRD whose front matter will not parse is deliberately kept, on the slug and "?" fallbacks, and two existing tests assert exactly that ("listPrds does not take a title or status from a PRD with no front matter"). A file that throws on read was the one case that got discarded instead.

Why it matters

The visible symptom is a missing row, but that is not the real cost. nextId takes its max from this list:

const ids = listPrds(root).map((p) => parseInt(p.id, 10)).filter(Number.isFinite);

So dropping a PRD hands its number straight back out, and two PRDs end up numbered the same in a scheme where the number is the identity.

A committed symlink whose target is not checked out is enough to trigger it. Git stores symlinks, so this arrives from a plain clone rather than a corrupt disk. readFileSync throws ENOENT.

Reproduced on main before touching anything, with 0001-first.md and a dangling 0002-search-ranking.md:

files on disk: 0001-first.md, 0002-search-ranking.md
listPrds ->    ["0001 0001-first.md"]
nextId   ->    0002          <-- 0002 is already taken on disk
createPrd wrote: prd/0002-search-ranking-v2.md
files after:   0001-first.md, 0002-search-ranking-v2.md, 0002-search-ranking.md

And what the user actually sees from the real CLI, before:

$ moshcode prd list
0001  draft     First
0002  Draft     Search Ranking v2

One 0002 listed, two 0002 files on disk, exit 0, no warning. After:

$ moshcode prd list
0001  draft     First
0002  ?         search-ranking
0003  Draft     Search Ranking v2

The fix

Keep the entry on the same fallbacks the unparseable case already uses. One line of behaviour: the continue becomes a no-op catch. The id stays reserved and the index row keeps its link.

Tests

Three regression tests and one control, all through the existing createPrd/listPrds/regenerateIndex exports:

  • listPrds keeps a PRD it cannot read instead of dropping it
  • createPrd does not reissue a number an unreadable PRD already holds — the headline
  • regenerateIndex keeps a row for a PRD it cannot read
  • listPrds is unchanged when every PRD is readablecontrol, asserts the listing and the index row are byte-identical on a clean tree, so this cannot churn existing indexes

Stashing only src/prd.mjs and rerunning: exactly the three bug tests fail, the control passes both ways.

Full suite green on fddd559 (v0.18.0): 1141 tests, 928 pass, 0 fail, 213 skipped. Baseline before the change was 1137/924/0/213, so it is +4 tests and +4 passes with nothing else moving.

Notes

  • npm install, and the generated package-lock.json was deleted rather than committed, since this is a pnpm repo.
  • Scratch dirs used for the reproduction (/tmp/prdrepro) were cleaned up.

listPrds discarded any PRD whose readFileSync threw. That contradicts the
rest of the same loop: a PRD whose front matter will not parse is kept on
the slug and "?" fallbacks, which two existing tests assert.

The consequence is not a missing row. nextId takes its max from this list,
so dropping a PRD hands its number straight back out and two PRDs end up
numbered the same, in a scheme where the number is the identity. A
committed symlink whose target is not checked out is enough to trigger it:
readFileSync throws ENOENT, so it arrives from a plain clone.

Keep the entry on the same fallbacks the unparseable case already uses.
The id stays reserved and the index row keeps its link.
@ralyodio
ralyodio merged commit 2415a89 into moshcoder:main Aug 3, 2026
4 checks passed
@ralyodio ralyodio mentioned this pull request Aug 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants