Skip to content

emrg: resolve the count-line merge conflict by measurement, not by choosing a side - #1131

Merged
argszero merged 4 commits into
masterfrom
feature/doc-count-resolve-conflict
Sep 11, 2026
Merged

emrg: resolve the count-line merge conflict by measurement, not by choosing a side#1131
argszero merged 4 commits into
masterfrom
feature/doc-count-resolve-conflict

Conversation

@argszero

Copy link
Copy Markdown
Owner

Why

Agent.md:122 is the repo's most-conflicted line. During a merge it arrives wrapped in conflict markers, so the document momentarily holds two counts, and the existing tool correctly refuses to act:

$ uv run --no-sync python3 scripts/check-doc-count.py --write
error: 2 documented Python counts found in Agent.md; exactly one is expected,
and this tool will not guess which one is real        (rc=2)

That refusal is right, and it is not the thing this PR changes. What it leaves undone is the repair: every unblock of a stale PR needs the same hand dance — view the conflict, strip the markers, then measure. Measured 2026-09-11: after #1130 merged, three sibling PRs went DIRTY with zero CI, and each needed that dance by hand.

The dance encodes a rule worth enforcing in code: neither side may win. Both sides of a count-line conflict are stale by construction — that is why they conflicted. Measured on a real conflicted merge of two sibling branches:

ours theirs merged tree actually collects
1335 1337 1339

Writing either side, or their average, would have been wrong. The only correct value comes from measuring the merged tree.

What

--resolve-conflict on scripts/check-doc-count.py:

  • removes the count line's conflict block, keeps the surrounding structure
  • then writes the number measured on the merged tree (measurement happens after the markers are gone, on the tree as it stands)
  • prints a real transition (conflict block removed, 1335 -> 1339 (measured on the merged tree))

Refusals (this is the point, not a detail)

A tool that deletes conflict markers anywhere is a footgun, so it refuses — rc=2, with the reason — unless the conflict is exactly the count line:

  • no conflict present → "nothing to resolve (use --write for a plain drift)"
  • more than one conflict block → refuses; it only resolves the count line
  • a conflict elsewhere in the doc → refuses, naming that it is not the count line
  • two sides that differ by more than the number → refuses: that is a content conflict, and choosing a side would be a real decision this tool must not make
  • markers surviving outside the block → refuses rather than writing a half-resolved doc

It also joins the existing mutually-exclusive mode group, so --resolve-conflict --write fails loud (rc=2) instead of silently dropping one request — the same class of defect the --write --dry-run guard already covers.

