diff --git a/.trinity/seals/claw_ClawCheckLatest.json b/.trinity/seals/claw_ClawCheckLatest.json new file mode 100644 index 0000000000..de787c43ae --- /dev/null +++ b/.trinity/seals/claw_ClawCheckLatest.json @@ -0,0 +1,41 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/claw/check_state.t27", + "spec_hash": "sha256:f5f05cce0d40d19693618461a81702761d27917cc6d1a3dfb53d8c06ba8b4ab4" + }, + { + "spec": "specs/claw/queue_cost.t27", + "spec_hash": "sha256:16789d92d878e85e3d02e56a9be4bffe0b565b5c14c270f79ddaedc6d6911f7e" + }, + { + "spec": "specs/claw/text.t27", + "spec_hash": "sha256:b65da69dff231c61a6d3a48558af9e17af42e1acbe3f4763348f1f0c2f118f21" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:28730ddb411cdbd8dab71a359b3198c5d9a604f801292681d99923aa29d83472", + "gen_hash_rust": "sha256:4859d66eed907385076ba205174f57e990c975f8cc5806db5b7ad4abfed807e0", + "gen_hash_verilog": "sha256:f9f5fd676b30d5ae414b77ed7139622591110b63f8bd819dc7381b6cd449ce8e", + "gen_hash_zig": "sha256:449674e0eaa92835b542ae7fe033667a048de932b3f260948fa9b52c93cd6679", + "module": "ClawCheckLatest", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T19:55:04Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:3195aefb8a0d3dc6cc0f2ba9e1ae064b5678446611cccf1bb80a8d8066c668b4", + "spec_path": "specs/claw/check_latest.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 11, + "total": 11, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/claw_ClawCheckState.json b/.trinity/seals/claw_ClawCheckState.json new file mode 100644 index 0000000000..b9cc7ef288 --- /dev/null +++ b/.trinity/seals/claw_ClawCheckState.json @@ -0,0 +1,37 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/claw/queue_cost.t27", + "spec_hash": "sha256:16789d92d878e85e3d02e56a9be4bffe0b565b5c14c270f79ddaedc6d6911f7e" + }, + { + "spec": "specs/claw/text.t27", + "spec_hash": "sha256:b65da69dff231c61a6d3a48558af9e17af42e1acbe3f4763348f1f0c2f118f21" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:a12ac6811d377dbe066936d7d83874b6500f8cdb1857f114657b1445842d0b49", + "gen_hash_rust": "sha256:bd33e233b20fd2a284adb6c1d4ec4f52a78c7e3dde773cbaffbf01c392e00919", + "gen_hash_verilog": "sha256:1aae7f4b971d6bb0c1797a7bedf54a461dd4f705efad6aa1cacb5db973ccd12d", + "gen_hash_zig": "sha256:84f004a0e9d83668dbd79f4a0eb7ec6c7f6ecd290a2f482faa27aced8b03037c", + "module": "ClawCheckState", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T19:55:01Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:f5f05cce0d40d19693618461a81702761d27917cc6d1a3dfb53d8c06ba8b4ab4", + "spec_path": "specs/claw/check_state.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 12, + "total": 12, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/claw_ClawEvidence.json b/.trinity/seals/claw_ClawEvidence.json new file mode 100644 index 0000000000..0305b2059b --- /dev/null +++ b/.trinity/seals/claw_ClawEvidence.json @@ -0,0 +1,33 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/claw/text.t27", + "spec_hash": "sha256:b65da69dff231c61a6d3a48558af9e17af42e1acbe3f4763348f1f0c2f118f21" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:bbfb20cf1fbbb543a8fe84d4ef9622fc0e87807da80333df3cbc676b7aa0eb19", + "gen_hash_rust": "sha256:8288152fa30d8dc5dfa969b69bee8bfec441abb3a45c7914d1c835d2e5699754", + "gen_hash_verilog": "sha256:4b00f9a86f368a483f6713203b4a708643b460f703c97a309cbc4fec238b922f", + "gen_hash_zig": "sha256:086ae5b1a55975b3a0d09f29d4ffbf6294f265fd304f4e45bc6aadc2a548754f", + "module": "ClawEvidence", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:53:51Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:4169daec6232a06a4ec3dceb3ebd4d49346ba02b784fb5c437bc1aab4fad344d", + "spec_path": "specs/claw/evidence.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 12, + "total": 12, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/claw_ClawEvidencePins.json b/.trinity/seals/claw_ClawEvidencePins.json new file mode 100644 index 0000000000..2e5aedc3b4 --- /dev/null +++ b/.trinity/seals/claw_ClawEvidencePins.json @@ -0,0 +1,37 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/claw/evidence.t27", + "spec_hash": "sha256:4169daec6232a06a4ec3dceb3ebd4d49346ba02b784fb5c437bc1aab4fad344d" + }, + { + "spec": "specs/t27f/spec_graph.t27", + "spec_hash": "sha256:66020d5b18fc1957c48b138f243e26bb0f961b279dcb2b0a29975f193aae1a73" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:eb792a5fc3d6d5fbd82174ccf34b6c50e83a07bea480e8cf016240e4b8f3360c", + "gen_hash_rust": "sha256:ee9032a1b3a2ff49b9def7c3d56d13fc21ada0f3c4171d4523e6bf9b8aa38e4a", + "gen_hash_verilog": "sha256:1eee39d4baa389027c073bbe90ab0fa226a69bad93f65acf34b35c32d65428cc", + "gen_hash_zig": "sha256:537b3938ec6202ecf7c34bfdff1782cb635410d9d57cf43c57576b729a8666b6", + "module": "ClawEvidencePins", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:53:54Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:955413d5b4aaa667aed654dfbaf2e81ba5d60d87d28ef568334a5d653c440601", + "spec_path": "specs/claw/evidence_pins.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/claw_ClawMergeLane.json b/.trinity/seals/claw_ClawMergeLane.json new file mode 100644 index 0000000000..b36782b45a --- /dev/null +++ b/.trinity/seals/claw_ClawMergeLane.json @@ -0,0 +1,37 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/claw/queue_cost.t27", + "spec_hash": "sha256:16789d92d878e85e3d02e56a9be4bffe0b565b5c14c270f79ddaedc6d6911f7e" + }, + { + "spec": "specs/claw/text.t27", + "spec_hash": "sha256:b65da69dff231c61a6d3a48558af9e17af42e1acbe3f4763348f1f0c2f118f21" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:2d4e93a910650e9b46d12fcc739be63ad7a240b8c13f835415bcf1ad313645f0", + "gen_hash_rust": "sha256:ab91d88ed9f58d800b1d0b8c5d0ae94d21531c5858b04f2aeff59ec552f5e4c3", + "gen_hash_verilog": "sha256:c90355854c1fe3836be5b2433b04686c263199de040e73793116228a4fc69178", + "gen_hash_zig": "sha256:0d2296bd2fd606c4a215fca1758c9613786ab5865f9ae758288e01a96e0c15b3", + "module": "ClawMergeLane", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T20:55:41Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:91d755a5c6b463f115856e61765ac8c7915f72ab988b53fcc0241edec08765ca", + "spec_path": "specs/claw/merge_lane.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 23, + "total": 23, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/claw_ClawPushGate.json b/.trinity/seals/claw_ClawPushGate.json new file mode 100644 index 0000000000..9d4b2355b6 --- /dev/null +++ b/.trinity/seals/claw_ClawPushGate.json @@ -0,0 +1,37 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/claw/refresh.t27", + "spec_hash": "sha256:9cbda0935680c5bf4dd24d9b68931b99d5e184339b3438399a647710eebb3cef" + }, + { + "spec": "specs/claw/text.t27", + "spec_hash": "sha256:b65da69dff231c61a6d3a48558af9e17af42e1acbe3f4763348f1f0c2f118f21" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:0e36ad6408ba73e3b2db496f6cc417b0c1236e82ae40add734cf51612263ebf3", + "gen_hash_rust": "sha256:1eb6f86199f548d3b36099f89b6f91c4b6bccef17ce8a8f2b631462dc15c497c", + "gen_hash_verilog": "sha256:0ec11c9d0223665c89b150a8147411831d0c46e9baf251076370de4a7c6c22d1", + "gen_hash_zig": "sha256:3c10671b2b202f661735d742eb2f3e6ff2d695afd42c48f5ed543a8e7846d157", + "module": "ClawPushGate", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:49:41Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:0395e6a85e44f9dbe8d26766fa23693967135ea13946423bd514d82fa37240a6", + "spec_path": "specs/claw/push_gate.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 14, + "total": 14, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/claw_ClawQueueCost.json b/.trinity/seals/claw_ClawQueueCost.json new file mode 100644 index 0000000000..067c49fad0 --- /dev/null +++ b/.trinity/seals/claw_ClawQueueCost.json @@ -0,0 +1,33 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/claw/text.t27", + "spec_hash": "sha256:b65da69dff231c61a6d3a48558af9e17af42e1acbe3f4763348f1f0c2f118f21" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:b2e8310b04602772ae4c26f782651814781f800ad546dda08aad328eae380a7d", + "gen_hash_rust": "sha256:2139d76ca0e026b76e2d306c1fb4ace20241826dc9faa24b78c85672732afdf3", + "gen_hash_verilog": "sha256:d444d186653d294157ee175cafc704c5dbae2039d327fe5861bb372f67ec2cbd", + "gen_hash_zig": "sha256:23901c784d363d5d67ac4c8d2ed4e8e864fc7dd984a4aa49633222e5b66a4eb4", + "module": "ClawQueueCost", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T19:42:07Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:16789d92d878e85e3d02e56a9be4bffe0b565b5c14c270f79ddaedc6d6911f7e", + "spec_path": "specs/claw/queue_cost.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 13, + "total": 13, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/claw_ClawRequiredRed.json b/.trinity/seals/claw_ClawRequiredRed.json new file mode 100644 index 0000000000..4954d22210 --- /dev/null +++ b/.trinity/seals/claw_ClawRequiredRed.json @@ -0,0 +1,45 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/claw/check_latest.t27", + "spec_hash": "sha256:3195aefb8a0d3dc6cc0f2ba9e1ae064b5678446611cccf1bb80a8d8066c668b4" + }, + { + "spec": "specs/claw/check_state.t27", + "spec_hash": "sha256:f5f05cce0d40d19693618461a81702761d27917cc6d1a3dfb53d8c06ba8b4ab4" + }, + { + "spec": "specs/claw/queue_cost.t27", + "spec_hash": "sha256:16789d92d878e85e3d02e56a9be4bffe0b565b5c14c270f79ddaedc6d6911f7e" + }, + { + "spec": "specs/claw/text.t27", + "spec_hash": "sha256:b65da69dff231c61a6d3a48558af9e17af42e1acbe3f4763348f1f0c2f118f21" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:c717b993844f5ea9e505638b7dfae7c8e471b554d6b2a210ab05576aa54ed8f3", + "gen_hash_rust": "sha256:70d7923ef8e74fc126f6316f1fe4937e39c40dbe2fbd9e9e2561c71cda0c194a", + "gen_hash_verilog": "sha256:9fbeec197b7b09796269f91e84dd050683379b0e82cc9215afb1bdf4b128134c", + "gen_hash_zig": "sha256:fa1b7c3383c64c7c111598dc9dca071f2208c152fd51a55c64733d422d2b83c3", + "module": "ClawRequiredRed", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T20:08:50Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:ad8660400d420444a1a3c8858866b4cf7447f60027e27e1bb333a9463e1decea", + "spec_path": "specs/claw/required_red.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 9, + "total": 9, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/claw_ClawSuggestFeed.json b/.trinity/seals/claw_ClawSuggestFeed.json new file mode 100644 index 0000000000..c6ae419889 --- /dev/null +++ b/.trinity/seals/claw_ClawSuggestFeed.json @@ -0,0 +1,41 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/claw/suggest.t27", + "spec_hash": "sha256:43afee144a92a59e27539c51a344ae9e03d88c14e3c5ab3d9f24cddedab6c325" + }, + { + "spec": "specs/claw/text.t27", + "spec_hash": "sha256:b65da69dff231c61a6d3a48558af9e17af42e1acbe3f4763348f1f0c2f118f21" + }, + { + "spec": "specs/tri/fmt_source.t27", + "spec_hash": "sha256:14d8989621ffb0ad2e92e89686bcd6bad462f1341b436a22e976589c66d79d7d" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:e7d532aa35fd41b2c8d4790e155f066518dfbbbe2bc5cd50629d6fd26cab07ad", + "gen_hash_rust": "sha256:7560c931661dab40040b65ed0f62a5cf2bd4dde32b1f6e67effc49ade0669818", + "gen_hash_verilog": "sha256:b0f0b1ba70d52f92446d66525fee99c6a4dd78112344893fc295642354b4235d", + "gen_hash_zig": "sha256:44281c0b2d257b58fcb46becb5adcff4c1cc77c929f3c313a8f49f374d2fc212", + "module": "ClawSuggestFeed", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:53:54Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:27121a2d0dad7d0b597fda267aace1ae09ae3d896e4cb6bf59ba5ad1af1733b3", + "spec_path": "specs/claw/suggest_feed.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 19, + "total": 19, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/claw/README.md b/docs/claw/README.md index 989fe8ff50..01aa558d7b 100644 --- a/docs/claw/README.md +++ b/docs/claw/README.md @@ -16,6 +16,12 @@ behind one front door, with the gate logic in `.t27` specs. Epic | SARIF code-scanning alerts on a pull request | `specs/claw/sarif_feed.t27` over `specs/claw/sarif.t27`, the `claw-sarif` job of `gate-topology.yml` | #10356, live once the owner steps below are done | | Suggestion review on a pull request | `specs/claw/suggest_feed.t27` over `specs/claw/suggest.t27` and `specs/tri/fmt_source.t27`, the `claw-suggest` job of `gate-topology.yml` | #10365, live once the owner steps below are done | | Service skeleton | Railway, thin I/O glue only | T5, merged (spec contract) | +| Queue cost: which workflow holds the runners, and what the queued runs wait for | `specs/claw/queue_cost.t27` (job rows and queued-run rows in, two summary lines out; not wired to a job yet) | #10600 | +| Push gate: whether a lane push helps the PR or only restarts its required checks | `specs/claw/push_gate.t27` (push kind and three flags in, one `V` line out: push, hold or skip; not wired to a job yet) | #10606 | +| Check state: whether a PR's checks are red, pending, none or green, with a queued check read as pending and a commit status read by its state | `specs/claw/check_state.t27` (rollup rows in, a count line and one `V` line out; keeps the PR Dashboard's current per-PR rule as a named function; not wired to a job yet) | #10617 | +| Check latest: a re-run supersedes the run it replaces, so an old failure next to its queued re-run reads pending, not red | `specs/claw/check_latest.t27` (rollup rows with name and run id in; per name the largest run id is live, classed by check_state; three lines out; not wired to a job yet) | #10617 | +| Required red: push_gate's req_red flag from the rollup, newest run per required check, so a red check that is not required never triggers a fix push | `specs/claw/required_red.t27` (check_latest rows in; an R count line and an `F req_red true/false/unknown` line out; not wired to a job yet) | #10606 | +| Merge lane: is the merger starved, and does it merge anything? Counts its runs, the oldest open age, the PRs each run merged, and its share of the whole queue, so an hour-long wait, a zero yield or a tenth of the queue shows up as data | `specs/claw/merge_lane.t27` (rows per merger run or per queued run; `F merger`, `F merger-yield` and `F merger-share low/high/unknown/none` out) | #10644 | | Model routing: Opus 5.5 orchestrates, Sonnet 5.5 runs sub-tasks | `specs/claw/model_route.t27`, skill `model-routing`, `.claude/settings.json` + `.claude/agents/*` | owner directive 2026-10-18 | | Positioning / pricing | `docs/claw/competitors.md` | T6, started | diff --git a/docs/claw/competitors.md b/docs/claw/competitors.md index 9693304b87..1a8bb5acb8 100644 --- a/docs/claw/competitors.md +++ b/docs/claw/competitors.md @@ -128,3 +128,31 @@ VibeCheck (arXiv 2609.05978); Tessl $125M (Maginative); GitHub API stars and pushed_at for infer, kani, creusot, prusti-dev, gobra, keploy, hypothesis, fast-check, spec-kit (2026-10-12). Full research transcript in the loop ledger, ~/t27-lanes/claw/STATE.md. + +## Addendum 2026-10-18: who decides when a PR's checks run again + +The lane's PRs wait on required checks in a queue of 800 to 900 runs, and every +push to a head restarts them (#10590, #10600, #10606). The tools that land PRs +for other teams take one of three answers to that cost: + +- **GitHub merge queue.** Required checks run on the `merge_group` event, + against the base branch plus the PRs ahead in the queue. Authors never update + their branch by hand. "Build concurrency" caps how many groups are built at + once (1 to 100), and "merge limits" set the group size. t27 has none of this + yet: the `t27-master-protection` ruleset has no merge-queue rule, and none of + the 67 workflows triggers on `merge_group`. +- **Kodiak.** With `update.always` (default `false`), it updates every + out-of-date PR that carries the automerge label. Its own docs warn this costs + at least N(N-1)/2 updates for N PRs, against N-1 when the option is off. +- **TRI CLAW.** `refresh.t27` updates only what the merger lands next, caps the + batch at 5, and stops above 150 queued runs. `push_gate.t27` decides each lane + push: it skips a push whose only effect would be to restart required checks. + The decision is a spec with tests, not a config flag. + +The lesson TRI CLAW takes: a merge queue moves the expensive checks from every +push to once per group. Whether to turn it on is a ruleset change for the owner, +and it pairs with #10193. The decision is #10607. + +Sources: GitHub Docs, "Managing a merge queue"; Kodiak config reference +(`update.always`, `update.require_automerge_label`), both read 2026-10-18; +`gh api repos/gHashTag/t27/rulesets` and a grep of `.github/workflows`, same day. diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 074f9e335a..1cb9bc9b5d 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -187,6 +187,8 @@ {"path": "specs/ci/gates/verilog_widths.t27", "t27b": "pass"}, {"path": "specs/ci/schema.t27", "t27b": "pass"}, {"path": "specs/ci/suite_ledger_steward.t27", "t27b": "pass"}, + {"path": "specs/claw/check_latest.t27", "t27b": "pass"}, + {"path": "specs/claw/check_state.t27", "t27b": "pass"}, {"path": "specs/claw/control.t27", "t27b": "pass"}, {"path": "specs/claw/control_pins.t27", "t27b": "pass"}, {"path": "specs/claw/engine.t27", "t27b": "pass"}, @@ -196,10 +198,14 @@ {"path": "specs/claw/gates_pins.t27", "t27b": "pass"}, {"path": "specs/claw/github_surface.t27", "t27b": "pass"}, {"path": "specs/claw/ledger_union.t27", "t27b": "pass"}, + {"path": "specs/claw/merge_lane.t27", "t27b": "pass"}, {"path": "specs/claw/model_route.t27", "t27b": "pass"}, {"path": "specs/claw/preview.t27", "t27b": "pass"}, + {"path": "specs/claw/push_gate.t27", "t27b": "pass"}, + {"path": "specs/claw/queue_cost.t27", "t27b": "pass"}, {"path": "specs/claw/recipes.t27", "t27b": "pass"}, {"path": "specs/claw/refresh.t27", "t27b": "pass"}, + {"path": "specs/claw/required_red.t27", "t27b": "pass"}, {"path": "specs/claw/sarif.t27", "t27b": "pass"}, {"path": "specs/claw/sarif_feed.t27", "t27b": "pass"}, {"path": "specs/claw/sarif_pins.t27", "t27b": "pass"}, diff --git a/specs/claw/check_latest.t27 b/specs/claw/check_latest.t27 new file mode 100644 index 0000000000..597c1f7eab --- /dev/null +++ b/specs/claw/check_latest.t27 @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/claw/check_latest.t27 -- a re-run supersedes the run it replaces (#10617). +; -- +; check_state.t27 reads every rollup row. A PR's rollup keeps the old run of a check next to its +; re-run: on 2026-10-19 #10563 held "Issue Gate/check-linked-issue" twice, run 39151562346 +; COMPLETED FAILURE (before its body was fixed) and run 39157315158 QUEUED (the re-run the body +; edit started). check_state reads that as red. The failure is history: the check that decides the +; merge is the queued re-run, so the PR is pending. A reader who trusts "red" pushes a fix, and the +; push restarts every check at the back of the queue (#10606). +; -- +; Input, one line per rollup item, four TAB fields: +; name TAB run TAB status TAB conclusion a check run; name is workflow/job, run its run id +; name TAB - TAB CONTEXT TAB state a commit status +; A row is superseded when another readable row has the same name and a larger run id. Run ids are +; compared as numbers (1000 is newer than 999), so row order does not matter. A commit status is +; never superseded and supersedes nothing. Every live row is classed by check_state's rule. +; A row is unreadable (and supersedes nothing) when it is not four fields, its name is empty, its +; run is not 1 to 20 digits without a leading zero, a commit status carries a run id, a check run +; carries none, or it lies past CHECK_MAX_ROWS. Unreadable is never green (check_state). +; Output, latest_plan: +; L TAB latest: of rows, superseded +; C TAB checks:

