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

Criteria completion

Criteria completion: 4 of 4 (100%) checked; tracking started on Oct 16, no incremental history yet0%25%50%75%100%Oct 16Oct 28

Tracking started Oct 16 — check back for trends.

Branches 1
History 6
  1. 7fc6957
    Created (fixed)by bjornolofandersson
  2. 281937c
    Content editedby Claude
    ci: keep the checkout shallow; unshallow only in the --against step
  3. df590cc
    Content editedby Claude
    plan: mark WORK-641..644 done and BUG-040/041 fixed, SPEC-176 implemente
  4. 25ad932
    Content editedby Claude
    BUG-040: plan validate --against reads the merge-base
  5. 711f598
    Content editedby Claude
    plan: v0.41.1 milestone for readable release notes and BUG-040
  6. 7ed317e
    Content editedby Claude
    plan: BUG-040 validate --against misses IDs the base added after the bra

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).