Skip to content

fix(bin): resolve the fork trunk as a landing target - #47

Merged
sbracewell64 merged 1 commit into
mainfrom
fm/landed-check-measures-against-upstream-not-fork
Aug 6, 2026
Merged

fix(bin): resolve the fork trunk as a landing target#47
sbracewell64 merged 1 commit into
mainfrom
fm/landed-check-measures-against-upstream-not-fork

Conversation

@sbracewell64

Copy link
Copy Markdown
Owner

The defect

The "has this landed?" check measured against a trunk this fleet does not land on, so proven-landed work read as unlanded and held its worktree slot indefinitely.

origin fetches kunchenguid/firstmate but pushes sbracewell64/firstmate. Both refs fm_landed_candidate_refs considered were wrong at once: refs/remotes/origin/main tracked upstream, and refs/heads/main had not been fast-forwarded since the fork trunk advanced five times that day. Nothing tracked the trunk this fleet actually lands on.

Measured on the held slot (PR 44, squash-merged, recorded head 2582c15, fork trunk f90ed1d):

check result
git diff --stat 2582c15 f90ed1d empty - content demonstrably landed
git merge-base --is-ancestor 2582c15 f90ed1d false - squash, ancestry cannot hold
containment vs refs/heads/main 1 - proven not contained
containment vs refs/remotes/origin/main 2 - inconclusive
containment vs the fork trunk 0 - proven contained

The squash case is exactly what fm_landed_tree_contains exists to handle, and it works. It simply never saw the fork trunk.

The fix

The default fetch refspec cannot reach the fork, because it points at the fetch url. So:

  • fm_landed_push_url resolves the push url when it differs from the fetch url. This is the same fetch-versus-push comparison bin/fm-task-base-lib.sh already made at the spawn site in fix(bin): separate task read and contribution bases (land of upstream #1613) #43; it now has one owner and both libraries read it - that one names the upstream side, this one the landing side.
  • fm_landed_refresh_push_target fetches that trunk into refs/fm-landing/origin/<name>.
  • fm_landed_candidate_refs offers it like any other candidate.

Why this is not a more permissive check

  • The added ref is a landing target only because this fleet demonstrably pushes there. It is never inferred from a remote's name.
  • Only a caller that already refreshes remotes performs the fetch, so bin/fm-worktree-guard.sh stays local and grows no network dependency.
  • The three-way distinction is preserved. A push url that exists but cannot be read leaves the landing target unread, and teardown refuses rather than falling back to the upstream answer. That case previously passed on that fallback, so this direction is strictly stricter.
  • A repository whose fetch and push urls agree resolves no landing ref at all and takes an identical path.
  • No second landed-check was added; both call sites still run the one containment instrument.

Verification

Every negative control was watched red first.

Pre-fix, with the new tests in place:

not ok - fork-split-landed: teardown should succeed when the fork trunk holds the content: expected exit 0, got 1
not ok - fork-split-unreadable: teardown should refuse when the landing target cannot be read: expected exit 1, got 0
not ok - (o6) guard refused a slot whose content is on the fork trunk

The unlanded, upstream-only and single-remote controls pass both before and after, so they are not vacuous. All green post-fix.

Against real state, using the real remote urls, the real commits and a real network fetch:

resolved push url : https://github.com/sbracewell64/firstmate.git
landing ref named : refs/fm-landing/origin/main
containment vs upstream origin/main -> 2   (inconclusive - what held the slot)
containment vs FORK trunk           -> 0   (proven landed)

The same slot plus one genuinely unlanded commit still reports 1 (not contained), and with the push url made unreadable the refresh returns 1, which the caller must treat as a refusal.

New coverage: tests/fm-teardown.test.sh cases (u) through (y) and tests/fm-worktree-guard.test.sh cases (o6) through (o8).

Test run

Full changed-scope selection: 75 scripts, 69 pass, 6 fail. Those 6 fail identically on the unmodified fork trunk - tmux readiness, node ESM module resolution, and harness env vars - so they are pre-existing and environmental. Zero new failures. bin/fm-lint.sh and bin/fm-doc-audience-check.sh both clean.

Known limitation, disclosed

Teardown now performs one additional network fetch per cleanup on a fetch/push-split home. The existing origin fetch already carried the same exposure and neither has a timeout, so this is not a new class of failure, but it is one more remote read on that path.

The "has this landed?" check measured against a trunk this fleet does not
land on, so proven-landed work read as unlanded and held its worktree slot.

With `origin` fetching kunchenguid/firstmate and pushing sbracewell64/
firstmate, both refs the check considered were wrong at once: origin/main
tracked upstream, and refs/heads/main had not been fast-forwarded since the
fork trunk advanced five times that day. Measured on the held slot:

  PR 44 squash-merged, recorded head 2582c15, fork trunk f90ed1d
  git diff --stat 2582c15 f90ed1d              -> empty (content landed)
  git merge-base --is-ancestor 2582c15 f90ed1d -> false (squash)
  fm_landed_tree_contains vs refs/heads/main       -> 1 (not contained)
  fm_landed_tree_contains vs origin/main           -> 2 (inconclusive)
  fm_landed_tree_contains vs the fork trunk        -> 0 (contained)

The default fetch refspec cannot reach the fork, because it points at the
fetch url. fm_landed_push_url resolves the push url when it differs from
the fetch url - the same fetch-versus-push comparison bin/fm-task-base-lib.sh
already made at the spawn site, now owned in one place and read by both -
and fm_landed_refresh_push_target fetches that trunk into
refs/fm-landing/origin/<name> for fm_landed_candidate_refs to offer.

The check is not made more permissive. The added ref is a landing target
only because this fleet demonstrably pushes there, never inferred from a
remote's name. Only a caller that already refreshes remotes fetches it, so
the guard stays local. A push url that exists but cannot be read leaves the
landing target unread, and teardown refuses rather than falling back to the
upstream answer - a case that previously passed on that fallback. A
repository whose fetch and push urls agree resolves no landing ref at all
and takes an identical path.

Every negative control was watched red first: pre-fix, the fork-landed slot
is refused at both the teardown and guard sites, and the unreadable-push-url
case wrongly passes. The unlanded, upstream-only, and single-remote controls
pass before and after, so they are not vacuous. Verified against the live
held slot: the resolved push url fetches the real fork trunk, which reports
the merged work contained while upstream stays inconclusive, and the same
slot plus one unlanded commit still reports not contained.

Full changed-scope run: 75 scripts, 6 failing identically on the unmodified
fork trunk (tmux readiness, node ESM resolution, harness env), 0 new.
@sbracewell64
sbracewell64 merged commit 744f982 into main Aug 6, 2026
12 of 14 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