BUG-040
ID:BUG-040Status:fixed

plan validate --against misses a collision with an ID the base added after the branch point

refrakt plan validate --against <ref> exists to catch an ID collision on the branch, before the merge makes every reference to that ID ambiguous (SPEC-135 D8, WORK-582). It does not catch the most common one: two branches that each create the next free ID. It reports "No ID collisions" and exits 0, so the PR job passes it.

The cause is the deletion check in collisionsFrom (plugins/plan/src/commands/against.ts):

Severity:majorMilestone:v0.41.1Source:SPEC-135
claude/site-changelog-readability-k4p902 View source

Criteria completion

Criteria completion: 4 of 4 (100%) checked; history from Oct 15 to Oct 150%25%50%75%100%Oct 15Oct 15
Branches 1
claude/site-changelog-readability-k4p902 current fixed
main fixed
History 6
  1. 2153967
    Content editedby bjornolofandersson
  2. 281937c
    Content editedby bjornolofandersson
  3. df590cc
    Content editedby bjornolofandersson
  4. 25ad932
    • ☑ A collision with an ID the base added after the branch point is reported, naming both files
    • ☑ A base claimant the branch deleted or renamed is still not reported
    • ☑ A ref with no merge-base with `HEAD` fails with an error instead of reporting a clean run
    • ☑ `against.test.ts` has a test that advances the base after the branch point, and the existing tests still pass
    by bjornolofandersson
  5. 711f598
    Content editedby bjornolofandersson
  6. 7ed317e
    Created (confirmed)by bjornolofandersson

Steps to Reproduce

  1. On main, commit plan/work/WORK-001-alpha.md.
  2. Branch off, and add plan/work/WORK-002-from-branch.md there.
  3. Back on main, commit plan/work/WORK-002-from-main.md.
  4. On the branch, run refrakt plan validate --against main.

Expected

A collision on WORK-002, naming both files, and a non-zero exit.

Actual

No ID collisions with main (2 ids compared)., exit 0.

Environment

  • @refrakt-md/plan at the current main.
  • refrakt plan migrate ids --against shares readRefIds with the validator through baseClaimants, but it only acts on duplicates already present in the working tree, so it is not affected.

Approach

Tell the two cases apart with the merge-base, which git already knows. A base-side file that is absent from the working tree:

  • existed at git merge-base HEAD <ref>: the branch deleted or renamed it. One claimant survives the merge, so not a collision. This is the case the check was written for.
  • did not exist there: the base added it after the branch point. Both claimants survive the merge, so it is a collision.

When no merge-base exists (unrelated histories), fail loudly rather than guess, matching how an unresolvable ref is handled.

Acceptance Criteria

  • A collision with an ID the base added after the branch point is reported, naming both files
  • A base claimant the branch deleted or renamed is still not reported
  • A ref with no merge-base with HEAD fails with an error instead of reporting a clean run
  • against.test.ts has a test that advances the base after the branch point, and the existing tests still pass

References

  • SPEC-135 D8 and WORK-582: the check and why it runs pre-merge.

Resolution

Completed: 2026-10-15

Branch: claude/site-changelog-readability-k4p902 PR: refrakt-md/refrakt#704

What was done

  • plugins/plan/src/commands/against.ts: branchPointPaths lists plan files at git merge-base HEAD <ref>; collisionsFrom forgives a base claimant missing from the tree only if it existed there, plus a mirror rule for base-side renames/deletions. No merge-base is an error naming the fix (git fetch --unshallow).
  • cli-plugin.ts wires it into plan validate --against.
  • against.test.ts: five tests that advance the base after the branch point, plus unrelated history. Verified the main regression tests fail against the old condition.
  • .github/workflows/validate.yml: fetch-depth: 0; base fetched without --depth=1; base ref via env.
  • CLAUDE.md notes the history requirement; patch changeset.

Notes

  • migrate ids --against is unaffected: it does not call the new function.

Completed: 2026-10-16

Branch: claude/site-changelog-readability-k4p902 PR: refrakt-md/refrakt#704

What was done

  • plugins/plan/src/commands/against.ts: branchPointPaths lists plan files at git merge-base HEAD <ref>; collisionsFrom forgives a base claimant missing from the tree only if it existed there, plus a mirror rule for base-side renames/deletions. No merge-base is an error naming the fix (git fetch --unshallow).
  • cli-plugin.ts wires it into plan validate --against.
  • against.test.ts: five tests that advance the base after the branch point, plus unrelated history. Verified the main regression tests fail against the old condition.
  • .github/workflows/validate.yml: the --against step runs last and unshallows the checkout itself (git fetch --unshallow) before fetching the base; base ref via env.
  • CLAUDE.md notes the history requirement; patch changeset.

Notes

  • migrate ids --against is unaffected: it does not call the new function.
  • The first version used fetch-depth: 0 on the checkout. That made two tests that build real plan content time out (~170s on 3,473 commits against ~17s shallow), because the plan pipeline reads git history for timestamps and plan-history. The checkout stays shallow; only the last step pays for full history (~6s).