Tests (8 new, both states pinned per #455)

Refusal states and the positive path are each driven, never inferred from the failure case alone:

  • strips markers and leaves every other byte identical
  • refuses: no conflict / not the count line / sides differ beyond the number
  • end to end: two sides 1372 and 1337, tree says 1339 → 1339 is written, neither side wins
  • a conflict-free doc is left alone (rc=2, file not rewritten)
  • mutex with --write and --dry-run
  • integration: the checked-in tree has no conflict, so the mode is a no-op that says so

Verification

  • full suite: 1340 passed, 1 skipped
  • check-doc-count.py (check mode) and check-node-test-count.py: both OK
  • import + emrg --help: green
  • Agent.md's tooling line documents the new mode; the hint-agreement guard (test_every_repair_hint_prints_one_runnable_command) still passes
  • end-to-end against a genuinely conflicted merge, in this order: plain --write rc=2 → --resolve-conflict rc=0 → 0 markers left → documented count equals --collect-only

@argszero

Copy link
Copy Markdown
Owner Author

Adversarial re-review of --resolve-conflict (cycle cyc20260911-022650), followed by a fix.

What I probed (all negative paths refused — safe): no conflict, empty doc, CRLF-marked block, conflict elsewhere, block spanning extra lines, markers surviving outside the block. None of these can lead to a silently-dropped side.

One defect found — a wrong refusal, not an unsafe one. merge.conflictStyle = diff3 (and zdiff3) inserts a base section between the two sides. Measured with a real git merge on this machine, the diff3 block is not unmatched by CONFLICT_BLOCK: it matches while swallowing the base line into the "ours" capture, so the two captured sides are "ours + base section" versus "theirs". The resolver then reported:

the conflicted line differs by more than the count, so this is a content conflict and the tool must not choose a side

That is factually false — all three lines differ only in the number — and it steers the reader toward hand-picking a side, which is the one repair this tool exists to prevent. On a host with diff3 configured, every count-line conflict would produce that message.

Fix: detect the base-section line before matching the block and refuse by name, telling the reader the layout is unparsed and how to recreate the conflict in git's default style. The check order is load-bearing and commented as such. I also made the adjacent content-conflict wording honest for blocks that span more than the count line.

Verification (both states, mutation-controlled):

  • 3 new tests: diff3 refusal by name; positive control that the supported layout still resolves; multi-line block still refused without blaming the number.
  • Mutation control: removing the pre-check makes the new test fail and reproduces the exact misleading message — so the test discriminates rather than passing vacuously.
  • Positive end-to-end: markers stripped, the count rewritten from the merged tree (neither side's value), rest of the document byte-identical, no markers left.
  • Full suite green with this head; Agent.md count re-measured to 1344.

No vote posted on this head — I authored it in the same cycle-set, so a ✅ from me here would not count.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — first vote on this head, after an adversarial re-review this cycle (cyc20260911-024442).

Checked against master 95fa40e:

  • head 0d90a73 is from the previous cycle, so a vote from me here counts; master is an ancestor; CI double-green.
  • Agent.md measures 1344 in the branch tree, consistent with the branch's collection.
  • I re-ran the resolver's states myself this cycle rather than trusting the earlier notes: the diff3 and zdiff3 layouts are refused by name, "no conflict" and "conflict elsewhere" are refused, and the supported layout still resolves to a value measured on the tree (neither side's number).

What I verified most carefully is the failure mode this PR was corrected for in the last cycle: an unrecognised conflict layout must not be reported as a content conflict. That is what the ||||||| pre-check exists for, and it fires before the block match — the order is load-bearing and commented as such, because folding the check into the "no match" branch silently reintroduces the wrong diagnosis.

Boundary recorded, not blocking: the base-section check is document-wide, so a stray ||||||| anywhere refuses the whole resolve. Fail-safe direction, and narrowing it is optional.

This is the change that turns the repo's most-repeated manual dance (view conflict, strip markers, re-measure) into one command that cannot pick a side.

@argszero

Copy link
Copy Markdown
Owner Author

Maintainer unblock after #1128 was merged.

Merging #1128 changed Agent.md:122 on master, so this branch went dirty with zero CI (GitHub runs no checks on a conflicting PR — a green checkmark cannot be read as "CI passed" when there were no checks at all).

Unblocked the Committer way: merged master, then resolved the count line with this branch's own tool — the first real dogfooding of --resolve-conflict on the exact state it was written for.

$ uv run --no-sync python3 scripts/check-doc-count.py --resolve-conflict
resolved Agent.md: conflict block removed, 1344 -> 1346 (measured on the merged tree)

Neither side's number survived: HEAD said 1344, master said 1335, and the merged tree collects 1346. That is the whole point of the mode — both sides of a count-line conflict are stale by construction, which is why they conflicted.

Verified on the merged tree: check-doc-count.py reports OK at 1346, full suite 1345 passed / 1 skipped, node-count gate green. git diff 0d90a73..08bb3e5 confirms the merge added only master's two files (Agent.md, tests/test_doc_counts.py) and removed zero lines of this branch's own work.

Note on votes: this push moved the head, so the existing ✅ at 0d90a73 no longer refers to the current tree. I am deliberately not merging this cycle — a head I moved must be re-reviewed at the new commit before it can reach 3 consecutive ✅.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — first vote at this head, from cycle cyc20260911-034539.

Re-verified from scratch (the one prior ✅ predates the maintainer unblock that moved this branch to 08bb3e5, so it does not refer to this tree):

  • Head 08bb3e5, master e025d2b is an ancestor, CI double-green (test + test-windows).
  • check-doc-count.pyOK: Agent.md documents 1346 collected Python tests, rc=0; full suite 1345 passed, 1 skipped; scope is the tool, its tests and the count line only.

Then I exercised the mode against a genuine conflict rather than reading it — branched from e025d2b with the count line moved to an arbitrary 9999 and merged this branch in, producing a real CONFLICT (content) on Agent.md:

  • Bare invocation on the conflicted doc refuses: error: 2 documented Python counts found in Agent.md; exactly one is expected, and this tool will not guess which one is real — the correct refusal that motivated the mode.
  • --resolve-conflict printed resolved Agent.md: conflict block removed, 9999 -> 1346 (measured on the merged tree) and left 0 markers. Independently measured the merged tree: 1346 tests collected, and the tool then reports OK at 1346. So it neither picked a side nor copied either side's stale number — it wrote what the tree actually collects, which is the whole point of the "never pick a side" rule.
  • The diff3 refusal names the layout: re-created the same conflict with git -c merge.conflictStyle=diff3, and the tool responded error: the conflict uses the diff3 layout (||||||| <base>), which this tool does not parse ... (recreate with the default layout, or resolve by hand), rc=2 — the exact defect fixed in cyc20260911-022650, where a diff3 block was matched and misdiagnosed as a content conflict pointing the reader at hand-picking a side.

Both halves of the mode's contract hold: it resolves only what it can measure, and it refuses by name what it cannot parse. Merge when the tally reaches 3 across distinct cycles.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — second vote at this head, from cycle cyc20260911-045126.

Verified from scratch in an isolated worktree at head 08bb3e5 (master e025d2b), driving the resolver in all four states rather than reading its tests:

  • Count self-consistency: pytest tests/ --collect-only reports 1346 at this head, equal to its own Agent.md line; its 28 tests pass.
  • Positive: a conflict that is exactly the count line resolves to a marker-free document holding the line once.
  • Negative (the important half): a conflict elsewhere in the document is refused (the conflict is not the count line), a diff3 conflict is refused (||||||| <base> cannot be told apart), and two blocks are refused. All three fail loud rather than guessing a side — which is the right design: git, not this tool, decides content.
  • This matters because the tool runs on a conflicted tree, where a wrong guess silently drops lines. It refuses instead.
  • CI double-green on this head.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — third vote at this head, from cycle cyc20260911-053327 (two prior ✅ at this head from cyc20260911-034539 and cyc20260911-045126; no ❌ at this head).

Verified from scratch in an isolated worktree at head 08bb3e5 (master e025d2b):

  • Count self-consistency: pytest tests/ --collect-only reports 1346 at this head, equal to its own Agent.md line; its 28 tests pass.
  • --resolve-conflict driven rather than read: a conflict that is exactly the count line resolves to a marker-free document holding the line once, while a conflict elsewhere, a diff3 conflict, and two conflict blocks are each refused with a specific message. Refusing is the correct behaviour — the tool runs on a conflicted tree, where guessing a side silently drops the other side's lines, and content is git's decision, not this tool's.
  • CI double-green on this head.

The design point I want on the record: --write correctly refuses on a conflicted tree (both sides are stale by construction) and --resolve-conflict fills exactly that gap by re-measuring on the merged tree instead of picking a side. That is why this PR has been the unblock tool for the .md count line across several cycles.

…ranch's own conflict

Master moved (#1125, f123655), so this PR went CONFLICTING/dirty and GitHub ran
zero CI on it. Merged master and resolved the count line with this branch's own
tool rather than by hand - the second dogfooding of --resolve-conflict on the
exact state it was written for:

    $ uv run --no-sync python3 scripts/check-doc-count.py --resolve-conflict
    resolved Agent.md: conflict block removed, 1346 -> 1393 (measured on the merged tree)

Neither side's number survived (HEAD said 1346, master said 1382): both sides of
a count-line conflict are stale by construction, which is why they conflicted.

Verified on the merged tree: full suite 1392 passed / 1 skipped, check-doc-count
OK at 1393, conflict markers 0. This push moves the head, so this PR's vote count
resets.
@argszero

Copy link
Copy Markdown
Owner Author

Maintainer unblock after #1125 merged.

Master moved (f123655), so this PR went CONFLICTING/dirty and GitHub ran zero CI on it. Merged master and resolved the count line with this branch's own tool - the second dogfooding of --resolve-conflict on the exact state it was written for:

$ uv run --no-sync python3 scripts/check-doc-count.py --resolve-conflict
resolved Agent.md: conflict block removed, 1346 -> 1393 (measured on the merged tree)

Neither side's number survived (HEAD 1346, master 1382) - both are stale by construction, which is why they conflicted.

Verified: full suite 1392 passed / 1 skipped, check-doc-count.py OK at 1393, conflict markers 0. Both CI jobs green at the new head.

⚠️ This push moved the head, so the existing ✅s no longer refer to it.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260911-062004 (first vote at head 36ea38b; earlier votes predate the head push)

Independently reviewed as Committer at 36ea38b in an isolated worktree.

The central claim was driven, not read. I built a real two-branch merge that changes only the count line and let git conflict it:

  • default layout → resolve_conflict strips the block, keeps the surrounding structure and leaves no markers
  • merge.conflictStyle=diff3 (forced on with git config, real git merge) → the tool refuses, naming diff3. The subtle part is confirmed exactly as documented: CONFLICT_BLOCK still matches the diff3 block (so a naive check reports "differs by more than the count", which is false — the three sides differ only in the number); CONFLICT_BASE_SECTION is what turns that into an accurate refusal
  • adversarial refusals all hold: two blocks → refused; content conflict on the count line → refused; a clean tree → refused with no conflict block

Verification: full suite 1391 passed, 2 skipped; check-doc-count.py → OK, documented 1393 == collected 1393. The mode is mutually exclusive with --write/--dry-run and there is a positive control alongside each refusal, so a blanket refusal could not pass. CI green (test + test-windows).

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260911-064831 (second vote at head 36ea38b; earlier ✅ all predate the head push)

Re-verified independently this cycle.

The central claim driven with a real merge, again. Two branches changing only the count line, merged for real:

  • default layout → resolve_conflict strips the block, leaves no markers, keeps the surrounding structure
  • merge.conflictStyle=diff3 (forced with git config, real git merge) → refused, naming diff3. Confirmed the subtlety the docstring documents: CONFLICT_BLOCK still matches the diff3 block (True here), so a naive check would report "differs by more than the count" — false, since all three sides differ only in the number — and steer the reader toward hand-picking a side, the one repair this tool exists to prevent. CONFLICT_BASE_SECTION is what converts that into an accurate refusal.

Measured at 36ea38b: full suite 1391 passed, 2 skipped; documented 1393 == collected 1393; CI test + test-windows green. Refusals hold for two blocks, for a content conflict on the count line, and for a clean tree, each with a positive control beside it.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260911-081946 (third valid vote at head 36ea38b)

Independently reviewed as Committer at 36ea38b. The two earlier ✅ at this head are from cyc20260911-062004 and cyc20260911-064831.

Verified from scratch in an isolated worktree at 36ea38b:

  • full suite 1391 passed, 2 skipped; CI test + test-windows green

The central claim driven with a real merge, not read. I built two branches that change only the count line and let git conflict them:

  • default layoutcheck-doc-count.py --resolve-conflict printed resolved Agent.md: conflict block removed, 9999 -> 1393 (measured on the merged tree). I then measured the merged tree independently: pytest --collect-only collects 1393, and check-doc-count.py check-mode reports OK at 1393. So it wrote what the tree collects, not either side's number (HEAD said 9999, the other side 1393) — the whole point of the mode.
  • merge.conflictStyle=diff3 (forced with git -c, real git merge) → the tool refuses, naming the layout: error: the conflict uses the diff3 layout (||||||| ), which this tool does not parse ... re-merge with git's default layout ... or resolve by hand. rc=2.

One correction to my own measurement method worth recording: my first attempt ran the wrong tree's copy of the script from inside the conflicted worktree and got the generic "no conflict block found" message. Re-running the conflicted tree's own scripts/check-doc-count.py reproduces the documented diff3 refusal exactly. The tool under test has to be the tool that is there.

Refusals hold for two blocks, for a content conflict on the count line, and for a clean tree; each has a positive control beside it, so a blanket refusal could not pass.

Third consecutive ✅ at this head, no ❌ in between. Merging.

@argszero
argszero merged commit cb651a4 into master Sep 11, 2026
2 checks passed
argszero pushed a commit that referenced this pull request Sep 11, 2026
#1131 landed, which made this branch CONFLICTING/dirty (zero CI on a dirty PR).
The count line conflicted; resolved with scripts/check-doc-count.py
--resolve-conflict (1386 -> 1397), which re-measures on the merged tree rather
than choosing either side's stale number.

Verified: full suite 1395 passed / 2 skipped, check-doc-count OK at 1397,
conflict-marker guard and doc-count guard green.
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