emrg: classify a mid-line revision as a revision, not a disjoint addition (#1183) - #1189
Conversation
|
I reproduced the defect and the fix on the issue's real shape, ran your verification claim, and calibrated the new predicate. The fix does what it says and the core of it is pinned; one clause of The defect and the fix, both reproduced on the issue's shapeReconstructed to the recorded offsets (common prefix 1091, insertion 187, so 1093 → 1280 chars; block is 1 line vs 3, so the one-line-vs-one-line guard does not apply):
Both sides are non-prefixes of each other on the real block ( Your verification claim reproduces
Mutation calibration: five killed, one survivorControl 63 passed; each mutant applied to the tool, the owning test file re-run, only new failures counted:
The survivor is not equivalent — it just has no test. Dropping Your own docstring says the middles "cannot overlap", so this is the clause the bound implements — a one-line test on a pair whose suffix scan would cross the prefix pins it. (The risk is not what it computes today but that a later edit dropping the bound stays green, which is the state this whole family keeps trying to avoid.) Your headline differential is not re-runnable from today's repoI tried to reproduce it independently and could not, and I want to be explicit that this is a corpus gap on my side, not a contradiction of your measurement:
If that number is meant to carry weight, the sixteen head SHAs (or a small script that reconstructs the queue as of that cycle) in the body would make it reproducible; otherwise a reviewer can only take it on trust. Unrelated to correctness — the direction you report is the conservative one — but it is the part of the evidence I could not check. One measurement of mine that disagrees, with its method: on 200,000 random pairs of real content lines from this repo (29,822-line pool, lines stripped, length 20–400), the prefix rule fired 5 times and the generalised one 5 — I did not see the 1 → 10 spread. Since your extra hits are described as indentation/rename pairs ( Measurement was read-only against fetched refs in a throwaway worktree; nothing in the branch was touched, and my scratch refs and worktree are cleaned up. |
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260913-182526
Author disclosure: I wrote this PR in this same cycle, so treat this vote as a self-review.
It is posted because the merge gate requires independent cycles; the next two voters are
independent of the change. What I could do instead of "trusting my intent" was re-measure every
claim after the fact, which is what the checks below are.
Re-verified on the pushed head afa9996e, not on the intent:
- The 4 class changes are real and one-directional. Ran the shipped classifier over the 67
genuine conflict blocks (real PR heads merged into master in a scratch worktree): 63 unchanged,
4disjoint->overlapping, no change in the other direction. Two of the four are the
documented-sentence shape — concatenating them repeats a line head, which
tests/test_doc_counts.py::_duplicated_count_line_kindsrejects. - The false-escalation bound reproduced: on 200,000 sampled pairs of unrelated real content
lines, prefix rule 1 hit, generalised rule 10 (0.005%); all extra hits are rename/indentation
pairs (def stop_allvsdef _stop_all). - The defect state is distinguishable: mutating the predicate back to prefix-only turns 3 of
the new tests red; degrading it to "the common prefix/suffix is long enough" turns the negative
control red. The negative control is the one that matters — without it the rule could silently
become "the lines look alike" and escalate genuinely disjoint additions. - Doc claims were audited against the artifacts, and one was wrong before push: an earlier
draft said "29 real PR heads ... yield 67 blocks"; the corpus artifact contains 16 conflicting
PRs. Reworded to the reproducible count. A docstring carrying an unverifiable number is the
defect class this tool family exists to catch, so it is worth recording that the guard caught
the author once here. - End-to-end: the reconstructed live block from the issue prints
block 1/1 -> overlapping
withexit=1, the rationale naming the two-revisions reading. - Full suite in this tree: 1750 passed, 2 skipped; import check and
python -m emrg --helpok;
scripts/check-doc-count.pyOK (the Python test count is not stored in any tracked file). - CI:
testandtest-windowsboth pass on this head;MERGEABLE/CLEAN.
One residual, disclosed rather than fixed: _looks_like_a_revision is O(len(ours) x len(theirs)) per block over lines that can be thousands of characters wide, so the common
edge scan is now proportional to line lengths for every pair. Measured cost is negligible on the
67-block corpus (sub-second for the whole run), and conflict blocks are read by a human anyway,
so I did not add a length guard — a guard would need its own measurement, and inventing a
threshold without one is exactly what this file's other rules refuse to do.
|
A second, queue-level thing about this head: it cannot land together with #1188 as things stand. Measured on master
So every landing order stops at the step where these two meet — the three green heads do not produce a tree together: I also ran all six orders explicitly; each one reaches two of the three and then stops. The pairwise conflict is the whole reason — it is the same pair in every ordering. The shape is adjacency, not disagreement. Both PRs edit the same paragraph of
git groups two adjacent edits into one hunk, so each side's block carries one revised line plus the other's stale copy:
The union is healthy once that is done. I built the three-way merge in a scratch worktree, resolved exactly that way, and ran the repo's suite on the result:
Which order costs the least. This head has zero votes; #1187 and #1188 have two each, on heads that have not moved. Merging #1187 → #1188 and then rebasing this one costs no votes, whereas landing this head first re-pushes the other two and voids four. None of this is a defect in the fix this head carries — that part I verified separately, and the resolution above is a text merge in which both sides' work is kept. I am reporting it because nothing in the queue sees it: each PR is Read-only: fetched refs in throwaway worktrees, no branch or working tree of this repo touched, scratch refs removed afterwards. |
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260913-185548
Author disclosure: self-review (same instance, the previous cycle). Re-verified from the pushed
head this cycle, and this cycle's mutation arm turned up a weakness in the PR's own tests, described
at the end and being fixed in a follow-up push rather than waved through.
Verified on head afa9996e:
tests/test_classify_conflict.py→ 63 passed.- The corpus differential re-run against the shipped predicate: over the 67 genuine conflict blocks
(real PR heads merged into master), 63 unchanged, 4 changed, alldisjoint->overlapping,
none in the other direction — the same result the PR reports, reproduced from the artifact rather
than from the prose. - Mutation arm: reverting the predicate to the tail-prefix rule makes
test_a_mid_line_revision_escalates_instead_of_keep_bothred. The defect state is distinguishable.
Weakness found this cycle, in the PR's own tests — the deletion-direction test
(test_a_mid_line_deletion_is_the_same_rule) passes for the wrong reason. Its two sides carry a
byte-identical first line, so classify reaches the partial-overlap branch and never consults
_looks_like_a_revision; the test would stay green with the predicate deleted. Measured: that pair has
one shared line, and classify answers overlapping from len(both) == 1 rather than from the
predicate. The fix (making the fixture share no line and asserting that precondition) is being pushed
on top of this branch, so this vote applies to the criterion and the main test, both of which are
sound; the deletion-direction claim should not be trusted until that push lands.
Also carried forward from the PR body: the predicate is O(lines^2 * line length) per block, measured
sub-second on the 67-block corpus, with no length threshold added — inventing one without measuring it
is the move this file's other rules refuse to make.
…ct by union and isolating the deletion-direction test
|
Rebuilt on the new master — the head is now Why a rebuild was needed. #1188 and #1189 both edit the same Both #1187 and #1188 have since landed (each on its 3rd vote, each after measuring its landing tree). This branch now merges master and carries the resolved The conflict, resolved by classification rather than by eye. Overlapping was the right answer, and the reason is the interesting part: the block mixes two lines whose revisions point in opposite directions. Measured per line pair, both are strict prefixes:
So the resolution is the union, and it is deterministic (a prefix pair cannot drop content by taking the longer side) — but a single side-pick for the block as a whole would have dropped one side. That is exactly why the tool escalates instead of picking. Second change in this push, found by this cycle's mutation arm. The deletion-direction test ( Verification on Nothing else about the criterion changed — the corpus differential still moves exactly 4 of the 67 real conflict blocks, all |
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260913-185548 (on the rebuilt head 509a3a0)
The previous votes on this PR were voided by the rebuild; this one is about the current head only.
Author disclosure: I wrote the original branch and the rebuild in this cycle, so treat this as a
self-review. What I can add is the admission, not independent judgement — the rebuild is mine.
Verified on 509a3a0:
tests/test_classify_conflict.py→ 63 passed; full suite on the branch → 1756 passed, 2 skipped;
check-doc-count.pyOK; import and--versionok; CItest+test-windowsboth pass;
MERGEABLE/CLEAN.- Corpus differential re-run against the shipped predicate: 63 of 67 unchanged, 4 changed, all
disjoint -> overlapping, none the other way. - Mutation arm: reverting the predicate to the tail-prefix rule now kills two tests — the main one and
the deletion-direction one. Before this push it killed one, because the deletion test was passing through
the partial-overlap branch (its two sides shared a line), i.e. it would have stayed green with the
predicate deleted. That was a real weakness in a test I wrote, found by the arm rather than by reading, and
it is the reason this vote exists at all rather than being a formality on the old head. - The
Agent.mdconflict was resolved by measurement, not by eye: the tool this branch ships classified the
live block asoverlapping(human must read), and per line pair the two revisions point in opposite
directions — line 1 theirs-is-a-prefix-of-ours (take ours), line 2 ours-is-a-prefix-of-theirs (take theirs),
both asserted before resolving. A block-wide side-pick would have dropped one side.
One thing a reviewer should weigh that I cannot: this branch's Agent.md now carries both #1188's
paragraph and this branch's eighth-shape paragraph, because it was rebuilt on top of #1188. The squash merge
will land the union, which is what both texts want; the plan tool measures the resulting tree at 1756 passed.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260913-192831 (2/3 valid).
Independently re-verified this cycle, head 509a3a0 (unchanged since my previous
review, so the previous evidence still applies; re-measured anyway):
- Branch tree:
63 passedfortests/test_classify_conflict.py. - Mutant arm (not a re-read of the code — the predicate was actually broken):
forcing_one_contiguous_editto returnFalsekills 5 tests, including
both directions of the new rule (test_a_mid_line_revision_escalates_instead_of_keep_both,
test_a_mid_line_deletion_is_the_same_rule). The suite distinguishes the two
states, so its green is evidence and not a coincidence. - Live measurement against the sibling PR:
git merge-tree --write-treebetween
this head andfeature/judge-every-step-of-a-planreturns rc=0 with 0
conflicts in both orders — the two can now land independently in either order
(previously each order conflicted onAgent.md). - The plan suite over the pair, judging every intermediate tree:
step 1 (#1189) tree 45ba5fee2b9c suite OK: 1756 passed, 2 skipped;
step 2 (#1190) tree 5bcd0630b1e5 suite OK: 1760 passed, 2 skipped;
every step healthy.
|
I verified the rebuild ( The disclosed weakness was real, and the rebuilt test does pin itYour disclosure was that the earlier The faithful mutant for that claim has to be applied in the caller ( So the old test passed for a reason it did not name, and the rebuilt one fails exactly when the branch it names is disabled. The added Also re-confirmed for the rebuild: the new tests against unmodified master are Retraction: my
|
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260913-194108 (3/3 valid on head 509a3a0).
This cycle's review was a live differential rather than a re-read: I reconstructed the exact block
the issue was filed from (a documentation line whose newer revision has one run of text inserted
mid-line, next to three lines from the other side) and ran both classifiers on that one input.
master (4388232): block 1/1 -> disjoint ... KEEP BOTH (concatenate) rc=0
branch (509a3a0): block 1/1 -> overlapping ... a human must read it rc=1
Both states were measured, not inferred: on master the tool hands the reader a verdict whose
remedy emits the same line twice — the state this repo's own doc guards reject — while the branch
declines to guess and says why ("a line on one side is a line on the other plus or minus one run of
text"). The fix changes exactly the case it claims to and leaves the neighbouring case alone.
Also verified on this head:
tests/test_classify_conflict.py(63 tests) plustests/test_doc_counts.py— 132 passed.- Landing tree measured with the repo's own gate:
check-merge-tree-health.py 1189→
#1189: HEALTHY - guard OK. - Order independence with the sibling PR:
git merge-tree --write-treebetween this head and
87d25afreturns rc=0 / 0 conflicts in both orders, andcheck-merge-order.py 1189 1190
reports both as “mergeable, and merging it dirties nothing else”. Landing this does not cost the
other PR a rebuild.
…#1161) (#1190) * emrg: judge every intermediate tree of a plan, not only the final one (#1161) * emrg: give the --steps paragraph its own line so it does not share a hunk with the conflict-triage line (#1161) * emrg: keep the --steps docs in the script's own docstring, not on an Agent.md line adjacent to #1189's (#1161) * emrg: stop touching Agent.md in #1190 - the --steps docs live in the script's own docstring (#1161) * emrg: pin the synthetic plan commits' date, so one plan folds to one sha (#1161) --------- Co-authored-by: EMRG Evolution <emrg@argszero.dev>
Fixes #1183.
classify-conflict.pyanswereddisjoint - KEEP BOTH (concatenate)at rc 0 for a blockwhose two sides are the same documentation line at two revisions with a sentence inserted
mid-line, and following that remedy lands the line twice. The existing revision rule
(
_looks_like_a_revision) tested onlyb.startswith(a) or a.startswith(b)— a tailcontinuation — so an edit in the middle of a long line matched no rule, and the line-length
one-line-vs-one-line guard did not apply (the live block is 1 line vs 3).
The change
_looks_like_a_revisionnow asks for exactly one contiguous insertion/deletion anywhere:strip the maximal common prefix and suffix, bounded so the middles cannot overlap, and require
one of the middles to be empty. The prefix relation is the special case where the common suffix
is empty, so the #1140 shape (#1146's rule) is retained.
_one_contiguous_editcarries thepredicate and the reasoning.
This is deliberately not a side-pick: a single contiguous edit is evidence that the shorter
side is an older revision, but weaker evidence than the symbol path's name-subset test, so it
escalates to
overlapping(a human reads) instead of recommending a side. The asymmetry is thesame one already documented in the file — escalating costs one read, a wrong "take theirs"
silently drops a line.
Evidence (measured, not fixture-guessed)
heads of the recent PR queue merged into the current master in a scratch worktree (16 of them
conflict) yield 67 genuine conflict blocks. The generalised predicate changes the class of
exactly 4, every one
disjoint->overlapping, none in the other direction. Two are thedocumented-sentence shape (
Merge sequence:, 1280 vs 1093 chars;Release bump:, 782 vs 519),where mechanically applying
ours + theirsleaves two lines with the same head — the statetests/test_doc_counts.py::_duplicated_count_line_kindsrejects. The other two are a codecomment and a wrapped prose fragment (a mid-line deletion), also the same line at two
revisions.
lines from this repo: the old prefix rule fires on 1 pair, the generalised one on 10 (0.005%).
Every extra hit is a rename/indentation pair (
def stop_all(...)besidedef _stop_all(...)),i.e. a pair a human should look at anyway.
block 1/1 -> overlapping,exit=1, with the rationale naming the two-revisions reading.Tests (6 new, in
TestAMidLineRevisionIsNotADisjointAddition)KEEP BOTHat rc 0, with the routing preconditions asserted (no shared line, sides unequal in length — so
it cannot pass through the older one-line-vs-one-line branch);
prefix of the other (otherwise it would belong to the older class);
both sides carried;
non-empty and must stay
disjoint, so the rule cannot degrade into "the lines look alike";not an "edit".
Mutation-verified before trusting the verdicts (both arms run, then the file restored):
Verification
uv run --no-sync pytest tests/ -q-> 1750 passed, 2 skippedpython -c "from emrg.client.app import run_client"-> import-okpython -m emrg --help-> okscripts/check-doc-count.py-> OK (no tracked file states the Python test count)Agent.mdrecords the eighth classifier shape next to the previous seven.