Operation robustness: one dangling symlink aborts a whole self-verify/weigh run; renumber apply has no partial-failure rollback #49

Closed
opened 2026-07-23 14:50:30 +00:00 by crenshawdev · 0 comments
crenshawdev commented 2026-07-23 14:50:30 +00:00 (Migrated from github.com)

Severity: low · Area: self-verify / renumber · surfaced in the post-v1.2.0 cross-model review sweep (bundled: two operation-resilience gaps). Related to #44 (the other self-verify robustness gap).

cadence-core/bin/lib/surface-weight.mjs:31,40,48 and cadence-core/bin/self-verify.mjs:117 guard files with f.endsWith('.md') && statSync(f).isFile(). statSync on a dangling .md symlink under agents/, skills/, or cadence-core/workflows/ throws ENOENT, which propagates out of the generator. The consumers' top-level try catches it and the whole run collapses to a single opaque {ok:false, reason:"internal"} — every real lint/weight result for that run is lost, not just the bad entry.

2. renumber apply has no partial-failure rollback

cadence-core/bin/planning.mjs:682-689: apply runs git rm → the directory moves → the file rewrites in sequence, with no transaction. A gitMv failure partway (permissions, collision, cross-device) leaves some phases/K directories moved and the ROADMAP/REQUIREMENTS/STATE not yet rewritten — an inconsistent tree — surfaced only as a generic fail('internal') that names neither what moved nor that the operation was partial. The --dry-run gate reduces likelihood but there is no rollback and no partial-state report.

Fix direction

Skip an entry whose statSync fails (e.g. statSync(f, { throwIfNoEntry: false }) or a try/catch) so one bad symlink can't sink the run; and for renumber apply, either stage the moves for rollback on failure or, at minimum, report which moves completed so a partial tree can be repaired deliberately.

**Severity:** low · **Area:** self-verify / renumber · surfaced in the post-v1.2.0 cross-model review sweep (bundled: two operation-resilience gaps). Related to #44 (the other self-verify robustness gap). ## 1. One dangling symlink aborts an entire self-verify / weigh run `cadence-core/bin/lib/surface-weight.mjs:31,40,48` and `cadence-core/bin/self-verify.mjs:117` guard files with `f.endsWith('.md') && statSync(f).isFile()`. `statSync` on a dangling `.md` symlink under `agents/`, `skills/`, or `cadence-core/workflows/` throws `ENOENT`, which propagates out of the generator. The consumers' top-level try catches it and the whole run collapses to a single opaque `{ok:false, reason:"internal"}` — every real lint/weight result for that run is lost, not just the bad entry. ## 2. `renumber` apply has no partial-failure rollback `cadence-core/bin/planning.mjs:682-689`: apply runs `git rm` → the directory moves → the file rewrites in sequence, with no transaction. A `gitMv` failure partway (permissions, collision, cross-device) leaves some `phases/K` directories moved and the ROADMAP/REQUIREMENTS/STATE not yet rewritten — an inconsistent tree — surfaced only as a generic `fail('internal')` that names neither what moved nor that the operation was partial. The `--dry-run` gate reduces likelihood but there is no rollback and no partial-state report. ## Fix direction Skip an entry whose `statSync` fails (e.g. `statSync(f, { throwIfNoEntry: false })` or a try/catch) so one bad symlink can't sink the run; and for `renumber` apply, either stage the moves for rollback on failure or, at minimum, report which moves completed so a partial tree can be repaired deliberately.
Sign in to join this conversation.
No description provided.