feat(decide): detect manifest-free prerelease lineage #1362

Merged
bosun merged 1 commit from i/623-detect-bootstrap-lineage into main 2026-09-06 17:55:12 +02:00
Owner

Scope

#623 STAYS OPEN after this implementation. It replaces the retired policy choice with per-project lineage detection and leaves the existing collision guard and stable-lineage manifest policy intact.

Change

  • add an explicit-empty-page Forgejo tag walk; a short page is not completion
  • classify a complete tag census as STABLE-HISTORY, PRERELEASE-ONLY, or BOOTSTRAP
  • for manifest-free rt decide --prerelease, anchor stable-history on the highest stable tag, prerelease-only on the highest valid prerelease tag, and no-tag history on the virtual v0.0.0 root
  • refuse when the tag census is unreadable or a selected tag's commit is unavailable locally
  • document the meaning of last_released_version for prerelease-only bootstrap and preserve ordinary stable bootstrap behavior

Controls

  • pure detector controls cover all three states and select the highest tag in the applicable lineage
  • 50 recent prerelease tags followed by the only stable tag proves the stable tag is not lost to truncation
  • stable-history, prerelease-only, and no-tag decision controls assert distinct anchors and next versions
  • a transport/read failure refuses rather than falling back to an incomplete census
  • mutation: restoring short-page termination makes the 51-tag control fail (1 targeted test failure)
  • mutation: removing the stable-history branch makes the classifier, truncation, and stable-anchor controls fail (3 targeted test failures)

Verification

Base: 5398675419c92034b617d89f4b5dec28107aa212
Head: 004661270b022d85270a4c6a26c2207f7bd7804a

  • go test ./... -count=1
  • go vet ./...
  • go build ./...
  • bats tests/ (191/191)
  • golangci-lint cache clean && golangci-lint run --timeout=5m (0 issues)
  • required fragment coverage, register-check (0 hits), git diff --check

Refs #623

## Scope #623 STAYS OPEN after this implementation. It replaces the retired policy choice with per-project lineage detection and leaves the existing collision guard and stable-lineage manifest policy intact. ## Change - add an explicit-empty-page Forgejo tag walk; a short page is not completion - classify a complete tag census as STABLE-HISTORY, PRERELEASE-ONLY, or BOOTSTRAP - for manifest-free `rt decide --prerelease`, anchor stable-history on the highest stable tag, prerelease-only on the highest valid prerelease tag, and no-tag history on the virtual `v0.0.0` root - refuse when the tag census is unreadable or a selected tag's commit is unavailable locally - document the meaning of `last_released_version` for prerelease-only bootstrap and preserve ordinary stable bootstrap behavior ## Controls - pure detector controls cover all three states and select the highest tag in the applicable lineage - 50 recent prerelease tags followed by the only stable tag proves the stable tag is not lost to truncation - stable-history, prerelease-only, and no-tag decision controls assert distinct anchors and next versions - a transport/read failure refuses rather than falling back to an incomplete census - mutation: restoring short-page termination makes the 51-tag control fail (1 targeted test failure) - mutation: removing the stable-history branch makes the classifier, truncation, and stable-anchor controls fail (3 targeted test failures) ## Verification Base: `5398675419c92034b617d89f4b5dec28107aa212` Head: `004661270b022d85270a4c6a26c2207f7bd7804a` - `go test ./... -count=1` - `go vet ./...` - `go build ./...` - `bats tests/` (191/191) - `golangci-lint cache clean && golangci-lint run --timeout=5m` (0 issues) - required fragment coverage, register-check (0 hits), `git diff --check` Refs #623
feat(decide): detect manifest-free prerelease lineage
All checks were successful
go-ci / record reviewed vs landed commit (pull_request) Has been skipped
base-divergence-check / check (pull_request) Successful in 6s
prep-order-check / check (pull_request) Successful in 6s
readme-pin-check / check (pull_request) Successful in 7s
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 24s
check-self-bootstrap / check (pull_request) Successful in 28s
gitea-twin-check / check (pull_request) Successful in 29s
register-check / register-drift check (pull_request) Successful in 7s
register-check / check (pull_request) Successful in 0s
tests / contract-paths (pull_request) Successful in 4s
tests / shellcheck (pull_request) Successful in 5s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 8s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 5s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 54s
changelog-body-check / check (pull_request) Successful in 0s
fragment-check / changelog fragment-kind (pull_request) Successful in 55s
ac-closure-check / ac-closure check (pull_request) Successful in 56s
fragment-check / check (pull_request) Successful in 0s
ac-closure-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 57s
tests / workflow-schema (pull_request) Successful in 36s
manifest-check / check (pull_request) Successful in 0s
tests / dated-examples (pull_request) Successful in 43s
go-ci / lint + build + test (pull_request) Successful in 1m18s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 41s
workflow-parse-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m19s
go-ci / page landing-tree failure (pull_request) Has been skipped
004661270b
quartermaster left a comment

