fix(869,893): re-establish the 1.25.4-dated CI claims on 1.27.1, and settle the page_statuses asymmetry from source #905

Merged
timothy merged 8 commits from fix/869-893-gitea-1271-claim-sweep into main 2026-09-02 22:20:45 +02:00
Owner

Finishes the 1.25.4-dated CI claim sweep #747 deliberately left incomplete (#869), and answers
#893's open question from the Gitea v1.27.1 source instead of inferring it from a header.

Docs and comments only — no executable behaviour changes.

Population

Derived with git ls-files, not from #869's item list: 17 files, 43 occurrences of 1.25.4,
materially wider than the four items the issue named. That gap is the point — #869 itself warned
against taking an issue body as the population.

Re-established on 1.27.1

Claim How Result
creator attribution (H10 allow-list rests on it) 4 merged PR heads, both endpoints exactly one .creator.login row per head, every other row null
no status token scope source @ v1.27.1 9 categories, zero occurrences of status
reqRepoWriter(unit.TypeCode) on POST /statuses/{sha} source @ v1.27.1 unchanged
write:package → 403 on a status POST live probe + 2 controls 403 (same message); read control 200, write control 201
no agent-side cancel route (REST) source @ v1.27.1 no cancel route in the whole runs group
pull_request vs pull_request_target definition resolution source @ v1.27.1 two separately resolved refs

The write:package probe ran in a throwaway repo with a freshly minted PAT; repo and token deleted
afterwards and deletion confirmed (repo GET → 404, token DELETE → 204). Nothing was written to a
real head.

#893 — answered

/statuses/{sha} does NOT drop rows after pagination. getCommitStatuses appends
unconditionally, and its only filter is a SQL WHERE the database evaluates in the same query as the
LIMIT/OFFSET. So an empty page really is the end, page_statuses terminating on its first empty
page is safe, and the asymmetry with count_pr_mutations is correct — recorded with its
reason and a date so it is not "tidied" away.

The same two handlers also explain the per-endpoint terminator shapes ([] vs bare null), which
were previously an empirical table: one builds make(..., 0, len), the other declares var.

Corrected, not just re-dated

  • The --depth=1 no-merge-base claim was filed against the wrong axis. It is a git property; a
    Gitea upgrade cannot change it. Re-probed on git 2.55.0.
  • enable_bypass_allowlist postdating 1.25.4 was sourced from an issue body. Now checked in both
    trees — it holds. Its neighbour block_admin_merge_override does not share that history and is
    present at 1.25.4 too, stated explicitly so the paragraph cannot invite the inference.

Claims that stay dated

They now carry a reason that was checked this session, not inherited: all six workflow triggers
re-enumerated, the direct-push half re-read live. git grep 1.25.4-dated finds them (a discovery
pointer that over-reports, and says so).

Review

Three cold review rounds, worktree-isolated, plus a cross-family Codex pass. They found real defects
each time and every one is fixed:

  • round 1 (8): the stated mechanism for creator: null was wrong — it said CreatorID == 0,
    but an Actions job authenticates as synthetic user -2, so that branch is taken and the null
    comes from a discarded GetUserByID miss. The measurement was right; the explanation was not.
  • round 2 (7): the fix's own residual list was incomplete within the same commit, and one
    correction over-retreated — pull_request_target runs are exercised continuously on 1.27.1, so
    only the secrets half deserved re-dating.
  • round 3: self-caught precision fix on the grep pointer.

The reason given for not re-probing MCP cancel_run was invented ("no disposable run to spend") and
is now the true one: the agent harness refused the call. That correction is in the record.

Verification

  • scripts/tests: 1565 passed, 3 skipped
  • decisions_validate.py: OK · catalog regenerated (unchanged — no rule: field moved)
  • ruff check / ruff format --check: clean · bash -n and YAML parse: clean
  • No .cs touched, so no BOM/format gate applies

fixes #869
fixes #893

🤖 Generated with Claude Code

https://claude.ai/code/session_01Whe75djeAEuZpdNk6KU7No

Finishes the 1.25.4-dated CI claim sweep #747 deliberately left incomplete (#869), and answers #893's open question from the Gitea v1.27.1 source instead of inferring it from a header. Docs and comments only — no executable behaviour changes. ## Population Derived with `git ls-files`, not from #869's item list: **17 files, 43 occurrences** of `1.25.4`, materially wider than the four items the issue named. That gap is the point — #869 itself warned against taking an issue body as the population. ## Re-established on 1.27.1 | Claim | How | Result | |---|---|---| | `creator` attribution (H10 allow-list rests on it) | 4 merged PR heads, both endpoints | exactly one `.creator.login` row per head, every other row null | | no `status` token scope | source @ v1.27.1 | 9 categories, zero occurrences of `status` | | `reqRepoWriter(unit.TypeCode)` on `POST /statuses/{sha}` | source @ v1.27.1 | unchanged | | `write:package` → 403 on a status POST | **live probe + 2 controls** | 403 (same message); read control 200, write control 201 | | no agent-side cancel route (REST) | source @ v1.27.1 | no cancel route in the whole runs group | | `pull_request` vs `pull_request_target` definition resolution | source @ v1.27.1 | two separately resolved refs | The `write:package` probe ran in a throwaway repo with a freshly minted PAT; repo and token deleted afterwards and deletion confirmed (repo GET → 404, token DELETE → 204). Nothing was written to a real head. ## #893 — answered `/statuses/{sha}` does **NOT** drop rows after pagination. `getCommitStatuses` appends unconditionally, and its only filter is a SQL `WHERE` the database evaluates in the same query as the `LIMIT`/`OFFSET`. So an empty page really is the end, `page_statuses` terminating on its first empty page is **safe**, and the asymmetry with `count_pr_mutations` is **correct** — recorded with its reason and a date so it is not "tidied" away. The same two handlers also explain the per-endpoint terminator shapes (`[]` vs bare `null`), which were previously an empirical table: one builds `make(..., 0, len)`, the other declares `var`. ## Corrected, not just re-dated - The `--depth=1` no-merge-base claim was filed against the wrong axis. It is a **git** property; a Gitea upgrade cannot change it. Re-probed on git 2.55.0. - `enable_bypass_allowlist` postdating 1.25.4 was sourced from an issue body. Now checked in both trees — it holds. Its neighbour `block_admin_merge_override` does **not** share that history and is present at 1.25.4 too, stated explicitly so the paragraph cannot invite the inference. ## Claims that stay dated They now carry a reason that was **checked this session**, not inherited: all six workflow triggers re-enumerated, the direct-push half re-read live. `git grep 1.25.4-dated` finds them (a discovery pointer that over-reports, and says so). ## Review Three cold review rounds, worktree-isolated, plus a cross-family Codex pass. They found real defects each time and every one is fixed: - **round 1 (8):** the stated *mechanism* for `creator: null` was wrong — it said `CreatorID == 0`, but an Actions job authenticates as synthetic user `-2`, so that branch is *taken* and the null comes from a discarded `GetUserByID` miss. The measurement was right; the explanation was not. - **round 2 (7):** the fix's own residual list was incomplete within the same commit, and one correction over-retreated — `pull_request_target` runs are exercised continuously on 1.27.1, so only the *secrets* half deserved re-dating. - **round 3:** self-caught precision fix on the grep pointer. The reason given for not re-probing MCP `cancel_run` was invented ("no disposable run to spend") and is now the true one: the agent harness refused the call. That correction is in the record. ## Verification - `scripts/tests`: **1565 passed, 3 skipped** - `decisions_validate.py`: OK · catalog regenerated (unchanged — no `rule:` field moved) - `ruff check` / `ruff format --check`: clean · `bash -n` and YAML parse: clean - No `.cs` touched, so no BOM/format gate applies fixes #869 fixes #893 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Whe75djeAEuZpdNk6KU7No
timothy added 7 commits 2026-09-02 21:41:01 +02:00
Finishes the claim sweep #747 deliberately left incomplete, and answers #893's
open question by reading the v1.27.1 source rather than inferring from a header.

Population derived with `git ls-files`, not from #869's item list: 17 files and
43 occurrences of `1.25.4`, materially wider than the four items the issue named.

Re-established on 1.27.1 (all previously 1.25.4-dated):

- creator attribution (the load-bearing one, since the H10 allow-list rests on
  it): four merged PR heads, exactly one `.creator.login` row per head and every
  other row `creator: null`, on BOTH the combined and list endpoints. The
  mechanism is in the serializer, so the two endpoints cannot disagree.
- scope enum: no `status` scope at v1.27.1; nine categories, zero occurrences.
- the `reqRepoWriter(unit.TypeCode)` gate on `POST /statuses/{sha}`: unchanged.
- the `write:package` 403: re-probed live with a read control (200) and a write
  control (201), in a throwaway repo, both artifacts deleted and confirmed gone.