pass, failing, pending, unreadable of +; V TAB red | pending | none | green +; The honest limit: two rows with the same name and the same run id (a job re-run inside one run, +; or a matrix that repeats a name) both stay live, and the worse one wins. +; ASCII only (L3). phi^2 + 1/phi^2 = 3 | TRINITY + +module ClawCheckLatest; + +use claw::text; +use claw::queue_cost; +use claw::check_state; + +pub const MAX_RUN_DIGITS : usize = 20; + +/// The superseded count's slot after check_state's four classes. +pub const K_SUPERSEDED : usize = 4; + +pub const L_HEAD : str = "L\tlatest: "; +pub const L_HEAD_LEN : usize = 10; + +/// raw[s..e) is a run id: 1 to MAX_RUN_DIGITS digits, no leading zero. +pub fn run_ok(raw : []const u8, s : usize, e : usize) -> bool { + if (e <= s or e - s > MAX_RUN_DIGITS) { return false; } + if (raw[s] == '0') { return false; } + var i : usize = s; + while (i < e) { + if (raw[i] < '0' or raw[i] > '9') { return false; } + i += 1; + } + return true; +} + +/// The run id in raw[a..ae) is larger than the one in raw[b..be); both are run_ok. +pub fn run_gt(raw : []const u8, a : usize, ae : usize, b : usize, be : usize) -> bool { + if (ae - a != be - b) { return ae - a > be - b; } + var i : usize = 0; + while (a + i < ae) { + if (raw[a + i] != raw[b + i]) { return raw[a + i] > raw[b + i]; } + i += 1; + } + return false; +} + +fn is_dash(raw : []const u8, s : usize, e : usize) -> bool { + return qc_range_is(raw, s, e, NO_CONCLUSION, 1); +} + +/// f holds the bounds of a four-field row: name, run, status, conclusion. +pub fn row_ok(raw : []const u8, f : []const usize) -> bool { + if (f[1] == f[0]) { return false; } + const ctx : bool = qc_range_is(raw, f[4], f[5], ST_CONTEXT, 7); + if (is_dash(raw, f[2], f[3])) { return ctx; } + if (ctx) { return false; } + return run_ok(raw, f[2], f[3]); +} + +/// Another readable row in raw[0..n) has f's name and a larger run id. self_s is f's line start. +pub fn is_superseded(raw : []const u8, n : usize, self_s : usize, f : []const usize) -> bool { + if (is_dash(raw, f[2], f[3])) { return false; } + var g : [8]usize = undefined; + var rows : u32 = 0; + var ls : usize = 0; + while (ls < n) { + const le : usize = qc_line_end(raw, n, ls); + if (le > ls) { + if (ls != self_s and rows < CHECK_MAX_ROWS and qc_split(raw, ls, le, &g, 4)) { + if (row_ok(raw, &g) and is_dash(raw, g[2], g[3]) == false) { + if (qc_range_eq(raw, g[0], g[1], f[0], f[1]) and run_gt(raw, g[2], g[3], f[2], f[3])) { + return true; + } + } + } + rows += 1; + } + ls = le + 1; + } + return false; +} + +/// Counts of the live rows in st[K_PASS..K_BAD], superseded rows in st[K_SUPERSEDED]; returns the +/// number of rows. +pub fn latest_counts(raw : []const u8, n : usize, st : []u32) -> u32 { + var f : [8]usize = undefined; + var c : [4]usize = undefined; + var k : usize = 0; + while (k < 5) { + st[k] = 0; + k += 1; + } + var rows : u32 = 0; + var ls : usize = 0; + while (ls < n) { + const le : usize = qc_line_end(raw, n, ls); + if (le > ls) { + var d : u8 = K_BAD; + var live : bool = true; + if (rows < CHECK_MAX_ROWS and qc_split(raw, ls, le, &f, 4)) { + if (row_ok(raw, &f)) { + if (is_superseded(raw, n, ls, &f)) { + live = false; + } else { + c[0] = f[4]; + c[1] = f[5]; + c[2] = f[6]; + c[3] = f[7]; + d = check_class(raw, &c); + } + } + } + if (live) { + st[d] += 1; + } else { + st[K_SUPERSEDED] += 1; + } + rows += 1; + } + ls = le + 1; + } + return rows; +} + +/// The verdict over the live rows. +pub fn latest_verdict(raw : []const u8, n : usize) -> u8 { + var st : [5]u32 = undefined; + const rows : u32 = latest_counts(raw, n, &st); + if (rows == 0) { return V_NONE; } + return check_verdict(st[K_PASS], st[K_FAIL], st[K_WAIT], st[K_BAD]); +} + +/// The three output lines into out, clipped at cap; returns the cursor, which still advances when +/// clipped. +pub fn latest_plan(raw : []const u8, n : usize, out : []u8, cap : usize) -> usize { + var st : [5]u32 = undefined; + const rows : u32 = latest_counts(raw, n, &st); + const live : u32 = rows - st[K_SUPERSEDED]; + var w : usize = put_str(out, cap, 0, L_HEAD, L_HEAD_LEN); + w = put_count(out, cap, w, live); + w = put_str(out, cap, w, " of ", 4); + w = put_count(out, cap, w, rows); + w = put_str(out, cap, w, " rows, ", 7); + w = put_count(out, cap, w, st[K_SUPERSEDED]); + w = put_str(out, cap, w, " superseded\n", 12); + w = put_str(out, cap, w, HEAD, HEAD_LEN); + var i : usize = 0; + while (i < 4) { + w = put_count(out, cap, w, st[i]); + w = put_str(out, cap, w, COUNT_TEXTS[i], COUNT_LENS[i]); + i += 1; + } + w = put_count(out, cap, w, live); + w = put_str(out, cap, w, "\n", 1); + const v : usize = check_verdict(st[K_PASS], st[K_FAIL], st[K_WAIT], st[K_BAD]) as usize; + return put_str(out, cap, w, VERDICT_TEXTS[v], VERDICT_LENS[v]); +} + +// ---- test helpers ---- + +/// The check-linked-issue pair and one passing check from #10563 on 2026-10-19. +fn t_10563(dst : []u8, cap : usize) -> usize { + var x : usize = put_str(dst, cap, 0, "Issue Gate/check-linked-issue\t39151562346\tCOMPLETED\tFAILURE\n", 60); + x = put_str(dst, cap, x, "Issue Gate/check-linked-issue\t39157315158\tQUEUED\t-\n", 51); + return put_str(dst, cap, x, "Required/validate\t39157315001\tCOMPLETED\tSUCCESS\n", 48); +} + +fn t_two(a : str, al : usize, b : str, bl : usize) -> u8 { + var raw : [128]u8 = undefined; + var x : usize = put_str(&raw, 128, 0, a, al); + x = put_str(&raw, 128, x, b, bl); + return latest_verdict(&raw, x); +} + +fn t_bad(row : str, row_len : usize) -> bool { + var st : [5]u32 = undefined; + const rows : u32 = latest_counts(row, row_len, &st); + return rows == 1 and st[K_BAD] == 1 and st[K_PASS] == 0; +} + +// ---- tests ---- + +test the_10563_rollup_is_pending_here_and_red_to_check_state { + var raw : [256]u8 = undefined; + const n : usize = t_10563(&raw, 256); + assert n == 159; + var st : [5]u32 = undefined; + assert latest_counts(&raw, n, &st) == 3; + assert st[K_SUPERSEDED] == 1; + assert st[K_FAIL] == 0; + assert st[K_WAIT] == 1; + assert st[K_PASS] == 1; + assert latest_verdict(&raw, n) == V_PENDING; + var two : [4]u32 = undefined; + assert check_counts("COMPLETED\tFAILURE\nQUEUED\t-\nCOMPLETED\tSUCCESS\n", 45, &two) == 3; + assert check_verdict(two[K_PASS], two[K_FAIL], two[K_WAIT], two[K_BAD]) == V_RED; +} + +test the_10563_plan_text { + var raw : [256]u8 = undefined; + const n : usize = t_10563(&raw, 256); + var out : [160]u8 = undefined; + const w : usize = latest_plan(&raw, n, &out, 160); + assert w == 104; + assert buf_has(&out, w, "L\tlatest: 2 of 3 rows, 1 superseded\n", 36) == true; + assert buf_has(&out, w, "C\tchecks: 1 pass, 0 failing, 1 pending, 0 unreadable of 2\n", 58) == true; + assert buf_has(&out, w, "V\tpending\n", 10) == true; +} + +test a_newer_failure_beats_an_older_queued_run { + assert t_two("g\t7\tQUEUED\t-\n", 13, "g\t8\tCOMPLETED\tFAILURE\n", 22) == V_RED; + assert t_two("g\t8\tCOMPLETED\tFAILURE\n", 22, "g\t7\tQUEUED\t-\n", 13) == V_RED; +} + +test run_ids_compare_as_numbers_in_any_order { + assert t_two("g\t999\tCOMPLETED\tFAILURE\n", 24, "g\t1000\tCOMPLETED\tSUCCESS\n", 25) == V_GREEN; + assert t_two("g\t1000\tCOMPLETED\tSUCCESS\n", 25, "g\t999\tCOMPLETED\tFAILURE\n", 24) == V_GREEN; + assert run_gt("1000999", 0, 4, 4, 7) == true; + assert run_gt("1000999", 4, 7, 0, 4) == false; +} + +test different_names_do_not_supersede_each_other { + assert t_two("a\t5\tCOMPLETED\tFAILURE\n", 22, "b\t5\tCOMPLETED\tSUCCESS\n", 22) == V_RED; + assert t_two("a\t4\tCOMPLETED\tFAILURE\n", 22, "b\t5\tCOMPLETED\tSUCCESS\n", 22) == V_RED; +} + +test a_commit_status_is_never_superseded { + assert t_two("s\t-\tCONTEXT\tFAILURE\n", 20, "s\t9\tCOMPLETED\tSUCCESS\n", 22) == V_RED; +} + +test malformed_rows_are_unreadable { + assert t_bad("x\t12a\tCOMPLETED\tSUCCESS\n", 24) == true; + assert t_bad("x\t0123\tCOMPLETED\tSUCCESS\n", 25) == true; + assert t_bad("\t5\tCOMPLETED\tSUCCESS\n", 21) == true; + assert t_bad("x\t5\tCONTEXT\tSUCCESS\n", 20) == true; + assert t_bad("x\t-\tCOMPLETED\tSUCCESS\n", 22) == true; + assert t_bad("x\t5\tCOMPLETED\n", 14) == true; + assert t_bad("x\t123456789012345678901\tCOMPLETED\tSUCCESS\n", 42) == true; + assert t_bad("x\t5\tCOMPLETED\tSUCCESS\n", 22) == false; +} + +test an_unreadable_row_supersedes_nothing { + assert t_two("x\t9\tCOMPLETED\tFAILURE\n", 22, "x\t1a0\tCOMPLETED\tSUCCESS\n", 24) == V_RED; +} + +test a_tie_keeps_both_rows_and_the_worse_wins { + assert t_two("m\t4\tCOMPLETED\tSUCCESS\n", 22, "m\t4\tCOMPLETED\tFAILURE\n", 22) == V_RED; +} + +test no_rows_is_none { + assert latest_verdict("", 0) == V_NONE; + assert latest_verdict("\n\n", 2) == V_NONE; +} + +test a_clipped_plan_still_advances_the_cursor { + var raw : [256]u8 = undefined; + const n : usize = t_10563(&raw, 256); + var out : [160]u8 = undefined; + assert latest_plan(&raw, n, &out, 50) == 104; +} diff --git a/specs/claw/check_state.t27 b/specs/claw/check_state.t27 new file mode 100644 index 0000000000..655d5e7852 --- /dev/null +++ b/specs/claw/check_state.t27 @@ -0,0 +1,312 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/claw/check_state.t27 -- whether a PR's checks are red, green or still waiting (#10617). +; -- +; pr-dashboard.yml posts a line per open PR: " failing / total". On 2026-10-18 at 19:40 UTC it +; said 21 failing for #10585, whose rollup held 21 QUEUED checks, 3 SKIPPED, 3 SUCCESS and no failure. +; Its rule counts a check as failing when conclusion is not SUCCESS, not SKIPPED and not null; a +; queued check's conclusion is the empty string, not null, so every queued check read as a failure. +; A commit status has no conclusion at all (its verdict is in state), so a failed one never did. +; A reader who trusts such a line pushes a "fix", and the push restarts every check at the back of +; the queue (#10606). This module is the rule, stated once. +; -- +; Input, one line per statusCheckRollup item, two TAB fields: +; status TAB conclusion a check run; conclusion is `-` while it has none +; CONTEXT TAB state a commit status +; The class, per row: +; wait a check run whose status is QUEUED, IN_PROGRESS, WAITING, PENDING or REQUESTED, with +; conclusion `-`; a commit status PENDING or EXPECTED +; pass COMPLETED with SUCCESS, SKIPPED or NEUTRAL; a commit status SUCCESS +; fail COMPLETED with FAILURE, CANCELLED, TIMED_OUT, ACTION_REQUIRED, STARTUP_FAILURE or STALE; +; a commit status FAILURE or ERROR +; bad anything else: an unknown word, a waiting check that carries a conclusion, a COMPLETED one +; that does not, a row that is not two fields, a trailing carriage return, a row past +; CHECK_MAX_ROWS +; Output, check_plan: +; C TAB checks:

pass, failing, pending, unreadable of +; V TAB red | pending | none | green +; The verdict, first match wins: red if any check fails; pending if any waits or is unreadable (a +; row this module cannot read is never green); none if there are no rows (no check has run, which +; is not green either, #3248); green otherwise. +; dashboard_counts_failing is the dashboard's per-PR rule as written in pr-dashboard.yml on +; 2026-10-18, kept so the test that says it reads 21 failing on #10585 is the measurement itself. +; The honest limit: the rows are the caller's reading of one PR's rollup at one moment. +; ASCII only (L3). phi^2 + 1/phi^2 = 3 | TRINITY + +module ClawCheckState; + +use claw::text; +use claw::queue_cost; + +pub const CHECK_MAX_ROWS : u32 = 1024; + +/// Classes. +pub const K_PASS : u8 = 0; +pub const K_FAIL : u8 = 1; +pub const K_WAIT : u8 = 2; +pub const K_BAD : u8 = 3; + +/// Verdicts. +pub const V_RED : u8 = 0; +pub const V_PENDING : u8 = 1; +pub const V_NONE : u8 = 2; +pub const V_GREEN : u8 = 3; + +pub const ST_COMPLETED : str = "COMPLETED"; +pub const ST_CONTEXT : str = "CONTEXT"; +pub const NO_CONCLUSION : str = "-"; + +pub const HEAD : str = "C\tchecks: "; +pub const HEAD_LEN : usize = 10; +pub const COUNT_TEXTS : [4]str = [" pass, ", " failing, ", " pending, ", " unreadable of "]; +pub const COUNT_LENS : [4]usize = [7, 10, 10, 15]; + +pub const VERDICT_TEXTS : [4]str = ["V\tred\n", "V\tpending\n", "V\tnone\n", "V\tgreen\n"]; +pub const VERDICT_LENS : [4]usize = [6, 10, 7, 8]; + +/// A check run's status while it has no verdict. +pub fn is_wait_status(raw : []const u8, s : usize, e : usize) -> bool { + return qc_range_is(raw, s, e, "QUEUED", 6) or qc_range_is(raw, s, e, "IN_PROGRESS", 11) + or qc_range_is(raw, s, e, "WAITING", 7) or qc_range_is(raw, s, e, "PENDING", 7) + or qc_range_is(raw, s, e, "REQUESTED", 9); +} + +pub fn is_pass_conclusion(raw : []const u8, s : usize, e : usize) -> bool { + return qc_range_is(raw, s, e, "SUCCESS", 7) or qc_range_is(raw, s, e, "SKIPPED", 7) + or qc_range_is(raw, s, e, "NEUTRAL", 7); +} + +pub fn is_fail_conclusion(raw : []const u8, s : usize, e : usize) -> bool { + return qc_range_is(raw, s, e, "FAILURE", 7) or qc_range_is(raw, s, e, "CANCELLED", 9) + or qc_range_is(raw, s, e, "TIMED_OUT", 9) or qc_range_is(raw, s, e, "ACTION_REQUIRED", 15) + or qc_range_is(raw, s, e, "STARTUP_FAILURE", 15) or qc_range_is(raw, s, e, "STALE", 5); +} + +/// The class of one row; f holds the bounds of its two fields. +pub fn check_class(raw : []const u8, f : []const usize) -> u8 { + const s0 : usize = f[0]; + const s1 : usize = f[1]; + const c0 : usize = f[2]; + const c1 : usize = f[3]; + if (qc_range_is(raw, s0, s1, ST_CONTEXT, 7)) { + if (qc_range_is(raw, c0, c1, "SUCCESS", 7)) { return K_PASS; } + if (qc_range_is(raw, c0, c1, "FAILURE", 7) or qc_range_is(raw, c0, c1, "ERROR", 5)) { return K_FAIL; } + if (qc_range_is(raw, c0, c1, "PENDING", 7) or qc_range_is(raw, c0, c1, "EXPECTED", 8)) { return K_WAIT; } + return K_BAD; + } + if (qc_range_is(raw, s0, s1, ST_COMPLETED, 9)) { + if (is_pass_conclusion(raw, c0, c1)) { return K_PASS; } + if (is_fail_conclusion(raw, c0, c1)) { return K_FAIL; } + return K_BAD; + } + if (is_wait_status(raw, s0, s1)) { + if (qc_range_is(raw, c0, c1, NO_CONCLUSION, 1)) { return K_WAIT; } + return K_BAD; + } + return K_BAD; +} + +/// The dashboard's per-PR rule (pr-dashboard.yml, 2026-10-18): failing when conclusion is not +/// SUCCESS, not SKIPPED and not null. A commit status has no conclusion key (null); a check run +/// with none has the empty string, which this module's rows write as `-`. +pub fn dashboard_counts_failing(raw : []const u8, f : []const usize) -> bool { + if (qc_range_is(raw, f[0], f[1], ST_CONTEXT, 7)) { return false; } + if (qc_range_is(raw, f[2], f[3], "SUCCESS", 7)) { return false; } + if (qc_range_is(raw, f[2], f[3], "SKIPPED", 7)) { return false; } + return true; +} + +pub fn check_verdict(pass : u32, fail : u32, wait : u32, bad : u32) -> u8 { + if (fail > 0) { return V_RED; } + if (wait > 0 or bad > 0) { return V_PENDING; } + if (pass == 0) { return V_NONE; } + return V_GREEN; +} + +/// Counts in st[K_PASS..K_BAD]; returns the number of rows. +pub fn check_counts(raw : []const u8, n : usize, st : []u32) -> u32 { + var f : [4]usize = undefined; + var k : usize = 0; + while (k < 4) { + st[k] = 0; + k += 1; + } + var rows : u32 = 0; + var ls : usize = 0; + while (ls < n) { + const le : usize = qc_line_end(raw, n, ls); + if (le > ls) { + var d : u8 = K_BAD; + if (rows < CHECK_MAX_ROWS and qc_split(raw, ls, le, &f, 2)) { d = check_class(raw, &f); } + st[d] += 1; + rows += 1; + } + ls = le + 1; + } + return rows; +} + +/// The dashboard's failing count over the same rows; an unsplittable row is not counted. +pub fn dashboard_failing(raw : []const u8, n : usize) -> u32 { + var f : [4]usize = undefined; + var c : u32 = 0; + var ls : usize = 0; + while (ls < n) { + const le : usize = qc_line_end(raw, n, ls); + if (le > ls and qc_split(raw, ls, le, &f, 2)) { + if (dashboard_counts_failing(raw, &f)) { c += 1; } + } + ls = le + 1; + } + return c; +} + +/// The two output lines into out, clipped at cap; returns the cursor, which still advances when +/// clipped. +pub fn check_plan(raw : []const u8, n : usize, out : []u8, cap : usize) -> usize { + var st : [4]u32 = undefined; + const rows : u32 = check_counts(raw, n, &st); + var w : usize = put_str(out, cap, 0, HEAD, HEAD_LEN); + var i : usize = 0; + while (i < 4) { + w = put_count(out, cap, w, st[i]); + w = put_str(out, cap, w, COUNT_TEXTS[i], COUNT_LENS[i]); + i += 1; + } + w = put_count(out, cap, w, rows); + w = put_str(out, cap, w, "\n", 1); + const v : usize = check_verdict(st[K_PASS], st[K_FAIL], st[K_WAIT], st[K_BAD]) as usize; + return put_str(out, cap, w, VERDICT_TEXTS[v], VERDICT_LENS[v]); +} + +// ---- test helpers ---- + +fn t_rows(dst : []u8, cap : usize, w : usize, row : str, row_len : usize, count : u32) -> usize { + var x : usize = w; + var i : u32 = 0; + while (i < count) { + x = put_str(dst, cap, x, row, row_len); + i += 1; + } + return x; +} + +/// The rollup of #10585 at 2026-10-18 19:40 UTC. +fn t_10585(dst : []u8, cap : usize) -> usize { + var x : usize = t_rows(dst, cap, 0, "QUEUED\t-\n", 9, 21); + x = t_rows(dst, cap, x, "COMPLETED\tSKIPPED\n", 18, 3); + return t_rows(dst, cap, x, "COMPLETED\tSUCCESS\n", 18, 3); +} + +fn t_class(row : str, row_len : usize) -> u8 { + var st : [4]u32 = undefined; + const rows : u32 = check_counts(row, row_len, &st); + if (rows != 1) { return 9; } + var k : u8 = 0; + while (k < 4) { + if (st[k as usize] == 1) { return k; } + k += 1; + } + return 9; +} + +// ---- tests ---- + +test the_dashboard_reads_21_failing_on_10585_which_had_no_failure { + var raw : [512]u8 = undefined; + const n : usize = t_10585(&raw, 512); + assert n == 189 + 54 + 54; + assert dashboard_failing(&raw, n) == 21; + var st : [4]u32 = undefined; + assert check_counts(&raw, n, &st) == 27; + assert st[K_FAIL] == 0; + assert st[K_WAIT] == 21; + assert st[K_PASS] == 6; + assert st[K_BAD] == 0; +} + +test the_10585_plan_says_pending_not_red { + var raw : [512]u8 = undefined; + const n : usize = t_10585(&raw, 512); + var out : [128]u8 = undefined; + const w : usize = check_plan(&raw, n, &out, 128); + assert buf_has(&out, w, "C\tchecks: 6 pass, 0 failing, 21 pending, 0 unreadable of 27\n", 60) == true; + assert buf_has(&out, w, "V\tpending\n", 10) == true; + assert buf_has(&out, w, "V\tred\n", 6) == false; +} + +test a_failed_commit_status_is_failing_here_and_invisible_to_the_dashboard { + assert t_class("CONTEXT\tFAILURE\n", 16) == K_FAIL; + assert t_class("CONTEXT\tERROR\n", 14) == K_FAIL; + assert dashboard_failing("CONTEXT\tFAILURE\n", 16) == 0; +} + +test each_conclusion_has_its_class { + assert t_class("COMPLETED\tSUCCESS\n", 18) == K_PASS; + assert t_class("COMPLETED\tNEUTRAL\n", 18) == K_PASS; + assert t_class("COMPLETED\tCANCELLED\n", 20) == K_FAIL; + assert t_class("COMPLETED\tTIMED_OUT\n", 20) == K_FAIL; + assert t_class("COMPLETED\tSTARTUP_FAILURE\n", 26) == K_FAIL; + assert t_class("COMPLETED\tSTALE\n", 16) == K_FAIL; + assert t_class("IN_PROGRESS\t-\n", 14) == K_WAIT; + assert t_class("CONTEXT\tEXPECTED\n", 17) == K_WAIT; + assert t_class("CONTEXT\tSUCCESS\n", 16) == K_PASS; +} + +test an_unknown_or_inconsistent_row_is_unreadable { + assert t_class("QUEUED\tSUCCESS\n", 15) == K_BAD; + assert t_class("COMPLETED\t-\n", 12) == K_BAD; + assert t_class("COMPLETED\tMAYBE\n", 16) == K_BAD; + assert t_class("DONE\tSUCCESS\n", 13) == K_BAD; + assert t_class("COMPLETED\tSUCCESS\textra\n", 24) == K_BAD; + assert t_class("COMPLETED\tSUCCESS\r\n", 19) == K_BAD; + assert t_class("completed\tsuccess\n", 18) == K_BAD; +} + +test an_unreadable_row_is_never_green { + assert check_verdict(5, 0, 0, 1) == V_PENDING; + var out : [128]u8 = undefined; + const w : usize = check_plan("COMPLETED\tSUCCESS\nCOMPLETED\tMAYBE\n", 34, &out, 128); + assert buf_has(&out, w, "V\tpending\n", 10) == true; +} + +test no_checks_is_none_and_not_green { + var out : [128]u8 = undefined; + const w : usize = check_plan("", 0, &out, 128); + assert buf_has(&out, w, "0 unreadable of 0\nV\tnone\n", 25) == true; + assert check_verdict(0, 0, 0, 0) == V_NONE; +} + +test one_failure_makes_it_red_whatever_else_waits { + assert check_verdict(3, 1, 20, 2) == V_RED; + var out : [128]u8 = undefined; + const w : usize = check_plan("QUEUED\t-\nCOMPLETED\tFAILURE\n", 27, &out, 128); + assert buf_has(&out, w, "V\tred\n", 6) == true; +} + +test all_concluded_and_passing_is_green { + var out : [128]u8 = undefined; + const w : usize = check_plan("COMPLETED\tSUCCESS\nCOMPLETED\tSKIPPED\nCONTEXT\tSUCCESS\n", 52, &out, 128); + assert buf_has(&out, w, "3 pass, 0 failing, 0 pending, 0 unreadable of 3\nV\tgreen\n", 56) == true; +} + +test the_dashboard_and_this_module_agree_once_nothing_waits { + const raw : str = "COMPLETED\tSUCCESS\nCOMPLETED\tFAILURE\nCOMPLETED\tSKIPPED\n"; + assert dashboard_failing(raw, 54) == 1; + var st : [4]u32 = undefined; + assert check_counts(raw, 54, &st) == 3; + assert st[K_FAIL] == 1; +} + +test blank_lines_are_not_rows { + var st : [4]u32 = undefined; + assert check_counts("\n\nQUEUED\t-\n\n", 12, &st) == 1; + assert st[K_WAIT] == 1; +} + +test a_clipped_plan_still_advances_the_cursor { + var out : [4]u8 = undefined; + const w : usize = check_plan("COMPLETED\tSUCCESS\n", 18, &out, 4); + assert w == 10 + 1 + 7 + 1 + 10 + 1 + 10 + 1 + 15 + 1 + 1 + 8; + assert out[0] == 'C'; + assert out[1] == '\t'; +} diff --git a/specs/claw/merge_lane.t27 b/specs/claw/merge_lane.t27 new file mode 100644 index 0000000000..49a0c151e5 --- /dev/null +++ b/specs/claw/merge_lane.t27 @@ -0,0 +1,666 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/claw/merge_lane.t27 -- is the merger starved? Read from its own runs (#10393). +; -- +; TRI CLAW's merger is the workflow "Auto Merge Ready PRs". Its merge job runs on ubuntu-latest, +; the same pool every pull_request job waits for. On 2026-10-18 at 20:22 UTC, 34 of its runs were +; queued, and the oldest was 6598 s old. Run 39158684665 was created at 19:25 and its job got a runner +; at 20:22. Of its last 200 completed runs, 22 succeeded, 88 were skipped, and 90 were cancelled by +; its concurrency groups. A green, reviewed PR therefore waits about an hour for a runner, not for review. +; The merge services built for this (GitHub merge queue, Mergify, Kodiak, bors, Aviator) decide outside +; the CI runner pool, so merging never waits behind CI. This module only measures; it moves nothing. +; -- +; Input: one row per merger run, status TAB conclusion TAB age. +; status queued, waiting, requested, pending (all open and not started), in_progress, completed +; conclusion `-` unless completed; when completed, success, skipped, cancelled or another +; GitHub conclusion (failure, neutral, timed_out, action_required, stale, +; startup_failure) +; age whole seconds since the run was created, 0..432000 (queue_cost's MAX_SECS) +; Output, merge_plan: +; G TAB merger: queued, running, success, skipped, cancelled, other, +; unreadable; oldest open s +; F TAB merger TAB idle | flowing | starved | unknown +; starved: an open run is older than ML_STARVE_SECS. A known starve wins over an unreadable row. +; unknown: not starved, and some row is unreadable. idle: no open run. flowing: otherwise. +; The honest limit: age is creation age. A queued run's age is how long it has waited. An +; in_progress run's age also includes the time it has run, and a merge job takes under a minute. +; -- +; Yield: does a merger run merge anything? (tick 14, 2026-10-18) +; Only 1 of the last 40 merges came through the merger workflow; the other 39 had GitHub's +; native auto-merge enabled (auto_squash_enabled in the PR timeline). The last 10 successful merger +; runs merged 0 PRs. 9 of them were workflow_run runs that each took a runner to log "no +; bee-reviewed label" for one PR. The schedule sweep skipped 6 old PRs whose label came from the wrong hand. +; Input: one row per merger run, event TAB merged TAB no_label TAB other. merged counts the PRs it +; merged. no_label counts PRs it skipped for a missing bee-reviewed label. other counts PRs it +; skipped for any other reason. event is workflow_run, schedule, pull_request_target or +; workflow_dispatch. +; Output, yield_plan: +; Y TAB merger yield: PRs merged in runs; merged nothing, of them only for a +; missing label; unreadable +; F TAB merger-yield TAB zero | some | unknown | none +; A known merge wins over an unreadable row; with no merge, an unreadable row makes it unknown. +; -- +; Share: how much of the whole queue is the merger? (tick 15, 2026-10-19) +; At 2026-10-19 the repository had 209 queued runs. 23 of them were the merger's, 22 of those +; workflow_run runs. That is 11% of the queue, spent on a workflow whose last 10 runs merged nothing. +; The merger's runs are the largest single group of queued runs, ahead of every PR check. +; Input: one row per queued run in the repository, workflow TAB event. Both fields are non-empty, +; and event is lower-case letters and `_` only. +; Output, share_plan: +; S TAB merger share: of queued runs are the merger's, of them workflow_run; +; unreadable +; F TAB merger-share TAB low | high | unknown | none +; high: merger runs are at least MS_SHARE_PCT % of all rows, counting each unreadable row as +; somebody else's. So a known high wins over an unreadable row. unknown: not high, and some row is +; unreadable. none: no rows. +; ASCII only (L3). phi^2 + 1/phi^2 = 3 | TRINITY + +module ClawMergeLane; + +use claw::text; +use claw::queue_cost; + +pub const ML_QUEUED : usize = 0; +pub const ML_RUNNING : usize = 1; +pub const ML_SUCCESS : usize = 2; +pub const ML_SKIPPED : usize = 3; +pub const ML_CANCELLED : usize = 4; +pub const ML_OTHER : usize = 5; +pub const ML_BAD : usize = 6; +/// The slot after the classes holds the oldest open age, in seconds. +pub const ML_OLDEST : usize = 7; +pub const ML_SLOTS : usize = 8; + +pub const ML_MAX_ROWS : u32 = 4096; +/// Thirty minutes: the backstop sweep runs every twenty, so an older open run is not a sweep gap. +pub const ML_STARVE_SECS : u32 = 1800; + +pub const ML_IDLE : u8 = 0; +pub const ML_FLOWING : u8 = 1; +pub const ML_STARVED : u8 = 2; +pub const ML_UNKNOWN : u8 = 3; + +pub const ML_HEAD : str = "G\tmerger: "; +pub const ML_HEAD_LEN : usize = 10; +pub const ML_COUNT_TEXTS : [7]str = [" queued, ", " running, ", " success, ", " skipped, ", " cancelled, ", " other, ", " unreadable; oldest open "]; +pub const ML_COUNT_LENS : [7]usize = [9, 10, 10, 10, 12, 8, 25]; +pub const ML_FLAG_HEAD : str = "F\tmerger\t"; +pub const ML_FLAG_HEAD_LEN : usize = 9; +pub const ML_FLAG_TEXTS : [4]str = ["idle\n", "flowing\n", "starved\n", "unknown\n"]; +pub const ML_FLAG_LENS : [4]usize = [5, 8, 8, 8]; + +/// The class of one row from its status and conclusion ranges, or ML_BAD. +pub fn ml_class(raw : []const u8, f : []const usize) -> usize { + const dash : bool = qc_range_is(raw, f[2], f[3], "-", 1); + const open : bool = qc_range_is(raw, f[0], f[1], "queued", 6) or qc_range_is(raw, f[0], f[1], "waiting", 7) or qc_range_is(raw, f[0], f[1], "requested", 9) or qc_range_is(raw, f[0], f[1], "pending", 7); + if (open) { + if (dash) { return ML_QUEUED; } + return ML_BAD; + } + if (qc_range_is(raw, f[0], f[1], "in_progress", 11)) { + if (dash) { return ML_RUNNING; } + return ML_BAD; + } + if (qc_range_is(raw, f[0], f[1], "completed", 9) == false) { return ML_BAD; } + if (qc_range_is(raw, f[2], f[3], "success", 7)) { return ML_SUCCESS; } + if (qc_range_is(raw, f[2], f[3], "skipped", 7)) { return ML_SKIPPED; } + if (qc_range_is(raw, f[2], f[3], "cancelled", 9)) { return ML_CANCELLED; } + const other : bool = qc_range_is(raw, f[2], f[3], "failure", 7) or qc_range_is(raw, f[2], f[3], "neutral", 7) or qc_range_is(raw, f[2], f[3], "timed_out", 9) or qc_range_is(raw, f[2], f[3], "action_required", 15) or qc_range_is(raw, f[2], f[3], "stale", 5) or qc_range_is(raw, f[2], f[3], "startup_failure", 15); + if (other) { return ML_OTHER; } + return ML_BAD; +} + +/// Rows by class in st[ML_QUEUED..ML_BAD]; the oldest open age in st[ML_OLDEST]. Returns the +/// number of rows. +pub fn ml_counts(raw : []const u8, n : usize, st : []u32) -> u32 { + var f : [6]usize = undefined; + var k : usize = 0; + while (k < ML_SLOTS) { + st[k] = 0; + k += 1; + } + var rows : u32 = 0; + var ls : usize = 0; + while (ls < n) { + const le : usize = qc_line_end(raw, n, ls); + if (le > ls) { + var cls : usize = ML_BAD; + var age : i32 = -1; + if (rows < ML_MAX_ROWS and qc_split(raw, ls, le, &f, 3)) { + age = qc_secs(raw, f[4], f[5]); + if (age >= 0) { cls = ml_class(raw, &f); } + } + st[cls] += 1; + const is_open : bool = cls == ML_QUEUED or cls == ML_RUNNING; + if (is_open and (age as u32) > st[ML_OLDEST]) { st[ML_OLDEST] = age as u32; } + rows += 1; + } + ls = le + 1; + } + return rows; +} + +/// The verdict from the counts: a known starve wins over an unreadable row. +pub fn ml_verdict_of(st : []const u32) -> u8 { + if (st[ML_OLDEST] > ML_STARVE_SECS) { return ML_STARVED; } + if (st[ML_BAD] > 0) { return ML_UNKNOWN; } + if (st[ML_QUEUED] + st[ML_RUNNING] == 0) { return ML_IDLE; } + return ML_FLOWING; +} + +pub fn ml_verdict(raw : []const u8, n : usize) -> u8 { + var st : [8]u32 = undefined; + const rows : u32 = ml_counts(raw, n, &st); + if (rows == 0) { return ML_IDLE; } + return ml_verdict_of(&st); +} + +/// The two output lines into out, clipped at cap; returns the cursor, which still advances when +/// clipped. +pub fn merge_plan(raw : []const u8, n : usize, out : []u8, cap : usize) -> usize { + var st : [8]u32 = undefined; + const rows : u32 = ml_counts(raw, n, &st); + var w : usize = put_str(out, cap, 0, ML_HEAD, ML_HEAD_LEN); + var k : usize = 0; + while (k < 7) { + w = put_count(out, cap, w, st[k]); + w = put_str(out, cap, w, ML_COUNT_TEXTS[k], ML_COUNT_LENS[k]); + k += 1; + } + w = put_count(out, cap, w, st[ML_OLDEST]); + w = put_str(out, cap, w, " s\n", 3); + w = put_str(out, cap, w, ML_FLAG_HEAD, ML_FLAG_HEAD_LEN); + var v : usize = ML_IDLE as usize; + if (rows > 0) { v = ml_verdict_of(&st) as usize; } + return put_str(out, cap, w, ML_FLAG_TEXTS[v], ML_FLAG_LENS[v]); +} + +// ---- yield ---- + +pub const MY_RUNS : usize = 0; +pub const MY_MERGED : usize = 1; +pub const MY_EMPTY : usize = 2; +pub const MY_LABEL_ONLY : usize = 3; +pub const MY_BAD : usize = 4; +pub const MY_SLOTS : usize = 5; + +pub const MY_ZERO : u8 = 0; +pub const MY_SOME : u8 = 1; +pub const MY_UNKNOWN : u8 = 2; +pub const MY_NONE : u8 = 3; + +pub const MY_HEAD : str = "Y\tmerger yield: "; +pub const MY_HEAD_LEN : usize = 16; +pub const MY_TEXTS : [5]str = [" PRs merged in ", " runs; ", " merged nothing, ", " of them only for a missing label; ", " unreadable\n"]; +pub const MY_LENS : [5]usize = [15, 7, 17, 35, 12]; +pub const MY_FLAG_HEAD : str = "F\tmerger-yield\t"; +pub const MY_FLAG_HEAD_LEN : usize = 15; +pub const MY_FLAG_TEXTS : [4]str = ["zero\n", "some\n", "unknown\n", "none\n"]; +pub const MY_FLAG_LENS : [4]usize = [5, 5, 8, 5]; + +/// raw[s..e) is an event that starts the merger. +pub fn my_event_ok(raw : []const u8, s : usize, e : usize) -> bool { + return qc_range_is(raw, s, e, "workflow_run", 12) or qc_range_is(raw, s, e, "schedule", 8) or qc_range_is(raw, s, e, "pull_request_target", 19) or qc_range_is(raw, s, e, "workflow_dispatch", 17); +} + +/// Yield counts in st[MY_RUNS..MY_BAD]. Returns the number of rows. +pub fn my_counts(raw : []const u8, n : usize, st : []u32) -> u32 { + var f : [8]usize = undefined; + var k : usize = 0; + while (k < MY_SLOTS) { + st[k] = 0; + k += 1; + } + var rows : u32 = 0; + var ls : usize = 0; + while (ls < n) { + const le : usize = qc_line_end(raw, n, ls); + if (le > ls) { + var ok : bool = false; + if (rows < ML_MAX_ROWS and qc_split(raw, ls, le, &f, 4)) { + const m : i32 = qc_secs(raw, f[2], f[3]); + const nl : i32 = qc_secs(raw, f[4], f[5]); + const ot : i32 = qc_secs(raw, f[6], f[7]); + if (my_event_ok(raw, f[0], f[1]) and m >= 0 and nl >= 0 and ot >= 0) { + ok = true; + st[MY_RUNS] += 1; + st[MY_MERGED] += m as u32; + if (m == 0) { + st[MY_EMPTY] += 1; + if (nl > 0 and ot == 0) { st[MY_LABEL_ONLY] += 1; } + } + } + } + if (ok == false) { st[MY_BAD] += 1; } + rows += 1; + } + ls = le + 1; + } + return rows; +} + +/// The yield verdict from the counts: a known merge wins over an unreadable row. +pub fn my_verdict_of(st : []const u32) -> u8 { + if (st[MY_MERGED] > 0) { return MY_SOME; } + if (st[MY_BAD] > 0) { return MY_UNKNOWN; } + if (st[MY_RUNS] == 0) { return MY_NONE; } + return MY_ZERO; +} + +pub fn my_verdict(raw : []const u8, n : usize) -> u8 { + var st : [5]u32 = undefined; + const rows : u32 = my_counts(raw, n, &st); + if (rows == 0) { return MY_NONE; } + return my_verdict_of(&st); +} + +/// The two yield lines into out, clipped at cap; returns the cursor, which still advances when +/// clipped. +pub fn yield_plan(raw : []const u8, n : usize, out : []u8, cap : usize) -> usize { + var st : [5]u32 = undefined; + const rows : u32 = my_counts(raw, n, &st); + var w : usize = put_str(out, cap, 0, MY_HEAD, MY_HEAD_LEN); + w = put_count(out, cap, w, st[MY_MERGED]); + w = put_str(out, cap, w, MY_TEXTS[0], MY_LENS[0]); + w = put_count(out, cap, w, st[MY_RUNS]); + w = put_str(out, cap, w, MY_TEXTS[1], MY_LENS[1]); + w = put_count(out, cap, w, st[MY_EMPTY]); + w = put_str(out, cap, w, MY_TEXTS[2], MY_LENS[2]); + w = put_count(out, cap, w, st[MY_LABEL_ONLY]); + w = put_str(out, cap, w, MY_TEXTS[3], MY_LENS[3]); + w = put_count(out, cap, w, st[MY_BAD]); + w = put_str(out, cap, w, MY_TEXTS[4], MY_LENS[4]); + w = put_str(out, cap, w, MY_FLAG_HEAD, MY_FLAG_HEAD_LEN); + var v : usize = MY_NONE as usize; + if (rows > 0) { v = my_verdict_of(&st) as usize; } + return put_str(out, cap, w, MY_FLAG_TEXTS[v], MY_FLAG_LENS[v]); +} + +// ---- share: the merger's part of the whole queue ---- + +/// The merger's workflow name, as GitHub lists it. +pub const MS_MERGER : str = "Auto Merge Ready PRs"; +pub const MS_MERGER_LEN : usize = 20; +/// At this share or more of the queued runs, the merger is a high cost. +pub const MS_SHARE_PCT : u32 = 10; + +pub const MS_ROWS : usize = 0; +pub const MS_MERGER_ROWS : usize = 1; +pub const MS_FOLLOW : usize = 2; +pub const MS_BAD : usize = 3; +pub const MS_SLOTS : usize = 4; + +pub const MS_LOW : u8 = 0; +pub const MS_HIGH : u8 = 1; +pub const MS_UNKNOWN : u8 = 2; +pub const MS_NONE : u8 = 3; + +pub const MS_HEAD : str = "S\tmerger share: "; +pub const MS_HEAD_LEN : usize = 16; +pub const MS_TEXTS : [4]str = [" of ", " queued runs are the merger's, ", " of them workflow_run; ", " unreadable\n"]; +pub const MS_LENS : [4]usize = [4, 31, 23, 12]; +pub const MS_FLAG_HEAD : str = "F\tmerger-share\t"; +pub const MS_FLAG_HEAD_LEN : usize = 15; +pub const MS_FLAG_TEXTS : [4]str = ["low\n", "high\n", "unknown\n", "none\n"]; +pub const MS_FLAG_LENS : [4]usize = [4, 5, 8, 5]; + +/// raw[s..e) is a non-empty event name: lower-case letters and `_` only. +pub fn ms_event_ok(raw : []const u8, s : usize, e : usize) -> bool { + if (e <= s) { return false; } + var i : usize = s; + while (i < e) { + const b : u8 = raw[i]; + if ((b >= 'a' and b <= 'z') == false and b != '_') { return false; } + i += 1; + } + return true; +} + +/// Share counts in st[MS_ROWS..MS_BAD]. Returns the number of rows. +pub fn ms_counts(raw : []const u8, n : usize, st : []u32) -> u32 { + var f : [4]usize = undefined; + var k : usize = 0; + while (k < MS_SLOTS) { + st[k] = 0; + k += 1; + } + var rows : u32 = 0; + var ls : usize = 0; + while (ls < n) { + const le : usize = qc_line_end(raw, n, ls); + if (le > ls) { + var ok : bool = false; + if (rows < ML_MAX_ROWS and qc_split(raw, ls, le, &f, 2)) { + if (f[1] > f[0] and ms_event_ok(raw, f[2], f[3])) { + ok = true; + st[MS_ROWS] += 1; + if (qc_range_is(raw, f[0], f[1], MS_MERGER, MS_MERGER_LEN)) { + st[MS_MERGER_ROWS] += 1; + if (qc_range_is(raw, f[2], f[3], "workflow_run", 12)) { st[MS_FOLLOW] += 1; } + } + } + } + if (ok == false) { st[MS_BAD] += 1; } + rows += 1; + } + ls = le + 1; + } + return rows; +} + +/// The share verdict from the counts. An unreadable row counts as somebody else's run, so a high +/// share that survives that is known. +pub fn ms_verdict_of(st : []const u32) -> u8 { + const total : u32 = st[MS_ROWS] + st[MS_BAD]; + if (total == 0) { return MS_NONE; } + if (st[MS_MERGER_ROWS] * 100 >= MS_SHARE_PCT * total) { return MS_HIGH; } + if (st[MS_BAD] > 0) { return MS_UNKNOWN; } + return MS_LOW; +} + +pub fn ms_verdict(raw : []const u8, n : usize) -> u8 { + var st : [4]u32 = undefined; + if (ms_counts(raw, n, &st) == 0) { return MS_NONE; } + return ms_verdict_of(&st); +} + +/// The two share lines into out, clipped at cap; returns the cursor, which still advances when +/// clipped. +pub fn share_plan(raw : []const u8, n : usize, out : []u8, cap : usize) -> usize { + var st : [4]u32 = undefined; + const rows : u32 = ms_counts(raw, n, &st); + var w : usize = put_str(out, cap, 0, MS_HEAD, MS_HEAD_LEN); + w = put_count(out, cap, w, st[MS_MERGER_ROWS]); + w = put_str(out, cap, w, MS_TEXTS[0], MS_LENS[0]); + w = put_count(out, cap, w, rows); + w = put_str(out, cap, w, MS_TEXTS[1], MS_LENS[1]); + w = put_count(out, cap, w, st[MS_FOLLOW]); + w = put_str(out, cap, w, MS_TEXTS[2], MS_LENS[2]); + w = put_count(out, cap, w, st[MS_BAD]); + w = put_str(out, cap, w, MS_TEXTS[3], MS_LENS[3]); + w = put_str(out, cap, w, MS_FLAG_HEAD, MS_FLAG_HEAD_LEN); + const v : usize = ms_verdict_of(&st) as usize; + return put_str(out, cap, w, MS_FLAG_TEXTS[v], MS_FLAG_LENS[v]); +} + +// ---- test helpers ---- + +/// The merger on 2026-10-18 at 20:22 UTC, in miniature: the oldest queued run (6598 s), the +/// in_progress run created at 19:25 (3425 s), and one each of success, skipped and cancelled. +fn t_starved(dst : []u8, cap : usize) -> usize { + var x : usize = put_str(dst, cap, 0, "queued\t-\t6598\n", 14); + x = put_str(dst, cap, x, "in_progress\t-\t3425\n", 19); + x = put_str(dst, cap, x, "completed\tsuccess\t120\n", 22); + x = put_str(dst, cap, x, "completed\tskipped\t7\n", 20); + return put_str(dst, cap, x, "completed\tcancelled\t900\n", 24); +} + +fn t_verdict2(a : str, al : usize, b : str, bl : usize) -> u8 { + var raw : [96]u8 = undefined; + var x : usize = put_str(&raw, 96, 0, a, al); + x = put_str(&raw, 96, x, b, bl); + return ml_verdict(&raw, x); +} + +// ---- tests ---- + +test the_merger_on_2026_10_18_is_starved { + var raw : [128]u8 = undefined; + const n : usize = t_starved(&raw, 128); + assert n == 99; + var st : [8]u32 = undefined; + assert ml_counts(&raw, n, &st) == 5; + assert st[ML_QUEUED] == 1; + assert st[ML_RUNNING] == 1; + assert st[ML_SUCCESS] == 1; + assert st[ML_SKIPPED] == 1; + assert st[ML_CANCELLED] == 1; + assert st[ML_OTHER] == 0; + assert st[ML_BAD] == 0; + assert st[ML_OLDEST] == 6598; + assert ml_verdict(&raw, n) == ML_STARVED; +} + +test the_starved_plan_text { + var raw : [128]u8 = undefined; + const n : usize = t_starved(&raw, 128); + var out : [160]u8 = undefined; + const w : usize = merge_plan(&raw, n, &out, 160); + assert w == 125; + assert buf_has(&out, w, "G\tmerger: 1 queued, 1 running, 1 success, 1 skipped, 1 cancelled, 0 other, 0 unreadable; oldest open 6598 s\n", 108) == true; + assert buf_has(&out, w, "F\tmerger\tstarved\n", 17) == true; +} + +test the_age_of_a_completed_run_does_not_starve { + assert t_verdict2("completed\tsuccess\t9000\n", 23, "queued\t-\t60\n", 12) == ML_FLOWING; + assert t_verdict2("completed\tcancelled\t9000\n", 25, "completed\tskipped\t9000\n", 23) == ML_IDLE; +} + +test the_limit_is_strict { + assert t_verdict2("queued\t-\t1800\n", 14, "in_progress\t-\t5\n", 16) == ML_FLOWING; + assert t_verdict2("queued\t-\t1801\n", 14, "in_progress\t-\t5\n", 16) == ML_STARVED; + assert t_verdict2("pending\t-\t5\n", 12, "in_progress\t-\t1801\n", 19) == ML_STARVED; +} + +test an_open_run_needs_a_dash_and_a_completed_run_needs_a_conclusion { + assert t_verdict2("queued\tsuccess\t5\n", 17, "queued\t-\t5\n", 11) == ML_UNKNOWN; + assert t_verdict2("completed\t-\t5\n", 14, "queued\t-\t5\n", 11) == ML_UNKNOWN; + assert t_verdict2("in_progress\tsuccess\t5\n", 22, "queued\t-\t5\n", 11) == ML_UNKNOWN; + assert t_verdict2("completed\tfailure\t5\n", 20, "completed\ttimed_out\t5\n", 22) == ML_IDLE; +} + +test an_unreadable_row_makes_it_unknown_and_a_known_starve_still_wins { + assert t_verdict2("queued\t-\t9x\n", 12, "queued\t-\t5\n", 11) == ML_UNKNOWN; + assert t_verdict2("queued\t-\t432001\n", 16, "queued\t-\t5\n", 11) == ML_UNKNOWN; + assert t_verdict2("Queued\t-\t5\n", 11, "queued\t-\t2000\n", 14) == ML_STARVED; + assert t_verdict2("queued\t-\n", 9, "queued\t-\t2000\n", 14) == ML_STARVED; +} + +test no_rows_is_idle { + assert ml_verdict("", 0) == ML_IDLE; + var out : [160]u8 = undefined; + const w : usize = merge_plan("", 0, &out, 160); + assert w == 119; + assert buf_has(&out, w, "F\tmerger\tidle\n", 14) == true; +} + +test a_clipped_plan_still_advances_the_cursor { + var raw : [128]u8 = undefined; + const n : usize = t_starved(&raw, 128); + var out : [160]u8 = undefined; + assert merge_plan(&raw, n, &out, 40) == 125; +} + +// ---- yield tests ---- + +/// The last 10 successful merger runs on 2026-10-18: nine workflow_run runs that each skipped one +/// PR for a missing label, and the schedule sweep that skipped six PRs for other reasons. +fn t_yield_today(dst : []u8, cap : usize) -> usize { + var x : usize = 0; + var i : u32 = 0; + while (i < 9) { + x = put_str(dst, cap, x, "workflow_run\t0\t1\t0\n", 19); + i += 1; + } + return put_str(dst, cap, x, "schedule\t0\t0\t6\n", 15); +} + +fn t_yield2(a : str, al : usize, b : str, bl : usize) -> u8 { + var raw : [96]u8 = undefined; + var x : usize = put_str(&raw, 96, 0, a, al); + x = put_str(&raw, 96, x, b, bl); + return my_verdict(&raw, x); +} + +test the_merger_yield_on_2026_10_18_is_zero { + var raw : [256]u8 = undefined; + const n : usize = t_yield_today(&raw, 256); + assert n == 186; + var st : [5]u32 = undefined; + assert my_counts(&raw, n, &st) == 10; + assert st[MY_RUNS] == 10; + assert st[MY_MERGED] == 0; + assert st[MY_EMPTY] == 10; + assert st[MY_LABEL_ONLY] == 9; + assert st[MY_BAD] == 0; + assert my_verdict(&raw, n) == MY_ZERO; +} + +test the_zero_yield_plan_text { + var raw : [256]u8 = undefined; + const n : usize = t_yield_today(&raw, 256); + var out : [160]u8 = undefined; + const w : usize = yield_plan(&raw, n, &out, 160); + assert w == 129; + assert buf_has(&out, w, "Y\tmerger yield: 0 PRs merged in 10 runs; 10 merged nothing, 9 of them only for a missing label; 0 unreadable\n", 109) == true; + assert buf_has(&out, w, "F\tmerger-yield\tzero\n", 20) == true; +} + +test a_known_merge_wins_over_an_unreadable_row { + assert t_yield2("pull_request_target\t1\t0\t0\n", 26, "workflow_run\tx\t1\t0\n", 19) == MY_SOME; + assert t_yield2("workflow_run\tx\t1\t0\n", 19, "workflow_run\t0\t1\t0\n", 19) == MY_UNKNOWN; +} + +test label_only_needs_no_other_skip { + var st : [5]u32 = undefined; + assert my_counts("schedule\t0\t1\t2\n", 15, &st) == 1; + assert st[MY_EMPTY] == 1; + assert st[MY_LABEL_ONLY] == 0; + assert my_counts("workflow_run\t2\t1\t0\n", 19, &st) == 1; + assert st[MY_EMPTY] == 0; + assert st[MY_LABEL_ONLY] == 0; + assert st[MY_MERGED] == 2; +} + +test an_unknown_event_is_unreadable { + assert t_yield2("push\t0\t1\t0\n", 11, "schedule\t0\t0\t6\n", 15) == MY_UNKNOWN; + assert t_yield2("workflow_dispatch\t0\t0\t0\n", 24, "schedule\t0\t0\t6\n", 15) == MY_ZERO; + assert t_yield2("schedule\t0\t0\n", 13, "schedule\t0\t0\t6\n", 15) == MY_UNKNOWN; +} + +test no_yield_rows_is_none { + assert my_verdict("", 0) == MY_NONE; + var out : [160]u8 = undefined; + const w : usize = yield_plan("", 0, &out, 160); + assert buf_has(&out, w, "F\tmerger-yield\tnone\n", 20) == true; +} + +test a_clipped_yield_plan_still_advances_the_cursor { + var raw : [256]u8 = undefined; + const n : usize = t_yield_today(&raw, 256); + var out : [160]u8 = undefined; + assert yield_plan(&raw, n, &out, 30) == 129; +} + +// ---- share tests ---- + +/// The 2026-10-19 queue: 22 merger workflow_run runs, the merger's schedule run, and 186 runs of +/// other workflows. +fn t_share_today(dst : []u8, cap : usize) -> usize { + var x : usize = 0; + var i : u32 = 0; + while (i < 22) { + x = put_str(dst, cap, x, "Auto Merge Ready PRs\tworkflow_run\n", 34); + i += 1; + } + x = put_str(dst, cap, x, "Auto Merge Ready PRs\tschedule\n", 30); + i = 0; + while (i < 186) { + x = put_str(dst, cap, x, "x\tpull_request\n", 15); + i += 1; + } + return x; +} + +/// m merger rows, o rows of another workflow, then b one-field rows. +fn t_share(m : u32, o : u32, b : u32) -> u8 { + var raw : [1024]u8 = undefined; + var x : usize = 0; + var i : u32 = 0; + while (i < m) { + x = put_str(&raw, 1024, x, "Auto Merge Ready PRs\tworkflow_run\n", 34); + i += 1; + } + i = 0; + while (i < o) { + x = put_str(&raw, 1024, x, "x\tpull_request\n", 15); + i += 1; + } + i = 0; + while (i < b) { + x = put_str(&raw, 1024, x, "x\n", 2); + i += 1; + } + return ms_verdict(&raw, x); +} + +test the_merger_is_a_tenth_of_the_2026_10_19_queue { + var raw : [4096]u8 = undefined; + const n : usize = t_share_today(&raw, 4096); + assert n == 3568; + var st : [4]u32 = undefined; + assert ms_counts(&raw, n, &st) == 209; + assert st[MS_ROWS] == 209; + assert st[MS_MERGER_ROWS] == 23; + assert st[MS_FOLLOW] == 22; + assert st[MS_BAD] == 0; + assert ms_verdict(&raw, n) == MS_HIGH; +} + +test the_high_share_plan_text { + var raw : [4096]u8 = undefined; + const n : usize = t_share_today(&raw, 4096); + var out : [160]u8 = undefined; + const w : usize = share_plan(&raw, n, &out, 160); + assert w == 114; + assert buf_has(&out, w, "S\tmerger share: 23 of 209 queued runs are the merger's, 22 of them workflow_run; 0 unreadable\n", 94) == true; + assert buf_has(&out, w, "F\tmerger-share\thigh\n", 20) == true; +} + +test ten_percent_is_high_and_less_is_low { + assert t_share(1, 9, 0) == MS_HIGH; + assert t_share(1, 10, 0) == MS_LOW; + assert t_share(0, 3, 0) == MS_LOW; +} + +test an_unreadable_row_counts_as_somebody_elses { + assert t_share(1, 9, 1) == MS_UNKNOWN; + assert t_share(2, 8, 1) == MS_HIGH; + assert t_share(0, 0, 1) == MS_UNKNOWN; +} + +test a_carriage_return_or_a_near_name_is_not_the_merger { + var st : [4]u32 = undefined; + assert ms_counts("Auto Merge Ready PRs\tworkflow_run\r\n", 35, &st) == 1; + assert st[MS_BAD] == 1; + assert st[MS_MERGER_ROWS] == 0; + assert ms_counts("Auto Merge Ready PR\tworkflow_run\n", 33, &st) == 1; + assert st[MS_ROWS] == 1; + assert st[MS_MERGER_ROWS] == 0; + assert ms_counts("\tworkflow_run\n", 14, &st) == 1; + assert st[MS_BAD] == 1; + assert ms_counts("Auto Merge Ready PRs\tschedule\n", 30, &st) == 1; + assert st[MS_MERGER_ROWS] == 1; + assert st[MS_FOLLOW] == 0; +} + +test no_share_rows_is_none { + assert ms_verdict("", 0) == MS_NONE; + var out : [160]u8 = undefined; + const w : usize = share_plan("", 0, &out, 160); + assert buf_has(&out, w, "F\tmerger-share\tnone\n", 20) == true; +} + +test a_clipped_share_plan_still_advances_the_cursor { + var raw : [4096]u8 = undefined; + const n : usize = t_share_today(&raw, 4096); + var out : [160]u8 = undefined; + assert share_plan(&raw, n, &out, 30) == 114; +} + +test the_share_total_counts_unreadable_rows { + var out : [160]u8 = undefined; + const w : usize = share_plan("Auto Merge Ready PRs\tworkflow_run\nx\n", 36, &out, 160); + assert w == 110; + assert buf_has(&out, w, "S\tmerger share: 1 of 2 queued runs are the merger's, 1 of them workflow_run; 1 unreadable\n", 90) == true; + assert buf_has(&out, w, "F\tmerger-share\thigh\n", 20) == true; +} diff --git a/specs/claw/push_gate.t27 b/specs/claw/push_gate.t27 new file mode 100644 index 0000000000..627e2bc19c --- /dev/null +++ b/specs/claw/push_gate.t27 @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/claw/push_gate.t27 -- whether a lane push helps the PR or only costs it its place (#10606). +; -- +; Every push to a PR head starts its checks again, the required ones included, at the back of the +; Actions queue. On 2026-10-18 that queue held 800 to 900 runs, and the four required checks of a lane +; PR waited hours for a runner. An Auto-fix event then reported a red claw-sarif on #10585. The red was +; a token error master had already fixed (#10568), and claw-sarif is not a required check. The lane +; merged master and pushed. The push fixed nothing that would land, because the squash merge takes +; master's job anyway. It sent the PR's four pending required checks to the back of the queue. +; This module is the rule that push should have been checked against. +; -- +; push_reason(kind, req_red, own_red, master_fixed, queued), first match wins: +; kind K_NEW (opening a PR or pushing a held branch: a whole new run set) +; queued unread (< 0) or above QUEUE_LIMIT -> hold R_NEW_HELD +; otherwise -> push R_NEW_OK +; kind K_FIX (a push to an open PR because a check on its head is red) +; a required check is red -> push R_REQ_RED the PR cannot merge without it +; the PR's own diff causes the red -> push R_OWN_RED auto-merge would land the break +; master already fixed the red job -> skip R_MASTER_FIXED +; otherwise -> skip R_MASTER_RED the break is master's; fix it there +; A K_FIX push is never held for the queue: a fix that has to land must land, and one that need not +; land is skipped, not held. Any other kind is unreadable and holds (R_BAD), so an unknown caller +; never pushes. +; push_line writes V TAB TAB \n for one reason. +; The honest limit: the three flags are the caller's reading of the PR (which checks are required, +; which job the diff touches, what master's job says). This module decides from them; it does not +; read GitHub. +; ASCII only (L3). phi^2 + 1/phi^2 = 3 | TRINITY + +module ClawPushGate; + +use claw::text; +use claw::refresh; + +/// What the push is. +pub const K_NEW : u8 = 0; +pub const K_FIX : u8 = 1; + +/// Verdicts. +pub const V_PUSH : u8 = 0; +pub const V_HOLD : u8 = 1; +pub const V_SKIP : u8 = 2; + +/// Reasons, one per verdict line. +pub const R_NEW_OK : u8 = 0; +pub const R_NEW_HELD : u8 = 1; +pub const R_REQ_RED : u8 = 2; +pub const R_OWN_RED : u8 = 3; +pub const R_MASTER_FIXED : u8 = 4; +pub const R_MASTER_RED : u8 = 5; +pub const R_BAD : u8 = 6; +pub const R_COUNT : usize = 7; + +/// The verdict of each reason, in reason order. +pub const REASON_VERDICT : [7]u8 = [0, 1, 0, 0, 2, 2, 1]; + +pub const VERDICT_TEXTS : [3]str = ["V\tpush\t", "V\thold\t", "V\tskip\t"]; +pub const VERDICT_LENS : [3]usize = [7, 7, 7]; + +pub const WHY_TEXTS : [7]str = [ + "a new run set fits under the queue limit\n", + "a new PR adds a whole run set; hold it until the queue is at or under the limit\n", + "a required check is red, and the PR cannot merge until a push fixes it\n", + "the PR's own diff breaks the check, and auto-merge would land the break\n", + "master already fixed the red job and the PR does not touch it; the squash lands master's job, so a push would only restart the required checks\n", + "the red job is master's, not this PR's; fix it on master\n", + "unknown push kind; hold\n" +]; +pub const WHY_LENS : [7]usize = [41, 80, 71, 72, 143, 57, 24]; + +/// The reason for one push; its verdict is REASON_VERDICT[reason]. +pub fn push_reason(kind : u8, req_red : bool, own_red : bool, master_fixed : bool, queued : i32) -> u8 { + if (kind == K_NEW) { + if (queued < 0 or queued > QUEUE_LIMIT) { return R_NEW_HELD; } + return R_NEW_OK; + } + if (kind != K_FIX) { return R_BAD; } + if (req_red) { return R_REQ_RED; } + if (own_red) { return R_OWN_RED; } + if (master_fixed) { return R_MASTER_FIXED; } + return R_MASTER_RED; +} + +pub fn push_verdict(kind : u8, req_red : bool, own_red : bool, master_fixed : bool, queued : i32) -> u8 { + return REASON_VERDICT[push_reason(kind, req_red, own_red, master_fixed, queued)]; +} + +/// The verdict line for reason r into out at w, clipped at cap; returns the new cursor, which still +/// advances when clipped. A reason out of range writes the R_BAD line. +pub fn push_line(r : u8, out : []u8, cap : usize, w : usize) -> usize { + var k : usize = r as usize; + if (k >= R_COUNT) { k = R_BAD as usize; } + const v : usize = REASON_VERDICT[k] as usize; + const x : usize = put_str(out, cap, w, VERDICT_TEXTS[v], VERDICT_LENS[v]); + return put_str(out, cap, x, WHY_TEXTS[k], WHY_LENS[k]); +} + +// ---- tests: the cases the lane met on 2026-10-18 ---- + +test the_10585_push_restarted_required_checks_for_a_job_master_had_fixed { + // claw-sarif red, not required, not touched by the PR, fixed on master by #10568; 812 queued. + assert push_reason(K_FIX, false, false, true, 812) == R_MASTER_FIXED; + assert push_verdict(K_FIX, false, false, true, 812) == V_SKIP; +} + +test a_held_new_pr_waits_while_the_queue_is_above_the_limit { + assert push_verdict(K_NEW, false, false, false, 862) == V_HOLD; + assert push_reason(K_NEW, false, false, false, 151) == R_NEW_HELD; +} + +test a_new_pr_goes_at_or_under_the_limit { + assert push_reason(K_NEW, false, false, false, 150) == R_NEW_OK; + assert push_verdict(K_NEW, false, false, false, 0) == V_PUSH; +} + +test an_unread_queue_holds_a_new_pr { + assert push_verdict(K_NEW, false, false, false, -1) == V_HOLD; +} + +test the_limit_is_refresh_limit_and_not_a_copy { + assert QUEUE_LIMIT == 150; + assert push_reason(K_NEW, false, false, false, QUEUE_LIMIT) == R_NEW_OK; + assert push_reason(K_NEW, false, false, false, QUEUE_LIMIT + 1) == R_NEW_HELD; +} + +test a_red_required_check_is_fixed_whatever_the_queue { + assert push_verdict(K_FIX, true, false, true, 900) == V_PUSH; + assert push_reason(K_FIX, true, true, false, -1) == R_REQ_RED; +} + +test the_prs_own_break_is_fixed_before_auto_merge_lands_it { + assert push_reason(K_FIX, false, true, true, 900) == R_OWN_RED; + assert push_verdict(K_FIX, false, true, false, 900) == V_PUSH; +} + +test a_break_that_is_masters_is_not_fixed_on_a_pr { + assert push_reason(K_FIX, false, false, false, 10) == R_MASTER_RED; + assert push_verdict(K_FIX, false, false, false, 10) == V_SKIP; +} + +test a_fix_push_is_never_held_for_the_queue { + var rq : u32 = 0; + while (rq < 2) { + var ow : u32 = 0; + while (ow < 2) { + var mf : u32 = 0; + while (mf < 2) { + assert push_verdict(K_FIX, rq == 1, ow == 1, mf == 1, 900) != V_HOLD; + assert push_verdict(K_FIX, rq == 1, ow == 1, mf == 1, -1) != V_HOLD; + mf += 1; + } + ow += 1; + } + rq += 1; + } +} + +test an_unknown_kind_never_pushes { + assert push_reason(7, true, true, true, 0) == R_BAD; + assert push_verdict(2, false, false, false, 0) == V_HOLD; +} + +test every_reason_has_one_verdict_line { + var out : [256]u8 = undefined; + var r : u8 = 0; + while (r < 7) { + const n : usize = push_line(r, &out, 256, 0); + assert n == 7 + WHY_LENS[r as usize]; + assert out[n - 1] == '\n'; + r += 1; + } +} + +test the_skip_line_says_why_in_full { + var out : [256]u8 = undefined; + const n : usize = push_line(R_MASTER_FIXED, &out, 256, 0); + assert n == 150; + assert buf_has(&out, n, "V\tskip\tmaster already fixed the red job", 39) == true; + assert buf_has(&out, n, "only restart the required checks\n", 33) == true; +} + +test a_clipped_line_still_advances_the_cursor { + var out : [8]u8 = undefined; + const n : usize = push_line(R_NEW_OK, &out, 8, 0); + assert n == 48; + assert out[0] == 'V'; + assert out[6] == '\t'; + assert out[7] == 'a'; +} + +test an_out_of_range_reason_writes_the_hold_line { + var out : [64]u8 = undefined; + const n : usize = push_line(42, &out, 64, 0); + assert n == 31; + assert buf_has(&out, n, "V\thold\tunknown push kind; hold\n", 31) == true; +} diff --git a/specs/claw/queue_cost.t27 b/specs/claw/queue_cost.t27 new file mode 100644 index 0000000000..6459b96a15 --- /dev/null +++ b/specs/claw/queue_cost.t27 @@ -0,0 +1,548 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/claw/queue_cost.t27 -- where the Actions queue's time goes, and what is waiting in it (#10600). +; -- +; On 2026-10-18 the queue held 922 runs and lane PRs stayed BLOCKED for hours. The first guess was +; stale runs on superseded heads (#10590). Measured, they were 25 of 922; 798 were on the PR's current +; head, and one workflow, t27b native, held 88% of the job time. That measurement was taken by hand +; with jq and awk. This module is the measurement, so the next person who asks why the queue is long +; gets the same answer from the same rule and does not blame the wrong cause. +; -- +; Question 1, cost_plan: which workflow holds the runners. Input, one line per finished job, two TAB +; fields: workflow name TAB seconds the job ran (completed_at minus started_at; queue wait excluded) +; Output: +; M TAB queue-cost: min of job time in jobs, unreadable +; H TAB TAB the top holder, when any job time was read +; M TAB queue-cost: one workflow holds % of job time; ... when that share is HOT_PCT or more +; Jobs of one workflow are summed across rows. Ties go to the workflow seen first. Shares round half up. +; -- +; Question 2, class_plan: what the queued runs are waiting for. Input, one line per queued run, five +; TAB fields: event TAB PR state TAB PR isDraft TAB PR head sha TAB run head sha +; Fields a run has no value for are `-`. The class, first match wins: +; other the event is not pull_request or pull_request_target (push, schedule, issues, ...) +; closed the PR is CLOSED or MERGED; its run will judge nothing anybody waits for +; superseded the run is for a head the PR no longer has: a concurrency block would cancel it, +; draft or not +; draft the PR is a draft, and the run is for its current head +; current the run is for the PR's current head: real demand +; Output: +; M TAB queue-class: current head, superseded head, closed PR, draft PR, +; other event, unreadable +; M TAB queue-class: cancelling the runs on superseded heads frees % of the queue +; -- +; Fail closed: a row that is not the right number of fields, an empty name, seconds that are not one +; to six digits or exceed MAX_SECS, a state that is not OPEN, CLOSED or MERGED, a draft flag that is +; not `true` or `false`, a sha that is not 40 lowercase hex digits, or a row past MAX_ROWS is counted +; as unreadable. It is never summed as job time and never counted as a class. A trailing carriage +; return makes the last field unreadable. +; The honest limit: the answer is about the rows it is given. Which runs and jobs were listed, and +; when, is the caller's choice and belongs in the report next to this output. +; ASCII only (L3). phi^2 + 1/phi^2 = 3 | TRINITY + +module ClawQueueCost; + +use claw::text; + +/// Rows one answer reads; more are counted as unreadable, never silently dropped. +pub const MAX_ROWS : u32 = 4096; +/// Distinct workflows cost_plan keeps; a row of a further workflow is counted as unreadable. +pub const MAX_WF : usize = 64; +/// The longest a job can run on a self-hosted runner (5 days); a larger value is a misread. +pub const MAX_SECS : i32 = 432000; +/// At this share or more of the job time, one workflow is named as the lever. +pub const HOT_PCT : u32 = 50; + +/// Run classes. C_BAD is an unreadable row. +pub const C_CURRENT : u8 = 0; +pub const C_SUPERSEDED : u8 = 1; +pub const C_CLOSED : u8 = 2; +pub const C_DRAFT : u8 = 3; +pub const C_OTHER : u8 = 4; +pub const C_BAD : u8 = 5; +pub const C_COUNT : usize = 6; + +pub const COST_HEAD : str = "M\tqueue-cost: "; +pub const COST_HEAD_LEN : usize = 14; +pub const COST_MIN : str = " min of job time in "; +pub const COST_MIN_LEN : usize = 20; +pub const COST_JOBS : str = " jobs, "; +pub const COST_JOBS_LEN : usize = 7; +pub const UNREADABLE : str = " unreadable\n"; +pub const UNREADABLE_LEN : usize = 12; +pub const HOT_HEAD : str = "M\tqueue-cost: one workflow holds "; +pub const HOT_HEAD_LEN : usize = 33; +pub const HOT_TAIL : str = "% of job time; its runtime is the lever, not stale runs\n"; +pub const HOT_TAIL_LEN : usize = 56; + +pub const CLASS_HEAD : str = "M\tqueue-class: "; +pub const CLASS_HEAD_LEN : usize = 15; +/// After each count, in class order; the last one is UNREADABLE. +pub const CLASS_TEXTS : [5]str = [ + " current head, ", " superseded head, ", " closed PR, ", " draft PR, ", " other event, " +]; +pub const CLASS_LENS : [5]usize = [15, 18, 12, 11, 14]; +pub const FREE_HEAD : str = "M\tqueue-class: cancelling the runs on superseded heads frees "; +pub const FREE_HEAD_LEN : usize = 61; +pub const FREE_TAIL : str = "% of the queue\n"; +pub const FREE_TAIL_LEN : usize = 15; + +// ---- bytes and fields ---- + +fn qc_put_byte(dst : []u8, cap : usize, w : usize, b : u8) -> usize { + if (w < cap) { dst[w] = b; } + return w + 1; +} + +/// raw[s..e) into dst at w, clipped at cap; the cursor still advances. +fn qc_put_range(dst : []u8, cap : usize, w : usize, raw : []const u8, s : usize, e : usize) -> usize { + var x : usize = w; + var i : usize = s; + while (i < e) { + x = qc_put_byte(dst, cap, x, raw[i]); + i += 1; + } + return x; +} + +/// The newline that ends the line starting at s, or n. +pub fn qc_line_end(raw : []const u8, n : usize, s : usize) -> usize { + var i : usize = s; + while (i < n) { + if (raw[i] == '\n') { return i; } + i += 1; + } + return n; +} + +fn qc_field_end(raw : []const u8, e : usize, s : usize) -> usize { + var i : usize = s; + while (i < e) { + if (raw[i] == '\t') { return i; } + i += 1; + } + return e; +} + +/// Split raw[ls..le) into exactly nf TAB fields: f[2k], f[2k+1] are the bounds of field k. +pub fn qc_split(raw : []const u8, ls : usize, le : usize, f : []usize, nf : usize) -> bool { + var k : usize = 0; + var s : usize = ls; + while (k < nf) { + const e : usize = qc_field_end(raw, le, s); + f[2 * k] = s; + f[2 * k + 1] = e; + k += 1; + if (k < nf) { + if (e >= le) { return false; } + s = e + 1; + } else { + return e == le; + } + } + return false; +} + +/// raw[a..ae) and raw[b..be) hold the same bytes. +pub fn qc_range_eq(raw : []const u8, a : usize, ae : usize, b : usize, be : usize) -> bool { + if (ae - a != be - b) { return false; } + var i : usize = 0; + while (a + i < ae) { + if (raw[a + i] != raw[b + i]) { return false; } + i += 1; + } + return true; +} + +/// raw[s..e) is exactly the literal. +pub fn qc_range_is(raw : []const u8, s : usize, e : usize, lit : str, lit_len : usize) -> bool { + if (e - s != lit_len) { return false; } + var i : usize = 0; + while (i < lit_len) { + if (raw[s + i] != lit[i]) { return false; } + i += 1; + } + return true; +} + +/// Seconds in raw[s..e): one to six decimal digits, at most MAX_SECS; -1 otherwise. +pub fn qc_secs(raw : []const u8, s : usize, e : usize) -> i32 { + if (e <= s or e - s > 6) { return -1; } + var v : i32 = 0; + var i : usize = s; + while (i < e) { + const b : u8 = raw[i]; + if (b < '0' or b > '9') { return -1; } + v = v * 10 + ((b - '0') as i32); + i += 1; + } + if (v > MAX_SECS) { return -1; } + return v; +} + +/// raw[s..e) is 40 lowercase hex digits. +pub fn qc_sha_ok(raw : []const u8, s : usize, e : usize) -> bool { + if (e - s != 40) { return false; } + var i : usize = s; + while (i < e) { + const b : u8 = raw[i]; + const dig : bool = b >= '0' and b <= '9'; + const hex : bool = b >= 'a' and b <= 'f'; + if (dig == false and hex == false) { return false; } + i += 1; + } + return true; +} + +/// part of total in whole percent, rounded half up; 0 when total is 0. +pub fn share_pct(part : u64, total : u64) -> u32 { + if (total == 0) { return 0; } + return ((part * 200 + total) / (total * 2)) as u32; +} + +// ---- question 1: which workflow holds the runners ---- + +/// The answer for the job rows in raw[0..n), into out. Returns the bytes it needs; when that exceeds +/// cap the buffer holds a clipped answer. +pub fn cost_plan(raw : []const u8, n : usize, out : []u8, cap : usize) -> usize { + var ns : [64]usize = undefined; + var ne : [64]usize = undefined; + var sec : [64]u64 = undefined; + var f : [4]usize = undefined; + var nw : usize = 0; + var jobs : u32 = 0; + var bad : u32 = 0; + var total : u64 = 0; + var ls : usize = 0; + while (ls < n) { + const le : usize = qc_line_end(raw, n, ls); + if (le > ls) { + var ok : bool = false; + if (jobs + bad < MAX_ROWS and qc_split(raw, ls, le, &f, 2)) { + const s : i32 = qc_secs(raw, f[2], f[3]); + if (s >= 0 and f[1] > f[0]) { + var hit : usize = nw; + var k : usize = 0; + while (k < nw) { + if (hit == nw and qc_range_eq(raw, ns[k], ne[k], f[0], f[1])) { hit = k; } + k += 1; + } + if (hit == nw and nw < MAX_WF) { + ns[nw] = f[0]; + ne[nw] = f[1]; + sec[nw] = 0; + nw += 1; + } + if (hit < nw) { + sec[hit] = sec[hit] + (s as u64); + total = total + (s as u64); + jobs += 1; + ok = true; + } + } + } + if (ok == false) { bad += 1; } + } + ls = le + 1; + } + var w : usize = put_str(out, cap, 0, COST_HEAD, COST_HEAD_LEN); + w = put_count(out, cap, w, (total / 60) as u32); + w = put_str(out, cap, w, COST_MIN, COST_MIN_LEN); + w = put_count(out, cap, w, jobs); + w = put_str(out, cap, w, COST_JOBS, COST_JOBS_LEN); + w = put_count(out, cap, w, bad); + w = put_str(out, cap, w, UNREADABLE, UNREADABLE_LEN); + if (total == 0) { return w; } + var top : usize = 0; + var j : usize = 1; + while (j < nw) { + if (sec[j] > sec[top]) { top = j; } + j += 1; + } + const pct : u32 = share_pct(sec[top], total); + w = qc_put_byte(out, cap, w, 'H'); + w = qc_put_byte(out, cap, w, '\t'); + w = qc_put_range(out, cap, w, raw, ns[top], ne[top]); + w = qc_put_byte(out, cap, w, '\t'); + w = put_count(out, cap, w, pct); + w = qc_put_byte(out, cap, w, '\n'); + if (pct >= HOT_PCT) { + w = put_str(out, cap, w, HOT_HEAD, HOT_HEAD_LEN); + w = put_count(out, cap, w, pct); + w = put_str(out, cap, w, HOT_TAIL, HOT_TAIL_LEN); + } + return w; +} + +// ---- question 2: what the queued runs wait for ---- + +/// The class of one split run row (ten bounds in f). +pub fn run_class(raw : []const u8, f : []const usize) -> u8 { + const pr : bool = qc_range_is(raw, f[0], f[1], "pull_request", 12) or qc_range_is(raw, f[0], f[1], "pull_request_target", 19); + if (pr == false) { + if (f[1] > f[0]) { return C_OTHER; } + return C_BAD; + } + const open : bool = qc_range_is(raw, f[2], f[3], "OPEN", 4); + const closed : bool = qc_range_is(raw, f[2], f[3], "CLOSED", 6) or qc_range_is(raw, f[2], f[3], "MERGED", 6); + if (closed) { return C_CLOSED; } + if (open == false) { return C_BAD; } + const dt : bool = qc_range_is(raw, f[4], f[5], "true", 4); + const df : bool = qc_range_is(raw, f[4], f[5], "false", 5); + if (dt == false and df == false) { return C_BAD; } + if (qc_sha_ok(raw, f[6], f[7]) == false or qc_sha_ok(raw, f[8], f[9]) == false) { return C_BAD; } + if (qc_range_eq(raw, f[6], f[7], f[8], f[9]) == false) { return C_SUPERSEDED; } + if (dt) { return C_DRAFT; } + return C_CURRENT; +} + +/// The answer for the queued-run rows in raw[0..n), into out. Returns the bytes it needs. +pub fn class_plan(raw : []const u8, n : usize, out : []u8, cap : usize) -> usize { + var st : [6]u32 = undefined; + var f : [10]usize = undefined; + var c : usize = 0; + while (c < C_COUNT) { + st[c] = 0; + c += 1; + } + var rows : u32 = 0; + var ls : usize = 0; + while (ls < n) { + const le : usize = qc_line_end(raw, n, ls); + if (le > ls) { + var d : u8 = C_BAD; + if (rows < MAX_ROWS and qc_split(raw, ls, le, &f, 5)) { d = run_class(raw, &f); } + st[d] += 1; + rows += 1; + } + ls = le + 1; + } + var w : usize = put_str(out, cap, 0, CLASS_HEAD, CLASS_HEAD_LEN); + var i : usize = 0; + while (i < 5) { + w = put_count(out, cap, w, st[i]); + w = put_str(out, cap, w, CLASS_TEXTS[i], CLASS_LENS[i]); + i += 1; + } + w = put_count(out, cap, w, st[C_BAD]); + w = put_str(out, cap, w, UNREADABLE, UNREADABLE_LEN); + const read : u64 = (rows - st[C_BAD]) as u64; + if (read > 0) { + w = put_str(out, cap, w, FREE_HEAD, FREE_HEAD_LEN); + w = put_count(out, cap, w, share_pct(st[C_SUPERSEDED] as u64, read)); + w = put_str(out, cap, w, FREE_TAIL, FREE_TAIL_LEN); + } + return w; +} + +// ---- test helpers: a row from small codes, so a test states the case and not the bytes ---- + +fn t_job(dst : []u8, cap : usize, w : usize, name : str, name_len : usize, secs : u32) -> usize { + var x : usize = put_str(dst, cap, w, name, name_len); + x = qc_put_byte(dst, cap, x, '\t'); + x = put_count(dst, cap, x, secs); + return qc_put_byte(dst, cap, x, '\n'); +} + +fn t_sha(dst : []u8, cap : usize, w : usize, b : u8) -> usize { + var x : usize = w; + var i : usize = 0; + while (i < 40) { + x = qc_put_byte(dst, cap, x, b); + i += 1; + } + return x; +} + +/// Test-only kind: a draft PR's run on a head it no longer has. +const T_DRAFT_OLD : u8 = 6; + +/// One queued run of class kind (the C_ codes, or T_DRAFT_OLD); C_BAD writes a run whose shas are +/// three digits. +fn t_run(dst : []u8, cap : usize, w : usize, kind : u8) -> usize { + var x : usize = w; + if (kind == C_OTHER) { return put_str(dst, cap, x, "push\t-\t-\t-\t-\n", 13); } + if (kind == C_CLOSED) { return put_str(dst, cap, x, "pull_request_target\tMERGED\t-\t-\t-\n", 33); } + if (kind == C_BAD) { return put_str(dst, cap, x, "pull_request\tOPEN\tfalse\tabc\tabc\n", 32); } + x = put_str(dst, cap, x, "pull_request\tOPEN\t", 18); + if (kind == C_DRAFT or kind == T_DRAFT_OLD) { x = put_str(dst, cap, x, "true\t", 5); } else { x = put_str(dst, cap, x, "false\t", 6); } + x = t_sha(dst, cap, x, 'a'); + x = qc_put_byte(dst, cap, x, '\t'); + if (kind == C_SUPERSEDED or kind == T_DRAFT_OLD) { x = t_sha(dst, cap, x, 'b'); } else { x = t_sha(dst, cap, x, 'a'); } + return qc_put_byte(dst, cap, x, '\n'); +} + +fn t_runs(dst : []u8, cap : usize, w : usize, kind : u8, count : u32) -> usize { + var x : usize = w; + var i : u32 = 0; + while (i < count) { + x = t_run(dst, cap, x, kind); + i += 1; + } + return x; +} + +// ---- tests: the arithmetic ---- + +test a_share_rounds_half_up_and_is_zero_without_time { + assert share_pct(637, 726) == 88; + assert share_pct(1, 2) == 50; + assert share_pct(1, 3) == 33; + assert share_pct(2, 3) == 67; + assert share_pct(25, 922) == 3; + assert share_pct(5, 0) == 0; + assert share_pct(7, 7) == 100; +} + +test seconds_are_one_to_six_digits_and_at_most_five_days { + var r : [64]u8 = undefined; + const n : usize = put_str(&r, 64, 0, "0 504 432000 432001 1234567 5a ", 32); + assert n == 32; + assert qc_secs(&r, 0, 1) == 0; + assert qc_secs(&r, 2, 5) == 504; + assert qc_secs(&r, 6, 12) == 432000; + assert qc_secs(&r, 13, 19) == -1; + assert qc_secs(&r, 20, 27) == -1; + assert qc_secs(&r, 28, 30) == -1; + assert qc_secs(&r, 30, 30) == -1; +} + +// ---- tests: question 1 ---- + +test the_2026_10_18_job_time_names_t27b_native_at_88_percent { + var r : [512]u8 = undefined; + var n : usize = t_job(&r, 512, 0, "t27b native", 11, 30240); + n = t_job(&r, 512, n, "Emit Bit-Exact Gate", 19, 2100); + n = t_job(&r, 512, n, "t27b native", 11, 7980); + n = t_job(&r, 512, n, "Seal Coverage", 13, 1440); + n = t_job(&r, 512, n, "t27b native", 11, 0); + n = t_job(&r, 512, n, "every other workflow", 20, 1800); + var out : [512]u8 = undefined; + const d : usize = cost_plan(&r, n, &out, 512); + assert d <= 512; + assert buf_has(&out, d, "M\tqueue-cost: 726 min of job time in 6 jobs, 0 unreadable\n", 58) == true; + assert buf_has(&out, d, "H\tt27b native\t88\n", 17) == true; + assert buf_has(&out, d, "M\tqueue-cost: one workflow holds 88% of job time; its runtime is the lever, not stale runs\n", 91) == true; +} + +test a_workflow_under_half_the_time_is_named_without_a_verdict { + var r : [256]u8 = undefined; + var n : usize = t_job(&r, 256, 0, "a", 1, 40); + n = t_job(&r, 256, n, "b", 1, 35); + n = t_job(&r, 256, n, "c", 1, 25); + var out : [512]u8 = undefined; + const d : usize = cost_plan(&r, n, &out, 512); + assert buf_has(&out, d, "H\ta\t40\n", 7) == true; + assert buf_has(&out, d, "one workflow holds", 18) == false; +} + +test a_tie_goes_to_the_workflow_seen_first { + var r : [256]u8 = undefined; + var n : usize = t_job(&r, 256, 0, "late", 4, 10); + n = t_job(&r, 256, n, "early", 5, 60); + n = t_job(&r, 256, n, "late", 4, 50); + var out : [512]u8 = undefined; + const d : usize = cost_plan(&r, n, &out, 512); + assert buf_has(&out, d, "H\tlate\t50\n", 10) == true; + assert buf_has(&out, d, "H\tearly", 7) == false; +} + +test an_unreadable_job_row_is_counted_and_never_summed { + var r : [256]u8 = undefined; + const n : usize = put_str(&r, 256, 0, "x\t12a\nx\n\t5\nx\t5\textra\nx\t9999999\nx\t-1\n", 36); + var out : [512]u8 = undefined; + const d : usize = cost_plan(&r, n, &out, 512); + assert buf_has(&out, d, "M\tqueue-cost: 0 min of job time in 0 jobs, 6 unreadable\n", 56) == true; + assert t_has_line(&out, d, 'H') == false; +} + +test a_carriage_return_makes_the_seconds_unreadable { + var r : [64]u8 = undefined; + const n : usize = put_str(&r, 64, 0, "x\t600\r\ny\t60\n", 12); + var out : [512]u8 = undefined; + const d : usize = cost_plan(&r, n, &out, 512); + assert buf_has(&out, d, " in 1 jobs, 1 unreadable\n", 25) == true; + assert buf_has(&out, d, "H\ty\t100\n", 8) == true; +} + +test a_clipped_answer_still_reports_its_full_length { + var r : [64]u8 = undefined; + const n : usize = t_job(&r, 64, 0, "w", 1, 120); + var big : [512]u8 = undefined; + const full : usize = cost_plan(&r, n, &big, 512); + var small : [8]u8 = undefined; + const d : usize = cost_plan(&r, n, &small, 8); + assert d == full; + assert d > 8; + assert small[0] == 'M'; +} + +// ---- tests: question 2 ---- + +test each_kind_of_queued_run_gets_its_class { + var r : [1024]u8 = undefined; + var n : usize = t_run(&r, 1024, 0, C_CURRENT); + n = t_run(&r, 1024, n, C_SUPERSEDED); + n = t_run(&r, 1024, n, C_CLOSED); + n = t_run(&r, 1024, n, C_DRAFT); + n = t_run(&r, 1024, n, C_OTHER); + n = t_run(&r, 1024, n, C_BAD); + var out : [512]u8 = undefined; + const d : usize = class_plan(&r, n, &out, 512); + assert buf_has(&out, d, "M\tqueue-class: 1 current head, 1 superseded head, 1 closed PR, 1 draft PR, 1 other event, 1 unreadable\n", 103) == true; +} + +test a_short_sha_is_unreadable_and_never_current { + var r : [256]u8 = undefined; + const n : usize = t_run(&r, 256, 0, C_BAD); + var out : [512]u8 = undefined; + const d : usize = class_plan(&r, n, &out, 512); + assert buf_has(&out, d, " 0 current head, ", 17) == true; + assert buf_has(&out, d, " 1 unreadable\n", 14) == true; + assert buf_has(&out, d, "frees", 5) == false; +} + +test an_unknown_state_or_flag_is_unreadable { + var r : [256]u8 = undefined; + var n : usize = put_str(&r, 256, 0, "pull_request\tOPENED\tfalse\t-\t-\n", 30); + n = put_str(&r, 256, n, "pull_request\tOPEN\tmaybe\t-\t-\n", 28); + n = put_str(&r, 256, n, "\t-\t-\t-\t-\n", 9); + n = put_str(&r, 256, n, "push\t-\t-\t-\n", 11); + var out : [512]u8 = undefined; + const d : usize = class_plan(&r, n, &out, 512); + assert buf_has(&out, d, " 0 other event, 4 unreadable\n", 29) == true; +} + +test a_draft_run_on_a_superseded_head_counts_as_superseded { + var r : [256]u8 = undefined; + const n : usize = t_run(&r, 256, 0, T_DRAFT_OLD); + var out : [512]u8 = undefined; + const d : usize = class_plan(&r, n, &out, 512); + assert buf_has(&out, d, " 1 superseded head, 0 closed PR, 0 draft PR, ", 45) == true; +} + +/// The 2026-10-18 18:25 UTC queue: 922 runs. The first table published in #10590 counted the 97 +/// draft runs twice (94 on a current head, 3 on a superseded one) and summed to 1019; one row is +/// one class here, so the counts below sum to 922. +test the_2026_10_18_queue_is_mostly_current_heads { + var r : [131072]u8 = undefined; + var n : usize = t_runs(&r, 131072, 0, C_CURRENT, 704); + n = t_runs(&r, 131072, n, C_SUPERSEDED, 22); + n = t_runs(&r, 131072, n, T_DRAFT_OLD, 3); + n = t_runs(&r, 131072, n, C_CLOSED, 13); + n = t_runs(&r, 131072, n, C_DRAFT, 94); + n = t_runs(&r, 131072, n, C_OTHER, 86); + assert n <= 131072; + var out : [512]u8 = undefined; + const d : usize = class_plan(&r, n, &out, 512); + assert buf_has(&out, d, "M\tqueue-class: 704 current head, 25 superseded head, 13 closed PR, 94 draft PR, 86 other event, 0 unreadable\n", 109) == true; + assert buf_has(&out, d, "M\tqueue-class: cancelling the runs on superseded heads frees 3% of the queue\n", 77) == true; +} + +/// Some line of out[0..n) starts with byte b. +fn t_has_line(out : []const u8, n : usize, b : u8) -> bool { + var i : usize = 0; + while (i < n) { + if (out[i] == b and (i == 0 or out[i - 1] == '\n')) { return true; } + i += 1; + } + return false; +} diff --git a/specs/claw/required_red.t27 b/specs/claw/required_red.t27 new file mode 100644 index 0000000000..e41f8aee88 --- /dev/null +++ b/specs/claw/required_red.t27 @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/claw/required_red.t27 -- push_gate's req_red flag, read from the rollup (#10606, #10617). +; -- +; push_gate.t27 takes req_red ("a required check is red") from its caller. Until now the lane set it +; by eye, from a PR Dashboard line or an Auto-fix event. On 2026-10-18 that eye pushed #10585 for a +; red claw-sarif, which is not required (#10606), and the dashboard called 21 queued checks failing +; (#10617). This module computes the flag from the same rows check_latest.t27 reads. +; -- +; Input: check_latest's rows, name TAB run TAB status TAB conclusion (name is workflow/job). +; A row is required when its job (the part of name after the last `/`, or all of it) is exactly +; validate, check-linked-issue, parse-ratchet or own-language. Only live rows count: a re-run +; supersedes the run it replaces (check_latest). Live required rows are classed by check_state. +; A required check with no row at all is missing; missing is not red. +; Any row this module cannot read counts as unreadable, whatever its name: its name cannot be +; trusted, so it might be a required check. +; Output, required_plan: +; R TAB required:

pass, failing, pending, missing, unreadable rows +; F TAB req_red TAB true | false | unknown +; req_red is true when a live required check fails; otherwise unknown when any row is unreadable +; (a reader must look before pushing); otherwise false. +; The honest limit: the required list is the one the lane observed on 2026-10-18; the repository has +; no branch protection API answer for master (404), so this list is not read from GitHub. +; ASCII only (L3). phi^2 + 1/phi^2 = 3 | TRINITY + +module ClawRequiredRed; + +use claw::text; +use claw::queue_cost; +use claw::check_state; +use claw::check_latest; + +pub const RR_FALSE : u8 = 0; +pub const RR_TRUE : u8 = 1; +pub const RR_UNKNOWN : u8 = 2; + +/// Required checks; the slot after them in a counts array holds the missing count. +pub const RR_REQUIRED : u8 = 4; +pub const RR_MISSING : usize = 4; + +pub const RR_HEAD : str = "R\trequired: "; +pub const RR_HEAD_LEN : usize = 12; +pub const RR_COUNT_TEXTS : [4]str = [" pass, ", " failing, ", " pending, ", " missing, "]; +pub const RR_COUNT_LENS : [4]usize = [7, 10, 10, 10]; +pub const RR_FLAG_HEAD : str = "F\treq_red\t"; +pub const RR_FLAG_HEAD_LEN : usize = 10; +pub const RR_FLAG_TEXTS : [3]str = ["false\n", "true\n", "unknown\n"]; +pub const RR_FLAG_LENS : [3]usize = [6, 5, 8]; + +/// Where the job part of raw[s..e) starts: after the last `/`, or s. +pub fn job_start(raw : []const u8, s : usize, e : usize) -> usize { + var j : usize = s; + var i : usize = s; + while (i < e) { + if (raw[i] == '/') { j = i + 1; } + i += 1; + } + return j; +} + +/// The required check named by raw[s..e), or RR_REQUIRED when it is not one. +pub fn required_index(raw : []const u8, s : usize, e : usize) -> u8 { + const j : usize = job_start(raw, s, e); + if (qc_range_is(raw, j, e, "validate", 8)) { return 0; } + if (qc_range_is(raw, j, e, "check-linked-issue", 18)) { return 1; } + if (qc_range_is(raw, j, e, "parse-ratchet", 13)) { return 2; } + if (qc_range_is(raw, j, e, "own-language", 12)) { return 3; } + return RR_REQUIRED; +} + +/// Live required rows by class in st[K_PASS..K_WAIT]; every unreadable row in st[K_BAD]; required +/// checks with no row in st[RR_MISSING]. Returns the number of rows. +pub fn required_counts(raw : []const u8, n : usize, st : []u32) -> u32 { + var f : [8]usize = undefined; + var c : [4]usize = undefined; + var seen : [4]u32 = undefined; + var k : usize = 0; + while (k < 5) { + st[k] = 0; + k += 1; + } + k = 0; + while (k < 4) { + seen[k] = 0; + k += 1; + } + var rows : u32 = 0; + var ls : usize = 0; + while (ls < n) { + const le : usize = qc_line_end(raw, n, ls); + if (le > ls) { + var readable : bool = false; + if (rows < CHECK_MAX_ROWS and qc_split(raw, ls, le, &f, 4)) { + if (row_ok(raw, &f)) { + readable = true; + const r : u8 = required_index(raw, f[0], f[1]); + if (r < RR_REQUIRED) { + seen[r] += 1; + if (is_superseded(raw, n, ls, &f) == false) { + c[0] = f[4]; + c[1] = f[5]; + c[2] = f[6]; + c[3] = f[7]; + st[check_class(raw, &c)] += 1; + } + } + } + } + if (readable == false) { st[K_BAD] += 1; } + rows += 1; + } + ls = le + 1; + } + k = 0; + while (k < 4) { + if (seen[k] == 0) { st[RR_MISSING] += 1; } + k += 1; + } + return rows; +} + +/// The flag from the counts: a known red wins over an unreadable row. +pub fn req_red_flag(st : []const u32) -> u8 { + if (st[K_FAIL] > 0) { return RR_TRUE; } + if (st[K_BAD] > 0) { return RR_UNKNOWN; } + return RR_FALSE; +} + +pub fn required_flag(raw : []const u8, n : usize) -> u8 { + var st : [5]u32 = undefined; + const rows : u32 = required_counts(raw, n, &st); + if (rows == 0) { return RR_FALSE; } + return req_red_flag(&st); +} + +/// The two output lines into out, clipped at cap; returns the cursor, which still advances when +/// clipped. +pub fn required_plan(raw : []const u8, n : usize, out : []u8, cap : usize) -> usize { + var st : [5]u32 = undefined; + const rows : u32 = required_counts(raw, n, &st); + var w : usize = put_str(out, cap, 0, RR_HEAD, RR_HEAD_LEN); + w = put_count(out, cap, w, st[K_PASS]); + w = put_str(out, cap, w, RR_COUNT_TEXTS[0], RR_COUNT_LENS[0]); + w = put_count(out, cap, w, st[K_FAIL]); + w = put_str(out, cap, w, RR_COUNT_TEXTS[1], RR_COUNT_LENS[1]); + w = put_count(out, cap, w, st[K_WAIT]); + w = put_str(out, cap, w, RR_COUNT_TEXTS[2], RR_COUNT_LENS[2]); + w = put_count(out, cap, w, st[RR_MISSING]); + w = put_str(out, cap, w, RR_COUNT_TEXTS[3], RR_COUNT_LENS[3]); + w = put_count(out, cap, w, st[K_BAD]); + w = put_str(out, cap, w, " unreadable rows\n", 17); + w = put_str(out, cap, w, RR_FLAG_HEAD, RR_FLAG_HEAD_LEN); + var v : usize = RR_FALSE as usize; + if (rows > 0) { v = req_red_flag(&st) as usize; } + return put_str(out, cap, w, RR_FLAG_TEXTS[v], RR_FLAG_LENS[v]); +} + +// ---- test helpers ---- + +/// #10563's required checks on 2026-10-19: the old check-linked-issue failure, its queued re-run, +/// and the three that passed. +fn t_10563(dst : []u8, cap : usize) -> usize { + var x : usize = put_str(dst, cap, 0, "Issue Gate/check-linked-issue\t39151562346\tCOMPLETED\tFAILURE\n", 60); + x = put_str(dst, cap, x, "Issue Gate/check-linked-issue\t39157315158\tQUEUED\t-\n", 51); + x = put_str(dst, cap, x, "CI/validate\t11\tCOMPLETED\tSUCCESS\n", 33); + x = put_str(dst, cap, x, "Parse/parse-ratchet\t12\tCOMPLETED\tSUCCESS\n", 41); + return put_str(dst, cap, x, "Own/own-language\t13\tCOMPLETED\tSUCCESS\n", 38); +} + +fn t_flag2(a : str, al : usize, b : str, bl : usize) -> u8 { + var raw : [128]u8 = undefined; + var x : usize = put_str(&raw, 128, 0, a, al); + x = put_str(&raw, 128, x, b, bl); + return required_flag(&raw, x); +} + +// ---- tests ---- + +test the_10563_old_failure_next_to_its_rerun_is_not_req_red { + var raw : [256]u8 = undefined; + const n : usize = t_10563(&raw, 256); + assert n == 223; + var st : [5]u32 = undefined; + assert required_counts(&raw, n, &st) == 5; + assert st[K_PASS] == 3; + assert st[K_FAIL] == 0; + assert st[K_WAIT] == 1; + assert st[K_BAD] == 0; + assert st[RR_MISSING] == 0; + assert required_flag(&raw, n) == RR_FALSE; +} + +test the_10563_plan_text { + var raw : [256]u8 = undefined; + const n : usize = t_10563(&raw, 256); + var out : [128]u8 = undefined; + const w : usize = required_plan(&raw, n, &out, 128); + assert w == 87; + assert buf_has(&out, w, "R\trequired: 3 pass, 0 failing, 1 pending, 0 missing, 0 unreadable rows\n", 71) == true; + assert buf_has(&out, w, "F\treq_red\tfalse\n", 16) == true; +} + +test a_red_check_that_is_not_required_is_not_req_red { + assert t_flag2("CI/validate\t11\tCOMPLETED\tSUCCESS\n", 33, "Claw/claw-sarif\t14\tCOMPLETED\tFAILURE\n", 37) == RR_FALSE; +} + +test a_red_required_check_is_req_red { + assert t_flag2("CI/validate\t15\tCOMPLETED\tFAILURE\n", 33, "Claw/claw-sarif\t14\tCOMPLETED\tFAILURE\n", 37) == RR_TRUE; + assert t_flag2("own-language\t-\tCONTEXT\tERROR\n", 29, "CI/validate\t11\tCOMPLETED\tSUCCESS\n", 33) == RR_TRUE; +} + +test a_queued_rerun_clears_a_red_required_check_in_any_order { + assert t_flag2("CI/validate\t9\tCOMPLETED\tFAILURE\n", 32, "CI/validate\t10\tQUEUED\t-\n", 24) == RR_FALSE; + assert t_flag2("CI/validate\t10\tQUEUED\t-\n", 24, "CI/validate\t9\tCOMPLETED\tFAILURE\n", 32) == RR_FALSE; +} + +test the_job_name_must_match_exactly { + assert t_flag2("X/validate-extra\t16\tCOMPLETED\tFAILURE\n", 38, "CI/validate\t11\tCOMPLETED\tSUCCESS\n", 33) == RR_FALSE; + assert t_flag2("validate\t17\tCOMPLETED\tFAILURE\n", 30, "Claw/claw-sarif\t14\tCOMPLETED\tFAILURE\n", 37) == RR_TRUE; + assert required_index("A/b/own-language", 0, 16) == 3; + assert required_index("own-language/x", 0, 14) == RR_REQUIRED; +} + +test an_unreadable_row_makes_it_unknown_and_a_known_red_still_wins { + assert t_flag2("CI/validate\t1x\tCOMPLETED\tFAILURE\n", 33, "CI/validate\t11\tCOMPLETED\tSUCCESS\n", 33) == RR_UNKNOWN; + assert t_flag2("CI/validate\t1x\tCOMPLETED\tFAILURE\n", 33, "CI/validate\t15\tCOMPLETED\tFAILURE\n", 33) == RR_TRUE; +} + +test missing_required_checks_are_counted_and_are_not_red { + var st : [5]u32 = undefined; + assert required_counts("Claw/claw-sarif\t14\tCOMPLETED\tFAILURE\n", 37, &st) == 1; + assert st[RR_MISSING] == 4; + assert st[K_FAIL] == 0; + assert required_flag("", 0) == RR_FALSE; +} + +test a_clipped_plan_still_advances_the_cursor { + var raw : [256]u8 = undefined; + const n : usize = t_10563(&raw, 256); + var out : [128]u8 = undefined; + assert required_plan(&raw, n, &out, 40) == 87; +}