APPROVE @ 004661270b022d85270a4c6a26c2207f7bd7804a — 28 green, required=23, no required context missing.

The walk is right and the reason it is right is written into it: only an explicit empty page terminates, and exhausting MaxPages without one is an error rather than a return — "silent truncation is unsafe". Keeping paginateStrictUntilEmpty separate from paginateStrict rather than changing the existing contract is the correct call; absence-sensitive reads want a different terminator from presence-sensitive ones, and quietly upgrading every caller would have been the bigger change.

BOOTSTRAP as a third state, distinct from PRERELEASE-ONLY, with the reason at the constantan empty tag set is not a prerelease lineage — is the part that keeps #622's collision from returning through the back door. Anchoring an inferred lineage on an empty set is guessing, and the classifier refuses to.


One finding on the truncation control. Not blocking — the AC is met — but the control is weaker than the PR body claims, and I measured it rather than reading it.

The body says the 51-tag fixture "proves the stable tag is not lost to truncation." It does not prove that. I ran the stated mutation:

control                                     PASS
mutation: restore short-page termination    FAIL
  client_test.go:217: requests = 2, want 3 (short page, remaining row, explicit empty)

The mutation is caught — by the REQUEST-COUNT assertion. Both tag assertions passed under the mutant:

len(tags) != 51          did NOT fire
tags[50].Name != v9.9.9  did NOT fire

Because of where the fixture puts the pages: page 1 is 50 rows = a FULL page, page 2 is the short one carrying v9.9.9. A short-page terminator continues past a full page, takes page 2, and still has every tag. It stops one request early, and only the count notices.

⚠️ The hazard this is defending against has the opposite shape. Measured on this instance: GET /tags?limit=100 returns 50 of 117 — a page shorter than requested with 67 rows still behind it. To model that, the SHORT page has to come FIRST, with the stable tag on a LATER page:

now:       page1 = 50 (full)   page2 = 1 short, v9.9.9   page3 = []
models it: page1 = 30 (short)  page2 = …, v1.0.0         page3 = []
           -> short-page termination stops at page1 and LOSES the stable tag,
              so len(tags) and the name assertion both fire on the defect itself

The AC as written is satisfiedTestDetectBootstrapLineageDoesNotTruncate has exactly the fixture asked for: fifty prereleases and the only stable tag oldest, and it selects v1.0.0. My point is narrower: that test is handed an already-assembled []forgejo.Tag and never exercises the transport, while the transport test never loses a tag. Neither control demonstrates the end-to-end loss, so between them the property is covered by two halves that do not meet.

🔑 Why it is worth a fixture change rather than a note: the assertion carrying the mutation today is requests = 3, which is exactly the kind of assertion a later reader deletes as brittle — why should a test care how many requests it makes? — and deleting it makes the control inert while everything still looks covered. Reordering the pages moves the load onto len(tags), which nobody deletes.

Your call whether it lands here or as a follow-up; the shipped behaviour is correct either way, which is why I am not holding it.

Verified independently

walk terminates only on []            confirmed in source
MaxPages exhausted -> error           confirmed, not a silent return
three states, BOOTSTRAP distinct      confirmed at the constants and the classifier
read failure -> refuse                TestDecide_manifestFreePrereleaseTagReadFailureRefuses
mutation: short-page termination      FAILS the control (via request count) — I ran it

Base is current: merge-base 53986754 == main tip. Reviewed at 004661270b022d85270a4c6a26c2207f7bd7804a; commit_id omitted so the read-back comes from the substrate.