- the cancel route: absence confirmed in the v1.27.1 router rather than resting
  on an observed 404, which cannot distinguish absence from a refusal.
- `pull_request` vs `pull_request_target` definition resolution: read off
  `notifier_helper.go`, which is stronger than the four 1.25.4 scratch PRs.

Corrected rather than re-dated:

- the `--depth=1` no-merge-base claim was filed against the wrong axis. It is a
  GIT property; a Gitea upgrade cannot change it. Re-probed on git 2.55.0.
- `enable_bypass_allowlist` postdating 1.25.4 was sourced from an issue body.
  Now checked in both trees. It holds; the neighbouring
  `block_admin_merge_override` does NOT share that history and is present at
  1.25.4 too, stated so the paragraph cannot invite the inference.

#893: `/statuses/{sha}` does NOT drop rows after pagination. `getCommitStatuses`
appends unconditionally and its only filter is a SQL WHERE evaluated in the same
query as the LIMIT/OFFSET, so an empty page really is the end. The asymmetry with
`count_pr_mutations` is therefore correct and is recorded with its reason. The
same two handlers also explain the per-endpoint terminator shapes (`[]` vs bare
`null`) that were previously an empirical table.

Claims that stay dated now carry a reason that was CHECKED this session rather
than inherited: all six workflow triggers re-enumerated, and the direct-push
half re-read live.

fixes #869
fixes #893

Decisions-Edit: yes
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Whe75djeAEuZpdNk6KU7No
Round-2 fixes. Two independent cold reviews (a fresh Opus agent and a
cross-family Codex pass) agreed on the first finding; I re-verified every
one against the v1.27.1 source before changing anything.

The real defect — the stated MECHANISM for `creator: null` was wrong, and it
was labelled "re-derivable", which is the worst combination. It said null
means `CreatorID == 0`. It does not: an Actions job authenticates as the
synthetic Actions user `ActionsUserID = -2`, `NewCommitStatus` stores
`CreatorID = Creator.ID`, so the `if status.CreatorID != 0` branch is TAKEN.
The null comes from the next line — `GetUserByID(-2)` is a bare lookup with no
system-user mapping (contrast `GetPossibleUserByID`, which handles `id < 0`),
it misses, the error is discarded, and `ToUser(ctx, nil, nil)` returns nil.
The MEASUREMENT was right; only the explanation was wrong, which is exactly
the shape #881 is about. Carries the consequence the wrong version hid: null
means "no user row resolved", not "a machine wrote this" — a deleted author is
indistinguishable, which is safe here only because the test fires on a
POSITIVE allow-list match.

Overclaims, each narrowed to what the evidence supports:

- "the last load-bearing 1.25.4-dated claim" — false; the fence's
  `pull_request_target` half is load-bearing and still dated.
- the `notifier_helper.go` read discharges DEFINITION RESOLUTION only. It says
  nothing about event `types` or secrets availability, so those halves stay
  scratch-PR-attested on 1.25.4.
- "no agent-side cancel route" now scoped to the REST surface; `api.go` cannot
  speak for the web UI or the runner protocol.
- "#869 finished the sweep" — it swept and re-established most of it; three
  findings stay dated by decision.
- "two different commits by construction" -> two SEPARATELY RESOLVED refs;
  nothing forbids them resolving equal.

