From 7241dcf8ee9836b0b92d936d4321c274626be0e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 08:40:45 +0700 Subject: [PATCH 1/3] tri pr preflight: the glue that runs specs/tri/pr/preflight.t27, paid by deleting inflight's unused merge_outcome (Closes #11126) WIP commit for the real runs; the final message replaces it. Co-Authored-By: Claude Sonnet 5.5 --- cli/tri/src/inflight.rs | 52 ----------------------------------------- cli/tri/src/prcheck.rs | 40 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 52 deletions(-) diff --git a/cli/tri/src/inflight.rs b/cli/tri/src/inflight.rs index 5fc52b3385..ef8bd00baf 100644 --- a/cli/tri/src/inflight.rs +++ b/cli/tri/src/inflight.rs @@ -36,34 +36,6 @@ use anyhow::{Context, Result}; use clap::Args; use std::process::Command; -/// What `git merge` actually said. The exit code is the only channel that -/// separates these; both leave unmerged paths behind. -#[derive(Debug, PartialEq, Eq)] -pub enum Merge { - /// Merged cleanly. - Done, - /// Genuine conflict: the merge started and stopped in the middle. - Conflicted, - /// Refused to start. A merge was already in flight, or the tree was dirty. - /// Nothing about this run's merge is knowable from the working tree. - Refused, - /// Some other exit code. Reported as itself, never folded into a neighbour. - Other(i32), -} - -/// `git merge` answers with more than success and failure. -/// -/// 128 is refusal, not conflict, and the distinction is the whole point: after -/// a refusal there is no merge of yours to resolve. -pub fn merge_outcome(rc: i32) -> Merge { - match rc { - 0 => Merge::Done, - 1 => Merge::Conflicted, - 128 => Merge::Refused, - n => Merge::Other(n), - } -} - /// How many parents `git log -1 --format=%p` reported. /// /// The root commit prints an empty line, which is zero parents and not a @@ -185,30 +157,6 @@ pub fn run(a: &Merging) -> Result<()> { } } -#[cfg(test)] -mod outcome_tests { - use super::*; - - #[test] - fn refusal_is_not_conflict() { - assert_eq!(merge_outcome(1), Merge::Conflicted); - assert_eq!(merge_outcome(128), Merge::Refused); - assert_ne!(merge_outcome(1), merge_outcome(128)); - } - - #[test] - fn success_is_its_own_answer() { - assert_eq!(merge_outcome(0), Merge::Done); - } - - #[test] - fn an_unknown_code_stays_unknown() { - // Never folded into the nearest known neighbour. - assert_eq!(merge_outcome(2), Merge::Other(2)); - assert_eq!(merge_outcome(129), Merge::Other(129)); - } -} - #[cfg(test)] mod parent_tests { use super::*; diff --git a/cli/tri/src/prcheck.rs b/cli/tri/src/prcheck.rs index 8d49eb4eac..5a0d5c4002 100644 --- a/cli/tri/src/prcheck.rs +++ b/cli/tri/src/prcheck.rs @@ -19,6 +19,9 @@ use clap::Subcommand; use std::collections::{BTreeMap, BTreeSet}; use std::process::Command; +#[path = "../../../gen/rust/tri/pr/preflight.rs"] #[allow(dead_code, unused_parens, unused_mut, unused_variables, non_snake_case, unused_assignments, clippy::all)] +mod pf; // t27c gen-rust of specs/tri/pr/preflight.t27 (#10190, #11125): every rule of `tri pr preflight` + #[derive(Subcommand)] pub enum PrCmd { /// Classify every failing check and say plainly whether it is safe to merge. @@ -119,10 +122,17 @@ pub enum PrCmd { #[arg(long)] why: bool, }, + /// Run the rules of specs/tri/pr/preflight.t27 on this branch against origin/master, before `gh pr create`. Exit 0 every rule passed, 1 a rule FAILed, 2 evidence could not be read. T27C names the t27c program (default t27c on PATH). + Preflight { + /// Leave out the corpus ratchet, which takes minutes. + #[arg(long)] + quick: bool, + }, } pub fn run(cmd: &PrCmd) -> Result<()> { match cmd { + PrCmd::Preflight { quick } => std::process::exit(preflight(*quick)?), PrCmd::Ready { number, repo, @@ -153,6 +163,36 @@ pub fn run(cmd: &PrCmd) -> Result<()> { } } +/// `tri pr preflight` (#11126): every rule is specs/tri/pr/preflight.t27. This runs each command the spec names (`sh -c`; SPEC, T27C and TRI in the environment), frames the run as one evidence section, and prints the spec's verdict; the exit is the spec's. +fn preflight(quick: bool) -> Result { + let (t27c, tri) = (std::env::var("T27C").unwrap_or("t27c".into()), std::env::current_exe()?); + let (mut ev, mut c) = (String::new(), ""); + if let Some(o) = Command::new("git").args(["rev-parse", "--show-toplevel"]).output().ok().filter(|o| o.status.success()) { std::env::set_current_dir(String::from_utf8_lossy(&o.stdout).trim())?; } + for k in (0..pf::STEPS).filter(|&k| pf::step_runs(k, quick)) { + let each = pf::step_each(k); + for n in (0..if each { pf::entries(c) } else { 1 }).filter(|&n| !each || pf::is_target(c, n)) { + let spec = if each { &c[pf::path_start(c, n)..pf::path_end(c, n)] } else { "" }; + let (x, out) = match Command::new("sh").args(["-c", pf::step_cmd(k)]).env("SPEC", spec).env("T27C", &t27c).env("TRI", &tri).output() { + Ok(o) => (o.status.code().unwrap_or(255), format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr))), + Err(e) => (127, e.to_string()), + }; + ev += &format!("@@ preflight {k} {n} {x} {}\n{out}\n", out.len()); + eprintln!("preflight step {k} entry {n}: exit {x}"); + if k == pf::STEP_CHANGES && x == 0 { c = out.leak(); } + } + } + let ev: &'static str = ev.leak(); + for r in 0..pf::RULES { + let (v, n) = (pf::rule_verdict(c, ev, r, quick), pf::rule_at(c, ev, r, quick)); + println!("{:<8}{}{}", pf::rule_name(r), pf::name(v), if v == pf::P_PASS { String::new() } else { format!(" {}", pf::rule_why(r, v)) }); + let k = pf::evidence_step(c, ev, r, n); + if v != pf::P_PASS && k < pf::STEPS { println!("{}", String::from_utf8_lossy(&ev.as_bytes()[pf::evidence_from(ev, k, n)..pf::evidence_to(ev, k, n)])); } + } + let x = pf::preflight_exit(c, ev, quick); + println!("{}", pf::preflight_exit_why(x)); + Ok(x as i32) +} + /// Check that what the pull request introduced is present in the default /// branch, file by file. Status is not content: a merged pull request whose /// stack-mate was auto-closed leaves a list that reads as success. From 8bdaa1404307f65618ba254b84f6369e488a12ef Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 08:44:49 +0700 Subject: [PATCH 2/3] preflight spec: the dupes-base scratch checkout is made under TMPDIR, and a failed mktemp ends the step with 3 (Closes #11126) On macOS mktemp -d with no template ignores TMPDIR. In a sandbox it fails, and git worktree add was handed an empty path: "BUG: builtin/worktree.c:464" and exit 134. With a finding in dupe_scan this read as UNREAD for a reason that is not the branch's. The step now makes its directory as the budget step does and exits 3 before git is called, so dupe_scan's own exit 1 is never mistaken for it. Test first: the_dupes_base_scratch_is_made_under_tmpdir failed 24 pass, 1 FAIL, then 25/25 with 0 vacuous. gen/rust regenerated with t27c gen-rust (two lines differ), seal re-saved and --verify prints all hashes MATCH. Co-Authored-By: Claude Sonnet 5.5 --- .trinity/seals/pr_TriPrPreflight.json | 18 +++++++++--------- gen/rust/tri/pr/preflight.rs | 4 ++-- specs/tri/pr/preflight.t27 | 12 +++++++++++- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/.trinity/seals/pr_TriPrPreflight.json b/.trinity/seals/pr_TriPrPreflight.json index b4b451cf8c..6d6d624f31 100644 --- a/.trinity/seals/pr_TriPrPreflight.json +++ b/.trinity/seals/pr_TriPrPreflight.json @@ -1,5 +1,5 @@ { - "built_by": "t27c-bootstrap@0.5.2+b258b9007", + "built_by": "t27c-bootstrap@0.5.2+cf2d61eb1", "closure": [ { "spec": "specs/tri/lab/receipt.t27", @@ -11,23 +11,23 @@ } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:7b05ed840e722e114f7e7427e7808981f9fe5a01c2b45d5908c6dfef0394e952", - "gen_hash_rust": "sha256:d6ab06097d38589835a474ff94c2b97cccb59b0185844c936bbfd858b0a723de", - "gen_hash_verilog": "sha256:39f7e63349e6fe4ee7aa4e3fb71554cf09a3a9147f5faff0494d599239f4bb9a", - "gen_hash_zig": "sha256:6b0f5756fe610ad85c1b745922d896ce7f5d6a1f33668858c2964f6afe299465", + "gen_hash_c": "sha256:d6bb2e6658d4a30f087023af7ad08f5b7d4dff64e94deec05271f4bea855336d", + "gen_hash_rust": "sha256:9ae5246d4f986044240f97454eaf9967c685e9abdef001aad188993e7c9d2c8f", + "gen_hash_verilog": "sha256:1f3c5f2e2a3e752f779928e6dca2aa9b5d6f039af926aa7a6b01407acb10cc25", + "gen_hash_zig": "sha256:4c5b9c291c73cd30bf88628f0efe8cd0065df7ab687c2414e5bdc9e6229217d9", "module": "TriPrPreflight", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-19T18:07:06Z", + "sealed_at": "2026-10-20T01:43:21Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:ce7132cb0b49246d3d86bdde48ecad5bc41f252d721dbfa0b1912306b7a39897", + "spec_hash": "sha256:f9b7b84c125b47c1b5285bb98ab5532987bdef785319c0a3c60bbf7e87f5b6f6", "spec_path": "specs/tri/pr/preflight.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 24, - "total": 24, + "passed": 25, + "total": 25, "vacuous": 0 }, "toolchain": { diff --git a/gen/rust/tri/pr/preflight.rs b/gen/rust/tri/pr/preflight.rs index 714ce5928d..0ad752a601 100644 --- a/gen/rust/tri/pr/preflight.rs +++ b/gen/rust/tri/pr/preflight.rs @@ -1,6 +1,6 @@ // Generated from .t27 spec // DO NOT EDIT — generated by t27c -// NOT LOWERED BY THIS BACKEND: 24 test(s), 1 invariant(s), 0 function(s) with no body. +// NOT LOWERED BY THIS BACKEND: 25 test(s), 1 invariant(s), 0 function(s) with no body. // This backend emits declarations only. The spec's checks live in // the Zig and Verilog outputs; do not read this file as verified. @@ -144,7 +144,7 @@ pub fn step_cmd(k: u8) -> &'static str { return "\"$T27C\" suite --repo-root . --ratchet --corpus-only 2>&1"; } if (k == STEP_DUPES_BASE) { - return "d=\"$(mktemp -d)\"; git worktree add -q --detach \"$d\" origin/master 2>&1 && (cd \"$d\" && python3 tools/dupe_scan.py 2>&1); x=$?; git worktree remove --force \"$d\" 2>&1; exit $x"; + return "d=\"$(mktemp -d \"${TMPDIR:-/tmp}/dupes.XXXXXX\")\" || exit 3; git worktree add -q --detach \"$d\" origin/master 2>&1 && (cd \"$d\" && python3 tools/dupe_scan.py 2>&1); x=$?; git worktree remove --force \"$d\" 2>&1; exit $x"; } if (k == STEP_BUDGET) { return "{ b=\"$(git merge-base origin/master HEAD)\" && d=\"$(mktemp -d \"${TMPDIR:-/tmp}/budget.XXXXXX\")\" && git show \"${b}:gen/c/policy/own_language.c\" > \"$d/gate.c\" && printf '#include \"gate.c\"\\n#include \\nint main(void){static char b[1<<22],o[1<<22];unsigned n=check_budget((uint8_t*)b,fread(b,1,sizeof b,stdin),(uint8_t*)o,sizeof o-1);fputs(o,stdout);return n!=0;}\\n' | cc -w -I \"$d\" -x c -o \"$d/budget\" - && git diff --numstat --no-renames \"${b}...HEAD\" > \"$d/in\" && \"$d/budget\" < \"$d/in\"; x=$?; rm -rf \"$d\"; exit $x; } 2>&1"; diff --git a/specs/tri/pr/preflight.t27 b/specs/tri/pr/preflight.t27 index e69dd3768e..88dbf81b57 100644 --- a/specs/tri/pr/preflight.t27 +++ b/specs/tri/pr/preflight.t27 @@ -119,7 +119,7 @@ pub fn step_cmd(k: u8) -> string { if (k == STEP_SUITE_ROWS) { return "cat docs/reports/suite_expectations.json 2>&1"; } if (k == STEP_T27B_ROWS) { return "cat docs/reports/t27b_expectations.json 2>&1"; } if (k == STEP_SUITE) { return "\"$T27C\" suite --repo-root . --ratchet --corpus-only 2>&1"; } - if (k == STEP_DUPES_BASE) { return "d=\"$(mktemp -d)\"; git worktree add -q --detach \"$d\" origin/master 2>&1 && (cd \"$d\" && python3 tools/dupe_scan.py 2>&1); x=$?; git worktree remove --force \"$d\" 2>&1; exit $x"; } + if (k == STEP_DUPES_BASE) { return "d=\"$(mktemp -d \"${TMPDIR:-/tmp}/dupes.XXXXXX\")\" || exit 3; git worktree add -q --detach \"$d\" origin/master 2>&1 && (cd \"$d\" && python3 tools/dupe_scan.py 2>&1); x=$?; git worktree remove --force \"$d\" 2>&1; exit $x"; } // The gate is the merge base's own gen/c/policy/own_language.c, as the CI step reads it from BASE; the numstat goes // to a file first, so a git that fails leaves the gate no input to pass on. if (k == STEP_BUDGET) { return "{ b=\"$(git merge-base origin/master HEAD)\" && d=\"$(mktemp -d \"${TMPDIR:-/tmp}/budget.XXXXXX\")\" && git show \"${b}:gen/c/policy/own_language.c\" > \"$d/gate.c\" && printf '#include \"gate.c\"\\n#include \\nint main(void){static char b[1<<22],o[1<<22];unsigned n=check_budget((uint8_t*)b,fread(b,1,sizeof b,stdin),(uint8_t*)o,sizeof o-1);fputs(o,stdout);return n!=0;}\\n' | cc -w -I \"$d\" -x c -o \"$d/budget\" - && git diff --numstat --no-renames \"${b}...HEAD\" > \"$d/in\" && \"$d/budget\" < \"$d/in\"; x=$?; rm -rf \"$d\"; exit $x; } 2>&1"; } @@ -1073,6 +1073,16 @@ test the_budget_gate_is_compiled_from_the_merge_base { assert opens_with(step_cmd(STEP_DEVHOME), 0, step_cmd(STEP_DEVHOME).len, "python3 -I tools/check_devhome_paths.py"); } +// Scenario: the scratch checkout of origin/master is made under TMPDIR, as the budget's scratch dir is: macOS mktemp -d with no +// template ignores TMPDIR, so inside a sandbox it fails and git was handed an empty path (BUG, exit 134). A mktemp that fails ends the +// step with 3, never with the 1 that dupe_scan's own findings exit. +test the_dupes_base_scratch_is_made_under_tmpdir { + const s : string = step_cmd(STEP_DUPES_BASE); + assert find_in(s, 0, s.len, "mktemp -d \"${TMPDIR:-/tmp}/dupes.XXXXXX\"") < s.len; + assert find_in(s, 0, s.len, " || exit 3; git worktree add ") < s.len; + assert find_in(s, 0, s.len, "mktemp -d)") == s.len + 1; +} + // The home-path scan's four finding shapes (tools/check_devhome_paths.py), for a tree that holds one of each. const HOME_C : string = "A\tdocs/new.md\nM\tspecs/x/grew.t27\nM\tspecs/x/shrank.t27\nD\tspecs/x/gone.t27\nM\tdocs/a.md\n"; const HOME_FIND : string = "@@ preflight 18 0 1 371\nFAIL new docs/new.md (1 occurrence(s))\nFAIL grew specs/x/grew.t27 1 -> 2\nFAIL shrank specs/x/shrank.t27 2 -> 1 -- re-bless to pin the lower count\nFAIL fixed specs/x/gone.t27 was 3 -- remove it from the baseline\n\n Use `git rev-parse --show-toplevel`, or an environment variable the\n caller sets. Re-bless with: python3 tools/check_devhome_paths.py --bless\n\n"; From 59fdc5128d2132d179ecdd594563d8aa026e74f5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 08:53:10 +0700 Subject: [PATCH 3/3] docs(now): the preflight glue landed, 25 tests, and the rule list names budget and devhome (Closes #11126) The note said the glue was parked and that nothing calls the spec; the command now exists. It also listed 19 tests and 11 rules; the spec has 25 tests and 13 rules since #11125. Co-Authored-By: Claude Sonnet 5.5 --- docs/now/2026-10-18-pr-preflight.md | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/docs/now/2026-10-18-pr-preflight.md b/docs/now/2026-10-18-pr-preflight.md index 9cab3088c2..a2e1cc1345 100644 --- a/docs/now/2026-10-18-pr-preflight.md +++ b/docs/now/2026-10-18-pr-preflight.md @@ -10,7 +10,7 @@ its author could have seen. ## What - `specs/tri/pr/preflight.t27` owns every rule, every verdict (PASS, FAIL, UNREAD, - NOT RUN) and every message. 19 tests, 0 vacuous, 1 comptime invariant. + NOT RUN) and every message. 25 tests, 0 vacuous, 1 comptime invariant. - The evidence is data: each command the spec names runs once, per changed spec where the rule is per-spec. Its output is framed as `@@ preflight `, and the spec reads the frames. A @@ -20,16 +20,18 @@ its author could have seen. 2 evidence could not be read. `--quick` leaves out the corpus ratchet. The publisher gate runs it in the bee's worktree, with origin/master merged in, and skips a branch that did not exit 0. -- **The glue is parked, not in this change.** It is 38 lines in - `cli/tri/src/prcheck.rs` and 30 in `tools/queen/publish.py`. It was built and - replayed on the lab, but neither file is in `tools/policy/foreign-exceptions.txt` - on master, and `check_budget` refuses any net growth of hand-written foreign - lines (owner rule 2026-10-09). So lefthook refuses the commit and no label - lifts it. Landing the glue needs the owner: either the exceptions plus an - offsetting deletion, or a t27-native way to run commands. +- **The glue landed in #11126.** `preflight` in `cli/tri/src/prcheck.rs` is 40 lines + (a `#[path]` mount of `gen/rust/tri/pr/preflight.rs`, the `Preflight` subcommand and the + loop). It loops to the spec's STEPS and RULES, so a step added to the spec runs without a + glue change. It was parked until then because `check_budget` refuses any net growth of + hand-written foreign lines (owner rule 2026-10-09) and no label lifts it; it landed + net-zero, paid by deleting the 52 lines of `inflight.rs` that `cargo check -p tri` listed + as never used (`Merge`, `merge_outcome` and their three unit tests). The Queen's side, + about 30 lines in `tools/queen/publish.py`, is not part of that change. The rules are changes, base, check, tests, seal, types, dupes, ledgers, suite, -census and kept. Only a branch's own defects fail it: +census, budget (the own-language foreign-line budget, #11125), devhome (the +developer-home-path scan, #11125) and kept. Only a branch's own defects fail it: - dupes compares `dupe_scan.py` on the branch with the same scan on a clean origin/master worktree. A finding master already prints is not the branch's. @@ -49,7 +51,8 @@ census and kept. Only a branch's own defects fail it: ## Open -- Land the glue (see above); until then nothing calls this spec. +- `tools/queen/publish.py` does not call `tri pr preflight` yet; the publisher gate that + skips a branch that did not exit 0 is still to land, net-zero as well. - t27b's JIT runs on arm64 only and the lab is x86_64, so this PR's own `t27b_expectations.json` row is left to the tool after an arm64 run measures it. - cli-tri (Rust build) and "documented t27c subcommands" are outside the spec rules.