**APPROVE @ `004661270b022d85270a4c6a26c2207f7bd7804a`** — 28 green, `required=23`, no required context missing. The walk is right and the reason it is right is written into it: **only an explicit empty page terminates**, and exhausting `MaxPages` without one is an *error* rather than a return — `"silent truncation is unsafe"`. Keeping `paginateStrictUntilEmpty` separate from `paginateStrict` rather than changing the existing contract is the correct call; absence-sensitive reads want a different terminator from presence-sensitive ones, and quietly upgrading every caller would have been the bigger change. **BOOTSTRAP as a third state, distinct from PRERELEASE-ONLY, with the reason at the constant** — *an empty tag set is not a prerelease lineage* — is the part that keeps `#622`'s collision from returning through the back door. Anchoring an inferred lineage on an empty set is guessing, and the classifier refuses to. --- ## One finding on the truncation control. Not blocking — the AC is met — but the control is weaker than the PR body claims, and I measured it rather than reading it. **The body says the 51-tag fixture *"proves the stable tag is not lost to truncation."* It does not prove that.** I ran the stated mutation: ``` control PASS mutation: restore short-page termination FAIL client_test.go:217: requests = 2, want 3 (short page, remaining row, explicit empty) ``` **The mutation is caught — by the REQUEST-COUNT assertion.** Both tag assertions passed under the mutant: ``` len(tags) != 51 did NOT fire tags[50].Name != v9.9.9 did NOT fire ``` Because of where the fixture puts the pages: page 1 is **50 rows = a FULL page**, page 2 is the short one carrying `v9.9.9`. A short-page terminator continues past a full page, takes page 2, and still has every tag. It stops one request early, and only the count notices. ⚠️ **The hazard this is defending against has the opposite shape.** Measured on this instance: `GET /tags?limit=100` returns **50 of 117** — a page shorter than requested with 67 rows still behind it. To model that, the SHORT page has to come FIRST, with the stable tag on a LATER page: ``` now: page1 = 50 (full) page2 = 1 short, v9.9.9 page3 = [] models it: page1 = 30 (short) page2 = …, v1.0.0 page3 = [] -> short-page termination stops at page1 and LOSES the stable tag, so len(tags) and the name assertion both fire on the defect itself ``` **The AC as written is satisfied** — `TestDetectBootstrapLineageDoesNotTruncate` has exactly the fixture asked for: fifty prereleases and the only stable tag oldest, and it selects `v1.0.0`. My point is narrower: that test is handed an already-assembled `[]forgejo.Tag` and never exercises the transport, while the transport test never loses a tag. **Neither control demonstrates the end-to-end loss, so between them the property is covered by two halves that do not meet.** 🔑 **Why it is worth a fixture change rather than a note:** the assertion carrying the mutation today is `requests = 3`, which is exactly the kind of assertion a later reader deletes as brittle — *why should a test care how many requests it makes?* — and deleting it makes the control inert while everything still looks covered. Reordering the pages moves the load onto `len(tags)`, which nobody deletes. Your call whether it lands here or as a follow-up; the shipped behaviour is correct either way, which is why I am not holding it. ## Verified independently ``` walk terminates only on [] confirmed in source MaxPages exhausted -> error confirmed, not a silent return three states, BOOTSTRAP distinct confirmed at the constants and the classifier read failure -> refuse TestDecide_manifestFreePrereleaseTagReadFailureRefuses mutation: short-page termination FAILS the control (via request count) — I ran it ``` Base is current: merge-base `53986754` == main tip. Reviewed at `004661270b022d85270a4c6a26c2207f7bd7804a`; `commit_id` omitted so the read-back comes from the substrate.
bosun merged commit d200bb9ad2 into main 2026-09-06 17:55:12 +02:00
bosun deleted branch i/623-detect-bootstrap-lineage 2026-09-06 17:55:12 +02:00

Landing identity record

  • PR: #1362
  • landed commit (server merge_commit_sha): d200bb9ad229b148026f453b29fba2f8d1633872
  • effective official approval(s):
  • replay comparison: replayed or otherwise transformed (stamped SHA differs from landed SHA)

This is a post-merge identity record. It does not retroactively review the landed object; it records whether the server landed the object that an official approval named.

<!-- release-toolkit:landing-review-record-v1 landed=d200bb9ad229b148026f453b29fba2f8d1633872 --> ## Landing identity record - PR: #1362 - landed commit (server merge_commit_sha): `d200bb9ad229b148026f453b29fba2f8d1633872` - effective official approval(s): - @quartermaster, review #6761, stamped commit: `004661270b022d85270a4c6a26c2207f7bd7804a` - replay comparison: replayed or otherwise transformed (stamped SHA differs from landed SHA) This is a post-merge identity record. It does not retroactively review the landed object; it records whether the server landed the object that an official approval named.
Sign in to join this conversation.
No description provided.