Skip to content

feat(dev): add a docs/state.md rebase-conflict resolver - #1389

Draft
lusoris wants to merge 1 commit into
masterfrom
tools/state-md-conflict-resolver
Draft

feat(dev): add a docs/state.md rebase-conflict resolver#1389
lusoris wants to merge 1 commit into
masterfrom
tools/state-md-conflict-resolver

Conversation

@lusoris

@lusoris lusoris commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Summary

docs/state.md conflicts on nearly every rebase of a branch that touches it, and
unlike the other append-only bookkeeping files it is deliberately not in the
merge=union list — its rows move between "Open bugs" and "Recently closed",
so a union merge would duplicate the row and leave a closed bug reading as open
forever.

That leaves a by-hand resolution every time, with "keep both sides" as the
tempting wrong answer. scripts/ci/check-state-md-rows.sh gates it — and earned
its keep today: a rebase sweep of seven PRs onto master produced exactly that
breakage in all seven, and the gate caught every one.

This encodes the correct rule instead:

  • master's side wins for any row both sides carry — master is the more
    advanced state, already carrying every row merged ahead of the branch,
    including one the branch also touches but master has since moved or reworded.
  • the branch contributes only rows master does not have at all — its own new
    bug id.

Deduplication is by bug id rather than by line, so a row master reworded is not
re-added in its stale form.

The docs are explicit about the case the script cannot decide: a row the branch
itself moved to "Recently closed" loses to master's older "Open bugs" copy and
has to be redone by hand. Verifying with the gate afterwards is not optional.

Both scripts assemble conflict markers at runtime ("<" * 7) rather than
containing them literally, so they don't trip the no-conflict-markers hook on
themselves.

Reproducer / smoke-test command

# Regression test: 6 assertions over a synthetic three-case conflict
python3 scripts/dev/test-resolve-state-md-conflict.py
# -> PASS: all 6 assertions

# Real use, mid-rebase with docs/state.md conflicted:
python3 scripts/dev/resolve-state-md-conflict.py docs/state.md
scripts/ci/check-state-md-rows.sh
git add docs/state.md && git rebase --continue

Verified in anger on nine branches today (#1367-#1374, #1382, #1383): all report
check-state-md-rows: OK with no duplicate ids after resolution.

Deep-dive deliverables (ADR-0108)

  • Research digest — no digest needed: the rule is stated in .gitattributes' own exclusion comment; this encodes it.
  • Decision matrix — no alternatives: only-one-way fix. The obvious alternative (add docs/state.md merge=union) is the exact thing .gitattributes documents as wrong, and PR fix(docs): one row per bug id in state.md, and a gate that keeps it that way #1322 already removed it.
  • AGENTS.md invariant note — no rebase-sensitive invariants.
  • Reproducer / smoke-test command — above.
  • Changelog fragmentchangelog.d/added/0165-state-md-conflict-resolver.md, CHANGELOG.md regenerated.
  • Rebase note — no rebase impact: new fork-local dev tooling, no upstream counterpart.

Docs (rule 10)

docs/development/ci.md gains a
"Resolving a docs/state.md rebase conflict" section covering the rule, the
commands, and the case the script cannot decide.
scripts/ci/check-state-md-rows.sh names the script in its failure message.

State (rule 13)

no state delta: developer tooling, no bug opened or closed.

🤖 Generated with Claude Code

docs/state.md conflicts on nearly every rebase of a branch that touches it,
and unlike the other append-only bookkeeping files it is deliberately not in
the merge=union list: its rows move between "Open bugs" and "Recently closed",
so a union merge would duplicate the row and leave a closed bug reading as
open forever.

That leaves a by-hand resolution every time, with "keep both sides" as the
tempting wrong answer -- which is what check-state-md-rows.sh gates, and what
a rebase sweep of seven PRs onto master produced today before the gate caught
it.

Encode the correct rule instead. master's side wins for any row both sides
carry, because master is the more advanced state; the branch contributes only
rows master does not have at all. Deduplication is by bug id rather than by
line, so a row master reworded is not re-added in its stale form.

The docs and the gate's failure message both name the script, and the docs are
explicit about the case it cannot decide: a row the branch itself moved to
"Recently closed" loses to master's older "Open bugs" copy and has to be redone
by hand. Verifying with check-state-md-rows.sh afterwards is not optional.

Both scripts assemble conflict markers at runtime rather than containing them
literally, so they do not trip the no-conflict-markers hook on themselves.

Refs: ADR-0165

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant