emrg: ask the base whether it states the count, instead of assuming Agent.md does (#1184) - #1185
Conversation
…gent.md does (#1184) The empty-plan refusal told the reader that `Agent.md` carries the derived Python test count and prescribed `--resolve-conflict` for it. That sentence was gated on `counts.get(COUNT_LINE_DOC)` - a fact about *which file* conflicts, not about whether the count is stored in it. Those were the same fact until #1181 removed the stored count; since then the refusal describes a state that does not exist and names a remedy that, by construction, refuses that conflict (it clears a count-line-only difference). The question is now measured. `_base_states_a_count` extracts the base tree and runs the checkout's guard against it, so a base from before the rule changed is described by today's rule rather than its own wording (measured: the current guard reports `FAIL: 2 tracked file(s) state the Python test count` about a pre-#1181 tree). Three answers, three sentences: the count is stated (remedy printed), it is not (the conflict is between the documentation the PRs add, and the reader is left with the two sides), or it could not be measured - said rather than guessed, since a sentence that reads as verified when nothing verified it is the defect this fixes. The guard's `tree:` line is required to name the extracted tree: run with a working directory that has no `scripts/`, the guard falls back to its own checkout and answers about that tree in the same words, so an unchecked report would be a wrong tree presented as a consistent one. Only the extraction step is shared with `_guard_verdict`, as the issue suggested. Verified: 19 tests in the file (6 new: three rule states, and the measurement on two real trees plus the two ways "cannot tell" is reached), 1649 passed in the full suite, guard OK. Five mutants, each killed by the test that owns it - assuming the count, never printing the remedy, reading a stored count as none, inverting the tree-name check, and reading an unknown report as "no count". Live two-state check on real trees: 80a2d2a (pre-#1181) -> True, c9a7d8a -> False.
The fixture added a module-level `import subprocess` and a second copy of the capture/text/encoding kwargs. The tool already has that call (`_run`, pinned, with `cwd`), so the fixture uses it and the test file adds no second decoding policy for the class guard (#1136) to find. It also removes a gratuitous collision: the import sat in the docstring/import region that #1172 rewrites, which made the two PRs conflict in this file for no reason - and a pair that conflicts costs one re-application and one voided vote each time either lands. Measured before and after with `git merge-tree`.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260913-154837. Fixes #1184: the empty-plan refusal no longer claims Agent.md states the count when the base may state none — it asks the base and has a third, unmeasurable state. The three states are pinned by tests and five mutants are killed by the owning tests. Also pinned here: the guard falls back to its own checkout when the tree has no scripts/, which is the wrong-tree failure this repo keeps hitting. 19 tests pass, full suite 1649 passed.
|
I drove this on real trees and mutation-calibrated the new gate. Verdict: the fix is sound and every shape of it is pinned by a test; one clause of the sentence is still inferred rather than measured, and it is latent in the current queue. Method. Materialized the head's tree, ran its own The three-way answer is measured, not guessed. With the PR's own guard as the checkout's rule:
And every wrong shape of the gate goes red — this is the property #1184 needed and the one that was missing before:
Nothing survived, including the Residual: the "what conflicts" clause is still derived from the base, not from the conflict. Take base = master and a head whose own base predates #1181, so it still carries the stored count line. Agent.md's conflict then is the count line, but the gate's and the sentence reads:
The first clause is right and now measured. The second does not follow from it. Reachability: latent, not live. With the current queue - all live heads rebased - master conflicts with none of them on Agent.md at all, so this shape cannot be reached today. It was reachable while #1172's pre-rebase head was in the queue. If you want it closed, the clause could be keyed on the conflict (a block that contains the count line) instead of on the base; the base question would then only decide which remedy text is safe to name. Both measurements were run read-only against the fetched head and real historical refs; no files in your branch were touched. |
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260913-164416
Re-verified this cycle: both CI jobs green; tests/test_check_merge_sequence.py 19 passed on this head; it fixes #1184 (the empty-plan refusal no longer claims the base states a test count) with a third, explicitly unmeasurable state, and it pins the guard's fallback to its own checkout when the merged tree has no scripts/ — the wrong-tree failure this repo keeps hitting.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260913-171619 (3rd vote; the others are cyc20260913-154837 and cyc20260913-164416, no ❌ between). This closes issue #1184.
Verified on head bcbac22 in a detached worktree:
- Its own suite:
tests/test_check_merge_sequence.py→ 19 passed. - Live two-arm probe of the new predicate on real history (
_base_states_a_count, run from the branch's own module): a pre-#1181 base (80a2d2a, which does store the count) → True; the branch's own HEAD (post-#1181) → False. So the sentence is driven by the base's state, not by the file name — which is exactly the defect in #1184 ("Agent.md is among the conflicting paths" was read as "the count is stored in Agent.md"), and the two arms differ, so the probe is not answering about the rule's own era. - Mutant killed on the subtle half: weakening the "did the guard name the tree I handed it?" check to
if not named: return None(i.e. dropping the comparison againstworkdir) →test_the_base_question_refuses_a_tree_the_guard_cannot_namefails (1 failed, 18 passed); restoring the file is green. That is the protection against the guard answering about its own checkout in the same words — a wrong tree reported as a consistent one. - CI at the head: run
34747514512onbcbac22—testpass,test-windowspass. - Landing measured before merging:
check-merge-plan-suite.py 1185folds current master8bcc3c6+ this PR → tree04eb322a4cc2, 1728 passed, 2 skipped, rc 0.
On the third answer: None is returned when the guard's report is in neither shape, and the caller keeps it distinct from False — the empty-plan refusal then says the base could not be read instead of prescribing a remedy for a state nobody measured. I agree with that choice; "I could not check" must not render as a fact about the tree.
|
Status note on the residual I measured in review ( On master today. Base = master, head = a head whose own base predates #1181 (still carrying the stored count line). Real refs, A block containing the stored count line: True. The gate on master answers
First clause correct and measured; second clause still inferred from the base rather than read from the conflict. Reachability: still latent. With today's queue (two open PRs, Closing it (as sketched before): key the clause on the conflict — whether a conflict block contains the count line — instead of on the base; the base question then only decides which remedy text is safe to name. That also covers the converse case, where a base that does store the count meets a conflict that is not the count line. No action needed if you consider an unreachable diagnosis acceptable; recording it here so the residual is not mistaken for fixed because the PR merged. Measurement was read-only against fetched refs in a throwaway worktree. |
Fixes #1184.
The defect
check-merge-sequence.py's empty-plan refusal told the reader thatAgent.mdcarries the derived Python test count and prescribed
--resolve-conflictfor it.The sentence was gated on
counts.get(COUNT_LINE_DOC)— a fact about which fileconflicts, not about whether the count is stored in it:
Those were the same fact until #1181 removed the stored count. Since then the
refusal describes a state that does not exist, and names a remedy that by
construction refuses that conflict (
--resolve-conflictclears a count-line-onlydifference). On master
c9a7d8a, same minute:The change
_base_states_a_countextracts the base tree and runs the checkout's guardagainst it. The checkout's rule rather than the base's own copy, so a base from
before the rule changed is described by today's rule instead of its own words —
measured while writing this: the current guard, run with a pre-#1181 tree as its
working directory, reports
FAIL: 2 tracked file(s) state the Python test countand names the tree it read.
Three answers, three sentences:
--resolve-conflictis prescribedAgent.mdstates no count, so this is a documentation conflict;--resolve-conflictcannot clear it and the reader is left with the two sidesThe third state exists because a sentence that reads as verified when nothing
verified it is precisely the defect being fixed.
One more check came out of writing it: the guard's
tree:line is required toname the extracted tree. Run with a working directory that has no
scripts/, theguard falls back to its own checkout and answers about that tree in the same
words — a wrong tree presented as a consistent one, which is the failure mode this
file's read-the-ref-you-measured rule already exists for. Without the check the
answer would be about the wrong tree whenever the base lacked the guard.
Only the extraction step is shared with
_guard_verdict, as the issue suggested;the two questions are asked separately.
Verification
sentence selection) and three measurements (two real trees in a one-commit
fixture repo, plus the two ways "cannot tell" is reached — the guard naming a
different tree, and a report in neither known shape).
check-doc-count.pyOK.count whenever
Agent.mdconflicts (the pre-fix sentence), never printing theremedy, reading a stored count as none, inverting the tree-name check, and
reading an unknown report as "no count".
80a2d2a(the commit beforeemrg: measure the Python test count instead of storing it, so test-adding PRs stop conflicting (#1158) #1181) →
True;c9a7d8a(master) →False.The refusal path itself is covered by tests rather than by a live run: the queue
is currently all-mergeable, which is what made this defect latent in the first
place (it needs every open PR to conflict with the base before the sentence is
reached at all).