fix: normalize paths before deduping intent plan steps (#41) - #74
Merged
Conversation
The intent planner deduped reference files by raw string, so the same file reached as "src/a.ts", "./src/a.ts", and "/abs/proj/src/a.ts" produced one plan step per spelling, and a reference to the definition file spelled differently from the definition escaped the `!== definition.file` filter and got renamed a second time. Adds `src/core/path-normalize.ts` with `normalizePath` and `pathsEqual`, and routes the two `refFiles` computations in `generatePlan` through them. Rebuilt on current main rather than merged. The original branch introduced these helpers as `src/core/paths.ts`, which is now the write-boundary module (assertWritable, safeWrite, atomicWrite, findProjectRoot, PathDeniedError and the unoverridable deny-list) imported by seven modules; landing it as authored would have deleted every write guard in the codebase. The helpers live in a separate module so the two concerns cannot collide again. Also dropped from the original branch: - The remove-parameter dedupe test. Main refuses remove-parameter outright (UnsupportedIntentError, #66), so the test could not run. - An unused `relative` import, unused `beforeEach`/`afterEach` imports, and an unused `cwd` local. - The undefined-only guard in `pathsEqual`, which made the relation inconsistent: (null, undefined) compared false while (null, null) and (null, "") compared true. `normalizePath` already collapses every nullish and blank form to "", so the guard was both redundant and wrong. Adds coverage for the previously untested branches: cwd itself, paths outside cwd staying absolute, a sibling directory sharing a cwd prefix not being mistaken for a child, and the full nullish/blank equality matrix. Verified by mutation: reverting either normalization call site fails the two new intent tests. Full suite 614 pass / 0 fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New core module from #41 was missing from the architecture doc, which the Docs Verify gate caught. Records the cwd-relative constraint that makes it unsuitable as a lock key, alongside the locking.ts section that depends on the opposite property. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Owner
Author
|
🎉 This PR is included in version 4.0.6 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rebuilt replacement for #71, which could not be merged. Closes #41.
The bug
generatePlandeduped reference files by raw string. The same file reached assrc/a.ts,./src/a.ts, and/abs/proj/src/a.tsproduced one plan step per spelling. Worse, a reference to the definition file spelled differently from the definition escaped the!== definition.filefilter and got renamed a second time.Adds
src/core/path-normalize.ts(normalizePath,pathsEqual) and routes bothrefFilescomputations ingeneratePlanthrough it.Why this replaces #71 rather than updating it
#71 introduced these helpers as
src/core/paths.ts. That branch was cut atfb1172e, beforepaths.tsexisted on main; main then added it in #54 and #65 as the write-boundary module —assertWritable,safeWrite,atomicWrite,findProjectRoot,PathDeniedError, and the deny-list that no flag overrides (~/.ssh,~/.aws,/etc, shell rc files, the telemetry log).Seven modules import it:
router.ts,hash-edit.ts,diff-engine.ts,locking.ts,plan-executor.ts,verify.ts,index.ts. #71 arrived asnew file mode 100644at that path, so landing it as authored would have deleted every write guard in the codebase. That is the source of itsCONFLICTINGstatus, and a plausible conflict resolution ("take theirs") silently produces exactly that outcome.git rebase origin/mainon #71 hits an add/add conflict onpaths.tswhere resolving in commit order renames main's write boundary. Rebuilding on current main was the safer path. The helpers now live in a separate module so the two concerns cannot collide again.Also dropped from #71
remove-parameterdedupe test — main refuses that operation outright (UnsupportedIntentError, fix(intent): report unresolved plan steps instead of injecting placeholder comments #66), so the test could not run.relativeimport, unusedbeforeEach/afterEachimports, an unusedcwdlocal.pathsEqual, which made the relation inconsistent:(null, undefined)compared false while(null, null)and(null, "")compared true.normalizePathalready collapses every nullish and blank form to"", so the guard was redundant and wrong.Added coverage
The two non-trivial branches were untested in #71. Now covered: cwd itself, paths outside cwd staying absolute, a sibling directory sharing a cwd prefix not being mistaken for a child, and the full nullish/blank equality matrix.
Verification
Mutation-tested: reverting either normalization call site fails both new intent tests. The suite is 614 pass / 0 fail;
bun run lint:docsis green.Known constraint (documented, not changed)
normalizePathreturns cwd-relative for paths under cwd and absolute otherwise, so its output depends onprocess.cwd(). That is fine for this use — deduping within a singlegeneratePlancall — but makes it unsafe as a persisted key.locking.tsis deliberately cwd-independent for that reason. Recorded in the CLAUDE.md module map so the distinction survives.🤖 Generated with Claude Code