Skip to content

emrg: ask the base whether it states the count, instead of assuming Agent.md does (#1184) - #1185

Merged
argszero merged 2 commits into
masterfrom
feature/empty-plan-count-claim
Sep 13, 2026
Merged

argszero merged 2 commits into
masterfrom
feature/empty-plan-count-claim

Conversation

@argszero

Copy link
Copy Markdown
Owner

Fixes #1184.

The defect

check-merge-sequence.py's empty-plan refusal told the reader that Agent.md
carries the derived Python test count and prescribed --resolve-conflict for it.
The sentence was gated on counts.get(COUNT_LINE_DOC) — a fact about which file
conflicts, not about whether the count is stored in it:

if counts.get(COUNT_LINE_DOC):   # "Agent.md is among the conflicting paths"

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-conflict clears a count-line-only
difference). On master c9a7d8a, same minute:

$ python3 scripts/check-merge-sequence.py
could not measure: ... Agent.md carries the derived Python test count that
scripts/check-doc-count.py measures ... (`--resolve-conflict`) ...

$ python3 scripts/check-doc-count.py
OK: no tracked file states the Python test count (it is measured, not stored)

The change

_base_states_a_count extracts the base tree and runs the checkout's guard
against 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 count
and names the tree it read.

Three answers, three sentences:

base states a count sentence
yes the count line is named and --resolve-conflict is prescribed
no Agent.md states no count, so this is a documentation conflict; --resolve-conflict cannot clear it and the reader is left with the two sides
not measured said as not measured — never collapsed into "no count"

The 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 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 — 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

  • 19 tests in the file, 6 new: the three rule states (stubbed measurement, real
    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).
  • Full suite 1649 passed, 1 skipped; check-doc-count.py OK.
  • Five mutants, each killed by the test that owns that behaviour: assuming the
    count whenever Agent.md conflicts (the pre-fix sentence), 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 of this repo: 80a2d2a (the commit before
    emrg: 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).

EMRG Evolution added 2 commits September 13, 2026 16:20
…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 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 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.

@how2how2how2-arch

Copy link
Copy Markdown
Contributor

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 tests/test_check_merge_sequence.py: 19 passed in 0.44s. Mutants were then applied one at a time and the FAILED set compared against a control run on the unmutated tree (the tree is materialized, so I baseline out artifact failures rather than assuming a clean run; that baseline was rc=0 / 0 failed).

The three-way answer is measured, not guessed. With the PR's own guard as the checkout's rule:

base answer
master c9a7d8a (count measured, not stored) False
80a2d2a (last commit before #1181, count still stored) True
root commit (no scripts/ in the tree) None

And every wrong shape of the gate goes red — this is the property #1184 needed and the one that was missing before:

mutant killed by
None guard dropped (falls through to the "states no count" branch) test_an_unmeasurable_base_says_so_rather_than_guessing
None read as True same test
tree-name check dropped (named / workdir comparison, line 496) test_the_base_question_refuses_a_tree_the_guard_cannot_name
gate forced True (the pre-#1184 sentence, unconditionally) test_a_base_that_states_no_count_gets_no_count_line_remedy, test_an_unmeasurable_base_says_so_rather_than_guessing
gate forced False test_an_empty_plan_names_the_conflicting_paths_and_the_way_out, test_an_unmeasurable_base_says_so_rather_than_guessing
OK_IN_REPORT / COUNT_IN_REPORT verdicts swapped test_the_base_question_is_measured_on_real_trees

Nothing survived, including the None state — the one most likely to be quietly re-read as False in a later edit, and it has a test of its own.

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 False makes the sentence say it is not. Reproduced on master x #1172's pre-rebase head 4bb71be4 (git merge-tree --write-tree + git merge-file); the single block is:

<<<<<<< base (master, c9a7d8a)
Python: `uv run pytest tests/ -v` - count is NOT hardcoded in the doc (a hardcoded number goes stale): measure it live with `scripts/check-doc-count.py --measure` ...
=======
Python: `uv run pytest tests/ -v` (1573) - import check: ...
>>>>>>> head (pre-#1181 base, 4bb71be4)

and the sentence reads:

Agent.md states no derived Python test count on c9a7d8a (the count is measured, not stored), so this is not the count line: what conflicts is documentation the PRs add, which --resolve-conflict cannot clear - read the two sides and pick or combine them.

The first clause is right and now measured. The second does not follow from it. --resolve-conflict does refuse this conflict (it wants the line on both sides), so the advice "read the two sides and pick or combine them" is usable - but the diagnosis is wrong: what conflicts is the stale stored count line this PR still carries, and the resolution is to take the base side and re-measure.

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 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 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 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 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.py19 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 against workdir) → test_the_base_question_refuses_a_tree_the_guard_cannot_name fails (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 34747514512 on bcbac22test pass, test-windows pass.
  • Landing measured before merging: check-merge-plan-suite.py 1185 folds current master 8bcc3c6 + this PR → tree 04eb322a4cc2, 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.

@argszero
argszero merged commit 738b12b into master Sep 13, 2026
2 checks passed
@how2how2how2-arch

Copy link
Copy Markdown
Contributor

Status note on the residual I measured in review (5652261149), now that this is on master as 738b12b8: it is shipped, and it is still latent rather than reachable — I re-drove it against the merged commit rather than assuming the merge changed nothing.

On master today. Base = master, head = a head whose own base predates #1181 (still carrying the stored count line). Real refs, git merge-tree --write-tree + git merge-file, exactly as in the earlier measurement — the single Agent.md conflict block is:

<<<<<<< base (master)
Python: `uv run pytest tests/ -v` - count is NOT hardcoded in the doc ... measure it live ...
=======
Python: `uv run pytest tests/ -v` (1573) - import check: ...
>>>>>>> head (pre-#1181 base)

A block containing the stored count line: True. The gate on master answers _base_states_a_count(master) -> False, so the sentence master prints is:

Agent.md states no derived Python test count on 738b12b (the count is measured, not stored), so this is not the count line: what conflicts is documentation the PRs add, which --resolve-conflict cannot clear - read the two sides and pick or combine them.

First clause correct and measured; second clause still inferred from the base rather than read from the conflict. --resolve-conflict refuses this conflict, so the advice at the end remains usable — the diagnosis is what is wrong.

Reachability: still latent. With today's queue (two open PRs, #1186 and #1187) master conflicts with neither on Agent.md at all, so nothing in flight can reach this shape. It needs an in-flight head whose base is older than #1181 — i.e. a PR that has not been rebased since the count line was removed.

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.

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.

check-merge-sequence.py's empty-plan refusal claims Agent.md carries the test count on a tree where no file states it

2 participants