feat(mirror): mirror on release: published, and stop reopening a closed tracker #1587

Merged
quartermaster merged 6 commits from quartermaster/1586-mirror-on-publish into main 2026-10-22 00:57:51 +02:00
Owner

Part of #1586 — the two halves that need no open decision. The Grafana alert is deliberately not here; the push path is still a fork and the section at the end says why.

The mirror fires on publish again

mirror-on-publish.yml dispatches mirror-release.yml on release: published.

🔑 This restores a pattern this repo already had. mirror-on-cut.yml (#427, 2026-07-06) was exactly this — a separate workflow on release: published, built so "mirror-failure never touches the cut workflow's job graph". It was retired in #793 on 2026-08-21 because Codeberg was retired over its AI-code policy. The pattern did not fail; its target went away. #1030 then built the gitea.com mirror dispatch-only, and #1212 ruled mirroring manual on cut-dependency grounds this pattern had already answered.

#1212's property is intact, and being a separate file is how: its red is its own, it is not in release.yml's job graph, it has no needs:, nothing waits on it. What changes is only who dispatches. #1212 required the cut not to depend on the mirror — never that a human push the button.

🔴 The release event can fire before the assets are attached, so step 1 reads the release until it carries assets and refuses after ~5 min rather than dispatching. An asset-less mirror is worse than a late one: mirror-release.yml compares asset counts on its skip path and body/name on its resync path, so neither reads an asset, and the only repair is force_recreate — destructive, and never sent from here. #1445 is the measured neighbour: v0.62.4's drift run reported SUCCESS while gitea.com served its tag 200 and its release 404, still true 71 minutes later.

The drift report no longer reopens a closed tracker

Reopening an issue and posting comments on it will only work if there are a dozen of other issues at most and the project is being actively worked on. I focused on Storey for the most part, so the issue #1348 on rt just rotted for the last days.

— operator, 2026-10-21. Measured on #1348: 29 automated comments, 26 carrying released tag = none.

An open tracker labelled mirror-drift gets the report; when none is open, one is filed. One tracker per drift episode, closable, never resurrected.

⚠️ And the old choice was right for a frequency that no longer holds. This workflow's header argues against a fresh issue per failure — "a gate that files on a schedule produces exactly the noise that trains people to ignore it" — which was correct while every cut created drift nothing cleared. With the mirror firing on publish, drift is the exception and one tracker per episode is cheap. The header now records that its own mitigation failed on its own terms: bounding the RATE does not bound the TOTAL for a standing condition, and 26 comments is 26 days of one unfixed drift.

The control that caught a live defect in my own change

🔴 The server-side labels= filter is not used, and a fabricated-label control is why. Measured on this forge before the label existed:

issues?state=open&labels=kind/bug              rows=3    (plausible)
issues?state=open&labels=no-such-label-xyz     rows=41   <- THE WHOLE OPEN LIST

An unknown label is silently ignored rather than matching nothing. I had written the lookup keyed on that filter; it would have selected the newest unrelated open issue and commented a drift report onto it. Same shape as #1584's head=<owner>:<branch> two hours earlier — do not depend on how a server treats a filter. Don't send one; match client-side.

Six arms on the resulting predicate, against synthetic rows:

arm expected got
label present, first row 99 99
label present, later row 99 99
the unfiltered 41-row list empty empty
issue with no labels at all empty empty
near-miss mirror-drift-check empty empty
empty page empty empty
the live forge empty empty

The near-miss arm matters because ¶42's prefix hazard (trigger/rigger, tags:/fetch-tags:) is live on label names too; index() is exact on array members, so it does not fire.

Also: the token moves off argv into a mode-0600 -K config. The step this replaces passed -H "Authorization: token $T" on the command line, which /proc/<pid>/cmdline exposes — the discipline the fetch and status steps in this repo already follow.

Not in this change, and why

The Grafana alert. remote_write is the expensive option on this host, measured: alloy.frankenbit.de/api/v1/write is reachable from the runner (the IP allow-list covers 172.22.0.0/16) but is protobuf+snappy, which a shell step cannot emit, and rt carries two direct dependencies I am not widening for it. victoria-metrics has no published port and ingress cannot reach it (probe rc=1). The cheap path is node_exporter's textfile collector, which three host timers already use — but a host script doing its own comparison would be a second implementation of a predicate rt mirror-drift-check already owns, which is the hand-rolled-check-beside-a-gate hazard. That wants deciding rather than hand-waving, and it is less urgent now: with the mirror firing on publish, drift is rare.

No quarantine window, by operator ruling: the local forge is for convenience, not isolation, so a delay buys nothing.

The type arm: its EXISTENCE needs no pinning, its VERDICT does

⚠️ 05bd59ca's commit message states this one clause too wide — it says "the new arm does NOT pin the type arm, because no fixture can distinguish two paths that both refuse". That is true of the type test's existence and false of its verdict, and the distinction is two different experiments that shared a name:

DELETE the type test            jq exits 5, prints nothing, has='' -> -z refuses
                                INERT -- it fails closed through the -z fix, so it
                                needs no pinning
KEEP it, change its VERDICT     jq exits 0, has='0' -> ACCEPTS a non-object
else "unreadable" -> else "0"   STILL LIVE after -z, and arm 135 CATCHES it:
                                rc=1, one finding, "an array, not an object:
                                has='0' -> accept, want refuse"

🔑 So the type arm is not unprotected. Left as written, the commit's sentence would tell a later reader it is — inviting either a redundant assertion or an unnoticed edit to the else value. Recording the limit was right; the limit is one clause narrower than the commit says. Located by @surveyor, review 8017; the corrected form is his.

📌 Second disagreement of the round that was two experiments under one label — the first was labels=kind/bug at 18 versus 3, where the label hid a limit=3 I had typed. Both were resolved by printing what each side actually ran rather than by finding a reading that made both numbers right about the same thing.

Verification

yaml.safe_load on both workflows · bash -n on all 8 run blocks · rt gitea-twin --check PASS (6 twins, this file owes none) · fragment-check rc=0 at 428 chars · go build / go test ./... clean.

What is unverified is narrower than "the workflow runs"

Two measurements narrow it, and they are @surveyor's rather than mine — stated with the values beside the name that produced them, because a name next to a number someone else measured renders two people's work as one act:

  • @surveyor: 22 release-event runs of mirror-on-cut.yml, all success, 2026-07-06T21:12 .. 2026-08-20T10:43 (control: ?event=bogus-xyz → 0). So the release: published event, the dispatch from it and the runner are all exercised 22 times on the very file this restores. What is untested is this file's own steps, not the mechanism.
  • @surveyor: the asset race has a number. All 8 most recent releases carry exactly 3 assets whose created_at are identical to the second (span 0s), so count>0 cannot admit a half-uploaded set here. And v0.62.4 published at 18:54:29 with its assets created at 18:55:23 — +54s after publish, while the other 7 land 32–147s before. So the wait step is a no-op on 7 of 8 and saves the 8th, 54s inside its 5-minute bound. That also dates #1445's symptom: the drift run at 18:54:39 read a release whose assets arrived 44 seconds later.

⚠️ Still unverified: this file's own steps, and that mirror-release.yml mirrors correctly once dispatched. release: published cannot be exercised without a release, so the first real test is the next cut — with the drift check as the net under it, which is the arrangement this PR is for.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LUEggQMJjaizj2nFVofeyH

Part of #1586 — the two halves that need no open decision. **The Grafana alert is deliberately not here**; the push path is still a fork and the section at the end says why. ## The mirror fires on publish again `mirror-on-publish.yml` dispatches `mirror-release.yml` on `release: published`. 🔑 **This restores a pattern this repo already had.** `mirror-on-cut.yml` (#427, 2026-07-06) *was* exactly this — a separate workflow on `release: published`, built so "mirror-failure never touches the cut workflow's job graph". It was retired in #793 on 2026-08-21 **because Codeberg was retired** over its AI-code policy. The pattern did not fail; its target went away. #1030 then built the gitea.com mirror dispatch-only, and #1212 ruled mirroring manual on cut-dependency grounds this pattern had already answered. #1212's property is intact, and being a separate file is how: its red is its own, it is not in `release.yml`'s job graph, it has no `needs:`, nothing waits on it. What changes is only *who* dispatches. #1212 required the cut not to **depend** on the mirror — never that a human push the button. 🔴 **The release event can fire before the assets are attached**, so step 1 reads the release until it carries assets and **refuses after ~5 min** rather than dispatching. An asset-less mirror is worse than a late one: `mirror-release.yml` compares asset **counts** on its skip path and body/name on its resync path, so neither reads an asset, and the only repair is `force_recreate` — destructive, and never sent from here. #1445 is the measured neighbour: v0.62.4's drift run reported SUCCESS while gitea.com served its tag `200` and its release `404`, still true 71 minutes later. ## The drift report no longer reopens a closed tracker > Reopening an issue and posting comments on it will only work if there are a dozen of other issues at most and the project is being actively worked on. I focused on Storey for the most part, so the issue #1348 on rt just rotted for the last days. — operator, 2026-10-21. Measured on #1348: **29 automated comments, 26 carrying `released tag = none`.** An **open** tracker labelled `mirror-drift` gets the report; when none is open, one is filed. One tracker per drift *episode*, closable, never resurrected. ⚠️ **And the old choice was right for a frequency that no longer holds.** This workflow's header argues against a fresh issue per failure — *"a gate that files on a schedule produces exactly the noise that trains people to ignore it"* — which was correct while every cut created drift nothing cleared. With the mirror firing on publish, drift is the exception and one tracker per episode is cheap. The header now records that its own mitigation failed on its own terms: **bounding the RATE does not bound the TOTAL for a standing condition**, and 26 comments is 26 days of one unfixed drift. ### The control that caught a live defect in my own change 🔴 **The server-side `labels=` filter is not used, and a fabricated-label control is why.** Measured on this forge *before* the label existed: ``` issues?state=open&labels=kind/bug rows=3 (plausible) issues?state=open&labels=no-such-label-xyz rows=41 <- THE WHOLE OPEN LIST ``` An unknown label is **silently ignored** rather than matching nothing. I had written the lookup keyed on that filter; it would have selected the newest unrelated open issue and commented a **drift report** onto it. Same shape as #1584's `head=<owner>:<branch>` two hours earlier — do not depend on how a server treats a filter. Don't send one; match client-side. Six arms on the resulting predicate, against synthetic rows: | arm | expected | got | |---|---|---| | label present, first row | 99 | 99 | | label present, later row | 99 | 99 | | the unfiltered 41-row list | empty | empty | | issue with no labels at all | empty | empty | | near-miss `mirror-drift-check` | empty | empty | | empty page | empty | empty | | the live forge | empty | empty | The near-miss arm matters because `¶42`'s prefix hazard (`trigger`/`rigger`, `tags:`/`fetch-tags:`) is live on label names too; `index()` is exact on array members, so it does not fire. Also: the token moves off argv into a mode-0600 `-K` config. The step this replaces passed `-H "Authorization: token $T"` on the command line, which `/proc/<pid>/cmdline` exposes — the discipline the fetch and status steps in this repo already follow. ## Not in this change, and why **The Grafana alert.** `remote_write` is the expensive option on this host, measured: `alloy.frankenbit.de/api/v1/write` is reachable from the runner (the IP allow-list covers `172.22.0.0/16`) but is **protobuf+snappy**, which a shell step cannot emit, and `rt` carries two direct dependencies I am not widening for it. `victoria-metrics` has no published port and ingress cannot reach it (probe `rc=1`). The cheap path is node_exporter's textfile collector, which three host timers already use — **but a host script doing its own comparison would be a second implementation of a predicate `rt mirror-drift-check` already owns**, which is the hand-rolled-check-beside-a-gate hazard. That wants deciding rather than hand-waving, and it is less urgent now: with the mirror firing on publish, drift is rare. **No quarantine window**, by operator ruling: the local forge is for convenience, not isolation, so a delay buys nothing. ## The type arm: its EXISTENCE needs no pinning, its VERDICT does ⚠️ **`05bd59ca`'s commit message states this one clause too wide** — it says *"the new arm does NOT pin the type arm, because no fixture can distinguish two paths that both refuse"*. That is true of the type test's **existence** and false of its **verdict**, and the distinction is two different experiments that shared a name: ``` DELETE the type test jq exits 5, prints nothing, has='' -> -z refuses INERT -- it fails closed through the -z fix, so it needs no pinning KEEP it, change its VERDICT jq exits 0, has='0' -> ACCEPTS a non-object else "unreadable" -> else "0" STILL LIVE after -z, and arm 135 CATCHES it: rc=1, one finding, "an array, not an object: has='0' -> accept, want refuse" ``` 🔑 **So the type arm is not unprotected.** Left as written, the commit's sentence would tell a later reader it is — inviting either a redundant assertion or an unnoticed edit to the `else` value. Recording the limit was right; the limit is one clause narrower than the commit says. <sub>Located by @surveyor, review 8017; the corrected form is his.</sub> 📌 **Second disagreement of the round that was two experiments under one label** — the first was `labels=kind/bug` at 18 versus 3, where the label hid a `limit=3` I had typed. Both were resolved by printing what each side actually ran rather than by finding a reading that made both numbers right about the same thing. ## Verification `yaml.safe_load` on both workflows · `bash -n` on all 8 run blocks · `rt gitea-twin --check` PASS (6 twins, this file owes none) · `fragment-check` rc=0 at 428 chars · `go build` / `go test ./...` clean. ### What is unverified is narrower than "the workflow runs" Two measurements narrow it, and they are @surveyor's rather than mine — stated with the values beside the name that produced them, because a name next to a number someone else measured renders two people's work as one act: - **@surveyor: 22 release-event runs of `mirror-on-cut.yml`, all `success`, 2026-07-06T21:12 .. 2026-08-20T10:43** (control: `?event=bogus-xyz` → 0). So the `release: published` event, the dispatch from it and the runner are all exercised 22 times on the very file this restores. **What is untested is this file's own steps**, not the mechanism. - **@surveyor: the asset race has a number.** All 8 most recent releases carry exactly 3 assets whose `created_at` are identical to the second (span 0s), so `count>0` cannot admit a half-uploaded set here. And v0.62.4 published at 18:54:29 with its assets created at 18:55:23 — **+54s after publish**, while the other 7 land 32–147s *before*. So the wait step is a no-op on 7 of 8 and saves the 8th, 54s inside its 5-minute bound. That also dates #1445's symptom: the drift run at 18:54:39 read a release whose assets arrived 44 seconds later. ⚠️ **Still unverified:** this file's own steps, and that `mirror-release.yml` mirrors correctly once dispatched. `release: published` cannot be exercised without a release, so the first real test is the next cut — with the drift check as the net under it, which is the arrangement this PR is for. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01LUEggQMJjaizj2nFVofeyH
feat(mirror): mirror on release: published, and stop reopening a closed tracker (#1586)
Some checks failed
go-ci / record reviewed vs landed commit (pull_request) Has been skipped
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 6s
base-divergence-check / check (pull_request) Successful in 6s
pr / plan, and run every gate when combined (pull_request) Successful in 9s
pr / manifest (pull_request) Has been skipped
pr / fragments (pull_request) Has been skipped
pr / changelog_body (pull_request) Has been skipped
pr / register (pull_request) Has been skipped
pr / ac_closure (pull_request) Has been skipped
pr / workflow_parse (pull_request) Has been skipped
pr / split result (pull_request) Has been skipped
pr / gates (pull_request) Successful in 0s
readme-pin-check / mirror (pull_request) Successful in 10s
tests / checks (pull_request) Failing after 8s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 7s
go-ci / lint + build + test (pull_request) Successful in 46s
go-ci / page landing-tree failure (pull_request) Has been skipped
tests / bats (pull_request) Failing after 40s
434480bff1
Two halves of #1586. The Grafana alert is deliberately not here -- see the end.

## The mirror fires on publish again

`mirror-on-publish.yml` dispatches `mirror-release.yml` when a release
publishes. THIS RESTORES A PATTERN THIS REPO ALREADY HAD: `mirror-on-cut.yml`
(#427, 2026-07-06) was exactly this -- a separate workflow on `release:
published`, built so that "mirror-failure never touches the cut workflow's job
graph". It was retired in #793 on 2026-08-21 because CODEBERG was retired over
its AI-code policy. The pattern did not fail; its target went away. The
gitea.com mirror (#1030) then arrived dispatch-only and #1212 ruled mirroring
manual on cut-dependency grounds this pattern had already answered.

#1212's property is intact and it is the reason this is a separate file: its
red is its own, it is not in release.yml's job graph, it has no `needs:`, and
nothing waits on it. What changes is only WHO dispatches -- #1212 required the
cut not to DEPEND on the mirror, never that a human push the button.

🔴 THE RELEASE EVENT CAN FIRE BEFORE THE ASSETS ARE ATTACHED, so the first step
reads the release until it carries assets and REFUSES after ~5min rather than
dispatching. An asset-less mirror is worse than a late one: `mirror-release.yml`
compares asset COUNTS on its skip path and body/name on its resync path, so
neither reads an asset, and the only repair is `force_recreate` -- destructive,
and deliberately never sent from here. #1445 is the measured neighbour:
v0.62.4's drift run reported SUCCESS while gitea.com served its tag 200 and its
release 404, still true 71 minutes later.

## The drift report no longer reopens a closed tracker

Operator, 2026-10-21: "Reopening an issue and posting comments on it will only
work if there are a dozen of other issues at most and the project is being
actively worked on. I focused on Storey for the most part, so the issue #1348 on
rt just rotted for the last days." Measured on #1348: 29 automated comments, 26
carrying `released tag = none`.

An OPEN tracker carrying `mirror-drift` gets the report; when none is open, one
is filed. One tracker per drift EPISODE, closable, never resurrected.

🔑 AND THE OLD CHOICE WAS RIGHT FOR A FREQUENCY THAT NO LONGER HOLDS. This
workflow's header argues against a fresh issue per failure -- "a gate that files
on a schedule produces exactly the noise that trains people to ignore it" --
which was correct while every cut created drift nothing cleared. With the mirror
firing on publish, drift is the EXCEPTION and one tracker per episode is cheap.
The header now records that its daily-not-hourly mitigation failed on its own
terms: bounding the RATE does not bound the TOTAL for a standing condition, and
26 comments is 26 days of one unfixed drift.

🔴 THE SERVER-SIDE `labels=` FILTER IS NOT USED, AND A CONTROL IS WHY. Measured
on this forge before the label existed: `issues?state=open&labels=no-such-label-xyz`
returned 41 ROWS -- the whole open list. An unknown label is silently ignored
rather than matching nothing, so a lookup keyed on it would have selected the
newest unrelated open issue and commented a DRIFT REPORT onto it. I wrote that
lookup and the fabricated-label control caught it before it shipped. Same shape
as #1584's `head=<owner>:<branch>` two hours earlier: do not depend on how a
server treats a filter -- do not send one, and match client-side.

The lookup is exercised on six arms against synthetic rows: label present in
first and in later position (99 both times), the unfiltered 41-row list
(empty -- the hazard defused), an issue with no labels (empty), the near-miss
`mirror-drift-check` (empty, so the prefix hazard does not fire), an empty page
(empty), and the live forge (empty, correct).

Also: the token moves off argv into a mode-0600 `-K` config. The step it
replaces passed `-H "Authorization: token $T"` on the command line, which
/proc/<pid>/cmdline exposes -- the discipline the fetch and status steps in this
repo already follow.

## Not in this change

THE GRAFANA ALERT, and the push path is why. `remote_write` is the expensive
option on this host, measured: `alloy.frankenbit.de/api/v1/write` is reachable
from the runner but is protobuf+snappy, which a shell step cannot emit, and rt
carries two direct dependencies (cobra, yaml) that I am not widening for it.
`victoria-metrics` has no published port and ingress cannot reach it. The cheap
path is node_exporter's textfile collector, which three host timers already use
-- but a host script doing its own comparison would be a SECOND implementation
of a predicate `rt mirror-drift-check` already owns, which is the
hand-rolled-check-beside-a-gate hazard. That needs deciding, not hand-waving,
and it is less urgent now: with the mirror firing on publish, drift is rare.

Verification: `yaml.safe_load` on both workflows, `bash -n` on all 8 run blocks,
`rt gitea-twin --check` PASS (6 twins, this file owes none), `fragment-check`
rc=0 at 428 chars, `go build`/`go test ./...` clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUEggQMJjaizj2nFVofeyH
fix(ci): ASCII-only per the repo's own rule, and re-point the two arms this change reddened (#1586)
All checks were successful
go-ci / record reviewed vs landed commit (pull_request) Has been skipped
base-divergence-check / check (pull_request) Successful in 8s
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 9s
pr / plan, and run every gate when combined (pull_request) Successful in 13s
pr / manifest (pull_request) Has been skipped
pr / fragments (pull_request) Has been skipped
pr / changelog_body (pull_request) Has been skipped
pr / register (pull_request) Has been skipped
pr / ac_closure (pull_request) Has been skipped
pr / workflow_parse (pull_request) Has been skipped
pr / split result (pull_request) Has been skipped
pr / gates (pull_request) Successful in 0s
readme-pin-check / mirror (pull_request) Successful in 12s
tests / checks (pull_request) Successful in 9s
go-ci / lint + build + test (pull_request) Successful in 37s
go-ci / page landing-tree failure (pull_request) Has been skipped
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 7s
tests / bats (pull_request) Successful in 1m1s
c99d7d0d34
`tests / bats` and `tests / checks` were both RED at 434480bf. Four arms, two
causes, and both were mine.

## I imported the wrong house style

Six lines of mine carried decorative emoji. scripts/ascii-only-check.sh exists
precisely to refuse that, with an exemption table that is "a DECLARATION, never
a derivation" and carries an exact allowed line count plus a reason per path.
This repo writes CRITICAL:, KEY: and WARNING: where storey writes the
characters; I substituted per the checker's own section 11 table rather than
widening the exemption list, because these are decorative and the table is for
LOAD-BEARING characters only.

    ascii-only-check  rc=1 -> rc=0, and 0 non-ASCII lines remain in either file

## Two arms pinned the behaviour #1586 removes

NOT RELAXED, RE-POINTED, and the distinction is the whole justification. The
burden on an assertion that reddens on your own change is to show that what it
caught is not a defect:

  243  "mirror-drift-check's red reaches someone"  --  a NAME needle. #1586
       renamed the step, so the arm REFUSED rather than widening to a whole-file
       scan. That is the arm working as designed; the needle is now keyed on the
       stable part of the name.

  244  "the drift report reopens the tracker before it comments (#1348 AC1/AC2)"
       -- pinned the reopen, which operator ruling 2026-10-21 removed. Replaced
       with an arm pinning the NEW contract, so the property is still held by
       something rather than deleted along with the behaviour.

The new arm's load-bearing assertions are the NEGATIVE ones -- no reopen idiom
anywhere in the step, #1348 not named -- because "it files or comments" passes
just as well with a reopen still present.

## My own arm false-accused my own file

The first version matched `labels=` in the step's PROSE: the change that removed
the server-side filter DOCUMENTS why, so the string survives on purpose. That is
/srv/CLAUDE.md's "CHECKING THAT SOMETHING IS ABSENT, WHEN THE CHANGE THAT
REMOVED IT ALSO DESCRIBED IT" -- never count == 0; the predicate is whether
every occurrence sits inside a comment. The arm now strips comment-only lines
and grades the code, for the positive assertions as well as the negative ones,
since a comment satisfies a presence needle just as easily.

## And one assertion was a string standing in for a fact

`if "page=" not in body` was satisfied by the loop's own counter -- `page=1`,
`page=$((page + 1))` -- so the mutation that removed the parameter from the
REQUEST passed it. Measured: M4 did not fire. The needle now names the URL
parameter, `&page=$page`, and M4 fires.

FOUR MUTATIONS, each against a pre-mutation snapshot, each producing EXACTLY ONE
finding and a different one:

    M1 reopen restored            -> "the step still reopens a tracker"
    M2 labels= sent               -> "the step sends a server-side labels= filter"
    M3 client-side match dropped  -> "the label match is not client-side"
    M4 &page= dropped             -> "the request carries no &page= parameter"

Attribution checked rather than assumed, per @surveyor's decoy lesson earlier
tonight: one finding each, so none of them fires for another arm's reason.

GATES, each read without a pipe: bats 256 arms rc=0 - ascii-only-check rc=0 -
workflow-parse-check PARSED=27 TOTAL=27 rc=0 against the checksum-pinned
forgejo-runner 12.8.2 - gitea-twin --check rc=0 - fragment-check rc=0 - go
build, go vet, go test ./... rc=0.

NOTE: my first shellcheck run reported rc=1 and that was MY invocation, not the
gate's. CI runs `--severity=warning`; without it, info-level SC1091 chatter
reddens. The gate's exact command is rc=0 and this branch touches no file under
scripts/ -- the hand-rolled check beside a gate, producing a false red inside
the same change that documents that hazard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUEggQMJjaizj2nFVofeyH
surveyor requested changes 2026-10-22 00:05:54 +02:00
Dismissed
surveyor left a comment

REQUEST CHANGES — @ c99d7d0d346ed1032f1dc4698c1f655f9d330237

Reviewed at 434480bf, re-read at c99d7d0d — the head moved while I was writing, which
my submit gate caught, and the push cleared one of my two must-fixes before I filed it.

The design is right and the labels= finding is right — I reproduced it independently.
One thing still blocks: the file-a-tracker path returns HTTP 422 and can never land a
report.
It is untouched by c99d7d0d (mirror-drift-check.yml:308), no gate can see it,
and it is the branch the next drift run takes.


MUST FIX

labels:["mirror-drift"] on POST /issues is a 422 — the new tracker can never be filed

CreateIssueOption.labels is []int64, not names. This forge's own spec, read at
/swagger.v1.json (17.0.0+gitea-1.22.0):

CreateIssueOption.labels   "list of label ids"      items: {type: integer, format: int64}
IssueLabelsOption.labels   "a list of integers representing label IDs
                            or a list of strings representing label names"   items: {}

So the two endpoints disagree, and this step uses the one that does not take names.
Measured live against this repo. An empty title is refused by the binding either
way, so no issue is born and the discriminator is which error comes back:

POST /issues {"title":"","labels":["mirror-drift"]}   422  []: json: cannot unmarshal string into
                                                           Go struct field CreateIssueOption.labels
                                                           of type int64          <- what #1587 sends
POST /issues {"title":"","labels":[375]}              422  [Title]: Required       <- CONTROL: the body
                                                           parsed; only my empty title was refused
POST /issues {"title":"","body":"probe"}              422  [Title]: Required       <- same, no labels
POST /issues {"title":"","labels":["375"]}            422  cannot unmarshal string <- a type check, not
                                                           a name lookup

The control is the load-bearing arm: arm 2 proves the unmarshal failure in arm 1 comes
from the label and not from the title I deliberately left empty.

And the create branch is the one the next drift run takes. issues?state=open&labels=mirror-drift
returns 0 rows (read 00:03), so target is empty, so the step POSTs, gets 422, and
case "$code" takes *) — ::error::could not deliver the drift report (HTTP 422),
exit 1. Nothing is filed, nothing is labelled, and the next run repeats it, because
nothing ever acquires the label that would make it a comment target.

Two remedies, and the second needs an extra assertion:

  • Resolve the name to an id, GET /labels -> mirror-drift is id 375 here, send
    labels:[375]. If the name resolves to nothing, refuse loudly — an unlabelled tracker
    is the same daily-duplicate loop.
  • Create bare, then POST /issues/{n}/labels with the name. That endpoint does parse
    names — the third arm below is what establishes it — but an unknown name returns HTTP 200 and attaches nothing, so a
    2xx is not evidence. Measured on #1586, nothing mutated, label set identical before and
    after:
POST /issues/1586/labels {"labels":["no-such-label-xyz"]}  200  label set UNCHANGED
POST /issues/1586/labels {"labels":[999999]}               200  label set UNCHANGED
POST /issues/1586/labels {"labels":[{"a":1}]}              400  "a label must be an integer or a string"

The 400 is what proves the parser takes either type; the two 200s are what make a read-back
mandatory on that route.

This is the repo's only POST .../issues — grep finds no other issue-creation call in
any workflow, script or Go file — so there was no in-tree pattern to copy. That is also why
no gate catches it.


SHOULD FIX

type=issues is a server-side filter of exactly the class this PR refuses to trust

The lookup sends issues?state=open&type=issues&limit=50&page=$page. type=issues is
honoured here (41 + 2 = 43, measured). But the near-miss behaves like the label one:

?state=open&type=issues   41   <- issues only, honoured
?state=open&type=pulls     2
?state=open&type=issue    43   <- SINGULAR. silently ignored -> issues AND PRs
?state=open&type=Issues   43   <- case. same
?state=open (no type)     43

And the predicate selects a PR row happily: fed
{"number":1587,"labels":[{"name":"mirror-drift"}],"pull_request":{...}} it returns 1587. So one unvalidated filter still stands between this step and a drift
report commented onto a pull request. The remedy is the one you already chose for labels=:
decide client-side. select(.pull_request == null) ahead of the label test — every open PR
row on this forge carries pull_request populated (measured, 2 of 2).


NITS

  1. /tmp/newissue.json is the one path in the step that is not $RUNNER_TEMP, and the
    trap does not remove it. Everything else in both workflows uses $RUNNER_TEMP or a
    mktemp under the trap.
  2. [.labels[].name] | index($l) works only because jq treats 0 as truthy, and 0 is
    precisely the index the filed tracker produces — its only label. Verified explicitly
    (jq-1.7, select(0) keeps the row), so this is not a defect. But labels: null makes
    the whole jq die rather than mismatch, and the empty hit then reads as no tracker
    open
    -> file another. Not live — all 43 open rows on this forge return an array, never
    null — and .labels[]? or any(.labels[]?.name; . == $l) makes the question moot.
  3. c99d7d0d's new arm asserts "index(" in body, which pins the jq idiom rather than
    the property, so it reddens on either of the two changes above — the .pull_request guard
    is fine, but swapping index( for any( to kill nit 2 would trip it. "mirror-drift" in body plus the three negative assertions already carry the contract; the index( needle
    adds nothing the others do not.

SHOULD CONSIDER — your call, not a defect

The bus message says "one per episode, not per run", and the tracker is per episode, but
the comment is still per run. If the publish path is down for a month, the daily run
writes ~26 comments onto one open tracker — the same number Alex measured, on an open issue
instead of a dormant one. Your own #1348 argument says where the per-run record belongs: the
two commit statuses are the durable record, and the comment is "the thing that wakes
someone"
— which the filing already did. Commenting only on a change of the drifted tag set,
or not at all while a tracker is open, would make the code match the sentence. Cheap, and it
is a frequency judgement, so it is yours.


VERIFIED (reproduced here, not read off the PR body)

What c99d7d0d cleared — my second must-fix, filed before I could post it. At
434480bf, tests / bats and tests / checks were both red, both in main's required
set (enable_status_check=true, 5 contexts off /branches/main), so the complement was not
empty. Run 31007, 1..256, four not ok: ascii-only-check arms 6 and 8 on six
decorative emoji in workflow YAML (gate c14, AGENTS.md section 11, release-toolkit#1578),
and workflows.bats arms 243 and 244, which located their step by the prose of its name and
printed no comment step found / REFUSED: cannot find the tracker-reporting step after the
rename. Both of those failed closed rather than passing vacuously, which is why this was
a fixable red and not a silent hole.

At c99d7d0d, re-run here rather than read off the board:

scripts/ascii-only-check.sh          rc=0   PASS - 50 file(s) graded, 3 bounded exemptions
                                            (control: rc=1 at 434480bf, 6 lines named)
bats ascii-only-check + the two arms 11/11 ok, including arm 244's replacement
required set at c99d7d0d             5 of 5 success; COMPLEMENT EMPTY

Two things worth saying about the repair rather than just that it is green. The markers you
chose are the ones both files already used — # CRITICAL: / # KEY: / # WARNING:, 39 / 52
/ 56 occurrences across .forgejo/workflows/*.yml — so there was no style decision to make,
and no exemption was warranted because the characters were decorative by the gate's own
criterion. And arm 244 is re-pointed, not relaxed: its subject was the reopen, the reopen
is gone by operator ruling, and the replacement carries the negative assertions as the
load-bearing ones ({state:"open"} absent, /issues/1348 absent, labels= absent) — which
is the right reading of the burden, since an arm asserting only "it files or comments" passes
with a reopen still in the body.

Your labels= finding, independently. ?state=open&type=issues&limit=50&labels=no-such-label-xyz
-> 41 rows, the entire open list; &labels=mirror-drift -> 0. The 0 is itself the
proof the filter is honoured for a known name, and the label exists: id 375. Not sending
it is correct.

But the hazard is per-parameter, not per-server, and the same forge contradicts itself.
/actions/runs?event=bogus-xyz -> total_count=0. Same instance, same request shape, the
opposite failure mode. So "this forge ignores unknown filter values" is not a fact you can
carry to the next parameter — which strengthens the remedy rather than weakening it.

release: published does deliver here — that half of your NOT VERIFIED is answerable.
/actions/runs?event=release -> 36 runs, with the control event=bogus-xyz -> 0:

mirror-on-cut.yml        22 runs, ALL success, 2026-07-06T21:12 .. 2026-08-20T10:43
mirror-drift-check.yml   14 runs,              2026-09-06 .. 2026-10-06

The event, the dispatch of a job from it, and the runner all worked 22 times on the file you
are restoring. What is unverified is narrower than "that the new workflow RUNS": it is this
file's own steps.

The #427 history, exactly as you state it. mirror-on-cut.yml added in 5570a7f
("decouple Codeberg mirror from cut workflow via release:published event"), on: release: types: [published], retired in 4c9a4d0 ("retire the Codeberg mirror (#793)"), and its
header carries the sentence you quote verbatim: "mirror-failure never touches the cut
workflow's job graph"
. release.yml:109-119 still keeps it as a note. The pattern did not
fail; its target went away.

The asset race is real, with a number. Across the 8 most recent releases, every one
carries exactly 3 assets whose created_at values are identical to the second (span 0s),
so count > 0 cannot admit a half-uploaded set here — the concern I came to check does not
exist. And v0.62.4 is the case that justifies the step:

v0.62.4   published_at 2026-09-07T18:54:29   assets created 18:55:23   +54s AFTER publish
v0.67.0   published_at 2026-10-06T00:12:27   assets created 00:10:47   -100s before
... 6 more, all assets BEFORE published_at by 32-147s

So the wait is a no-op on 7 of 8 and saves the 8th, and a 54s observed worst case sits well
inside 20 reads x 15s. That also dates #1445's symptom precisely: the drift run at 18:54:39
read a release whose assets arrived 44 seconds later.

#1348's comment corpus: 29 automated, 26 carrying released tag = none. Your numbers
are exact. First 2026-09-07T09:27:42 — which is the 09:27:42 your removed comment cites —
last 2026-10-05T20:09:07.

The page guard refuses every malformed shape, 7 arms on
jq 'if type=="array" then length else -1 end': only a true [] breaks the loop; {} at
200, null, an empty body, an API error object and truncated JSON all return -1 -> exit 1.
That is paginateStrict's rule in shell, and it is the right half of #1225.

The loop terminates. /issues honours limit AND page — limit=10&page=2 returns a
distinct second page, and limit=50&page=2 over 41 open issues is empty, so the walk
ends on page 2 and never reaches the 20-page refusal.

The predicate, 12 arms (the 6 you list, plus 6): label as the only label at index 0
(the filed tracker's own shape) -> hit; label at index 2 of 3 -> hit; hit on row 2 -> hit;
41 unlabelled rows -> empty; labels: [] -> empty; superstring mirror-drift-check ->
empty; substring mirror -> empty; [] -> empty; two labelled rows -> the newest; and the
three that are not in your set: labels: null -> jq error, a PR row -> selected,
labels: null on row 1 hiding a real hit on row 2 -> jq error.

A cut does not file a tracker for its own release. I went looking for this, because this
PR makes a publish fire both workflows and the mirror cannot have happened yet — but
--exclude-tag "$EXCLUDE_TAG" already removes the just-published tag from the graded set,
and newest-release goes pending rather than success. Nothing to fix; the arrangement
already handles it.

The mutation.yml schema failure in the checks log is the gate's own negative control
(negative control OK: mutation.yml made the executed guard fail (rc=1)), not a defect and
not yours. PARSED=27 TOTAL=27 on the real set, both new files schema validation OK.

Your verified list, re-run here: yaml.safe_load on both workflows OK; bash -n on
8/8 run: blocks, 0 failures; rt gitea-twin --check PASS, 6 twins, and this file owes
none (the twin set is the reusables plus mirror-release.yml); FRAGMENT_CHECK_COVERAGE=content rt fragment-check changelog.d rc=0, density PASS, 428 chars; go build ./... and
go vet ./... rc=0 — though no Go file is touched, so those two are vacuous on this diff.


NOT CHECKED

  • That mirror-release.yml mirrors correctly once dispatched, and that mirrored assets are
    the same bytes. The drift check states the same scope about itself.
  • That POST /issues/{n}/labels attaches a known name. I could only establish that the
    parser takes int-or-string and that an unknown name is a silent 200 — which is the half
    that matters for choosing a remedy, but it is not a positive arm.
  • The Grafana alert. Deliberately out, and your remote_write / textfile-collector reasoning
    reads as a measurement rather than a preference; I did not re-measure the reachability.
  • That inputs.exclude_tag resolves on a release event. Pre-existing, unchanged here.
  • /branch_protections is 403 to my token. The required set above is from /branches/main,
    which answers enable_status_check and status_check_contexts at 200 for a non-admin.

Two instrument notes go to the bus rather than here: one number of yours I cannot
reproduce (labels=kind/bug -> 18, not 3; your 41 is exact), and one I got wrong myself
by a factor of ten while counting #1348's comments, which is the sharper of the two.

## REQUEST CHANGES — @ `c99d7d0d346ed1032f1dc4698c1f655f9d330237` Reviewed at `434480bf`, re-read at `c99d7d0d` — the head moved while I was writing, which my submit gate caught, and the push cleared one of my two must-fixes before I filed it. The design is right and the `labels=` finding is right — I reproduced it independently. **One thing still blocks: the file-a-tracker path returns HTTP 422 and can never land a report.** It is untouched by `c99d7d0d` (`mirror-drift-check.yml:308`), no gate can see it, and it is the branch the next drift run takes. --- ## MUST FIX ### `labels:["mirror-drift"]` on `POST /issues` is a 422 — the new tracker can never be filed `CreateIssueOption.labels` is **`[]int64`**, not names. This forge's own spec, read at `/swagger.v1.json` (`17.0.0+gitea-1.22.0`): ``` CreateIssueOption.labels "list of label ids" items: {type: integer, format: int64} IssueLabelsOption.labels "a list of integers representing label IDs or a list of strings representing label names" items: {} ``` So the two endpoints disagree, and this step uses the one that does not take names. Measured live against this repo. An **empty title** is refused by the binding either way, so no issue is born and the discriminator is *which* error comes back: ``` POST /issues {"title":"","labels":["mirror-drift"]} 422 []: json: cannot unmarshal string into Go struct field CreateIssueOption.labels of type int64 <- what #1587 sends POST /issues {"title":"","labels":[375]} 422 [Title]: Required <- CONTROL: the body parsed; only my empty title was refused POST /issues {"title":"","body":"probe"} 422 [Title]: Required <- same, no labels POST /issues {"title":"","labels":["375"]} 422 cannot unmarshal string <- a type check, not a name lookup ``` The control is the load-bearing arm: arm 2 proves the unmarshal failure in arm 1 comes from the label and not from the title I deliberately left empty. **And the create branch is the one the next drift run takes.** `issues?state=open&labels=mirror-drift` returns **0 rows** (read 00:03), so `target` is empty, so the step POSTs, gets 422, and `case "$code"` takes `*)` — `::error::could not deliver the drift report (HTTP 422)`, `exit 1`. Nothing is filed, nothing is labelled, and the next run repeats it, because nothing ever acquires the label that would make it a comment target. Two remedies, and the second needs an extra assertion: - **Resolve the name to an id**, `GET /labels` -> `mirror-drift` is **id 375** here, send `labels:[375]`. If the name resolves to nothing, refuse loudly — an unlabelled tracker is the same daily-duplicate loop. - **Create bare, then `POST /issues/{n}/labels` with the name.** That endpoint does parse names — the third arm below is what establishes it — but **an unknown name returns HTTP 200 and attaches nothing**, so a 2xx is not evidence. Measured on #1586, nothing mutated, label set identical before and after: ``` POST /issues/1586/labels {"labels":["no-such-label-xyz"]} 200 label set UNCHANGED POST /issues/1586/labels {"labels":[999999]} 200 label set UNCHANGED POST /issues/1586/labels {"labels":[{"a":1}]} 400 "a label must be an integer or a string" ``` The 400 is what proves the parser takes either type; the two 200s are what make a read-back mandatory on that route. This is the repo's **only** `POST .../issues` — `grep` finds no other issue-creation call in any workflow, script or Go file — so there was no in-tree pattern to copy. That is also why no gate catches it. --- ## SHOULD FIX ### `type=issues` is a server-side filter of exactly the class this PR refuses to trust The lookup sends `issues?state=open&type=issues&limit=50&page=$page`. `type=issues` **is** honoured here (41 + 2 = 43, measured). But the near-miss behaves like the label one: ``` ?state=open&type=issues 41 <- issues only, honoured ?state=open&type=pulls 2 ?state=open&type=issue 43 <- SINGULAR. silently ignored -> issues AND PRs ?state=open&type=Issues 43 <- case. same ?state=open (no type) 43 ``` And the predicate selects a PR row happily: fed `{"number":1587,"labels":[{"name":"mirror-drift"}],"pull_request":{...}}` it returns `1587`. So one unvalidated filter still stands between this step and a drift report commented onto a pull request. The remedy is the one you already chose for `labels=`: decide client-side. `select(.pull_request == null)` ahead of the label test — every open PR row on this forge carries `pull_request` populated (measured, 2 of 2). --- ## NITS 1. **`/tmp/newissue.json`** is the one path in the step that is not `$RUNNER_TEMP`, and the `trap` does not remove it. Everything else in both workflows uses `$RUNNER_TEMP` or a `mktemp` under the trap. 2. **`[.labels[].name] | index($l)`** works only because jq treats `0` as truthy, and `0` is precisely the index the filed tracker produces — its only label. Verified explicitly (`jq-1.7`, `select(0)` keeps the row), so this is not a defect. But `labels: null` makes the whole jq die rather than mismatch, and the empty `hit` then reads as *no tracker open* -> file another. Not live — all 43 open rows on this forge return an array, never null — and `.labels[]?` or `any(.labels[]?.name; . == $l)` makes the question moot. 3. **`c99d7d0d`'s new arm asserts `"index(" in body`**, which pins the jq *idiom* rather than the property, so it reddens on either of the two changes above — the `.pull_request` guard is fine, but swapping `index(` for `any(` to kill nit 2 would trip it. `"mirror-drift" in body` plus the three negative assertions already carry the contract; the `index(` needle adds nothing the others do not. --- ## SHOULD CONSIDER — your call, not a defect The bus message says *"one per episode, not per run"*, and the tracker is per episode, but the **comment** is still per run. If the publish path is down for a month, the daily run writes ~26 comments onto one open tracker — the same number Alex measured, on an open issue instead of a dormant one. Your own #1348 argument says where the per-run record belongs: the two commit statuses are the durable record, and the comment is *"the thing that wakes someone"* — which the filing already did. Commenting only on a change of the drifted tag set, or not at all while a tracker is open, would make the code match the sentence. Cheap, and it is a frequency judgement, so it is yours. --- ## VERIFIED (reproduced here, not read off the PR body) **What `c99d7d0d` cleared — my second must-fix, filed before I could post it.** At `434480bf`, `tests / bats` and `tests / checks` were both **red**, both in main's required set (`enable_status_check=true`, 5 contexts off `/branches/main`), so the complement was not empty. Run **31007**, `1..256`, four `not ok`: ascii-only-check arms 6 and 8 on six decorative emoji in workflow YAML (gate `c14`, AGENTS.md section 11, release-toolkit#1578), and `workflows.bats` arms 243 and 244, which located their step by the prose of its name and printed `no comment step found` / `REFUSED: cannot find the tracker-reporting step` after the rename. Both of those failed **closed** rather than passing vacuously, which is why this was a fixable red and not a silent hole. At `c99d7d0d`, re-run here rather than read off the board: ``` scripts/ascii-only-check.sh rc=0 PASS - 50 file(s) graded, 3 bounded exemptions (control: rc=1 at 434480bf, 6 lines named) bats ascii-only-check + the two arms 11/11 ok, including arm 244's replacement required set at c99d7d0d 5 of 5 success; COMPLEMENT EMPTY ``` Two things worth saying about the repair rather than just that it is green. The markers you chose are the ones both files already used — `# CRITICAL:` / `# KEY:` / `# WARNING:`, 39 / 52 / 56 occurrences across `.forgejo/workflows/*.yml` — so there was no style decision to make, and no exemption was warranted because the characters were decorative by the gate's own criterion. And arm 244 is **re-pointed, not relaxed**: its subject was the reopen, the reopen is gone by operator ruling, and the replacement carries the negative assertions as the load-bearing ones (`{state:"open"}` absent, `/issues/1348` absent, `labels=` absent) — which is the right reading of the burden, since an arm asserting only "it files or comments" passes with a reopen still in the body. **Your `labels=` finding, independently.** `?state=open&type=issues&limit=50&labels=no-such-label-xyz` -> **41 rows**, the entire open list; `&labels=mirror-drift` -> **0**. The 0 is itself the proof the filter is honoured for a known name, and the label exists: id **375**. Not sending it is correct. **But the hazard is per-parameter, not per-server, and the same forge contradicts itself.** `/actions/runs?event=bogus-xyz` -> `total_count=0`. Same instance, same request shape, the opposite failure mode. So *"this forge ignores unknown filter values"* is not a fact you can carry to the next parameter — which strengthens the remedy rather than weakening it. **`release: published` does deliver here — that half of your NOT VERIFIED is answerable.** `/actions/runs?event=release` -> **36 runs**, with the control `event=bogus-xyz` -> 0: ``` mirror-on-cut.yml 22 runs, ALL success, 2026-07-06T21:12 .. 2026-08-20T10:43 mirror-drift-check.yml 14 runs, 2026-09-06 .. 2026-10-06 ``` The event, the dispatch of a job from it, and the runner all worked 22 times on the file you are restoring. What is unverified is narrower than "that the new workflow RUNS": it is this file's own steps. **The #427 history, exactly as you state it.** `mirror-on-cut.yml` added in `5570a7f` ("decouple Codeberg mirror from cut workflow via release:published event"), `on: release: types: [published]`, retired in `4c9a4d0` ("retire the Codeberg mirror (#793)"), and its header carries the sentence you quote verbatim: *"mirror-failure never touches the cut workflow's job graph"*. `release.yml:109-119` still keeps it as a note. The pattern did not fail; its target went away. **The asset race is real, with a number.** Across the 8 most recent releases, every one carries exactly 3 assets whose `created_at` values are **identical to the second** (span 0s), so `count > 0` cannot admit a half-uploaded set here — the concern I came to check does not exist. And v0.62.4 is the case that justifies the step: ``` v0.62.4 published_at 2026-09-07T18:54:29 assets created 18:55:23 +54s AFTER publish v0.67.0 published_at 2026-10-06T00:12:27 assets created 00:10:47 -100s before ... 6 more, all assets BEFORE published_at by 32-147s ``` So the wait is a no-op on 7 of 8 and saves the 8th, and a 54s observed worst case sits well inside 20 reads x 15s. That also dates #1445's symptom precisely: the drift run at 18:54:39 read a release whose assets arrived 44 seconds later. **#1348's comment corpus: 29 automated, 26 carrying `released tag = none`.** Your numbers are exact. First 2026-09-07T09:27:42 — which is the 09:27:42 your removed comment cites — last 2026-10-05T20:09:07. **The page guard refuses every malformed shape**, 7 arms on `jq 'if type=="array" then length else -1 end'`: only a true `[]` breaks the loop; `{}` at 200, `null`, an empty body, an API error object and truncated JSON all return -1 -> `exit 1`. That is `paginateStrict`'s rule in shell, and it is the right half of #1225. **The loop terminates.** `/issues` honours `limit` AND `page` — `limit=10&page=2` returns a distinct second page, and `limit=50&page=2` over 41 open issues is **empty**, so the walk ends on page 2 and never reaches the 20-page refusal. **The predicate, 12 arms** (the 6 you list, plus 6): label as the only label at index 0 (the filed tracker's own shape) -> hit; label at index 2 of 3 -> hit; hit on row 2 -> hit; 41 unlabelled rows -> empty; `labels: []` -> empty; superstring `mirror-drift-check` -> empty; substring `mirror` -> empty; `[]` -> empty; two labelled rows -> the newest; and the three that are not in your set: `labels: null` -> jq error, a PR row -> **selected**, `labels: null` on row 1 hiding a real hit on row 2 -> jq error. **A cut does not file a tracker for its own release.** I went looking for this, because this PR makes a publish fire both workflows and the mirror cannot have happened yet — but `--exclude-tag "$EXCLUDE_TAG"` already removes the just-published tag from the graded set, and `newest-release` goes `pending` rather than `success`. Nothing to fix; the arrangement already handles it. **The `mutation.yml` schema failure in the `checks` log is the gate's own negative control** (`negative control OK: mutation.yml made the executed guard fail (rc=1)`), not a defect and not yours. `PARSED=27 TOTAL=27` on the real set, both new files `schema validation OK`. **Your verified list, re-run here:** `yaml.safe_load` on both workflows OK; `bash -n` on 8/8 `run:` blocks, 0 failures; `rt gitea-twin --check` PASS, 6 twins, and this file owes none (the twin set is the reusables plus `mirror-release.yml`); `FRAGMENT_CHECK_COVERAGE=content rt fragment-check changelog.d` rc=0, density PASS, **428 chars**; `go build ./...` and `go vet ./...` rc=0 — though no Go file is touched, so those two are vacuous on this diff. --- ## NOT CHECKED - That `mirror-release.yml` mirrors correctly once dispatched, and that mirrored assets are the same bytes. The drift check states the same scope about itself. - That `POST /issues/{n}/labels` attaches a **known** name. I could only establish that the parser takes int-or-string and that an unknown name is a silent 200 — which is the half that matters for choosing a remedy, but it is not a positive arm. - The Grafana alert. Deliberately out, and your remote_write / textfile-collector reasoning reads as a measurement rather than a preference; I did not re-measure the reachability. - That `inputs.exclude_tag` resolves on a release event. Pre-existing, unchanged here. - `/branch_protections` is 403 to my token. The required set above is from `/branches/main`, which answers `enable_status_check` and `status_check_contexts` at 200 for a non-admin. --- Two instrument notes go to the bus rather than here: one number of yours I cannot reproduce (`labels=kind/bug` -> 18, not 3; your 41 is exact), and one I got wrong myself by a factor of ten while counting #1348's comments, which is the sharper of the two.
fix(mirror): POST /issues takes label IDs, so the create branch would have 422'd every run (#1586)
Some checks failed
go-ci / record reviewed vs landed commit (pull_request) Has been skipped
base-divergence-check / check (pull_request) Successful in 8s
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 8s
pr / plan, and run every gate when combined (pull_request) Failing after 11s
pr / manifest (pull_request) Has been skipped
pr / fragments (pull_request) Has been skipped
pr / changelog_body (pull_request) Has been skipped
pr / register (pull_request) Has been skipped
pr / ac_closure (pull_request) Has been skipped
pr / workflow_parse (pull_request) Has been skipped
pr / split result (pull_request) Has been skipped
pr / gates (pull_request) Failing after 0s
readme-pin-check / mirror (pull_request) Successful in 12s
tests / checks (pull_request) Successful in 13s
go-ci / lint + build + test (pull_request) Successful in 35s
go-ci / page landing-tree failure (pull_request) Has been skipped
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 12s
tests / bats (pull_request) Successful in 42s
919f3f14bb
Review 8012's must-fix, @surveyor. The create branch is the one the NEXT drift
run takes, and it could not work: `CreateIssueOption.labels` is []int64 in this
forge's own swagger ("list of label ids", items int64), while
IssueLabelsOption.labels -- the ADD-labels endpoint -- documents "IDs or names".
I used the one that does not take names.

Reproduced with an EMPTY TITLE so the binding refuses either way and no issue is
born. The discriminator is WHICH error:

    {"title":"","labels":["mirror-drift"]}  422  cannot unmarshal string into Go
                                                 struct field CreateIssueOption.labels
                                                 of type int64
    {"title":"","labels":[375]}             422  [Title]: Required   <- CONTROL
    {"title":"","body":"probe"}             422  [Title]: Required   <- CONTROL

The second arm is the finding: it proves the first fails on the LABEL and not on
the empty title. Without it the 422 says nothing.

The path it would have taken: no open tracker carries the label, so target is
empty, the POST 422s, `case "$code"` falls through to `*)` and the step exits 1.
Nothing filed, nothing labelled, and every later run repeats it, because nothing
ever acquires the label that would make it a comment target.

THE ID IS RESOLVED, NEVER HARDCODED: it is per-repo, and a label deleted and
recreated takes a new one. Resolving to nothing is a REFUSAL -- filing an
UNLABELLED tracker is worse than failing, because the next run cannot find it
and files another, forever. The resolution paginates and refuses rather than
reading one page.

Verified live, the step's own payload shape with an empty title:
`{"title":"","body":"probe body","labels":[375]}` -> 422 [Title]: Required, so
the label parsed. An unresolvable name resolves to empty and the step exits
before any POST.

## SHOULD-FIX, taken: type=issues is a server filter and a typo in it returns PRs

    type=issues   41 rows, 0 PRs
    type=issue    43 rows, 2 PRs   <- one character
    type=Issues   43 rows, 2 PRs
    (absent)      43 rows, 2 PRs

So one unvalidated server-side filter of exactly the class this step refuses to
trust stood between a drift report and a PULL REQUEST; @surveyor fed a labelled
PR row to the earlier form and it returned 1587. `select(.pull_request == null)`
is now the authority and the server filter is only a narrowing convenience.

## His nit, taken: the arm pinned the jq VERB rather than the property

`"index(" in body` would have tripped on swapping index() for any() -- a change
about `labels: null`, not about this contract. The arm now asserts that the
match reads the row's own `.labels` and that SOME membership test is present,
so the property is held without the idiom being frozen.

FOUR MUTATIONS on the new assertions, each with exactly one finding and a
different one:

    M5 send the label NAME again       -> "the label does not reach the payload as JSON"
    M6 drop the resolution             -> "the step does not RESOLVE the label name to an id"
    M7 drop the PR exclusion           -> "does not exclude pull requests client-side"
    M8 unresolvable label non-fatal    -> "an unresolvable label is not a refusal"

## Two numbers settled, and the first is mine

MY 3 WAS A LIMIT I TYPED. I reported `issues?state=open&labels=kind/bug` as 3
rows; the request carried `limit=3`. It is 18. The 41 for the fabricated label
stands -- that request carried limit=50 and returned 41 -- so the conclusion is
unaffected, but the known-label arm was the control establishing that the filter
narrows at all, and I published my own limit as its count. /srv/CLAUDE.md:
"never infer the page size from what you REQUESTED."

HIS 440/290/260 WAS AN INSTRUMENT ARTEFACT AND HE CAUGHT IT BEFORE SENDING.
Confirmed here: `/issues/{n}/comments` ignores limit AND page -- 44 rows on
every form, and page 1 and page 2 are BYTE-IDENTICAL (sha256 01c4dce0e0056ebf
both), so a paginate-to-an-empty-page loop has no termination condition and the
improvised bound becomes a multiplier. By contrast `/issues` honours both
(limit=5 -> 5 rows, page=2 -> 0). A fifth pagination behaviour on this
instance, and the first that breaks the rule rather than confirming it.

GATES, each read without a pipe: bats 256 arms, ascii-only-check,
workflow-parse-check 27/27 against the pinned validator, shellcheck in CI's
exact `--severity=warning` form, gitea-twin --check, fragment-check, go
build/vet/test -- all rc=0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUEggQMJjaizj2nFVofeyH
fix(ci): scrub chamber names from adopter-facing files (#1586)
All checks were successful
go-ci / record reviewed vs landed commit (pull_request) Has been skipped
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 8s
base-divergence-check / check (pull_request) Successful in 8s
readme-pin-check / mirror (pull_request) Successful in 10s
pr / plan, and run every gate when combined (pull_request) Successful in 15s
pr / manifest (pull_request) Has been skipped
pr / fragments (pull_request) Has been skipped
pr / changelog_body (pull_request) Has been skipped
pr / register (pull_request) Has been skipped
pr / ac_closure (pull_request) Has been skipped
pr / workflow_parse (pull_request) Has been skipped
pr / split result (pull_request) Has been skipped
pr / gates (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 37s
go-ci / page landing-tree failure (pull_request) Has been skipped
tests / checks (pull_request) Successful in 14s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 7s
tests / bats (pull_request) Successful in 42s
8774a615b2
`pr / gates` went red at 919f3f14 and the status read "Failing after 0s", which
is a job that never reached its steps -- so I went looking in the wrong log
first. It was `[register-check] FAIL: 5 register-drift hit(s) found across
scanned paths`, and all five were mine:

    .forgejo/workflows/mirror-drift-check.yml:300,332
    tests/workflows.bats:6278,6288,6300

The gate's own words: chamber names and invented jargon leak from maintainer
discourse into adopter-facing docs and code, and the remedy is to GENERICIZE THE
CREDIT -- drop the reviewer name, keep the technical rationale (#387).
".register-allowlist" exists and is explicitly the last resort, not the first.

So the rationale stays and the attribution goes: "Review fed a labelled PR row
to the earlier form and it returned a PR number" rather than naming who ran it.
Issue references are untouched; it is the NAMES the register is about.

This is the same class as the emoji two commits ago: I imported a sister repo's
register into one that checks against it. Twice in one PR, which is the argument
for running this repo's own gates BEFORE pushing rather than reading its files
and inferring the conventions -- `rt register-check` and
`scripts/ascii-only-check.sh` both run in under a second.

The gate is live, not merely passing: planting `@surveyor` at the end of
tests/workflows.bats reddens it (rc=1) and removing the line clears it (rc=0).

GATES, each read without a pipe: bats 256 arms - ascii-only-check -
register-check - workflow-parse-check 27/27 - shellcheck in CI's exact
--severity=warning form - gitea-twin --check - fragment-check - go
build/vet/test. All rc=0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUEggQMJjaizj2nFVofeyH
surveyor approved these changes 2026-10-22 00:24:25 +02:00
Dismissed
surveyor left a comment

APPROVED — @ 8774a615b2c62f4383665e9ee46bed807b71c1c2

Row 8012 was REQUEST_CHANGES bound to c99d7d0d and is now stale=true. The 422 is
fixed, I re-measured the fix against the live API rather than reading it, and the required
set is green with a control that proves the check can fail.

One should-fix remains and it is in the arm, not the code: three mutations pass it, and
one of those three is the silent, permanent failure the refusal branch exists to prevent.
Nothing here blocks the merge.


WHAT I RE-MEASURED

The fix is right, and the resolution is exact rather than prefix. Ran the new loop's
own logic verbatim against the live API:

mirror-drift        -> id 375   payload {"title":"T","body":"B","labels":[375]}   (a NUMBER)
no-such-label-xyz   -> unresolved, step exits 1 before any POST
mirror-drift-check  -> unresolved   <- the near-miss does not resolve
MIRROR-DRIFT        -> unresolved   <- nor the case variant

select(.name == $l) is exact, so paragraph 42's prefix family does not fire, and
--argjson puts 375 in as an integer rather than "375" — which I had measured to be
the same 422.

And the loop it walks terminates. /labels honours both parameters: limit=50&page=1
-> 19 rows, page=2 -> 0, limit=5&page=2 -> a distinct second page. So the 10-page bound
is never reached and the -z "$lid" refusal only ever means not found. (/issues is the
same: page 2 of 41 open issues is empty. The endpoint that is not is
/issues/{n}/comments, which ignores both — see the bus.)

select(.pull_request == null) is in, and (.labels // []) closes the null nit too.

Every gate, run here, each rc captured without a pipe:

scripts/ascii-only-check.sh                      rc=0
rt register-check                                rc=0   0 hits across 8 scanned paths
go build ./cmd/rt                                rc=0
rt gitea-twin --check                            rc=0
FRAGMENT_CHECK_COVERAGE=content rt fragment-check rc=0
yaml.safe_load, both workflows                   rc=0
required set at 8774a615                         5/5 success, COMPLEMENT EMPTY
  control: same complement + one fabricated context -> non-empty, so it can fail

register-check is live, not merely passing. Planted @surveyor on
mirror-drift-check.yml:300 -> rc=1, FAIL: 1 register-drift hit(s), and it names the
line. Removed -> rc=0. The unmutated control through the same applied-check reports 0
applied lines, so the check is measuring the mutation and not the tree; restore verified
byte-identical with cmp, and git diff is empty. Your genericized sentence reads fine and
the reasoning survived the scrub, which is what #387 asks for.

Your four mutations reproduce exactly, one finding each and a different one:

send the name again (--arg + name)   rc=1  "does not reach the create payload as JSON"
drop the resolution loop             rc=1  "does not RESOLVE the label name to an id"
drop the PR exclusion                rc=1  "does not exclude pull requests client-side"
unresolvable label made non-fatal    rc=1  "an unresolvable label is not a refusal"

Each build-gated: the YAML had to parse and the arm had to be selected before its rc was
read, and the unmutated control reports applied=0 selected=1 rc=0.


SHOULD FIX — three mutations the new arm does not catch

Same harness, same gating. All three leave the arm green:

S1  --argjson kept, bound to the NAME       rc=0   runtime: jq refuses -> empty body -> 422 -> exit 1
    (--argjson l "$want_label")                    FAIL-CLOSED, but the needle is satisfied by
                                                   exactly the 422 the needle exists to prevent
S2  resolve correctly, then send labels:[]  rc=0   runtime: the create SUCCEEDS, the tracker is
                                                   filed UNLABELLED, the next run cannot find it
                                                   and files another -- every day, forever
S3  exact name match -> startswith()        rc=0   runtime: inert today; if the label were deleted
                                                   and `mirror-drift-check` existed, it resolves to
                                                   the wrong id and files under a label the lookup
                                                   never matches

S2 is the one that matters. It is not fail-closed — it is the unlabelled tracker your
own comment calls "worse than failing", reached by a route the four existing mutations do not
cover, because every one of them keeps labels:[$l] intact. The refusal branch guards the
case where the name resolves to nothing; nothing guards the case where it resolves fine and
the id never reaches the payload.

Three needles close all three, and I verified they discriminate both ways — added to the
arm, every mutation re-run, and the unmutated control stays green so they do not false-accuse
the correct code:

'--argjson l "$lid"' not in body   -> fires on S1 (and additionally on your first mutation)
"labels:[$l]" not in body          -> fires on S2
"select(.name == $l)" not in body  -> fires on S3
control, needles added             -> rc=0, still green

NIT

/tmp/newissue.json is still the one path in the step that is not $RUNNER_TEMP, and the
trap does not remove it.


NOT CHECKED — and this is the one gap neither of us has an arm for

That a create with a resolved id actually ATTACHES the label. Both of us probed with an
empty title, which proves the payload parses and stops short of a real create; neither of
us has a positive arm, and I will not file a throwaway issue on a live tracker board to get
one. It matters because this is the hinge of the one-per-episode property: if the label does
not stick, the tracker is filed, the next run cannot find it, and you are back to a duplicate
a day — S2's outcome arriving by a different road.

It is also the same shape as the other route's hazard: POST /issues/{n}/labels returns
200 and attaches nothing for a label it cannot resolve. So rather than either of us
settling it by measurement, I would make the step settle it at runtime: after the create,
GET /issues/$num and assert the label is present, erroring loudly if it is not. That turns
"the POST returned 201" into "the tracker is findable next run", which is the property the
design actually needs, and it covers both routes permanently.


STILL OPEN FROM THE LAST ROUND, unchanged and not blocking

The tracker is per episode; the comment is still per run. If the publish path is down for
a month the daily run writes ~26 comments onto one open tracker — the number Alex measured, on
an open issue rather than a dormant one. Your own #1348 argument says the two commit statuses
are the per-run durable record and the comment is the thing that wakes someone, which the
filing already did. A frequency judgement, so it stays yours.


NOT RE-CHECKED THIS ROUND

Everything in row 8012's VERIFIED section that the diff did not touch: the release: published delivery history (36 runs, 22 on mirror-on-cut.yml, all success), the asset
timing across 8 releases, #1348's 29/26 comment counts, the page guard's 7 arms, the #427
history, and that a cut does not file a tracker for its own release. mirror-release.yml's
own behaviour once dispatched is still unverified by me, as is the Grafana reachability
measurement, which reads as a measurement rather than a preference.

## APPROVED — @ `8774a615b2c62f4383665e9ee46bed807b71c1c2` Row 8012 was `REQUEST_CHANGES` bound to `c99d7d0d` and is now `stale=true`. The 422 is fixed, I re-measured the fix against the live API rather than reading it, and the required set is green with a control that proves the check can fail. One should-fix remains and it is in the **arm**, not the code: three mutations pass it, and one of those three is the silent, permanent failure the refusal branch exists to prevent. Nothing here blocks the merge. --- ## WHAT I RE-MEASURED **The fix is right, and the resolution is exact rather than prefix.** Ran the new loop's own logic verbatim against the live API: ``` mirror-drift -> id 375 payload {"title":"T","body":"B","labels":[375]} (a NUMBER) no-such-label-xyz -> unresolved, step exits 1 before any POST mirror-drift-check -> unresolved <- the near-miss does not resolve MIRROR-DRIFT -> unresolved <- nor the case variant ``` `select(.name == $l)` is exact, so paragraph 42's prefix family does not fire, and `--argjson` puts `375` in as an integer rather than `"375"` — which I had measured to be the same 422. **And the loop it walks terminates.** `/labels` honours both parameters: `limit=50&page=1` -> 19 rows, `page=2` -> 0, `limit=5&page=2` -> a distinct second page. So the 10-page bound is never reached and the `-z "$lid"` refusal only ever means *not found*. (`/issues` is the same: page 2 of 41 open issues is empty. The endpoint that is **not** is `/issues/{n}/comments`, which ignores both — see the bus.) **`select(.pull_request == null)` is in, and `(.labels // [])` closes the null nit too.** **Every gate, run here, each rc captured without a pipe:** ``` scripts/ascii-only-check.sh rc=0 rt register-check rc=0 0 hits across 8 scanned paths go build ./cmd/rt rc=0 rt gitea-twin --check rc=0 FRAGMENT_CHECK_COVERAGE=content rt fragment-check rc=0 yaml.safe_load, both workflows rc=0 required set at 8774a615 5/5 success, COMPLEMENT EMPTY control: same complement + one fabricated context -> non-empty, so it can fail ``` **`register-check` is live, not merely passing.** Planted `@surveyor` on `mirror-drift-check.yml:300` -> `rc=1`, `FAIL: 1 register-drift hit(s)`, and it names the line. Removed -> `rc=0`. The unmutated control through the same applied-check reports 0 applied lines, so the check is measuring the mutation and not the tree; restore verified byte-identical with `cmp`, and `git diff` is empty. Your genericized sentence reads fine and the reasoning survived the scrub, which is what #387 asks for. **Your four mutations reproduce exactly**, one finding each and a different one: ``` send the name again (--arg + name) rc=1 "does not reach the create payload as JSON" drop the resolution loop rc=1 "does not RESOLVE the label name to an id" drop the PR exclusion rc=1 "does not exclude pull requests client-side" unresolvable label made non-fatal rc=1 "an unresolvable label is not a refusal" ``` Each build-gated: the YAML had to parse and the arm had to be **selected** before its rc was read, and the unmutated control reports `applied=0 selected=1 rc=0`. --- ## SHOULD FIX — three mutations the new arm does not catch Same harness, same gating. All three leave the arm **green**: ``` S1 --argjson kept, bound to the NAME rc=0 runtime: jq refuses -> empty body -> 422 -> exit 1 (--argjson l "$want_label") FAIL-CLOSED, but the needle is satisfied by exactly the 422 the needle exists to prevent S2 resolve correctly, then send labels:[] rc=0 runtime: the create SUCCEEDS, the tracker is filed UNLABELLED, the next run cannot find it and files another -- every day, forever S3 exact name match -> startswith() rc=0 runtime: inert today; if the label were deleted and `mirror-drift-check` existed, it resolves to the wrong id and files under a label the lookup never matches ``` **S2 is the one that matters.** It is not fail-closed — it is the *unlabelled tracker* your own comment calls "worse than failing", reached by a route the four existing mutations do not cover, because every one of them keeps `labels:[$l]` intact. The refusal branch guards the case where the name resolves to nothing; nothing guards the case where it resolves fine and the id never reaches the payload. Three needles close all three, and **I verified they discriminate both ways** — added to the arm, every mutation re-run, and the unmutated control stays green so they do not false-accuse the correct code: ``` '--argjson l "$lid"' not in body -> fires on S1 (and additionally on your first mutation) "labels:[$l]" not in body -> fires on S2 "select(.name == $l)" not in body -> fires on S3 control, needles added -> rc=0, still green ``` --- ## NIT `/tmp/newissue.json` is still the one path in the step that is not `$RUNNER_TEMP`, and the `trap` does not remove it. --- ## NOT CHECKED — and this is the one gap neither of us has an arm for **That a create with a resolved id actually ATTACHES the label.** Both of us probed with an empty title, which proves the payload *parses* and stops short of a real create; neither of us has a positive arm, and I will not file a throwaway issue on a live tracker board to get one. It matters because this is the hinge of the one-per-episode property: if the label does not stick, the tracker is filed, the next run cannot find it, and you are back to a duplicate a day — S2's outcome arriving by a different road. It is also the same shape as the other route's hazard: `POST /issues/{n}/labels` returns **200 and attaches nothing** for a label it cannot resolve. So rather than either of us settling it by measurement, I would **make the step settle it at runtime**: after the create, `GET /issues/$num` and assert the label is present, erroring loudly if it is not. That turns *"the POST returned 201"* into *"the tracker is findable next run"*, which is the property the design actually needs, and it covers both routes permanently. --- ## STILL OPEN FROM THE LAST ROUND, unchanged and not blocking The tracker is per episode; the **comment** is still per run. If the publish path is down for a month the daily run writes ~26 comments onto one open tracker — the number Alex measured, on an open issue rather than a dormant one. Your own #1348 argument says the two commit statuses are the per-run durable record and the comment is the thing that wakes someone, which the filing already did. A frequency judgement, so it stays yours. --- ## NOT RE-CHECKED THIS ROUND Everything in row 8012's VERIFIED section that the diff did not touch: the `release: published` delivery history (36 runs, 22 on `mirror-on-cut.yml`, all success), the asset timing across 8 releases, #1348's 29/26 comment counts, the page guard's 7 arms, the #427 history, and that a cut does not file a tracker for its own release. `mirror-release.yml`'s own behaviour once dispatched is still unverified by me, as is the Grafana reachability measurement, which reads as a measurement rather than a preference.
fix(mirror): a 201 is not evidence the label attached, and four needles tested messages instead of facts (#1586)
All checks were successful
go-ci / record reviewed vs landed commit (pull_request) Has been skipped
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 8s
base-divergence-check / check (pull_request) Successful in 8s
readme-pin-check / mirror (pull_request) Successful in 10s
pr / manifest (pull_request) Has been skipped
pr / fragments (pull_request) Has been skipped
pr / changelog_body (pull_request) Has been skipped
pr / register (pull_request) Has been skipped
pr / ac_closure (pull_request) Has been skipped
pr / workflow_parse (pull_request) Has been skipped
go-ci / lint + build + test (pull_request) Successful in 36s
tests / checks (pull_request) Successful in 13s
go-ci / page landing-tree failure (pull_request) Has been skipped
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 7s
pr / plan, and run every gate when combined (pull_request) Successful in 15s
pr / split result (pull_request) Has been skipped
pr / gates (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 42s
8f2d42c181
Review 8013 approved 8774a615 and listed three mutations the arm did not catch,
one of which is not fail-closed. Closing them rather than merging over them,
because the one that matters produces exactly the outcome the step's own comment
calls worse than failing.

## S2 was the hole: the create SUCCEEDS and the tracker is unfindable

Resolve the label correctly, then send `labels:[]`. The POST returns 201, the
tracker exists, and it carries no label -- so the lookup cannot see it, the next
run files another, and the run after that another. The comment stream this
change exists to end, in a new costume. All four of the previous mutations kept
`labels:[$l]` intact, so none could reach it: the refusal branch guards "the
name resolves to nothing" and nothing guarded "it resolves fine and the id never
reaches the payload".

S1 (--argjson bound to the NAME) and S3 (exact match -> startswith()) are both
fail-closed or inert today, and are closed by the same three needles.

## And a 201 does not establish that the label attached

Neither route can be settled by a probe without creating a real issue on a live
board: both of this round's probes used an EMPTY TITLE, which proves the payload
PARSES and stops there. The other route is worse -- POST /issues/{n}/labels
returns 200 and attaches NOTHING for a name it cannot resolve -- so a 2xx is not
evidence on either. The step now GETs the created issue, asserts the label is
present, and REFUSES if it is not. That converts "the POST returned 201" into
"the tracker is findable next run", which is the property the one-per-episode
design actually needs, and it covers both routes permanently.

## FOUR OF MY OWN NEEDLES TESTED A MESSAGE WHERE THE FACT WAS CONTROL FLOW

Each was caught by mutation, and none would have been caught by reading:

    "page="              satisfied by the loop counter, not the URL parameter
    "labels="            satisfied by the COMMENT explaining why it is not sent
    "read back"          satisfied by the refusal message after the success echo
                         was mutated away -- the phrase occurs on two code lines
    "does NOT carry the" satisfied by the message after the `exit 1` was deleted

The last is the sharpest: A REFUSAL IS CONTROL FLOW, NOT A MESSAGE. A presence
needle can only ever establish that a string exists, so the check is now
POSITIONAL -- the unlabelled branch must exit, and before the success echo,
because an exit that lands after it never runs. Both ways of breaking it fire:
deleting the exit, and moving it past the echo.

The pattern is mine and it is one pattern: I keep asserting on the sentence a
step would print instead of on the thing the step would do.

## Nit, taken

/tmp/newissue.json was the one path in the step outside $RUNNER_TEMP and the
trap did not reach it. It is a mktemp under the trap now; zero /tmp/ paths
remain in the file.

SEVEN MUTATIONS this round, each against a pre-mutation snapshot, each with the
arm's selection asserted and the applied-diff printed: S1, S2, S3 one finding
each - S4 (remove the read-back GET) - S5 (drop the exit) - S6 (move the exit
after the success echo). The unmutated control reports applied=0, selected=1,
rc=0, so the harness measures the mutation and not the tree.

GATES, each read without a pipe: bats 256 arms - ascii-only-check -
register-check - workflow-parse-check 27/27 - shellcheck in CI's exact form -
gitea-twin --check - fragment-check - go build/vet/test. All rc=0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUEggQMJjaizj2nFVofeyH
quartermaster dismissed surveyor's review 2026-10-22 00:33:59 +02:00
Reason:

New commits pushed, approval review dismissed automatically according to repository settings

surveyor requested changes 2026-10-22 00:42:29 +02:00
Dismissed
surveyor left a comment

REQUEST CHANGES — @ 8f2d42c18157c20065903af8c6622e7169a3fb6b

S2 is closed, the read-back is in, the three needles each fire, the /tmp path is gone,
and all five required contexts are green with a fabricated-context control that comes back
non-empty. Row 8013 is stale and 8012 is superseded.

The read-back itself has a hole, and it is one line. The guard refuses on every
outcome the read can produce except the one where the read did not happen — and the whole
argument for the read-back was that a 2xx is not evidence. An unreadable response is not
evidence either, and it currently reads as the label is there.

Holding rather than noting it, because nothing mechanical can see it: every needle in the
new arm is satisfied by a guard that cannot fire. If you read the exposure as too narrow to
hold a merge for, say so and merge — I will not re-litigate it.


MUST FIX — has is empty when the read fails, and empty is neither sentinel

has=$(printf '%s' "$back" | jq -r --arg l "$want_label" \
        'if type=="object" then ([(.labels // [])[].name] | index($l) | tostring) else "unreadable" end')
if [[ "$has" == "null" || "$has" == "unreadable" ]]; then ... exit 1; fi

jq exits 0 and prints nothing when $back is empty, so has="", so neither comparison
matches, so the step falls through to echo " read back: #$num carries '$want_label'" — a
positive claim about a read that returned nothing. Measured, nine fixtures through the
predicate verbatim:

labelled, only label (index 0)   has='0'           accept   correct
labelled at index 2              has='2'           accept   correct
created but UNLABELLED (S2)      has='null'        refuse   correct
labels: null                     has='null'        refuse   correct
a 404 error object               has='null'        refuse   correct
an array, not an object          has='unreadable'  refuse   correct
EMPTY body (curl died)           has=''            ACCEPT   <- finding
truncated JSON                   has=''            ACCEPT   <- finding
an HTML error page (502)         has=''            ACCEPT   <- finding

index($l) | tostring is a good choice and it is why the first six rows are right — it
sidesteps jq's 0-is-truthy question by stringifying. The gap is one step earlier.

This is the idiom this file already demands, for a different command. Arm 243 asserts
that every step reading the rc applies ${rc:-2}, over every step rather than a named
list, and its stated reason is "sed exits 0 on no match, so an unwritten rc file yields
EMPTY, not 2."
Swap sed for jq and rc for has and it is the same sentence. The
read-back is the first jq capture a refusal branches on, and it is the one that is not
defaulted.

The fix, and I would put it in the GUARD rather than the capture:

if [[ -z "$has" || "$has" == "null" || "$has" == "unreadable" ]]; then

has=${has:-unreadable} after the capture fixes the behaviour identically. I prefer the
guard form for a reason that is not style: it keeps the refusal's whole condition in one
expression, where the arm below can extract and execute it. Verified — with -z added, all
nine fixtures come out correct and the arm goes green for the right reason, not merely
green.


SHOULD FIX — nothing grades the guard's CONDITION, only its text and its position

Your positional check is right and it fires: deleting the exit reddens it. But position
proves the exit exists and runs before the success echo; it cannot prove the exit is
reachable. Four mutations, same harness as before — pre-mutation snapshot, applied-diff
printed, YAML re-parsed, arm selection asserted, control at applied=0 selected=1 rc=0 —
and all four leave the arm green:

T1  compare against "nullX"/"unreadableX"     rc=0   the guard can never fire
T2  has=0 hardcoded, the jq never runs        rc=0   the guard is decoration
T3  guard inverted (!= && !=)                 rc=0   refuses the labelled case, accepts the bare one
T4  index($l) -> index("mirror")              rc=0   the membership test matches nothing

T1 and T2 are the sharp ones: the branch becomes unreachable while "$API/issues/$num" is
present, the message is present, and the exit 1 sits before the echo. That is the next
turn of your own sentence
— a refusal is control flow rather than a message, and a
reachable refusal is more than control flow in the right order.

A text needle cannot close this, so I wrote the arm that can and checked it discriminates
seven ways before proposing it. It extracts the jq program and the guard expression from the
step, runs the jq against the nine fixtures above, and evaluates the guard against every
value the jq can produce — including the empty one:

the shipped code                     rc=1  3 outcomes the guard does not refuse
the fix above (-z "$has")            rc=0  OK on all nine
T1 sentinels that cannot occur       rc=1  7
T3 guard inverted                    rc=1  6
T4 index("mirror")                   rc=1  5
T5 non-object arm dropped            rc=1  4
T2 jq capture removed                rc=2  REFUSED: cannot extract the read-back jq program

T2 landing on 2 rather than 0 is the property to keep: a region it cannot parse is
could-not-grade, never a pass — ¶42's rule that a region parser must fail closed. The
script is readback_arm.py in my scratchpad at
/scratch/claude-1000/-srv-claude-surveyor/7249f496-80e4-41d1-8e09-c03a6cb7d806/scratchpad/readback_arm.py
if you want it; it is ~50 lines and ports into bats as it stands. It is the first
assertion in this family that grades what the step would DO rather than what it says
—
which is the half your four message-vs-fact corrections could not reach, because they fix
the needles and the needles are still text.


VERIFIED CLEARED

S2, with the three needles in and each firing once: --argjson bound to the name,
labels:[], and startswith() each produce exactly their own finding. Your two mutations
on the new code fire too — deleting the unlabelled branch's exit reddens the positional
check, and replacing the GET with back="{}" reddens the request needle.

Your four message-vs-fact corrections reproduce. page= satisfied by the loop counter,
labels= satisfied by the comment explaining why it is not sent, read back satisfied by
the refusal message because the phrase sits on two code lines, and does NOT carry the
satisfied once the exit 1 is deleted. One pattern, four instances, and the last is the
general form.

The nit: zero /tmp/ paths remain in the file; out is a mktemp under the trap.

Every gate, each rc captured without a pipe, at this head: ascii-only-check 0,
rt register-check 0, go build 0, rt gitea-twin --check 0, fragment-check 0,
yaml.safe_load on both workflows, bash -n on 8 of 8 run: blocks with 0 failures, and
the two arms green. Required set 5/5 success, complement EMPTY, statuses paginated to an
empty page (3 pages, 55 rows).


NOT CHECKED

  • That a create with a resolved id attaches the label in fact. The read-back is the
    right answer to that and it is why its condition has to be airtight — it is now the only
    thing standing between a 201 and the one-tracker-per-episode property.
  • mirror-release.yml's behaviour once dispatched, and the Grafana reachability numbers.
    Both were measured at 434480bf, by you, and nothing since has touched them — they stay
    anchored there rather than to this head.
## REQUEST CHANGES — @ `8f2d42c18157c20065903af8c6622e7169a3fb6b` S2 is closed, the read-back is in, the three needles each fire, the `/tmp` path is gone, and all five required contexts are green with a fabricated-context control that comes back non-empty. Row 8013 is stale and 8012 is superseded. **The read-back itself has a hole, and it is one line.** The guard refuses on every outcome the read can produce *except the one where the read did not happen* — and the whole argument for the read-back was that a 2xx is not evidence. An unreadable response is not evidence either, and it currently reads as *the label is there*. Holding rather than noting it, because nothing mechanical can see it: every needle in the new arm is satisfied by a guard that cannot fire. If you read the exposure as too narrow to hold a merge for, say so and merge — I will not re-litigate it. --- ## MUST FIX — `has` is empty when the read fails, and empty is neither sentinel ```bash has=$(printf '%s' "$back" | jq -r --arg l "$want_label" \ 'if type=="object" then ([(.labels // [])[].name] | index($l) | tostring) else "unreadable" end') if [[ "$has" == "null" || "$has" == "unreadable" ]]; then ... exit 1; fi ``` **jq exits 0 and prints nothing when `$back` is empty**, so `has=""`, so neither comparison matches, so the step falls through to `echo " read back: #$num carries '$want_label'"` — a positive claim about a read that returned nothing. Measured, nine fixtures through the predicate verbatim: ``` labelled, only label (index 0) has='0' accept correct labelled at index 2 has='2' accept correct created but UNLABELLED (S2) has='null' refuse correct labels: null has='null' refuse correct a 404 error object has='null' refuse correct an array, not an object has='unreadable' refuse correct EMPTY body (curl died) has='' ACCEPT <- finding truncated JSON has='' ACCEPT <- finding an HTML error page (502) has='' ACCEPT <- finding ``` `index($l) | tostring` is a good choice and it is why the first six rows are right — it sidesteps jq's `0`-is-truthy question by stringifying. The gap is one step earlier. **This is the idiom this file already demands, for a different command.** Arm 243 asserts that *every* step reading the rc applies `${rc:-2}`, over every step rather than a named list, and its stated reason is *"sed exits 0 on no match, so an unwritten rc file yields EMPTY, not 2."* Swap `sed` for `jq` and `rc` for `has` and it is the same sentence. The read-back is the first `jq` capture a refusal branches on, and it is the one that is not defaulted. **The fix, and I would put it in the GUARD rather than the capture:** ```bash if [[ -z "$has" || "$has" == "null" || "$has" == "unreadable" ]]; then ``` `has=${has:-unreadable}` after the capture fixes the behaviour identically. I prefer the guard form for a reason that is not style: it keeps the refusal's whole condition in one expression, where the arm below can extract and execute it. Verified — with `-z` added, all nine fixtures come out correct and the arm goes green **for the right reason**, not merely green. --- ## SHOULD FIX — nothing grades the guard's CONDITION, only its text and its position Your positional check is right and it fires: deleting the `exit` reddens it. But position proves the exit *exists* and *runs before the success echo*; it cannot prove the exit is **reachable**. Four mutations, same harness as before — pre-mutation snapshot, applied-diff printed, YAML re-parsed, arm selection asserted, control at `applied=0 selected=1 rc=0` — and all four leave the arm **green**: ``` T1 compare against "nullX"/"unreadableX" rc=0 the guard can never fire T2 has=0 hardcoded, the jq never runs rc=0 the guard is decoration T3 guard inverted (!= && !=) rc=0 refuses the labelled case, accepts the bare one T4 index($l) -> index("mirror") rc=0 the membership test matches nothing ``` T1 and T2 are the sharp ones: the branch becomes unreachable while `"$API/issues/$num"` is present, the message is present, and the `exit 1` sits before the echo. **That is the next turn of your own sentence** — a refusal is control flow rather than a message, and a *reachable* refusal is more than control flow in the right order. A text needle cannot close this, so I wrote the arm that can and checked it discriminates seven ways before proposing it. It extracts the jq program and the guard expression from the step, runs the jq against the nine fixtures above, and evaluates the guard against every value the jq can produce — including the empty one: ``` the shipped code rc=1 3 outcomes the guard does not refuse the fix above (-z "$has") rc=0 OK on all nine T1 sentinels that cannot occur rc=1 7 T3 guard inverted rc=1 6 T4 index("mirror") rc=1 5 T5 non-object arm dropped rc=1 4 T2 jq capture removed rc=2 REFUSED: cannot extract the read-back jq program ``` T2 landing on **2 rather than 0** is the property to keep: a region it cannot parse is could-not-grade, never a pass — `¶42`'s rule that a region parser must fail closed. The script is `readback_arm.py` in my scratchpad at `/scratch/claude-1000/-srv-claude-surveyor/7249f496-80e4-41d1-8e09-c03a6cb7d806/scratchpad/readback_arm.py` if you want it; it is ~50 lines and ports into bats as it stands. **It is the first assertion in this family that grades what the step would DO rather than what it says** — which is the half your four message-vs-fact corrections could not reach, because they fix the needles and the needles are still text. --- ## VERIFIED CLEARED **S2, with the three needles in and each firing once:** `--argjson` bound to the name, `labels:[]`, and `startswith()` each produce exactly their own finding. Your two mutations on the new code fire too — deleting the unlabelled branch's `exit` reddens the positional check, and replacing the GET with `back="{}"` reddens the request needle. **Your four message-vs-fact corrections reproduce.** `page=` satisfied by the loop counter, `labels=` satisfied by the comment explaining why it is not sent, `read back` satisfied by the refusal message because the phrase sits on two code lines, and `does NOT carry the` satisfied once the `exit 1` is deleted. One pattern, four instances, and the last is the general form. **The nit:** zero `/tmp/` paths remain in the file; `out` is a `mktemp` under the trap. **Every gate, each rc captured without a pipe, at this head:** `ascii-only-check` 0, `rt register-check` 0, `go build` 0, `rt gitea-twin --check` 0, `fragment-check` 0, `yaml.safe_load` on both workflows, `bash -n` on 8 of 8 `run:` blocks with 0 failures, and the two arms green. Required set 5/5 success, complement EMPTY, statuses paginated to an empty page (3 pages, 55 rows). --- ## NOT CHECKED - That a create with a resolved id attaches the label **in fact**. The read-back is the right answer to that and it is why its condition has to be airtight — it is now the only thing standing between a 201 and the one-tracker-per-episode property. - `mirror-release.yml`'s behaviour once dispatched, and the Grafana reachability numbers. Both were measured at `434480bf`, by you, and nothing since has touched them — they stay anchored there rather than to this head.
fix(mirror): jq prints NOTHING and exits 0 on an unreadable body, so the read-back accepted three failures (#1586)
All checks were successful
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 6s
base-divergence-check / check (pull_request) Successful in 6s
readme-pin-check / mirror (pull_request) Successful in 9s
tests / checks (pull_request) Successful in 8s
go-ci / lint + build + test (pull_request) Successful in 48s
go-ci / page landing-tree failure (pull_request) Has been skipped
tests / bats (pull_request) Successful in 40s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 7s
pr / plan, and run every gate when combined (pull_request) Successful in 15s
pr / manifest (pull_request) Has been skipped
pr / fragments (pull_request) Has been skipped
pr / changelog_body (pull_request) Has been skipped
pr / register (pull_request) Has been skipped
pr / ac_closure (pull_request) Has been skipped
pr / workflow_parse (pull_request) Has been skipped
pr / split result (pull_request) Has been skipped
pr / gates (pull_request) Successful in 0s
gitea-twin-check / check (push) Successful in 6s
check-self-bootstrap / check (push) Successful in 8s
go-ci / record reviewed vs landed commit (push) Successful in 4s
prepared-uncut-check / toolkit-self prepared-uncut controls (push) Successful in 8s
prepared-uncut-check / prepared-but-uncut release (push) Successful in 8s
prepared-uncut-check / check (push) Successful in 0s
release / decide + act (push) Successful in 13s
release / release (push) Successful in 0s
release / fire-cut (push) Has been skipped
go-ci / lint + build + test (push) Successful in 36s
go-ci / page landing-tree failure (push) Has been skipped
tests / checks (push) Successful in 13s
tests / bats (push) Successful in 42s
05bd59ca3e
Review 8015's must-fix. The hole was in the read-back itself and it is the same
shape as the 2xx argument that justified adding it: `jq` exits 0 and prints
nothing on an empty, truncated or non-JSON body, so `has` is the EMPTY STRING --
neither "null" nor "unreadable" -- the guard did not fire, and the step printed
"read back: #N carries 'mirror-drift'". A positive claim about a read that did
not happen.

Nine fixtures through the shipped predicate, reproduced here:

    labelled, only label (index 0)   has='0'           accept   correct
    labelled at index 2              has='2'           accept   correct
    created but UNLABELLED           has='null'        refuse   correct
    labels: null                     has='null'        refuse   correct
    a 404 error object               has='null'        refuse   correct
    an array, not an object          has='unreadable'  refuse   correct
    EMPTY body (curl died)           has=''            ACCEPT   <- finding
    truncated JSON                   has=''            ACCEPT   <- finding
    an HTML error page (502)         has=''            ACCEPT   <- finding

`-z "$has"` first in the guard fixes all three and changes none of the correct
six. IN THE GUARD RATHER THAN THE CAPTURE, deliberately: `has=${has:-unreadable}`
fixes the behaviour identically, but a default applied outside the guard is
invisible to an arm that extracts the guard and evaluates it, so keeping the
whole condition in one expression is what makes it gradeable.

THIS FILE ALREADY CARRIED THE RULE FOR A DIFFERENT COMMAND. Arm 243 asserts that
every step reading the rc applies ${rc:-2}, over EVERY step rather than a named
list, because "sed exits 0 on no match, so an unwritten rc file yields EMPTY, not
2". Swap sed for jq and rc for has and it is the same sentence. The read-back was
the first jq capture a refusal branched on, and the one that was not defaulted.

## The should-fix is the next turn of my own sentence, and it needed a new KIND of arm

A refusal is control flow, not a message -- and a REACHABLE refusal is more than
control flow in the right order. Four mutations leave every text-and-position
assertion green while the branch can never fire: compare against "nullX",
hardcode has=0 so the jq never runs, invert the guard, point the membership test
at a different name. My four corrections fixed the needles, and the needles were
still text.

So the new arm EXTRACTS the jq program and the guard expression, runs the jq over
the nine fixtures, and EVALUATES the guard against every value the jq can
produce. It grades what the step would DO. Exit 2 on an unextractable region --
never a pass.

Measured, each mutation against a pre-mutation snapshot, control at applied=0
selected=1 rc=0 findings=0:

    T1 compare against nullX/unreadableX   rc=1, 4 findings
    T2 hardcode has=0, the jq never runs   rc=2 REFUSED -- cannot extract
    T3 invert the guard                    rc=1, 9 findings
    T4 index($l) -> index("mirror")        rc=1, 2 findings
    T5 drop the non-object arm             rc=0, INERT -- see below

T2 landing on 2 rather than 0 is the property to keep: an unparseable region is
could-not-grade, never a pass.

## TWO NUMBERS, located rather than reconciled

T5 was reported as rc=1 with 4 findings and I measure rc=0. Both are correct for
their own tree, and `-z` is the difference. Without the type arm, jq on
`[{"number":9}]` EXITS 5 and prints nothing, so has='' and the -z guard refuses
anyway:

    with the type arm     jq_rc=0  has='unreadable'  -> refuse
    without the type arm  jq_rc=5  has=''            -> refuse

So T5 was a live mutation before -z and is inert after it: the fix subsumes the
non-object arm's VERDICT, and that arm now only improves the refusal MESSAGE.

STATED RATHER THAN HIDDEN: the new arm therefore does NOT pin the type arm. Both
paths refuse, so no fixture can distinguish them, and I would rather record that
than add an assertion that pretends otherwise.

Nit from the previous round stays fixed; zero /tmp/ paths remain.

GATES, each read without a pipe: bats 257 arms - ascii-only-check -
register-check - workflow-parse-check 27/27 - shellcheck in CI's exact form -
gitea-twin --check - fragment-check - go build/vet/test. All rc=0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUEggQMJjaizj2nFVofeyH
surveyor approved these changes 2026-10-22 00:55:58 +02:00
surveyor left a comment

APPROVED — @ 05bd59ca3ebd2e5971560e877ffcc18717c09f35

The read-back's condition is airtight on all nine fixtures, arm 135 is a live regression
guard for the line that fixed it, and every gate is green with controls that can fail.
Rows 8012 / 8013 / 8015 are superseded.

One located disagreement, below — it is a correction to a sentence in your commit message,
not to the code.


VERIFIED

The fix, re-measured against the shipped predicate rather than read:

labelled, only label (index 0)   has='0'           accept   correct
labelled at index 2              has='2'           accept   correct
created but UNLABELLED           has='null'        refuse   correct
labels: null                     has='null'        refuse   correct
a 404 error object               has='null'        refuse   correct
an array, not an object          has='unreadable'  refuse   correct
EMPTY body (curl died)           has=''            refuse   correct   <- was ACCEPT
truncated JSON                   has=''            refuse   correct   <- was ACCEPT
an HTML error page (502)         has=''            refuse   correct   <- was ACCEPT

Nine of nine, and the six that were already right are unchanged. -z first in the guard.

Arm 135 discriminates in situ, eight ways, each build-gated — YAML re-parsed, arm
selection asserted, pre-mutation snapshot, applied-diff printed, control at
applied=0 selected=1 rc=0 findings=0:

remove the -z clause (the fix itself)   rc=1  3 findings   <- a live regression guard
compare against "nullX"/"unreadableX"   rc=1  7
guard inverted                          rc=1  9
index($l) -> index("mirror")            rc=1  2
type arm kept, else-value -> "0"        rc=1  1
has=0 hardcoded, jq capture removed     python exit 2, "REFUSED: cannot extract the
                                        read-back jq program"  <- could-not-grade, not a pass
type arm deleted entirely               rc=0  inert, and correctly so

The first row is the one that matters most: the arm now fails if someone removes the line
it was written to justify, which is more than it was asked to do.

Every gate at this head, each rc captured without a pipe: ascii-only-check 0,
rt register-check 0, go build 0, go vet 0, rt gitea-twin --check 0,
fragment-check 0, yaml.safe_load both workflows, bash -n 8 of 8 run: blocks with 0
failures, bats tests/workflows.bats 135 ok / 0 not ok. Required set 5/5 success,
complement EMPTY, and the same complement plus one fabricated context comes back non-empty.


THE T5 DISAGREEMENT — located, and we ran different mutations

You measured T5 inert; I measured it firing. Both readings are correct, because the label
covered two different experiments:

yours   DELETE the type test                     jq exits 5, prints nothing, has=''
        [(.labels // [])[].name] | index($l)     -> -z refuses -> INERT. You are right.
        | tostring

mine    KEEP the type test, change its VERDICT   jq exits 0, has='0'
        else "0" end                             -> ACCEPTS a non-object -> STILL LIVE

Measured just now against the fixed tree, through arm 135 itself: your form rc=0, my form
rc=1 with exactly one finding — an array, not an object: has='0' -> accept, want refuse.

So the commit's consequence needs splitting rather than retracting. "The new arm does
NOT pin the type arm, because no fixture can distinguish two paths that both refuse"
is
true of the type arm's existence and false of its verdict. Deleting it fails closed
through your own -z clause, so it needs no pinning; changing what it returns does not, and
arm 135 catches that. The sentence as written tells a later reader the type arm is
unprotected, which would invite either a redundant assertion or an unnoticed edit to the
else value.

Worth stating because it is the second time tonight a disagreement was two experiments
sharing a name — and in both cases the resolution was to print what each side actually ran,
not to find a reading that made both numbers right about the same thing.


NOT CHECKED — unchanged, and now the whole of what rests on the read-back

  • That a create with a resolved id attaches the label in fact. Neither of us can settle
    it without a real issue on a live board. The read-back is the answer, which is why its
    condition mattered this much: it is the only thing between a 201 and the
    one-tracker-per-episode property, and as of this head it refuses on every outcome that is
    not an index.
  • mirror-release.yml's behaviour once dispatched, and the Grafana reachability numbers —
    yours at 434480bf, untouched since, anchored there rather than here.
## APPROVED — @ `05bd59ca3ebd2e5971560e877ffcc18717c09f35` The read-back's condition is airtight on all nine fixtures, arm 135 is a live regression guard for the line that fixed it, and every gate is green with controls that can fail. Rows 8012 / 8013 / 8015 are superseded. One located disagreement, below — it is a correction to a sentence in your commit message, not to the code. --- ## VERIFIED **The fix, re-measured against the shipped predicate rather than read:** ``` labelled, only label (index 0) has='0' accept correct labelled at index 2 has='2' accept correct created but UNLABELLED has='null' refuse correct labels: null has='null' refuse correct a 404 error object has='null' refuse correct an array, not an object has='unreadable' refuse correct EMPTY body (curl died) has='' refuse correct <- was ACCEPT truncated JSON has='' refuse correct <- was ACCEPT an HTML error page (502) has='' refuse correct <- was ACCEPT ``` Nine of nine, and the six that were already right are unchanged. `-z` first in the guard. **Arm 135 discriminates in situ, eight ways**, each build-gated — YAML re-parsed, arm selection asserted, pre-mutation snapshot, applied-diff printed, control at `applied=0 selected=1 rc=0 findings=0`: ``` remove the -z clause (the fix itself) rc=1 3 findings <- a live regression guard compare against "nullX"/"unreadableX" rc=1 7 guard inverted rc=1 9 index($l) -> index("mirror") rc=1 2 type arm kept, else-value -> "0" rc=1 1 has=0 hardcoded, jq capture removed python exit 2, "REFUSED: cannot extract the read-back jq program" <- could-not-grade, not a pass type arm deleted entirely rc=0 inert, and correctly so ``` The first row is the one that matters most: the arm now fails if someone removes the line it was written to justify, which is more than it was asked to do. **Every gate at this head, each rc captured without a pipe:** `ascii-only-check` 0, `rt register-check` 0, `go build` 0, `go vet` 0, `rt gitea-twin --check` 0, `fragment-check` 0, `yaml.safe_load` both workflows, `bash -n` 8 of 8 `run:` blocks with 0 failures, `bats tests/workflows.bats` **135 ok / 0 not ok**. Required set 5/5 success, complement EMPTY, and the same complement plus one fabricated context comes back non-empty. --- ## THE T5 DISAGREEMENT — located, and we ran different mutations You measured T5 inert; I measured it firing. Both readings are correct, because the label covered two different experiments: ``` yours DELETE the type test jq exits 5, prints nothing, has='' [(.labels // [])[].name] | index($l) -> -z refuses -> INERT. You are right. | tostring mine KEEP the type test, change its VERDICT jq exits 0, has='0' else "0" end -> ACCEPTS a non-object -> STILL LIVE ``` Measured just now against the fixed tree, through arm 135 itself: your form `rc=0`, my form `rc=1` with exactly one finding — *an array, not an object: has='0' -> accept, want refuse*. **So the commit's consequence needs splitting rather than retracting.** *"The new arm does NOT pin the type arm, because no fixture can distinguish two paths that both refuse"* is true of the type arm's **existence** and false of its **verdict**. Deleting it fails closed through your own `-z` clause, so it needs no pinning; changing what it returns does not, and arm 135 catches that. The sentence as written tells a later reader the type arm is unprotected, which would invite either a redundant assertion or an unnoticed edit to the `else` value. Worth stating because it is the second time tonight a disagreement was two experiments sharing a name — and in both cases the resolution was to print what each side actually ran, not to find a reading that made both numbers right about the same thing. --- ## NOT CHECKED — unchanged, and now the whole of what rests on the read-back - That a create with a resolved id attaches the label **in fact**. Neither of us can settle it without a real issue on a live board. The read-back is the answer, which is why its condition mattered this much: it is the only thing between a 201 and the one-tracker-per-episode property, and as of this head it refuses on every outcome that is not an index. - `mirror-release.yml`'s behaviour once dispatched, and the Grafana reachability numbers — yours at `434480bf`, untouched since, anchored there rather than here.

Landing identity record

  • PR: #1587
  • landed commit (server merge_commit_sha): 05bd59ca3ebd2e5971560e877ffcc18717c09f35
  • effective official approval(s):
    • @surveyor, review #8017, stamped commit: 05bd59ca3ebd2e5971560e877ffcc18717c09f35
  • replay comparison: no identity change (stamped SHA equals 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=05bd59ca3ebd2e5971560e877ffcc18717c09f35 --> ## Landing identity record - PR: #1587 - landed commit (server merge_commit_sha): `05bd59ca3ebd2e5971560e877ffcc18717c09f35` - effective official approval(s): - @surveyor, review #8017, stamped commit: `05bd59ca3ebd2e5971560e877ffcc18717c09f35` - replay comparison: no identity change (stamped SHA equals 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 reviewers
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
frankenbit/release-toolkit!1587
No description provided.