diff --git a/.claude/skills/t27b-loop/SKILL.md b/.claude/skills/t27b-loop/SKILL.md index 6f88e5c0e4..8bfaff0b5a 100644 --- a/.claude/skills/t27b-loop/SKILL.md +++ b/.claude/skills/t27b-loop/SKILL.md @@ -65,7 +65,9 @@ Take the first unchecked item of epic #8326 that is not already claimed, either - a fresh `git worktree add --detach /data/w- FETCH_HEAD` from the lab clone; - `cargo build --release -p t27c` with a shared `CARGO_TARGET_DIR`. - **Bring the files back to a sparse worktree on the Mac.** `git worktree add --no-checkout`, `git sparse-checkout set --no-cone `, then copy each file with `ssh ... cat`. `gen/` is gitignored: use `git add -f`. -- **Every new spec in a PR also gets its row** in `docs/reports/t27b_expectations.json`. A missing row turns master's `t27b-native-ratchet` red for everyone (UNLISTED). That happened 6 times in 30 minutes on 2026-10-09. +- **Ledger rows: the steward writes them.** The t27b ledger steward (`specs/jobs/t27b_ledger_steward.t27`, rules in `specs/tri/t27b/ledger_steward.t27`, #8654) adds the row from master's next run after a spec PR merges, and opens one ledger PR with auto-merge. Once the Queen runs that card (see gHashTag/trios#1751), spec PRs leave `docs/reports/t27b_expectations.json` alone. + - **Until then, every new spec in a PR still gets its row.** A missing row turns master's `t27b-native-ratchet` red for everyone (UNLISTED). That happened 6 times in 30 minutes on 2026-10-09. + - **The steward refuses** UNEXPECTED FAILURE, MOVED, BAD REASON, OVER CAP, a defect where the reference passes, a cap rise larger than the new rows, and more than 16 STALE rows. For a refused run it opens one issue labelled `t27b-ledger-steward` and no PR. Those issues are this loop's to read. - **Receipts:** while master moves every 10-20 minutes, compare a lane's squash commit on master against its parent after the merge: - `cd /work/t27 && /work/t27c-master corpus-receipt compare BASE.receipt.json HEAD.receipt.json --challenge-head ` on t27b-lab; - a master receipt carries no nonce and is accepted as BASE; diff --git a/.trinity/seals/jobs_JobT27bLedgerSteward.json b/.trinity/seals/jobs_JobT27bLedgerSteward.json new file mode 100644 index 0000000000..28400547fc --- /dev/null +++ b/.trinity/seals/jobs_JobT27bLedgerSteward.json @@ -0,0 +1,27 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+55eb214ac", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:ac9254171852a74a08f26cee469556437586afc64ff5d04662493e1b07fd122b", + "gen_hash_rust": "sha256:ed77d5abb6c3a351e727532d015617c31ea4ee337554970c03adcabc187398f4", + "gen_hash_verilog": "sha256:34d6ca3be4983dbdcea17448983cd327c17e8032a9108e06e263af180a84d899", + "gen_hash_zig": "sha256:18a382e630aa893eb6da3f1885b5eb72f705fe28491e6461e64b62643e8ae0a8", + "module": "JobT27bLedgerSteward", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-10T14:43:21Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:869202696ec9a11cd45189276828631c767967d44ee6ba7f91404ba214e43a27", + "spec_path": "specs/jobs/t27b_ledger_steward.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 2, + "total": 2 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27b_T27bLedgerSteward.json b/.trinity/seals/t27b_T27bLedgerSteward.json new file mode 100644 index 0000000000..2c88da5f25 --- /dev/null +++ b/.trinity/seals/t27b_T27bLedgerSteward.json @@ -0,0 +1,32 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+55eb214ac", + "closure": [ + { + "spec": "specs/tri/t27b/steward.t27", + "spec_hash": "sha256:70867c8cd8a8155d82f352b6833168f04c846a736413d6b2420d54774994123e" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:e68d532298ecd4aac96e32088f179d581147705775933c8f93ebda82221aabab", + "gen_hash_rust": "sha256:d8faa3518e7703b7f8c7e3cac99fce073a4ae01c1a7c3f1b0195e502226d3b74", + "gen_hash_verilog": "sha256:806a630d9e2970ef50e7ea4b777b101b6c24616736caa484b3dc012f55c914f9", + "gen_hash_zig": "sha256:cf898366b4bc6cd30fc0232d7dde252a94afaf03e0868d783e15ef7bf4338cd9", + "module": "T27bLedgerSteward", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-10T14:40:39Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:e2a58a7ef6fb10c371af1dcb8cf02cd1f206f3327dc4851546f5eb4696ce4132", + "spec_path": "specs/tri/t27b/ledger_steward.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 10, + "total": 10 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/now/2026-10-10-t27b-ledger-steward.md b/docs/now/2026-10-10-t27b-ledger-steward.md new file mode 100644 index 0000000000..12e1203647 --- /dev/null +++ b/docs/now/2026-10-10-t27b-ledger-steward.md @@ -0,0 +1,17 @@ +# NOW -- the t27b ledger steward: master's run writes the ledger rows, spec PRs stop touching them (2026-10-10) + +## specs/tri/t27b/ledger_steward.t27, specs/jobs/t27b_ledger_steward.t27 (Closes #8654; see #8637 item 1, #8321 option 3) + +- **The rules.** `ledger_steward.t27` decides what the steward does with each ratchet finding on a master run. It applies UNLISTED (a non-pass row gets reason `unimplemented`), UNEXPECTED PASS (a gain) and STALE (the row is dropped). The cap may rise by at most the new non-pass rows (steward.t27 `cap_rise_is_new`). + - It refuses UNEXPECTED FAILURE, MOVED, BAD REASON, OVER CAP, a defect where the reference passes, a cap rise larger than the new rows, and more than 16 STALE rows in one run. A refused run gets one issue and no PR. + - The tests use the three hand batches as vectors. #8289 and #8646 are applied, with caps 18 -> 20 and 25 -> 26. #8548 is refused for its two failures; without them it is applied, with cap 23 -> 25. +- **The job card.** `t27b_ledger_steward.t27` is a jobs.t27 card with eight steps: + - CHECK the run is a counted master run on master's ledger; + - EFFECT the refusal issue; + - CHECK every finding is appliable; + - EFFECT the ledger issue, the composed ledger, the PR, auto-merge; + - WAIT merged. +- **Measured on the t27c lab:** + - `t27c test-report`: 10 tests with 0 vacuous, and 2 with 0 vacuous; + - `t27b corpus --blockers --reference` under qemu: both pass, with 108 and 26 runtime asserts and 0 reference disagreements. +- **Runtime.** The Queen's job runner (gHashTag/trios `queen-jobs.ts`) runs only the release card today. The missing loader, trigger, executors, journal kinds and credential are listed in gHashTag/trios#1751. Until it runs, spec PRs keep adding their own rows. diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 7bce233841..371ca2b9a8 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -363,6 +363,7 @@ {"path": "specs/isa/tri27_bytecode.t27", "t27b": "pass"}, {"path": "specs/isa/tri27_machine.t27", "t27b": "pass"}, {"path": "specs/jobs/release_t27c.t27", "t27b": "pass"}, + {"path": "specs/jobs/t27b_ledger_steward.t27", "t27b": "pass"}, {"path": "specs/lsp/language.t27", "t27b": "pass_vacuous"}, {"path": "specs/math/constants.t27", "t27b": "blocked", "blocker": "StmtAssign", "reason": "unimplemented"}, {"path": "specs/math/gf_competitive.t27", "t27b": "pass"}, @@ -1194,6 +1195,7 @@ {"path": "specs/tri/t27b/fuzz_oracle.t27", "t27b": "pass"}, {"path": "specs/tri/t27b/int_cast_plan.t27", "t27b": "pass"}, {"path": "specs/tri/t27b/lazy_sig_plan.t27", "t27b": "pass"}, + {"path": "specs/tri/t27b/ledger_steward.t27", "t27b": "pass"}, {"path": "specs/tri/t27b/lencall.t27", "t27b": "pass"}, {"path": "specs/tri/t27b/libm.t27", "t27b": "pass"}, {"path": "specs/tri/t27b/libm_plan.t27", "t27b": "pass"}, diff --git a/specs/jobs/t27b_ledger_steward.t27 b/specs/jobs/t27b_ledger_steward.t27 new file mode 100644 index 0000000000..ba45b18c39 --- /dev/null +++ b/specs/jobs/t27b_ledger_steward.t27 @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/jobs/t27b_ledger_steward.t27 -- the t27b ledger steward, as a job the Queen runs (specs/queen/jobs.t27) +; The steps are the ones a Claude session ran by hand three times in two days (#8289, #8548, #8646): +; read a red ratchet on a master run, write the rows the run states into +; docs/reports/t27b_expectations.json with t27b.dump_ledger's layout, open an issue and a PR that +; closes it, and enable auto-merge (#8654; see #8637 item 1). What the steward may apply and what it +; refuses is decided in specs/tri/t27b/ledger_steward.t27 (RULES), compiled to wasm like every card +; decision; this card names the steps. +; A job starts when a master run's ratchet is red and no job was started for its commit +; (ledger_steward.t27 run_starts_job), from either trigger: +; lab-latest the t27b lab publishes a master run at LAB_RUN_URL: results[] and steps.ratchet +; (verdict, counts, findings) +; native-ratchet master's NATIVE_CHECK job of NATIVE_WORKFLOW completes; its run.json and +; ratchet.json are read from the run's NATIVE_ARTIFACT (the workflow does not upload +; that artifact yet: until it does, lab-latest is the only trigger) +; The job's parameters are the run (its URL or Actions run id) and the run's commit. +; 0 CHECK steward-run the run is pinned as the subject: its ref is master, it counted +; runtime asserts, and master's ledger is the ledger the run was +; compared against (run_stewardable); otherwise the job fails as SKIP +; 1 EFFECT steward-refusals-issue when run_verdict is REFUSE: one issue for this run, listing each +; refused finding with its refusal_reason (or run_refusal) and the +; rows the steward would have applied; otherwise PASS, nothing done +; 2 CHECK steward-appliable run_verdict is APPLY: every finding is the steward's and the cap +; fits (cap_fits); REFUSE fails the job, pointing at the issue +; 3 EFFECT steward-ledger-issue the issue the ledger commit and the PR close (the Issue Gate: +; `Closes #N` in the commit and on line 1 of the PR body), +; labelled ISSUE_LABEL +; 4 EFFECT steward-compose compose the ledger from master's, finding by finding +; (finding_action: ADD, SET, DROP; KEEP leaves the row), rows in +; path order, a non-pass row with its first blocker and row_reason, +; max_not_pass = cap_after, source = the run; push it as one commit +; to BRANCH_PREFIX + the run's commit (12 hex) +; 5 EFFECT steward-pull-request the PR from that branch to master, body line 1 `Closes #`, +; with a table of the rows by finding, as #8548's +; 6 EFFECT steward-auto-merge enable auto-merge with MERGE_METHOD while the checks are pending +; (GitHub refuses it once they finished with a non-required red) +; 7 WAIT steward-merged the PR merged; a day without it fails the job (jobs.t27) +; Every EFFECT goes through the effects journal (control.t27 section 7) under the key +; JOURNAL_PREFIX + the run's commit + ":" + its STEP_WHAT, so a Queen that restarts mid-step looks +; before she acts and never opens a second issue or PR for one run. STEP_EFFECT names the effect kind +; each step needs from the supervisor. +; Read at run time by the supervisor's vendored compiler wasm: the supervisor reads the constants; +; the two functions are only what the card's tests ask of them. +; ASCII only (L3). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module JobT27bLedgerSteward; + +pub const JOB_NAME : str = "t27b-ledger-steward"; +pub const REPO : str = "gHashTag/t27"; +pub const BASE_BRANCH : str = "master"; +pub const LEDGER : str = "docs/reports/t27b_expectations.json"; +pub const RULES : str = "specs/tri/t27b/ledger_steward.t27"; + +/// Where a master run is read, by trigger. +pub const TRIGGERS : [2]str = ["lab-latest", "native-ratchet"]; +pub const LAB_RUN_URL : str = "https://t27b-lab-production.up.railway.app/latest.json"; +pub const NATIVE_WORKFLOW : str = "t27b-native.yml"; +pub const NATIVE_CHECK : str = "t27b-native-ratchet"; +pub const NATIVE_ARTIFACT : str = "t27b-native-ratchet"; + +/// The two parameters a job of this card starts with. +pub const PARAMS : [2]str = ["run", "commit"]; + +/// What the steward writes. +pub const BRANCH_PREFIX : str = "steward/t27b-ledger-"; +pub const ISSUE_LABEL : str = "t27b-ledger-steward"; +pub const GENERATED_BY : str = "t27b ledger steward (specs/jobs/t27b_ledger_steward.t27), #8654"; +pub const MERGE_METHOD : str = "squash"; +pub const JOURNAL_PREFIX : str = "t27b-ledger:"; + +/// Step kinds by number (specs/queen/jobs.t27: CHECK 0, WAIT 1, EFFECT 2), and the executor each +/// step names. A name the supervisor does not know fails the step: it never guesses. +pub const STEP_COUNT : u32 = 8; +pub const STEP_KIND : [8]u8 = [0, 2, 0, 2, 2, 2, 2, 1]; +pub const STEP_WHAT : [8]str = [ + "steward-run", "steward-refusals-issue", "steward-appliable", "steward-ledger-issue", + "steward-compose", "steward-pull-request", "steward-auto-merge", "steward-merged" +]; + +/// The effect kind each step needs ("" for a CHECK or a WAIT). +pub const STEP_EFFECT : [8]str = ["", "issue", "", "issue", "push", "pull-request", "auto-merge", ""]; + +/// Which ledger_steward.t27 effect code (EF_*) each EFFECT step is; 255 for a CHECK or a WAIT. +/// effect_wanted(run_verdict, code) says whether the step acts or answers PASS with nothing done. +pub const STEP_EF : [8]u8 = [255, 0, 255, 2, 1, 3, 4, 255]; + +/// The refusal issue is the only effect before the appliable CHECK; the ledger issue, the ledger and +/// its PR come only after it, so every check passed at the subject before the first of them +/// (jobs.t27 effect_may_run). +pub const REFUSAL_STEP : u32 = 1; +pub const APPLIABLE_STEP : u32 = 2; +pub const LEDGER_ISSUE_STEP : u32 = 3; +pub const COMPOSE_STEP : u32 = 4; + +/// Whether a step writes the steward's batch: an EFFECT after the appliable CHECK (the ledger issue, +/// the branch, the PR, auto-merge). The refusal issue before it only reports. +pub fn writes_the_repo(step: u32) -> bool { + if (step >= STEP_COUNT) { return false; } + if (STEP_KIND[step] != 2) { return false; } + return step > APPLIABLE_STEP; +} + +/// The ledger_steward.t27 effect code of a step, 255 for a CHECK, a WAIT or a step past the end. +pub fn step_ef(step: u32) -> u8 { + if (step >= STEP_COUNT) { return 255; } + return STEP_EF[step]; +} + +test the_card_writes_the_ledger_only_after_its_checks { + assert writes_the_repo(0) == false; + assert writes_the_repo(REFUSAL_STEP) == false; + assert writes_the_repo(APPLIABLE_STEP) == false; + assert writes_the_repo(LEDGER_ISSUE_STEP) == true; + assert writes_the_repo(COMPOSE_STEP) == true; + assert writes_the_repo(5) == true; + assert writes_the_repo(6) == true; + assert writes_the_repo(7) == false; + assert writes_the_repo(STEP_COUNT) == false; + assert STEP_KIND[0] == 0; + assert STEP_KIND[APPLIABLE_STEP] == 0; + assert STEP_KIND[7] == 1; +} + +test every_effect_step_names_its_effect { + assert step_ef(REFUSAL_STEP) == 0; + assert step_ef(LEDGER_ISSUE_STEP) == 2; + assert step_ef(COMPOSE_STEP) == 1; + assert step_ef(5) == 3; + assert step_ef(6) == 4; + assert step_ef(0) == 255; + assert step_ef(APPLIABLE_STEP) == 255; + assert step_ef(7) == 255; + assert step_ef(STEP_COUNT) == 255; +} + +invariant the_steps_are_ordered { + assert REFUSAL_STEP == 1; + assert APPLIABLE_STEP == 2; + assert LEDGER_ISSUE_STEP == 3; + assert COMPOSE_STEP == 4; + assert COMPOSE_STEP < STEP_COUNT; +} diff --git a/specs/tri/t27b/ledger_steward.t27 b/specs/tri/t27b/ledger_steward.t27 new file mode 100644 index 0000000000..c44ce7abb0 --- /dev/null +++ b/specs/tri/t27b/ledger_steward.t27 @@ -0,0 +1,427 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/tri/t27b/ledger_steward.t27 -- which ratchet findings on a master run the t27b ledger steward +; applies by itself, and which it refuses and reports to a person (#8654; see #8637 item 1, #8321 +; option 3). +; docs/reports/t27b_expectations.json must name every spec the reference passes. Spec PRs from many +; sessions landed without a row, master's t27b-native-ratchet went red (UNLISTED) on every PR, and the +; lanes conflicted on the file. The loop wrote the rows by hand three times in two days: #8289 (9 rows, +; max_not_pass 18 -> 20), #8548 (58 row changes: UNLISTED 8, UNEXPECTED PASS 44, STALE 4, UNEXPECTED +; FAILURE 2; 23 -> 27) and #8646 (9 rows, 25 -> 26). Every one of those rows was copied from a master +; run, which is a decision this file can make. The steward job (specs/jobs/t27b_ledger_steward.t27) +; reads a master run, asks this file about each finding, and either opens one ledger PR with +; auto-merge or opens one issue for the run. +; What the steward applies, as the run states it: +; UNLISTED the row is added; a non-pass row gets reason "unimplemented" (steward.t27 +; bless_reason), and only a construct t27b rejects is unimplemented +; UNEXPECTED PASS a gain: the row becomes the run's pass (or pass_vacuous) +; STALE the reference no longer passes it, or the spec is gone: the row is dropped +; the cap max_not_pass is recounted; it may rise by at most the new non-pass rows +; (steward.t27 cap_rise_is_new) +; What it refuses, so a person reads it: +; UNEXPECTED FAILURE (a possible t27b regression, or a spec another session rewrote: see #8637 item +; 5), MOVED, BAD REASON, OVER CAP (the master ledger is already over its own cap: a hand edit), an +; UNLISTED spec t27b fails, mismatches, crashes or times out on (a defect is never "unimplemented"), +; a cap rise larger than the new rows, more STALE rows in one run than STALE_LIMIT (the reference +; moved, not the specs), and any finding code this file does not know. +; One refused finding refuses the run: a ledger PR that leaves the ratchet red is noise, and the person +; who reads the issue writes the whole batch, as #8548 was written. +; Codes are steward.t27's; the invariant at the end checks the numbering against its entry_code. +; ASCII only (L3). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module T27bLedgerSteward; + +use tri::t27b::steward::is_pass; +use tri::t27b::steward::bless_reason; +use tri::t27b::steward::cap_rise_is_new; +use tri::t27b::steward::entry_code; +use tri::t27b::steward::unlisted; + +/// Ratchet finding codes (steward.t27): what the ratchet said about one ledger row or one spec. +pub const RC_NONE : u8 = 0; +pub const RC_VACUITY_MEASURED : u8 = 1; +pub const RC_UNEXPECTED_FAILURE : u8 = 2; +pub const RC_UNEXPECTED_PASS : u8 = 3; +pub const RC_MOVED : u8 = 4; +pub const RC_STALE : u8 = 5; +pub const RC_UNJUDGED : u8 = 6; +pub const RC_UNLISTED : u8 = 7; +pub const RC_OVER_CAP : u8 = 8; +pub const RC_BAD_REASON : u8 = 9; +pub const RC_KINDS : u8 = 10; + +/// t27b verdict codes of one spec in one run (steward.t27). +pub const T_PASS : u8 = 0; +pub const T_PASS_VACUOUS : u8 = 1; +pub const T_BLOCKED : u8 = 2; +pub const T_FRONTEND : u8 = 3; +pub const T_CODEGEN : u8 = 4; +pub const T_FAIL : u8 = 5; +pub const T_MISMATCH : u8 = 6; +pub const T_CRASH : u8 = 7; +pub const T_TIMEOUT : u8 = 8; +pub const T_NOT_RUN : u8 = 9; +pub const T_MISSING : u8 = 10; + +/// Reason codes of a non-pass row (steward.t27): NONE = 0, UNIMPLEMENTED = 1. +pub const RS_NONE : u8 = 0; +pub const RS_UNIMPLEMENTED : u8 = 1; + +/// What the steward does with one finding. +/// KEEP information only: the row stays as it is (VACUITY MEASURED, UNJUDGED, NONE) +/// ADD UNLISTED: the row is added as the run states it +/// SET UNEXPECTED PASS: the row becomes the run's pass +/// DROP STALE: the row goes +/// REFUSE a person reads it: the run gets one issue and no PR +pub const ACT_KEEP : u8 = 0; +pub const ACT_ADD : u8 = 1; +pub const ACT_SET : u8 = 2; +pub const ACT_DROP : u8 = 3; +pub const ACT_REFUSE : u8 = 4; + +/// Why a run was refused, as the issue words it. +pub const RR_NONE : u8 = 0; +pub const RR_REGRESSION : u8 = 1; +pub const RR_MOVED : u8 = 2; +pub const RR_BAD_REASON : u8 = 3; +pub const RR_DEFECT : u8 = 4; +pub const RR_OVER_CAP : u8 = 5; +pub const RR_CAP_RISE : u8 = 6; +pub const RR_STALE_FLOOD : u8 = 7; +pub const RR_UNKNOWN : u8 = 8; + +/// The run's verdict. +/// NOTHING no finding to apply: the ratchet is green, no job +/// APPLY every finding is the steward's: compose the ledger, one PR, auto-merge +/// REFUSE some finding is a person's: one issue for the run, no PR +/// SKIP not a run the steward reads: not a master run, asserts not counted, or master's ledger +/// moved since the run (the next run is read instead) +pub const RV_NOTHING : u8 = 0; +pub const RV_APPLY : u8 = 1; +pub const RV_REFUSE : u8 = 2; +pub const RV_SKIP : u8 = 3; + +/// More rows going STALE in one run than this says the reference moved, not the specs: #8548, the +/// largest hand batch, dropped 4. A spec PR that deletes a whole directory trips it too, and a person +/// then reads one issue, which is the cheap side of the mistake. +pub const STALE_LIMIT : u32 = 16; + +/// The effects a steward job may run, by the job card's effect steps. +pub const EF_REFUSAL_ISSUE : u8 = 0; +pub const EF_COMPOSE : u8 = 1; +pub const EF_LEDGER_ISSUE : u8 = 2; +pub const EF_PULL_REQUEST : u8 = 3; +pub const EF_AUTO_MERGE : u8 = 4; + +/// The reason the steward writes on a row it adds: none on a pass, "unimplemented" on a construct +/// t27b rejects (blocked, frontend, codegen), and none on a defect -- which the steward never writes. +pub fn row_reason(got: u8) -> u8 { + if (is_pass(got)) { return RS_NONE; } + return bless_reason(got, RS_NONE); +} + +/// The steward's action on one finding. `got` is t27b's verdict on that spec in the run. +pub fn finding_action(code: u8, got: u8) -> u8 { + if (code == RC_UNLISTED) { + if (is_pass(got)) { return ACT_ADD; } + if (row_reason(got) == RS_UNIMPLEMENTED) { return ACT_ADD; } + return ACT_REFUSE; + } + if (code == RC_UNEXPECTED_PASS) { + if (is_pass(got)) { return ACT_SET; } + return ACT_REFUSE; + } + if (code == RC_STALE) { return ACT_DROP; } + if (code == RC_NONE) { return ACT_KEEP; } + if (code == RC_VACUITY_MEASURED) { return ACT_KEEP; } + if (code == RC_UNJUDGED) { return ACT_KEEP; } + return ACT_REFUSE; +} + +/// Why one finding is refused; NONE when the steward applies or keeps it. +pub fn refusal_reason(code: u8, got: u8) -> u8 { + if (finding_action(code, got) != ACT_REFUSE) { return RR_NONE; } + if (code == RC_UNEXPECTED_FAILURE) { return RR_REGRESSION; } + if (code == RC_MOVED) { return RR_MOVED; } + if (code == RC_BAD_REASON) { return RR_BAD_REASON; } + if (code == RC_UNLISTED) { return RR_DEFECT; } + if (code == RC_OVER_CAP) { return RR_OVER_CAP; } + return RR_UNKNOWN; +} + +/// Is this finding a non-pass row new to the ledger? Only those may raise the cap. +pub fn new_not_pass_row(code: u8, got: u8) -> bool { + if (code != RC_UNLISTED) { return false; } + if (finding_action(code, got) != ACT_ADD) { return false; } + if (is_pass(got)) { return false; } + return true; +} + +/// Folds over one run's findings, grouped by (code, t27b verdict) with `n` findings in the group. +pub fn tally_refused(acc: u32, code: u8, got: u8, n: u32) -> u32 { + if (finding_action(code, got) == ACT_REFUSE) { return acc + n; } + return acc; +} + +pub fn tally_applied(acc: u32, code: u8, got: u8, n: u32) -> u32 { + var a : u8 = finding_action(code, got); + if (a == ACT_KEEP) { return acc; } + if (a == ACT_REFUSE) { return acc; } + return acc + n; +} + +pub fn tally_new_not_pass(acc: u32, code: u8, got: u8, n: u32) -> u32 { + if (new_not_pass_row(code, got)) { return acc + n; } + return acc; +} + +pub fn tally_stale(acc: u32, code: u8, n: u32) -> u32 { + if (code == RC_STALE) { return acc + n; } + return acc; +} + +pub fn stale_flood(stale: u32) -> bool { + return stale > STALE_LIMIT; +} + +/// The cap the composed ledger writes: its own non-pass count, which falls with every gain and rises +/// by at most its new non-pass rows. A rise larger than that is refused, and the old cap stays. +pub fn cap_fits(not_pass_after: u32, old_cap: u32, new_rows: u32) -> bool { + return cap_rise_is_new(not_pass_after, old_cap, new_rows); +} + +pub fn cap_after(not_pass_after: u32, old_cap: u32, new_rows: u32) -> u32 { + if (cap_fits(not_pass_after, old_cap, new_rows)) { return not_pass_after; } + return old_cap; +} + +/// A run the steward reads: a master run, with runtime asserts counted (otherwise pass and +/// pass_vacuous cannot be told apart), compared against the ledger master has now (otherwise its +/// findings describe a file that is gone). +pub fn run_stewardable(is_master: bool, asserts_counted: bool, same_ledger: bool) -> bool { + if (is_master == false) { return false; } + if (asserts_counted == false) { return false; } + if (same_ledger == false) { return false; } + return true; +} + +pub fn run_verdict(stewardable: bool, refused: u32, applied: u32, stale: u32, cap_ok: bool) -> u8 { + if (stewardable == false) { return RV_SKIP; } + if (refused > 0) { return RV_REFUSE; } + if (stale_flood(stale)) { return RV_REFUSE; } + if (cap_ok == false) { return RV_REFUSE; } + if (applied == 0) { return RV_NOTHING; } + return RV_APPLY; +} + +/// The run-level reason the issue leads with when no single finding was refused. +pub fn run_refusal(refused: u32, stale: u32, cap_ok: bool) -> u8 { + if (refused > 0) { return RR_NONE; } + if (stale_flood(stale)) { return RR_STALE_FLOOD; } + if (cap_ok == false) { return RR_CAP_RISE; } + return RR_NONE; +} + +/// Which effects a verdict runs. An effect not wanted answers PASS ("nothing to do") and writes no +/// journal entry: a refused run opens its issue and nothing else, an applied run its PR and nothing +/// else. +pub fn effect_wanted(verdict: u8, effect: u8) -> bool { + if (effect == EF_REFUSAL_ISSUE) { return verdict == RV_REFUSE; } + if (effect > EF_AUTO_MERGE) { return false; } + return verdict == RV_APPLY; +} + +/// A run starts a steward job when it is a master run whose ratchet is red and no job was started +/// for its commit yet (jobs.t27 job_may_start keeps it to one at a time). +pub fn run_starts_job(is_master: bool, ratchet_red: bool, commit_seen: bool) -> bool { + if (is_master == false) { return false; } + if (ratchet_red == false) { return false; } + if (commit_seen) { return false; } + return true; +} + +test batch_8289_nine_rows_two_blocked { + // #8289: UNLISTED -- 7 pass; tri/t27b/eval_arith and type_text blocked; max_not_pass 18 -> 20 + var refused : u32 = tally_refused(0, RC_UNLISTED, T_PASS, 7); + refused = tally_refused(refused, RC_UNLISTED, T_BLOCKED, 2); + var applied : u32 = tally_applied(0, RC_UNLISTED, T_PASS, 7); + applied = tally_applied(applied, RC_UNLISTED, T_BLOCKED, 2); + var fresh : u32 = tally_new_not_pass(0, RC_UNLISTED, T_PASS, 7); + fresh = tally_new_not_pass(fresh, RC_UNLISTED, T_BLOCKED, 2); + assert refused == 0; + assert applied == 9; + assert fresh == 2; + assert row_reason(T_BLOCKED) == RS_UNIMPLEMENTED; + assert row_reason(T_PASS) == RS_NONE; + assert cap_fits(20, 18, fresh) == true; + assert cap_after(20, 18, fresh) == 20; + assert run_verdict(run_stewardable(true, true, true), refused, applied, 0, cap_fits(20, 18, fresh)) == RV_APPLY; +} + +test batch_8646_nine_rows_one_codegen { + // #8646, run 20624dd85: UNLISTED 9 -- 8 pass; compiler/lexer codegen (FnDecl frame size); 25 -> 26 + var refused : u32 = tally_refused(0, RC_UNLISTED, T_PASS, 8); + refused = tally_refused(refused, RC_UNLISTED, T_CODEGEN, 1); + var applied : u32 = tally_applied(0, RC_UNLISTED, T_PASS, 8); + applied = tally_applied(applied, RC_UNLISTED, T_CODEGEN, 1); + var fresh : u32 = tally_new_not_pass(0, RC_UNLISTED, T_PASS, 8); + fresh = tally_new_not_pass(fresh, RC_UNLISTED, T_CODEGEN, 1); + assert refused == 0; + assert applied == 9; + assert fresh == 1; + assert row_reason(T_CODEGEN) == RS_UNIMPLEMENTED; + assert cap_after(26, 25, fresh) == 26; + assert run_verdict(true, refused, applied, 0, cap_fits(26, 25, fresh)) == RV_APPLY; + assert effect_wanted(RV_APPLY, EF_PULL_REQUEST) == true; + assert effect_wanted(RV_APPLY, EF_REFUSAL_ISSUE) == false; +} + +test batch_8548_is_refused_for_its_two_failures { + // #8548, run c9d36f314: UNLISTED 8 (6 pass, claude_md and trinity_tokenizer blocked), UNEXPECTED + // PASS 44 (pass_vacuous -> pass), STALE 4, UNEXPECTED FAILURE 2 (ternary_gates and + // arty_a7_integration now blocked); the hand batch took the cap 23 -> 27 + var refused : u32 = tally_refused(0, RC_UNLISTED, T_PASS, 6); + refused = tally_refused(refused, RC_UNLISTED, T_BLOCKED, 2); + refused = tally_refused(refused, RC_UNEXPECTED_PASS, T_PASS, 44); + refused = tally_refused(refused, RC_STALE, T_PASS, 4); + refused = tally_refused(refused, RC_UNEXPECTED_FAILURE, T_BLOCKED, 2); + var fresh : u32 = tally_new_not_pass(0, RC_UNLISTED, T_BLOCKED, 2); + fresh = tally_new_not_pass(fresh, RC_UNEXPECTED_FAILURE, T_BLOCKED, 2); + var stale : u32 = tally_stale(0, RC_STALE, 4); + assert refused == 2; + assert fresh == 2; + assert stale == 4; + assert refusal_reason(RC_UNEXPECTED_FAILURE, T_BLOCKED) == RR_REGRESSION; + // the two failures count against the cap as old rows: 27 is 2 more than the new rows allow + assert cap_fits(27, 23, fresh) == false; + assert cap_after(27, 23, fresh) == 23; + assert run_verdict(true, refused, 56, stale, cap_fits(27, 23, fresh)) == RV_REFUSE; + assert effect_wanted(RV_REFUSE, EF_REFUSAL_ISSUE) == true; + assert effect_wanted(RV_REFUSE, EF_COMPOSE) == false; + assert effect_wanted(RV_REFUSE, EF_AUTO_MERGE) == false; +} + +test batch_8548_without_its_failures_is_applied { + // the same run with the two failures taken out: 56 row changes, cap 23 -> 25 + var applied : u32 = tally_applied(0, RC_UNLISTED, T_PASS, 6); + applied = tally_applied(applied, RC_UNLISTED, T_BLOCKED, 2); + applied = tally_applied(applied, RC_UNEXPECTED_PASS, T_PASS, 44); + applied = tally_applied(applied, RC_STALE, T_PASS, 4); + var fresh : u32 = tally_new_not_pass(0, RC_UNLISTED, T_BLOCKED, 2); + var refused : u32 = tally_refused(0, RC_UNEXPECTED_PASS, T_PASS, 44); + assert applied == 56; + assert fresh == 2; + assert refused == 0; + assert cap_after(25, 23, fresh) == 25; + assert run_verdict(true, refused, applied, 4, cap_fits(25, 23, fresh)) == RV_APPLY; +} + +test the_steward_applies_these { + assert finding_action(RC_UNLISTED, T_PASS) == ACT_ADD; + assert finding_action(RC_UNLISTED, T_PASS_VACUOUS) == ACT_ADD; + assert finding_action(RC_UNLISTED, T_FRONTEND) == ACT_ADD; + assert finding_action(RC_UNEXPECTED_PASS, T_PASS) == ACT_SET; + assert finding_action(RC_UNEXPECTED_PASS, T_PASS_VACUOUS) == ACT_SET; + assert finding_action(RC_STALE, T_PASS) == ACT_DROP; + assert finding_action(RC_STALE, T_MISSING) == ACT_DROP; + assert finding_action(RC_VACUITY_MEASURED, T_PASS_VACUOUS) == ACT_KEEP; + assert finding_action(RC_UNJUDGED, T_BLOCKED) == ACT_KEEP; + assert refusal_reason(RC_STALE, T_PASS) == RR_NONE; + assert new_not_pass_row(RC_UNLISTED, T_PASS_VACUOUS) == false; + assert new_not_pass_row(RC_UNLISTED, T_FRONTEND) == true; + assert new_not_pass_row(RC_UNEXPECTED_FAILURE, T_BLOCKED) == false; +} + +test the_steward_refuses_these { + assert refusal_reason(RC_UNEXPECTED_FAILURE, T_FAIL) == RR_REGRESSION; + assert refusal_reason(RC_MOVED, T_BLOCKED) == RR_MOVED; + assert refusal_reason(RC_BAD_REASON, T_BLOCKED) == RR_BAD_REASON; + assert refusal_reason(RC_OVER_CAP, T_PASS) == RR_OVER_CAP; + assert refusal_reason(RC_KINDS, T_PASS) == RR_UNKNOWN; + // an UNLISTED defect is never "unimplemented" + assert refusal_reason(RC_UNLISTED, T_FAIL) == RR_DEFECT; + assert refusal_reason(RC_UNLISTED, T_MISMATCH) == RR_DEFECT; + assert refusal_reason(RC_UNLISTED, T_CRASH) == RR_DEFECT; + assert refusal_reason(RC_UNLISTED, T_TIMEOUT) == RR_DEFECT; + assert refusal_reason(RC_UNLISTED, T_NOT_RUN) == RR_DEFECT; + assert row_reason(T_MISMATCH) == RS_NONE; + // a gain the run does not show is no gain + assert refusal_reason(RC_UNEXPECTED_PASS, T_BLOCKED) == RR_UNKNOWN; + assert tally_refused(3, RC_MOVED, T_BLOCKED, 2) == 5; + assert tally_applied(3, RC_MOVED, T_BLOCKED, 2) == 3; +} + +test a_cap_rise_larger_than_the_new_rows_is_refused { + assert cap_fits(26, 25, 1) == true; + assert cap_fits(27, 25, 1) == false; + // the cap falls with every gain + assert cap_after(20, 26, 0) == 20; + assert run_verdict(true, 0, 9, 0, false) == RV_REFUSE; + assert run_refusal(0, 0, false) == RR_CAP_RISE; + assert run_refusal(2, 0, false) == RR_NONE; +} + +test a_stale_flood_is_refused { + assert stale_flood(STALE_LIMIT) == false; + assert stale_flood(STALE_LIMIT + 1) == true; + var stale : u32 = tally_stale(0, RC_STALE, 17); + stale = tally_stale(stale, RC_UNLISTED, 5); + assert stale == 17; + assert run_verdict(true, 0, 17, stale, true) == RV_REFUSE; + assert run_refusal(0, stale, true) == RR_STALE_FLOOD; + assert run_verdict(true, 0, 16, 16, true) == RV_APPLY; +} + +test only_a_counted_master_run_on_the_current_ledger_is_read { + assert run_stewardable(true, true, true) == true; + assert run_stewardable(false, true, true) == false; + assert run_stewardable(true, false, true) == false; + assert run_stewardable(true, true, false) == false; + assert run_verdict(run_stewardable(true, true, false), 0, 9, 0, true) == RV_SKIP; + assert run_verdict(true, 0, 0, 0, true) == RV_NOTHING; + assert effect_wanted(RV_NOTHING, EF_COMPOSE) == false; + assert effect_wanted(RV_SKIP, EF_REFUSAL_ISSUE) == false; + assert effect_wanted(RV_APPLY, EF_AUTO_MERGE + 1) == false; +} + +test a_red_master_run_starts_one_job { + assert run_starts_job(true, true, false) == true; + assert run_starts_job(true, true, true) == false; + assert run_starts_job(true, false, false) == false; + assert run_starts_job(false, true, false) == false; +} + +// The codes are steward.t27's: its entry_code and unlisted answer them. +invariant the_codes_are_the_stewards { + assert entry_code(0, 0, 2, true, true) == RC_UNEXPECTED_FAILURE; + assert entry_code(2, 0, 0, true, false) == RC_UNEXPECTED_PASS; + assert entry_code(2, 0, 3, true, false) == RC_MOVED; + assert entry_code(0, 5, 0, true, true) == RC_STALE; + assert entry_code(0, 8, 0, true, true) == RC_UNJUDGED; + assert entry_code(0, 0, 1, false, true) == RC_VACUITY_MEASURED; + assert unlisted(T_PASS, false) == true; + assert bless_reason(T_BLOCKED, RS_NONE) == RS_UNIMPLEMENTED; +} + +invariant a_regression_is_never_applied { + assert finding_action(RC_UNEXPECTED_FAILURE, T_PASS) == ACT_REFUSE; + assert finding_action(RC_UNEXPECTED_FAILURE, T_BLOCKED) == ACT_REFUSE; + assert finding_action(RC_MOVED, T_BLOCKED) == ACT_REFUSE; + assert finding_action(RC_BAD_REASON, T_PASS) == ACT_REFUSE; + assert new_not_pass_row(RC_UNEXPECTED_FAILURE, T_BLOCKED) == false; +} + +invariant the_steward_codes_never_move { + assert ACT_KEEP == 0; + assert ACT_ADD == 1; + assert ACT_SET == 2; + assert ACT_DROP == 3; + assert ACT_REFUSE == 4; + assert RV_NOTHING == 0; + assert RV_APPLY == 1; + assert RV_REFUSE == 2; + assert RV_SKIP == 3; + assert RR_UNKNOWN == 8; + assert EF_AUTO_MERGE == 4; + assert STALE_LIMIT == 16; +}