feat(gates): refuse a cut that leaves a fragment unconsumed on its base (#1305) #1319

Merged
bosun merged 1 commit from i/1305-cut-fragment-coverage into main 2026-09-06 14:50:25 +02:00
Owner

A cut can leave a fragment behind on its base, so a release ships a change whose note rolls to the next version — and no gate sees it.

Intended-targets: #1305

The defect, reproduced from the run that shipped

v0.61.1   base       800a546e   .keep .template.md 1253.internal.md 1283.internal.md 1295.fixed.md
          merge-base 4d8828df   .keep .template.md 1283.internal.md
          head       12c2b6c6   .keep .template.md
          DELETED = merge-base - head            1283.internal.md
          UNCONSUMED = base - DELETED            1253.internal.md  1295.fixed.md

#1297 and #1298 merged twenty minutes after that branch was built, so their fragments were never in its tree and its commits could not delete them. 28/28 green, complement empty, fabricated control firing. fragment-check grades the fragments in the PR; a fragment that exists only on the base is outside every required context.

🔴 The predicate, and the wrong one that looks right

DELETED   = fragments(merge_base) - fragments(head)
UNCONSUMED = fragments(base) - DELETED

⚠️ Comparing the base against the HEAD instead is wrong in the direction that matters. A fragment added to the base after the fork is absent from the head too, so base - head reports it as consumed — the v0.61.1 defect, reproduced by the check written to catch it. TestCutFragmentCoverage_ABaseOnlyFragmentIsNotConsumedMerelyByBeingAbsentFromTheHead pins exactly that, and mutating the predicate to base - head reddens it plus the v0.61.1 arm.

AC1 — merge time, not review time

The verb reads base.sha (a live pointer to the target's tip) on every invocation and prints it, and the PASS says so:

"This is a claim about the base AS OF THIS RUN and nothing re-runs it: a merge to the base after this invalidates it."

That is the whole tracker. Instances 1 and 2 were caught by a reviewer reading the PR; instance 3 shipped because the base moved between a correct review and the merge.

AC3 — a mechanism, and deliberately not a review row

⚠️ It must not become a required status context. A status posted at PR time answers about a base that moves afterwards — precisely the failure it exists to catch. And a REQUEST_CHANGES is worse: internal/prep/pr.go skips rolling-PR regeneration while one stands (crew-doctrine#135), so a rejection freezes the branch at the state being objected to. A refusing verb the merger runs is the shape that does not deadlock. Excluded from C5 for that reason, with the row and the map entry (§13's two edits).

ListDirectory makes exactly one call, deliberately

?limit=1  -> the whole directory        &page=2 -> THE SAME ENTRIES, not an empty page

🔑 So the crew's paginate to completion, stop on an empty page rule would never terminate here. Measured on 15.0.7+gitea-1.22.0 and stated at the interface. It also avoids /pulls/{n}/files, which is capped at 50 and path-ordered — a 33-file cut PR is exactly the shape that truncates.

Mutation verification

Six, each verified applied by git diff --numstat and restored clean:

mutant red arms
DELETED from base - head 2 the v0.61.1 arm and the base-only arm
grade .keep / .template.md 3 every passing arm — a cut would always refuse
grade dir entries 1 NonFileEntriesAreIgnored
grade a closed PR 1 AClosedPRIsCannotGrade
read failure → empty set 4 all three read-failure subtests
drop the missing-ref guard 4 all three missing-ref subtests

📌 The first two are the ones that matter: one is the defect itself, the other is the failure mode that gets a gate switched off — a check that named all three fragments would refuse every cut.

Live

--pr 1303 (the merged v0.61.1 cut)   rc=2   "is \"closed\", not open — its base has moved on"

No open cut PR exists right now to demonstrate the pass; the regenerated-cut shape is pinned as an arm with the real SHAs instead.

Verification

what how result
§13's three artifacts TestHelpSnapshot · TestSubcommandsMatchC5Contract · TestC5ExclusionsAreDocumented all green
the main.go / help.txt churn is only realignment entry COUNT, not line count: 42→43, 39→40, 41→42, and git diff -w shows only the three added lines confirmed
gates go build · go vet · gofmt -l empty · go test ./... 27 ok / 0 FAIL · fragment-check 0 (zero findings on 1305.added.md) · register-check 0 · gitea-twin --check 0

🤖 Generated with Claude Code

https://claude.ai/code/session_011VD4JoNbNqJkS8H1RdJfZj

A cut can leave a fragment behind on its base, so a release ships a change whose note rolls to the next version — and no gate sees it. Intended-targets: #1305 ## The defect, reproduced from the run that shipped ``` v0.61.1 base 800a546e .keep .template.md 1253.internal.md 1283.internal.md 1295.fixed.md merge-base 4d8828df .keep .template.md 1283.internal.md head 12c2b6c6 .keep .template.md DELETED = merge-base - head 1283.internal.md UNCONSUMED = base - DELETED 1253.internal.md 1295.fixed.md ``` **`#1297` and `#1298` merged twenty minutes after that branch was built**, so their fragments were never in its tree and its commits could not delete them. **28/28 green, complement empty, fabricated control firing.** `fragment-check` grades the fragments *in* the PR; a fragment that exists only on the **base** is outside every required context. ## 🔴 The predicate, and the wrong one that looks right ``` DELETED = fragments(merge_base) - fragments(head) UNCONSUMED = fragments(base) - DELETED ``` ⚠️ **Comparing the base against the HEAD instead is wrong in the direction that matters.** A fragment added to the base after the fork is absent from the head too, so `base - head` reports it as **consumed** — the v0.61.1 defect, reproduced by the check written to catch it. **`TestCutFragmentCoverage_ABaseOnlyFragmentIsNotConsumedMerelyByBeingAbsentFromTheHead` pins exactly that**, and mutating the predicate to `base - head` reddens it plus the v0.61.1 arm. ## AC1 — merge time, not review time The verb reads `base.sha` (a live pointer to the target's tip) on **every** invocation and prints it, and the PASS says so: > *"This is a claim about the base AS OF THIS RUN and nothing re-runs it: a merge to the base after this invalidates it."* **That is the whole tracker.** Instances 1 and 2 were caught by a reviewer reading the PR; instance 3 shipped because the base moved between a correct review and the merge. ## AC3 — a mechanism, and deliberately not a review row ⚠️ **It must not become a required status context.** A status posted at PR time answers about a base that moves afterwards — **precisely the failure it exists to catch.** And a `REQUEST_CHANGES` is worse: `internal/prep/pr.go` skips rolling-PR regeneration while one stands (`crew-doctrine#135`), so a rejection **freezes the branch at the state being objected to**. A refusing verb the merger runs is the shape that does not deadlock. Excluded from C5 for that reason, with the row and the map entry (§13's two edits). ## `ListDirectory` makes exactly one call, deliberately ``` ?limit=1 -> the whole directory &page=2 -> THE SAME ENTRIES, not an empty page ``` 🔑 **So the crew's *paginate to completion, stop on an empty page* rule would never terminate here.** Measured on `15.0.7+gitea-1.22.0` and stated at the interface. It also avoids `/pulls/{n}/files`, which is capped at 50 and path-ordered — a 33-file cut PR is exactly the shape that truncates. ## Mutation verification Six, each verified applied by `git diff --numstat` and restored clean: | mutant | red | arms | |---|---|---| | `DELETED` from `base - head` | 2 | the v0.61.1 arm **and** the base-only arm | | grade `.keep` / `.template.md` | 3 | every passing arm — a cut would always refuse | | grade `dir` entries | 1 | `NonFileEntriesAreIgnored` | | grade a closed PR | 1 | `AClosedPRIsCannotGrade` | | read failure → empty set | 4 | all three read-failure subtests | | drop the missing-ref guard | 4 | all three missing-ref subtests | 📌 **The first two are the ones that matter**: one is the defect itself, the other is the failure mode that gets a gate switched off — a check that named all three fragments would refuse every cut. ## Live ``` --pr 1303 (the merged v0.61.1 cut) rc=2 "is \"closed\", not open — its base has moved on" ``` **No open cut PR exists right now to demonstrate the pass**; the regenerated-cut shape is pinned as an arm with the real SHAs instead. ## Verification | what | how | result | |---|---|---| | §13's three artifacts | `TestHelpSnapshot` · `TestSubcommandsMatchC5Contract` · `TestC5ExclusionsAreDocumented` | all green | | the `main.go` / `help.txt` churn is only realignment | **entry COUNT, not line count**: 42→43, 39→40, 41→42, and `git diff -w` shows only the three added lines | confirmed | | gates | `go build` · `go vet` · `gofmt -l` empty · `go test ./...` 27 ok / 0 FAIL · `fragment-check` 0 (zero findings on `1305.added.md`) · `register-check` 0 · `gitea-twin --check` 0 | | 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_011VD4JoNbNqJkS8H1RdJfZj
feat(gates): refuse a cut that leaves a fragment unconsumed on its base
All checks were successful
gitea-twin-check / check (pull_request) Successful in 5s
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 30s
check-self-bootstrap / check (pull_request) Successful in 29s
base-divergence-check / check (pull_request) Successful in 29s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 35s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 38s
go-ci / lint + build + test (pull_request) Successful in 29s
tests / workflow-schema (pull_request) Successful in 4s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 28s
ac-closure-check / ac-closure check (pull_request) Successful in 58s
ac-closure-check / check (pull_request) Successful in 0s
fragment-check / changelog fragment-kind (pull_request) Successful in 1m2s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 1m2s
prep-order-check / check (pull_request) Successful in 33s
fragment-check / check (pull_request) Successful in 0s
changelog-body-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 24s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 36s
go-ci / page landing-tree failure (pull_request) Has been skipped
workflow-parse-check / workflow parse and schema (pull_request) Successful in 5s
workflow-parse-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 57s
tests / shellcheck (pull_request) Successful in 23s
manifest-check / check (pull_request) Successful in 0s
tests / contract-paths (pull_request) Successful in 32s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 29s
tests / dated-examples (pull_request) Successful in 35s
register-check / register-drift check (pull_request) Successful in 57s
register-check / check (pull_request) Successful in 0s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 51s
74d60e4654
A cut PR deletes the fragments its branch carried. A fragment merged to
the base after the branch was built is not in that branch's tree, so the
cut's commits cannot delete it -- replayed onto the base it SURVIVES, and
the release ships the change with its note deferred to the next version.

Measured twice, and the second one shipped:

  v0.61.0  1287.fixed.md existed only on the base; held, regenerated
  v0.61.1  base carried 1253 and 1295, the cut consumed only 1283

No existing gate sees it. fragment-check grades the fragments IN the PR,
so a fragment that exists only on the BASE is outside every required
context: v0.61.1 was 28/28 green with an empty complement and a firing
control while carrying this defect.

The predicate is base MINUS (merge-base MINUS head). Comparing the base
against the HEAD instead is wrong in the direction that matters, because
a fragment added to the base after the fork is absent from the head too
and would read as consumed -- the defect, reproduced by the check meant
to catch it. An arm pins that specifically.

Run at merge time, not review time: it re-reads the base's current tip on
every invocation and the PASS states its own expiry. It is deliberately
not a required context and not a review row -- rolling-PR regeneration is
skipped while a live REQUEST_CHANGES stands (crew-doctrine#135), so a
rejection would freeze the branch at the state being objected to.

ListDirectory is new. It makes exactly one call: measured on this forge,
the contents endpoint ignores `limit` and returns the same entries for
`page=2`, so the crew's stop-on-an-empty-page rule would never terminate.

Intended-targets: #1305

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011VD4JoNbNqJkS8H1RdJfZj
bosun requested review from lookout 2026-09-06 13:39:13 +02:00
surveyor force-pushed i/1305-cut-fragment-coverage from 74d60e4654
All checks were successful
gitea-twin-check / check (pull_request) Successful in 5s
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 30s
check-self-bootstrap / check (pull_request) Successful in 29s
base-divergence-check / check (pull_request) Successful in 29s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 35s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 38s
go-ci / lint + build + test (pull_request) Successful in 29s
tests / workflow-schema (pull_request) Successful in 4s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 28s
ac-closure-check / ac-closure check (pull_request) Successful in 58s
ac-closure-check / check (pull_request) Successful in 0s
fragment-check / changelog fragment-kind (pull_request) Successful in 1m2s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 1m2s
prep-order-check / check (pull_request) Successful in 33s
fragment-check / check (pull_request) Successful in 0s
changelog-body-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 24s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 36s
go-ci / page landing-tree failure (pull_request) Has been skipped
workflow-parse-check / workflow parse and schema (pull_request) Successful in 5s
workflow-parse-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 57s
tests / shellcheck (pull_request) Successful in 23s
manifest-check / check (pull_request) Successful in 0s
tests / contract-paths (pull_request) Successful in 32s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 29s
tests / dated-examples (pull_request) Successful in 35s
register-check / register-drift check (pull_request) Successful in 57s
register-check / check (pull_request) Successful in 0s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 51s
to dddd02258d
All checks were successful
ac-closure-check / ac-closure check (pull_request) Successful in 8s
ac-closure-check / check (pull_request) Successful in 0s
prep-order-check / check (pull_request) Successful in 6s
register-check / register-drift check (pull_request) Successful in 8s
register-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 4s
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 28s
gitea-twin-check / check (pull_request) Successful in 27s
base-divergence-check / check (pull_request) Successful in 28s
check-self-bootstrap / check (pull_request) Successful in 27s
tests / bats (pull_request) Successful in 24s
tests / shellcheck (pull_request) Successful in 26s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 54s
fragment-check / changelog fragment-kind (pull_request) Successful in 54s
changelog-body-check / check (pull_request) Successful in 0s
fragment-check / check (pull_request) Successful in 0s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
tests / contract-paths (pull_request) Successful in 32s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 59s
manifest-check / check (pull_request) Successful in 0s
tests / dated-examples (pull_request) Successful in 36s
go-ci / lint + build + test (pull_request) Successful in 1m10s
go-ci / page landing-tree failure (pull_request) Has been skipped
workflow-parse-check / workflow parse and schema (pull_request) Successful in 30s
workflow-parse-check / check (pull_request) Successful in 0s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 59s
2026-09-06 14:21:42 +02:00
Compare
lookout approved these changes 2026-09-06 14:49:15 +02:00
lookout left a comment

Reviewed exact head dddd02258d. APPROVED. The new cut-fragment-coverage-check computes deleted fragments from merge-base minus head, then checks that every gradeable fragment on the live base is in that deletion set; the base-only regression and v0.61.1 replay controls distinguish this from the unsafe base-minus-head predicate. It ignores only the deliberate .keep/.template.md entries and non-file directory entries, refuses closed/malformed/read-error cases as cannot-grade, names the base SHA and expiry, and keeps the verb out of C5/required CI because it must run at merge time. The new Forgejo directory read is one-call, strict-array, and fail-closed, with the configured three-ref read and mutation controls. Targeted exact-tree Go tests (cmd/rt, internal/forgejo, verdict), gofmt, and terminal 26/26 CI are green.

Reviewed exact head dddd02258d3c8b55e4f3ce6e7de7933e5db56cfb. APPROVED. The new cut-fragment-coverage-check computes deleted fragments from merge-base minus head, then checks that every gradeable fragment on the live base is in that deletion set; the base-only regression and v0.61.1 replay controls distinguish this from the unsafe base-minus-head predicate. It ignores only the deliberate .keep/.template.md entries and non-file directory entries, refuses closed/malformed/read-error cases as cannot-grade, names the base SHA and expiry, and keeps the verb out of C5/required CI because it must run at merge time. The new Forgejo directory read is one-call, strict-array, and fail-closed, with the configured three-ref read and mutation controls. Targeted exact-tree Go tests (cmd/rt, internal/forgejo, verdict), gofmt, and terminal 26/26 CI are green.
bosun merged commit e27b696e5c into main 2026-09-06 14:50:25 +02:00
bosun deleted branch i/1305-cut-fragment-coverage 2026-09-06 14:50:25 +02:00
Sign in to join this conversation.
No description provided.