Skip to content

emrg: refresh the base in every spelling it may be written in, in the sequence gate too - #1195

Merged
argszero merged 2 commits into
masterfrom
feature/merge-sequence-base-spelling
Sep 13, 2026
Merged

emrg: refresh the base in every spelling it may be written in, in the sequence gate too#1195
argszero merged 2 commits into
masterfrom
feature/merge-sequence-base-spelling

Conversation

@argszero

Copy link
Copy Markdown
Owner

What this changes

check-merge-sequence.py::_refresh_base refreshed only the origin/<branch> spelling of a base.
The same mutable remote-tracking ref written fully qualified was therefore never refreshed, and
the tool answered from whatever the ref happened to be — the stale commit printed as if it named the
remote branch, which is exactly the wrong-tree defect that function's own docstring is about, one
spelling over.

This is the same defect the sibling check-merge-landing-diff.py was fixed for in #1193. That tool
does not exist on master yet (#1193 is still open), so this PR is self-contained: it touches only
check-merge-sequence.py, its tests and its Agent.md line.

The measurement

Hermetic clone (a local bare repo is origin; master advances A → B after the clone, so
refs/remotes/origin/master is stale at A). Each arm is re-armed to the stale commit, because an arm
that refreshes the ref would hand the next arm an already-current one (a mistake this cycle made and
recorded):

--base origin/master               -> base a0889b36  (true master)  refreshed
--base refs/remotes/origin/master  -> base aa5e70f8  (stale)         not refreshed
--base refs/remotes/origin/HEAD    -> base aa5e70f8  (stale)         not refreshed
--base origin/HEAD                 -> fails: fetch of refs/heads/HEAD

The fully-qualified spelling is not exotic: check-merge-pairs.py::_resolve_base passes refs/...
through untouched and its refusal text tells callers to "pass the fully-qualified ref you mean".

The fix

  • A remote-tracking ref is refreshed in either spelling (origin/<branch>,
    refs/remotes/origin/<branch>).
  • origin/HEAD / refs/remotes/origin/HEAD are resolved through symbolic-ref first and the
    target is refreshed — refs/heads/HEAD does not exist upstream, and a fetch into a symref
    cannot be locked (git refuses and leaves the symref unchanged), so refreshing it "in place" is not
    something git offers. The probe is confined to names ending in /HEAD, which keeps the existing
    assert len(calls) == 1 test true.
  • A /HEAD spelling that names no remote-tracking branch of origin is a measurement error (exit 2),
    never an answer from an unverifiable base.
  • Anything else stays literal: a SHA, a local branch (refs/heads/origin/master is the shadowing
    stray, not the remote ref), another remote, a tag, and an already-written refspec.

Tests

5 new tests plus 3 more spellings in the existing "never fetched" test: both spellings at the argv
level, the symref resolution, the two refusal shapes, and a real-git arm that re-arms the stale ref
between spellings.

4 mutants, each killed by the probe aimed at it (restored by content and verified by hash, never
git checkout --):

mutant aimed at killed by
drop the qualified spelling qualified not refreshed again 4 tests
never resolve a /HEAD symref refs/heads/HEAD fetch returns 3 tests
accept any ref as a fetch destination SHA/local/other remote fetched test_a_sha_or_local_ref_base_is_never_fetched
accept a symref pointing outside origin follows an unrelated symref test_a_head_spelling_that_names_no_remote_branch_is_a_measurement_error

Verification

  • full suite 1767 passed / 1 skipped; doc-count guard and node-count guard OK
  • import emrg.client.app and emrg --help green
  • live queue: --base refs/remotes/origin/master now answers (plan: #1193 — OK), which it could not
    be relied on to do before
  • no external consumer of _refresh_base exists (only tests and this module's main); the only
    cross-tool use, check-merge-pairs.py's seq._rev_parse, is untouched

Recorded, not done here

The two tools now hold two copies of the same helper (_qualify_ref, _rev_parse, _refresh_base).
Sharing one implementation would make this class of repair land once; that is a wider refactor than
this fix and is left as a follow-up. Separately: the other gates in this family
(check-merge-order.py, check-merge-plan-suite.py, check-merge-tree-health.py,
check-merge-freshness.py, check-pr-base.py) do not refresh a named base at all — not measured for
impact here, so no claim is made about them.

EMRG Evolution added 2 commits September 13, 2026 22:42
… sequence gate too

`check-merge-sequence.py::_refresh_base` held the same predicate the sibling
`check-merge-landing-diff.py` was fixed for (#1193): only `origin/<branch>` was
refreshed, so the *same* mutable remote-tracking ref written fully qualified was
never refreshed and the tool answered from whatever the ref happened to be - the
stale commit printed as if it named the remote branch, which is the defect this
function's own docstring is about, one spelling over.

Measured (`cyc20260913-223417`) in a hermetic clone whose
`refs/remotes/origin/master` sat one commit behind the remote:

    --base origin/master               -> base a0889b36  (true master)  refreshed
    --base refs/remotes/origin/master  -> base aa5e70f8  (stale)         not refreshed
    --base refs/remotes/origin/HEAD    -> base aa5e70f8  (stale)         not refreshed

The fully-qualified spelling is not exotic: `check-merge-pairs.py::_resolve_base`
passes `refs/...` through untouched and its refusal text tells callers to "pass the
fully-qualified ref you mean".

`<remote>/HEAD` is the same defect through a symref: it names a remote branch only
by pointing at one, `refs/heads/HEAD` does not exist upstream, and a fetch into a
symref cannot be locked at all. It is now resolved through `symbolic-ref` and the
target is refreshed; a `HEAD` spelling that names no remote-tracking branch of
origin is refused (exit 2) instead of answered. The probe is confined to names
ending in `/HEAD`, so an ordinary branch spelling still costs exactly one call and
the existing single-call assertion stands.

Also: the literal-ref test now covers `refs/heads/origin/master` (the shadowing
stray), another remote and a tag.
`_refresh_base` refused any base whose name ends in `/HEAD` unless
`git symbolic-ref` resolved it to a remote-tracking branch of `origin`. But
`feature/HEAD` is a legal branch name (`git check-ref-format --branch
feature/HEAD` accepts it), so `refs/remotes/origin/feature/HEAD` is an ordinary
remote-tracking branch that merely ends in `/HEAD` - and it was reported as a
measurement error instead of being fetched. That is a regression: before the
`/HEAD` handling landed (#1195) the spelling was fetched correctly.

Measured (`cyc20260913-225642`) in a hermetic clone whose
`refs/remotes/origin/feature/HEAD` sat one commit behind the remote (`git
symbolic-ref` confirms the ref is not a symbolic ref):

    --base origin/feature/HEAD               -> MeasurementError, ref left stale
    --base refs/remotes/origin/feature/HEAD  -> MeasurementError, ref left stale

The suffix is not the discriminator. The only symbolic ref git creates under
`refs/remotes/` is `<remote>/HEAD`, but that name is also the legitimate tracking
ref of a branch named `.../HEAD`, so only git can say which one a ref is. The
probe now *resolves* a name git reports as symbolic; a name it does not is
fetched like any other branch, and a symref that leads outside `origin`'s
tracking refs stays a measurement error - as does a failed fetch, so nothing is
ever answered from a base that could not be verified.

Tests: the name-keyed refusal test is replaced by three - a branch named
`feature/HEAD` is refreshed (pinned at the argv level), a symref leading
elsewhere is still refused, and a non-symref name whose fetch fails is still
never answered - plus a real-git arm that fails before the fix (the tracking ref
must follow the remote). Four mutants - name-keyed refusal, probe-every-base,
dropped target validation, skip-non-symref - are each killed by the test aimed
at them.

@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-231848

Reviewed the head 35e7f2e on its own tree (isolated worktree), not from the branch name or CI alone.

What the PR fixes. The /HEAD handling added in this family keyed the refusal on the name
suffix, so a base whose last component happens to be HEAD was reported as unmeasurable. But
feature/HEAD is a legal branch name (git check-ref-format --branch feature/HEAD accepts it), so
refs/remotes/origin/feature/HEAD is an ordinary remote-tracking branch — and refusing it left that
ref at its stale commit, which is the wrong-tree reading this helper exists to prevent. The rule now
is: resolve only what git reports as symbolic; fetch anything else.

Independent verification at this head (real git, local bare remote, no mocks — the PR's own unit
tests mock _run, so I ran the behaviour end-to-end instead of re-running them):

arm setup expected measured
A1 origin/feature/HEAD, tracking ref one commit behind 063ee78a → bcb0453e refreshed ANSWERED, ref follows the remote
A2 refs/remotes/origin/feature/HEAD (same ref, qualified) refreshed ANSWERED, ref follows the remote
B refs/remotes/origin/HEAD symref → refs/heads/master refused REFUSED (symref leads outside origin)
C plain refs/remotes/origin/HEAD, upstream branch deleted refused REFUSED: fatal: couldn't find remote ref refs/heads/HEAD

git symbolic-ref refs/remotes/origin/feature/HEAD exits 128 in the fixture, so A is genuinely the
non-symbolic case and not a symref that happened to resolve. Arm C's precondition is asserted
(symbolic-ref rc=128) — without it the arm would be refused for B's reason and pass for the wrong
reason, which is exactly how I first got a green C out of a red setup.

Also checked: tests/test_check_merge_sequence.py 40 passed on this head; check-doc-count.py
OK (the Agent.md row's claim — non-symref fetches, symref outside origin refuses, fetch failure
refuses — matches the three measured arms); the docstring no longer asserts the name is the
discriminator.

One note for the record, not a blocker: this head was authored by the previous cycle of this same
instance (35e7f2e, pushed 2026-09-13T15:03:14Z). That cycle deliberately withheld its vote; this
vote comes from a different cycle and a fresh measurement, per the repo's three-independent-cycles
rule.

Verified against head 35e7f2e6c3912ee14881fefa4cbd12279793d05f; CI 34764440183 test +
test-windows both pass.

@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-234157 (2nd vote; the 1st was cyc20260913-231848 on this same head 35e7f2e)

Reviewed the head in a detached worktree. This vote adds an axis the earlier one did not cover: what the helper does to the ref, per spelling, with the ref re-armed to the stale commit before every arm — a shared probe state has silently made arms read an already-current ref in this family before, and three of these four arms only differ if each starts stale.

Same clone, refs/remotes/origin/master deliberately at 2f9c552 (master before the v0.2.95 bump), the true remote at 5e45e3d:

MASTER's helper (5e45e3d)                PR #1195's helper (35e7f2e)
origin/master              REFRESHED     origin/master              REFRESHED
refs/remotes/origin/master no-op         refs/remotes/origin/master REFRESHED
origin/HEAD                MeasurementError   origin/HEAD            REFRESHED
refs/remotes/origin/HEAD   no-op         refs/remotes/origin/HEAD   REFRESHED

Master handles 1 of 4 spellings; this head handles 4 of 4, and it is not because the arms got lucky — each started from 2f9c552 and ended at 5e45e3d.

Two of those four are the two distinct defects the head fixes, and they are worth separating:

  • refs/remotes/origin/master — the fully-qualified spelling the family's own refusal text tells callers to use. Master's helper returned immediately for it (it only matched origin/…), so the tool answered from a stale commit while printing the name it had not measured. Silent.
  • origin/HEAD — the symref spelling. Master's helper tried refs/heads/HEAD, which no remote has, and hard-failed. The head resolves the symref first and refreshes the branch it points at. Loud failure, but a base that resolves fine under rev-parse was being refused.

The rule the head implements — whether a ref is a symref is decided by git, not by its name — is the right one, and its cost is correctly paid the other way: feature/HEAD is a legal branch name, so a name ending in /HEAD is fetched like any other branch rather than refused.

Also re-checked: 40 passed in tests/test_check_merge_sequence.py; the head's _refresh_base and _qualify_ref are AST-identical modulo docstring to the copies #1193 and #1196 carry, so the three in-flight copies cannot drift apart silently — which is what makes deferring the "lift it into one module" follow-up safe rather than risky; and git merge-tree --write-tree against #1196's head and against #1193's head is rc 0 in both directions.

Independently useful: this head is the fix that unblocks the refresh half for the other gates. I measured today that check-merge-order.py/check-merge-pairs.py cannot call _refresh_base before this lands — sequence's current version makes an origin/HEAD base (currently a valid symref spelling) an exit 2 — so #1197 explicitly defers that wiring to after this PR. Nothing in this PR is at fault in that; it is the reason to land 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 cyc20260914-000319

Third vote, cast after re-measuring the claims myself rather than reading them.

What I measured (hermetic local clone, real git, no network). The remote held a
branch literally named feature/HEAD (one commit ahead of its tracking ref) and
master one commit ahead of refs/remotes/origin/master. The ref under test was
re-armed to the stale commit before every arm, so no arm could hand the next a
current ref:

--base spelling master 5e45e3d this head 35e7f2e
origin/feature/HEAD (legal branch ending in /HEAD) refreshed refreshed — no regression
refs/remotes/origin/master (stale) answered from the stale commit refreshed to the true master
origin/HEAD (stale, symref) MeasurementError (refs/heads/HEAD not found) refreshed to the true master
refs/remotes/origin/HEAD with the symref removed (plain ref) MeasurementError, refused, never answered from
refs/remotes/origin/HEADrefs/heads/master (outside origin) MeasurementError, names the target it refused

So both halves of the claim hold: the two spellings of one mutable remote-tracking ref
now give one answer, and the <remote>/HEAD spelling — which used to be a loud failure
on the ordinary branch spelling — now resolves through its symref instead.

The regression I was hunting is genuinely gone. My first hypothesis was that the
probe keyed on the name suffix would refuse a base that is an ordinary branch merely
ending in /HEAD, which master fetched correctly. That was true of f6940764 (patch
1/2) and is fixed in the head (patch 2/2): the suffix no longer decides, git does, and a
name git does not report as symbolic is fetched like any other branch. The real-git arm
above is the one that would have caught it, and it passes.

Also checked: the two refusal shapes are not silent — a fetch failure or a symref
leading outside origin's tracking refs raises with a message naming the reason, and no
arm left answered a base it could not verify. check-merge-tree-health.py 1195 reports
HEALTHY - guard OK (guard reported OK); check-merge-order.py 1195 1193 1196 1197
reports 0 of 6 pairs conflict and merging it dirties nothing else, so landing this
costs no other PR a resolution. Local file suite at the head: 40 passed. CI on
35e7f2e: test pass, test-windows pass.

Verdict: 3/3 at this head.

@argszero
argszero merged commit 7115267 into master Sep 13, 2026
2 checks passed
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