And the one I am least comfortable with, now stated honestly: the reason given
for not re-probing MCP `cancel_run` was invented. I wrote "there is no
disposable run to spend", which is false — four of six workflows carry
`workflow_dispatch:` (renovate's defaults to `dryRun: full`), and the record
itself says `dispatch_workflow` still works. It was also self-refuting, since
the REST half was probed by observing a 404, which cancels nothing. The true
reason is that the agent harness refused the call, and that is what the record
now says. Removing that false reason also made two neighbours stale, and both
are fixed in the same commit.

Refs #869
Refs #893

Decisions-Edit: yes
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Whe75djeAEuZpdNk6KU7No
Round-3 fixes from a cold re-review of the round-2 fix commit. That review
verified the corrected creator-null mechanism step by step against the v1.27.1
source and found it sound, then found seven new defects in the fixes
themselves — which is the pattern this repo keeps hitting: each fix round
introduces an adjacent one.

The instructive one: round 2 replaced an overclaim ("#869 finished the sweep")
with a precise list of what remains dated. The list was wrong within the same
commit — it named three residuals and omitted three the commit itself created.
A second copy of a set is exactly what goes stale, so there is now no list:
the reason lives with each finding and `git grep 1.25.4-dated` is the
population.

Over-retreat, corrected. Round 2 re-dated BOTH halves the notifier_helper read
does not cover. Only the secrets half deserved it. That this workflow's
`types:` produce pull_request_target runs on 1.27.1 is exercised continuously
in production — every PR merged since the upgrade carries this job's context,
and runs 2537/2538/2539 are three on the single head sha 3332662c, which one
synchronize cannot produce alone. Retreating past what the corpus already
measures is its own defect.

Also:
- the forward reference promising a "checked" reason for both remaining
  cancel-route findings; the MCP one is session-local and the web-UI one has
  no reason at all, and it now says so.
- a self-refuting argument removed rather than patched: the REST 404 cost
  nothing BECAUSE the route was absent, which is the proposition under test.
  The finished-run-id argument that follows it is the one that works.
- a wrong internal citation ("two paragraphs up", scoped to re-triggering main).
- the round-2 pronoun fix turned "NOT measured with this token" into a false
  claim: the `container:` pull IS exercised by the shipped credential on every
  PR. Now says not COVERED BY EITHER PROBE, and that this means unprobed
  rather than unexercised.
- one blanket reason covering three measurements fit only the third; the first
  two were probe-PR-measured and simply not re-run.

Refs #869
Refs #893

Decisions-Edit: yes
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Whe75djeAEuZpdNk6KU7No
Replacing an enumerated residual list with `git grep 1.25.4-dated` traded one
staleness mode for a precision one. The grep also matches negations ("never
1.25.4-dated in the first place"), discharged history, and the sentence that
recommends it — so it finds every residual and some non-residuals.

Say which direction it errs in. Over-reporting costs a reader one extra look
and cannot hide a residual; the reverse would defeat the point of the pointer.

Refs #869

Decisions-Edit: yes
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Whe75djeAEuZpdNk6KU7No
Round-4. Three cold review rounds each found real defects, and nearly every one
sat in the same narrow place: prose tracking how complete the sweep is and
which residuals remain. The technical core — the source reads, the live probes,
#893's answer — came back clean from every reviewer. So this removes the
mechanism rather than fixing its latest output, which is what this repo's own
precedent says to do once a construct has been patched three times.

Two HIGH findings, both self-inflicted by round 3, both verified before acting:

- The `git grep 1.25.4-dated` pointer UNDER-reported, which is the unsafe
  direction, and the commit that added it claimed it "cannot hide a residual".
  Round 3 had introduced a second spelling, `1.25.4-attested`, in the same
  commit; the grep matched none of its four instances. Measured: 16 hits for
  the first term, 0 of the 4 for the second.
- "every PR merged since the upgrade carries this job's context" is false, and
  refuted by a counterexample the same record already documents: PR #863 merged
  carrying no review-verdict context at all, because a PR into a scratch base
  does not match `branches: [main]`.

So: no list, no derived pointer, no second vocabulary. Both attempts at tracking
the residual set failed inside the commit that wrote them, which is the argument
against keeping a copy of a set at all. Each residual states its reason where it
lives — the copy that cannot drift from its subject.

The per-type evidence is replaced with what was actually measured and nothing
wider: pull_request_target fires continuously on 1.27.1 (258 runs across 233
distinct head shas), which establishes neither WHICH types fire nor whether the
stored secret store is reachable. Both of those stay dated. That also removes a
false binding — runs 2537/2538/2539 were cited as merged PRs and belong to
PR #761, which is open — and a colon that read as introducing the opposite arm.

Refs #869
Refs #893

Decisions-Edit: yes
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Whe75djeAEuZpdNk6KU7No
Round-5, from the round-4 gate. The technical core came back verified clean —
every source quote, every live measurement, and the whole #893 asymmetry
argument. Three residual findings, all in the evidence I added in round 4.

The substantive one: the sentence is headed "What IS established on 1.27.1" and
cited 258 runs / 233 head shas, which is the ALL-TIME count. Measured
independently before changing anything: 258 splits 44 before the upgrade date +
4 on it + 210 after. So 17% of the evidence came from the exact version the
paragraph exists to distinguish itself from. Now 210 runs / 189 distinct head
shas, strictly after 2026-08-05, with the split spelled out so the arithmetic
checks — an earlier draft of this fix said "44 excluded" while excluding 48.

The 4 upgrade-day runs are excluded rather than assigned. The upgrade landed
mid-session and their 17:41-22:36 start times cannot be placed either side of it
without the exact upgrade time, so they go to the side that weakens the claim.

Scoping also resolves a "fires here" ambiguity for free: all 8 non-review-verdict
runs in the all-time set are pre-upgrade probe workflows, so every one of the 210
is this file, under either reading of "here".

And a "two paragraphs down" pointer that was three paragraphs down — the same
class as a round-2 finding, so it is now cited by CONTENT instead. A positional
reference breaks whenever anything is inserted above it, which is precisely what
kept happening.

Refs #869
Refs #893

Decisions-Edit: yes
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Whe75djeAEuZpdNk6KU7No
docs(869): mark the run timestamps UTC and the all-time figure repo-wide
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 26s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 35s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 47s
PR Gates / Docs update reminder (pull_request) Successful in 47s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 10s
PR Gates / decisions lifecycle (pull_request) Successful in 25s
review-verdict/h10 Awaiting review verdict for 24b770e
Review verdict / Set review-verdict status (pull_request_target) Successful in 35s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m44s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Canceled after 4m30s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Canceled after 0s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Canceled after 0s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Canceled after 0s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Canceled after 0s
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Canceled after 13m27s
24b770e5ef
Two clarifications from the round-5 gate, both raised as non-blocking and both
the kind of thing that sends a later reader off to "correct" a number that is
already right.

The `started_at` values are UTC, while other Gitea timestamps in this repo
render +02:00 and the host is UTC+2 — so "17:41-22:36" does not match what the
run shows in the web UI. Under local-time bucketing the split would read
44/2/212 rather than 44/4/210; the stated numbers stay conservative either way
(210 <= 212), but only if the reader knows which clock they are on.

And 258/233 is the REPO-WIDE all-time count. This workflow's own all-time count
is 250 — the other 8 are three since-deleted probe workflows, all pre-upgrade,
which is also why every one of the 210 post-upgrade runs is this file. Someone
recomputing the workflow-scoped figure gets 250 and would otherwise think 258
was wrong.

Refs #869

Decisions-Edit: yes
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Whe75djeAEuZpdNk6KU7No
timothy added 1 commit 2026-09-02 21:55:42 +02:00
docs(869): make the 258-250 arithmetic close, and scope the host offset to DST
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 7s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 10s
PR Gates / Docs update reminder (pull_request) Successful in 15s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 11s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 23s
PR Gates / decisions lifecycle (pull_request) Successful in 23s
Review verdict / Set review-verdict status (pull_request_target) Successful in 19s
review-verdict/h10 Review-verdict: MERGEABLE @ 24b8741 (base: main)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m23s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m26s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 17m14s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m0s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 6s
24b874102a
Two precision findings from a cold review of the previous commit. Both claims
in that commit verified true; these are wording defects in my own text, and the
first is exactly the failure mode that commit set out to prevent.

The record said the difference between the repo-wide all-time figure and this
workflow's own was "three since-deleted probe workflows". 258 - 250 = 8, and 8
appeared nowhere in that file — so a reader checking the arithmetic finds a
mismatch and goes off to "correct" a number that is already right. The residual
is 8 runs from 3 workflows; both copies now say so, and they agree.

And "+02:00 / the host is UTC+2" is DST-scoped. The same API payload renders
`+01:00` for winter-dated fields, so a reader checking in January sees a doc
that contradicts what is on screen. Now named as the zone — Europe/Brussels,
+02:00 in August, +01:00 in winter — which is true year-round.

Also folded a duplicated sentence in the workflow comment: "every one of the 210
is this file" appeared twice with the 258 figure wedged between.

Refs #869

Decisions-Edit: yes
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Whe75djeAEuZpdNk6KU7No
Author
Owner

Review-verdict: MERGEABLE @ 24b8741

5 cold review rounds (worktree-isolated) plus a cross-family Codex pass; final round CLEAN on this head. Docs+comments only - zero non-comment changes in scripts/ and .gitea/, verified mechanically. Every Gitea source quote re-checked at v1.27.1/v1.25.4 and every live measurement reproduced independently by the reviewer.

Review-verdict: MERGEABLE @ 24b8741 5 cold review rounds (worktree-isolated) plus a cross-family Codex pass; final round CLEAN on this head. Docs+comments only - zero non-comment changes in scripts/ and .gitea/, verified mechanically. Every Gitea source quote re-checked at v1.27.1/v1.25.4 and every live measurement reproduced independently by the reviewer.
timothy merged commit fb55929711 into main 2026-09-02 22:20:45 +02:00
timothy deleted branch fix/869-893-gitea-1271-claim-sweep 2026-09-02 22:20:48 +02:00
Sign in to join this conversation.