From f6ec31f2719055738192a67701814fca95ab9c78 Mon Sep 17 00:00:00 2001 From: Vasilev Dmitrii Date: Sun, 30 Aug 2026 22:53:21 +0700 Subject: [PATCH 1/4] fix(tri): reachability is not staleness, and withdraw a claim of mine Skill 389 and the header of harness-scratch.yml both said emit-bitexact "has NEVER run on master". It has run on master twice: 2026-08-28 workflow_dispatch success run 33150988445 2026-08-20 workflow_dispatch failure run 32319733329 The 2026-08-28 success is from two days before the change I was judging -- the baseline I said did not exist, while I borrowed one from sibling branches instead. The wrong reading came from `gh run list --branch master -L 40 --json name,conclusion` filtered by workflow name. That is a WINDOW over all workflows, forty runs deep, which on this repository is under an hour. A workflow absent from it has not been shown to be absent from the branch. And the instrument already existed: `tri gates unmeasured` queries per workflow by lifetime through the API's total_count, and does not list emit-bitexact precisely because its dispatch is recent. I reached for a raw gh window instead of the command written here for this question. What the tool DID lack is orthogonal to staleness: whether a workflow can produce a default-branch run at all without a human. One dispatched by hand yesterday is not stale and still cannot take tomorrow's reading by itself -- exactly the state emit-bitexact was in, and why its absence from the stale table read as health. `unmeasured` now prints that as its own section. Measured: 17 workflows have no `push:` covering master, 5 of them merge-critical (catalog-count-gate, check-now-freshness, emit-bitexact-gate, issue-gate, phi-loop-ci). check-now-freshness is one of the four contexts the ruleset requires and has no standing master reading; Issue Gate's last one is 2026-04-08. All 17 carry workflow_dispatch, so every missing reading can be TAKEN rather than inferred. has_auto_default_run is read by line, matching the two helpers beside it, with 11 tests. Two are counterexamples a review would not produce: a `push:` inside a COMMENT is not a trigger -- harness-scratch.yml's own header recommends `push: branches: [master]` in prose, and a naive scan reads its own advice as compliance -- and a `push` nested below the first level inside `on:` is not one either. Also covered: bare `push:`, the inline `on: [push, ...]` form, block lists, a `'**'` wildcard, and the default branch as a parameter rather than the literal master. One test reads every workflow in the tree and fails if it read fewer than ten, because a test that reads nothing passes vacuously. Section 389 is rewritten IN PLACE, not corrected beside itself, and now names the three statements that look identical in a terminal: has not run recently (a window), has never run on this branch (per-workflow), cannot run on this branch (the `on:` block). Reporting the third when you measured the first is the error. Closes #2959 Refs #2957 Co-Authored-By: Claude Opus 5 --- .claude/skills/ci-gates/SKILL.md | 82 ++++-- .github/workflows/harness-scratch.yml | 17 +- cli/tri/src/gates.rs | 272 ++++++++++++++++++ ...0-a-window-read-as-a-lifetime-withdrawn.md | 8 + 4 files changed, 355 insertions(+), 24 deletions(-) create mode 100644 docs/now/2026-08-30-a-window-read-as-a-lifetime-withdrawn.md diff --git a/.claude/skills/ci-gates/SKILL.md b/.claude/skills/ci-gates/SKILL.md index 54e513fb86..ad9b52913a 100644 --- a/.claude/skills/ci-gates/SKILL.md +++ b/.claude/skills/ci-gates/SKILL.md @@ -9739,29 +9739,77 @@ against the *actual generated output* rather than reasoning about which code path it came from. The third row above was confirmed by generating the file and running the regex, not by reading the emitter. -## 389. A gate that never runs on master has no baseline — borrow one from siblings +## 389. Query the workflow, not a window — and take the missing reading on purpose -`emit-bitexact-gate.yml` is `on: pull_request` with a `paths:` filter and no -`push:`. It has therefore **never run on master**, so "is it red on master too?" -is unanswerable, and `gh run list --branch master` returns nothing for it — which -reads like "no data" and is easy to mistake for "no problem". +**This section carried a false claim for one day. The claim was mine and it is +corrected here rather than left standing beside a correction.** -The baseline is still available: the same workflow ran on every other recent -branch. +What I wrote: *"`emit-bitexact-gate.yml` has therefore NEVER run on master, so +`gh run list --branch master` returns nothing for it."* + +What is true: -```bash -gh run list --repo --workflow .yml -L 20 \ - --json headBranch,conclusion,createdAt ``` +$ gh run list --repo gHashTag/t27 --workflow emit-bitexact-gate.yml \ + --branch master -L 20 --json event,conclusion,createdAt + 2026-08-28 workflow_dispatch success run 33150988445 + 2026-08-20 workflow_dispatch failure run 32319733329 +``` + +Two master runs, and the most recent is a **success from two days before the +change I was trying to judge** — precisely the baseline I said did not exist. + +### How the wrong reading was produced + +I ran `gh run list --repo --branch master -L 40 --json name,conclusion`, +filtered the result by workflow name, found nothing, and printed +"нет в выборке" — then wrote it up as "never runs on master". + +That query is a **window over all workflows**, forty runs deep. This repository +pushes to master often enough that forty runs is under an hour. A workflow +absent from that window has not been shown to be absent from the branch; it has +been shown to be absent from the last forty runs. The per-workflow query above +is a different question with a different answer, and it costs the same. + +The trap is already written down: `-L N` is a window, not a lifetime count. I put +that sentence into the instructions for a fan-out **in the same session**, and +then read a window as a lifetime myself. + +### What survives, and it is the more useful half + +* **No `push:` trigger means no baseline is produced automatically.** That part + was right and is the thing worth fixing: `on: pull_request` + `paths:` and no + `push:` yields zero automatic master history, so the ordinary run of days + leaves nothing to compare against. +* **`workflow_dispatch` was there the whole time**, since the file's creating + commit `1b47f8b85`. The missing measurement did not have to be borrowed — it + could have been **taken**. That is exactly what this repository's own + `tri gates unmeasured` doctrine says to do with an unmeasured gate, and what + `gate-topology.yml`'s header says `workflow_dispatch` is for. +* **Borrowing from siblings still works and still gave the right verdict** — + green on five other branches, red on exactly one, which convicted the change. + It is a good second instrument. It is not the first thing to reach for when + the gate can simply be fired at master. + +So the order is: query the workflow directly; if that is genuinely empty and the +gate carries `workflow_dispatch`, fire it at master and take the reading; only +then borrow a baseline sideways from sibling branches. + +### The general form + +Three statements look identical in a terminal and are not: + +| statement | how to establish it | +|---|---| +| "it has not run recently" | a window: `gh run list --branch master -L N` | +| "it has not run on this branch, ever" | per-workflow: `--workflow --branch master` | +| "it cannot run on this branch" | read the `on:` block | -Green on five sibling branches, including two from other agents and one of my -own from the same hour — red on exactly one. That convicts the change without -any master run existing. It is also the cheapest way to separate "my change did -this" from "this gate is just broken today". +Reporting the third when you measured the first is the error. Say which one you +took. -(See also 348 on gates with `paths:` and no `push:`. The complement of that -lesson is this one: no baseline does not mean no measurement, it means look -sideways instead of backwards.) +(See also 348 on gates with `paths:` and no `push:`, and 395 on a guard whose +clean line was a claim about a population it never read.) ## 390. `git checkout` does not rebuild diff --git a/.github/workflows/harness-scratch.yml b/.github/workflows/harness-scratch.yml index 174eb8cbc1..56412d1917 100644 --- a/.github/workflows/harness-scratch.yml +++ b/.github/workflows/harness-scratch.yml @@ -22,13 +22,16 @@ # NO `paths:` FILTER, deliberately, for two reasons. The first is the same one # the orphan gate gives: the file that introduces the collision is not # necessarily the file that dies of it, so a filter on the failing path would be -# silent for exactly this defect. The second is newer and cost a measurement -# today -- `emit-bitexact-gate.yml` is `pull_request` with a `paths:` filter and -# no `push:`, so it has NEVER run on master. When a change made it fail there was -# no baseline to compare against and `gh run list --branch master` returned -# nothing, which reads like "no data" and is one careless sentence away from -# "no problem". A gate with no master history cannot answer the only question -# that matters when it goes red. +# silent for exactly this defect. The second is newer and cost +# a measurement today -- `emit-bitexact-gate.yml` is `pull_request` with a +# `paths:` filter and no `push:`, so no master run is ever produced +# automatically. Two exist, both fired by hand through `workflow_dispatch` +# (2026-08-20 failure, 2026-08-28 success), and I did not find them: I read a +# forty-run window across ALL workflows on master, saw the name absent, and +# wrote it up as "never runs on master". A window is not a lifetime. +# `push: branches: [master]` costs one line and means the answer to "is it red +# on master too?" is always already there, instead of depending on somebody +# having remembered to dispatch it. # # A refusal, not a ratchet: the class is CLOSED as of #2949 and #2955 -- the # command reports `none` on master today, and there is no ceiling to hold, no diff --git a/cli/tri/src/gates.rs b/cli/tri/src/gates.rs index ee222bd9c5..8f848e7168 100644 --- a/cli/tri/src/gates.rs +++ b/cli/tri/src/gates.rs @@ -2369,6 +2369,108 @@ fn has_path_filter(root: &std::path::Path, rel: &str) -> bool { } } +/// Can this workflow ever produce a default-branch run WITHOUT a human? +/// +/// `unmeasured` above reports staleness: how long since the last default-branch +/// run. That is the right question and it has a blind spot, which cost a +/// measurement on 2026-08-30. `emit-bitexact-gate.yml` has no `push:` trigger at +/// all, so it produces no automatic default-branch history ever -- but it had +/// been DISPATCHED by hand two days earlier, so it was not stale, so it never +/// appeared in that table, and its absence read as health. +/// +/// Staleness and reachability are different facts. A workflow can be fresh and +/// still be structurally incapable of producing the next reading on its own. +/// +/// Parsed by line rather than by a YAML crate, matching `has_path_filter` and +/// `has_dispatch` above -- and with the two traps that reading takes: +/// * a `push:` inside a COMMENT is not a trigger. This repository has a +/// workflow whose header comment recommends `push: branches: [master]` in +/// prose, and a naive scan reads its own advice as compliance. +/// * `push:` nested under something else (a job `if:`, a `paths:` list) is not +/// the trigger either, so only the first indent level inside `on:` counts. +fn has_auto_default_run(text: &str, default_branch: &str) -> bool { + let mut in_on = false; + let mut on_indent = usize::MAX; + let mut push_indent = usize::MAX; + let mut in_push = false; + let mut saw_push = false; + let mut branches: Option> = None; + + for raw in text.lines() { + let trimmed = raw.trim_start(); + if trimmed.is_empty() || trimmed.starts_with('#') { + continue; + } + let indent = raw.len() - trimmed.len(); + + if !in_on { + if indent == 0 && (trimmed.starts_with("on:") || trimmed.starts_with("\"on\":")) { + // Inline form: `on: [push, pull_request]` or `on: push`. + let rest = trimmed.splitn(2, ':').nth(1).unwrap_or("").trim(); + if !rest.is_empty() { + return rest.contains("push"); + } + in_on = true; + } + continue; + } + + // A column-0 key ends the `on:` block. + if indent == 0 { + break; + } + if on_indent == usize::MAX { + on_indent = indent; + } + + if indent == on_indent { + in_push = trimmed.starts_with("push:"); + if in_push { + saw_push = true; + push_indent = indent; + let rest = trimmed.splitn(2, ':').nth(1).unwrap_or("").trim(); + if !rest.is_empty() { + branches = Some(parse_branch_list(rest)); + } + } + continue; + } + + if in_push && indent > push_indent && trimmed.starts_with("branches:") { + let rest = trimmed.splitn(2, ':').nth(1).unwrap_or("").trim(); + branches = Some(parse_branch_list(rest)); + } + // `branches:` written as a block list under `push:`. + if in_push && branches.is_some() && trimmed.starts_with("- ") { + if let Some(b) = branches.as_mut() { + b.push(trimmed[2..].trim().trim_matches('"').trim_matches('\'').to_string()); + } + } + } + + if !saw_push { + return false; + } + match branches { + // `push:` with nothing under it fires on every branch. + None => true, + Some(list) if list.is_empty() => true, + Some(list) => list + .iter() + .any(|b| b == default_branch || b == "*" || b == "**"), + } +} + +/// `[master, main]`, `[ "master" ]`, or an empty string for a block list below. +fn parse_branch_list(rest: &str) -> Vec { + let inner = rest.trim().trim_start_matches('[').trim_end_matches(']'); + inner + .split(',') + .map(|s| s.trim().trim_matches('"').trim_matches('\'').to_string()) + .filter(|s| !s.is_empty()) + .collect() +} + /// Can a human get the missing reading at all? Without `workflow_dispatch:` /// there is no way to fire it against the default branch on purpose, so the /// gap cannot be closed even by someone who wants to. @@ -2383,6 +2485,10 @@ fn has_dispatch(root: &std::path::Path, rel: &str) -> bool { fn unmeasured(repos: &[String], stale_days: u64) -> Result<()> { let root = repo_root()?; + let mut no_auto: Vec<(String, String, bool, String)> = Vec::new(); + // Named once, so the printed sentence quotes the branch actually queried + // rather than the word "master" hardcoded into a message. + let mut default_branch_seen = String::new(); let mut rows: Vec<(String, String, String, bool, bool)> = Vec::new(); let mut checked = 0usize; let mut unreadable = 0usize; @@ -2393,6 +2499,7 @@ fn unmeasured(repos: &[String], stale_days: u64) -> Result<()> { .trim() .to_string(); + default_branch_seen = default_branch.clone(); let listing = gh(&[ "api", &format!("repos/{repo}/actions/workflows?per_page=100"), @@ -2458,6 +2565,25 @@ fn unmeasured(repos: &[String], stale_days: u64) -> Result<()> { None => true, } }; + // Reachability, not staleness: asked of EVERY workflow with a file, + // including the ones that are perfectly fresh. A workflow that was + // dispatched by hand yesterday is not stale and still cannot take + // tomorrow's reading by itself. + if let Ok(text) = std::fs::read_to_string(root.join(path)) { + if !has_auto_default_run(&text, &default_branch) { + no_auto.push(( + repo.clone(), + name.to_string(), + has_dispatch(&root, path), + if last.is_empty() { + "never".to_string() + } else { + last[..10].to_string() + }, + )); + } + } + if stale { rows.push(( repo.clone(), @@ -2492,6 +2618,40 @@ fn unmeasured(repos: &[String], stale_days: u64) -> Result<()> { either fresh or stale." ); } + if no_auto.is_empty() { + println!( + " Workflows with no automatic default-branch trigger: 0 \ + (every one can produce its own baseline)." + ); + } else { + no_auto.sort_by(|a, b| a.1.cmp(&b.1)); + println!( + "{} workflow(s) can NEVER produce a default-branch run on their own -- no\n\ + `push:` covering `{}`. Their default-branch history exists only where a\n\ + human dispatched them, so \"is it red on the default branch too?\" has no\n\ + standing answer when one of them goes red on a pull request.\n", + no_auto.len(), + if default_branch_seen.is_empty() { "the default branch" } else { default_branch_seen.as_str() } + ); + println!(" {:<10} {:<9} {}", "LAST", "dispatch", "WORKFLOW"); + for (repo, name, dispatch, last) in &no_auto { + println!( + " {:<10} {:<9} {} ({})", + last, + if *dispatch { "yes" } else { "NO" }, + name, + repo + ); + } + println!( + "\n `dispatch: yes` means the missing reading can be TAKEN -- fire it at the\n\ + default branch rather than inferring the baseline from sibling branches.\n\ + `LAST` here is a lifetime per-workflow query, not a window over recent\n\ + runs: reading a window and reporting a lifetime is how this section came\n\ + to exist.\n" + ); + } + if rows.is_empty() { println!( "Every active workflow has run on the default branch within {stale_days} days \ @@ -3854,3 +4014,115 @@ def main():\n if problems:\n return 2\n return 0\n"; assert!(out.is_empty(), "no site, no verdict: {out:?}"); } } + +#[cfg(test)] +mod auto_default_run_tests { + use super::has_auto_default_run; + + /// The shape that cost the measurement: `pull_request` and a `paths:` + /// filter, no `push:` at all. Two default-branch runs exist for this file, + /// both dispatched by hand, so a STALENESS check reports it healthy. + #[test] + fn pull_request_only_cannot_produce_a_baseline() { + let y = "name: x\non:\n pull_request:\n paths:\n - 'bootstrap/**'\n workflow_dispatch:\njobs: {}\n"; + assert!(!has_auto_default_run(y, "master")); + } + + #[test] + fn push_to_the_default_branch_can() { + let y = "name: x\non:\n pull_request:\n push:\n branches: [master]\n workflow_dispatch:\njobs: {}\n"; + assert!(has_auto_default_run(y, "master")); + } + + /// `push:` with nothing under it fires on every branch, the default + /// included. Reading "no branches listed" as "no branches" would call a + /// workflow unreachable that runs on every push in the repository. + #[test] + fn a_bare_push_covers_every_branch() { + let y = "name: x\non:\n push:\n pull_request:\njobs: {}\n"; + assert!(has_auto_default_run(y, "master")); + } + + #[test] + fn push_to_some_other_branch_does_not() { + let y = "name: x\non:\n push:\n branches: [release, staging]\njobs: {}\n"; + assert!(!has_auto_default_run(y, "master")); + } + + #[test] + fn a_block_list_is_read_as_well_as_an_inline_one() { + let y = "name: x\non:\n push:\n branches:\n - main\n - master\njobs: {}\n"; + assert!(has_auto_default_run(y, "master")); + } + + #[test] + fn the_inline_on_form_is_read() { + assert!(has_auto_default_run("on: [push, pull_request]\njobs: {}\n", "master")); + assert!(!has_auto_default_run("on: [pull_request]\njobs: {}\n", "master")); + } + + /// COUNTEREXAMPLE. `harness-scratch.yml` recommends `push: branches: + /// [master]` in prose in its own header. A scan that trims whitespace and + /// looks for `push:` reads that advice as compliance and reports the file + /// covered when it is not. + #[test] + fn a_push_inside_a_comment_is_not_a_trigger() { + let y = "# `push: branches: [master]` costs one line and is worth it.\n\ + name: x\n\ + on:\n\ + \x20 pull_request:\n\ + jobs: {}\n"; + assert!(!has_auto_default_run(y, "master")); + } + + /// COUNTEREXAMPLE. A `push` deeper than the first level inside `on:` -- or + /// anywhere in the jobs below -- is not the trigger either. + #[test] + fn a_nested_push_is_not_the_trigger() { + let y = "name: x\non:\n pull_request:\n paths:\n - 'push:'\njobs:\n a:\n steps:\n - run: git push\n"; + assert!(!has_auto_default_run(y, "master")); + } + + /// A wildcard branch list covers the default branch. + #[test] + fn a_wildcard_branch_list_covers_it() { + let y = "name: x\non:\n push:\n branches: ['**']\njobs: {}\n"; + assert!(has_auto_default_run(y, "master")); + } + + /// The default branch is a parameter, not the literal `master`. + #[test] + fn the_default_branch_is_not_hardcoded() { + let y = "name: x\non:\n push:\n branches: [main]\njobs: {}\n"; + assert!(has_auto_default_run(y, "main")); + assert!(!has_auto_default_run(y, "master")); + } + + /// Every workflow file in this repository is read, and the answer is + /// compared against a second reader written differently: the file has a + /// `push:` line at the first level inside `on:` at all. The two may + /// disagree only where a branch filter excludes the default branch, and + /// that disagreement is the whole point of the function. + #[test] + fn every_workflow_in_the_tree_parses_without_panicking() { + let dir = std::path::Path::new(".github/workflows"); + if !dir.is_dir() { + return; // run from a subdirectory; nothing to say + } + let mut read = 0usize; + for e in std::fs::read_dir(dir).expect("read .github/workflows") { + let p = e.expect("entry").path(); + if p.extension().and_then(|s| s.to_str()) != Some("yml") { + continue; + } + let t = std::fs::read_to_string(&p).expect("read workflow"); + let _ = has_auto_default_run(&t, "master"); + read += 1; + } + assert!( + read > 10, + "expected to read the workflow directory, read {read} file(s) -- a test \ + that reads nothing passes vacuously" + ); + } +} diff --git a/docs/now/2026-08-30-a-window-read-as-a-lifetime-withdrawn.md b/docs/now/2026-08-30-a-window-read-as-a-lifetime-withdrawn.md new file mode 100644 index 0000000000..b1f1faec03 --- /dev/null +++ b/docs/now/2026-08-30-a-window-read-as-a-lifetime-withdrawn.md @@ -0,0 +1,8 @@ +# NOW -- A window read as a lifetime, withdrawn (2026-08-30) + +## A window read as a lifetime, withdrawn (Closes #2959) + +- skill 389 and the harness-scratch header both said emit-bitexact has NEVER run on master; it has run twice, and the 2026-08-28 success is the baseline I said did not exist +- the wrong reading came from gh run list --branch master -L 40, a window over ALL workflows, filtered by name -- and tri gates unmeasured already answered this correctly by lifetime +- what the tool did lack is reachability rather than staleness: 17 workflows can never produce a default-branch run without a human, 5 of them merge-critical, one of those a required context +- has_auto_default_run has 11 tests including two counterexamples a review would not produce -- a push: inside a comment, and a push nested below the first level of on: From 841a0f1dd79cba7ce370bbca8e4ebbff56cc708c Mon Sep 17 00:00:00 2001 From: Vasilev Dmitrii Date: Sun, 30 Aug 2026 23:01:22 +0700 Subject: [PATCH 2/4] fix(tri): branch filters are patterns, and two readers agree at 17 The first version tested branch names for equality, so `branches: ['ma*']` would have read as "does not cover master" and reported a workflow structurally unmeasurable while it runs on every push. No workflow here uses such a pattern today -- an independent yaml+fnmatch reader agrees with this function on all 49 files -- so equality was correct here and wrong in principle, which is the worst kind of correct. GitHub's subset implemented: `*` matches within one path segment, `**` across them. `?` and character classes are treated as literals, which errs toward REPORTING a workflow as uncovered: a false entry in a work list is visible, a wrong silence is not. Five more tests, including the live shape that separates the two questions. notebook-sync.yml pushes on four patterns and none is master, so counting the push KEY gives 16 files without one and counting COVERAGE gives 17. 17 is the number that answers "can this produce a baseline", and it is where an independent sweep's 16 and this command's 17 turned out to agree rather than conflict -- they were answering different questions. Refs #2959 Co-Authored-By: Claude Opus 5 --- cli/tri/src/gates.rs | 101 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 98 insertions(+), 3 deletions(-) diff --git a/cli/tri/src/gates.rs b/cli/tri/src/gates.rs index 8f848e7168..28538f037d 100644 --- a/cli/tri/src/gates.rs +++ b/cli/tri/src/gates.rs @@ -2455,10 +2455,50 @@ fn has_auto_default_run(text: &str, default_branch: &str) -> bool { // `push:` with nothing under it fires on every branch. None => true, Some(list) if list.is_empty() => true, - Some(list) => list - .iter() - .any(|b| b == default_branch || b == "*" || b == "**"), + Some(list) => list.iter().any(|b| branch_pattern_matches(b, default_branch)), + } +} + +/// GitHub branch filters are PATTERNS, not names. +/// +/// An equality test reads `branches: ['ma*']` as "does not cover master" and +/// reports a workflow structurally unmeasurable when it runs on every push. The +/// list in this repository happens to contain no such pattern today -- an +/// independent yaml+fnmatch reader agrees with this function on all 49 files -- +/// so the equality version would have been correct here and wrong in principle, +/// which is the worst kind of correct. +/// +/// The subset implemented is GitHub's: `*` matches within one path segment, +/// `**` matches across segments, and everything else is literal. `?`, `+` and +/// character classes are NOT implemented and are treated as literals, which +/// errs toward reporting a workflow as uncovered -- a false entry in a work +/// list rather than a silence. +fn branch_pattern_matches(pattern: &str, branch: &str) -> bool { + fn walk(p: &[u8], b: &[u8]) -> bool { + if p.is_empty() { + return b.is_empty(); + } + if p[0] == b'*' { + let double = p.len() > 1 && p[1] == b'*'; + let rest = if double { &p[2..] } else { &p[1..] }; + // `*` stops at a `/`; `**` crosses them. + let mut i = 0; + loop { + if walk(rest, &b[i..]) { + return true; + } + if i >= b.len() { + return false; + } + if !double && b[i] == b'/' { + return false; + } + i += 1; + } + } + !b.is_empty() && p[0] == b[0] && walk(&p[1..], &b[1..]) } + walk(pattern.as_bytes(), branch.as_bytes()) } /// `[master, main]`, `[ "master" ]`, or an empty string for a block list below. @@ -4126,3 +4166,58 @@ mod auto_default_run_tests { ); } } + +#[cfg(test)] +mod branch_pattern_tests { + use super::{branch_pattern_matches, has_auto_default_run}; + + #[test] + fn a_literal_matches_itself_and_nothing_else() { + assert!(branch_pattern_matches("master", "master")); + assert!(!branch_pattern_matches("master", "main")); + assert!(!branch_pattern_matches("mast", "master")); + assert!(!branch_pattern_matches("master", "mast")); + } + + /// The case an equality test gets wrong: a pattern that covers the default + /// branch without naming it. + #[test] + fn a_star_covers_the_default_branch() { + assert!(branch_pattern_matches("ma*", "master")); + assert!(branch_pattern_matches("*", "master")); + assert!(branch_pattern_matches("**", "master")); + let y = "on:\n push:\n branches: ['ma*']\njobs: {}\n"; + assert!(has_auto_default_run(y, "master")); + } + + /// `*` stops at a slash and `**` does not -- GitHub's rule, and the reason + /// `feature/**` does not cover `master` while `**` does. + #[test] + fn one_star_stops_at_a_slash_and_two_do_not() { + assert!(!branch_pattern_matches("feature/*", "feature/a/b")); + assert!(branch_pattern_matches("feature/**", "feature/a/b")); + assert!(branch_pattern_matches("feature/*", "feature/a")); + assert!(!branch_pattern_matches("*", "a/b")); + assert!(branch_pattern_matches("**", "a/b")); + } + + /// The live shape that separates "has a push key" from "has a push covering + /// master": notebook-sync.yml pushes on four patterns, none of them master. + /// Counting the KEY gives 16 files with no push; counting COVERAGE gives 17, + /// and 17 is the number that answers "can this produce a baseline". + #[test] + fn a_push_whose_patterns_exclude_the_default_is_not_coverage() { + let y = "on:\n push:\n branches: ['feature/**', 'fix/**', 'ring-*/**', 'issue-*/**']\njobs: {}\n"; + assert!(!has_auto_default_run(y, "master")); + assert!(has_auto_default_run(y, "feature/x")); + } + + /// Unimplemented syntax is treated as a literal, so it fails to match rather + /// than matching everything. A wrong answer in a work list is visible; a + /// wrong silence is not. + #[test] + fn unimplemented_syntax_errs_toward_reporting() { + assert!(!branch_pattern_matches("mast?r", "master")); + assert!(!branch_pattern_matches("+([a-z])", "master")); + } +} From 2d75a60695b33223c7797ed2df39fb029b8f231e Mon Sep 17 00:00:00 2001 From: Vasilev Dmitrii Date: Sun, 30 Aug 2026 23:05:07 +0700 Subject: [PATCH 3/4] fix(tri): 'can be started' is not 'can be measured' A correction to a message this command printed for about an hour. It said "dispatch: yes means the missing reading can be TAKEN" -- and for five of the seventeen it says that about, dispatching starts the workflow and measures nothing, because the check's subject IS a pull request. tools/check_now_entry_shape.py is explicit and deliberately so: on any event that is not pull_request it prints "NOT APPLICABLE ... Nothing was checked and nothing is claimed" and exits 0. Honest in its log, green in the checks list -- and `check` is one of the four contexts the ruleset requires. New `pr-only` column, measured by the pull-request context the workflow passes down. Of the 17: 12 takeable, 5 not -- Check Now Freshness, Issue Gate, L1 TRACEABILITY, NotebookLM Auto-Sync, PR Dashboard. Three of those five are merge-critical or required. The grep is weaker than reading the scripts each workflow calls, and it errs toward marking FEWER files: a false `-` sends someone to dispatch a gate that declines, which is recoverable; a false `YES` would tell them not to bother taking a reading that was available. A test pins that direction. Refs #2959 Co-Authored-By: Claude Opus 5 --- cli/tri/src/gates.rs | 102 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 93 insertions(+), 9 deletions(-) diff --git a/cli/tri/src/gates.rs b/cli/tri/src/gates.rs index 28538f037d..6a7a1c0680 100644 --- a/cli/tri/src/gates.rs +++ b/cli/tri/src/gates.rs @@ -2369,6 +2369,43 @@ fn has_path_filter(root: &std::path::Path, rel: &str) -> bool { } } +/// Does this workflow READ pull-request context? +/// +/// A correction to a message this command printed for about an hour. It said +/// "`dispatch: yes` means the missing reading can be TAKEN" -- and for five of +/// the seventeen it says that about, dispatching starts the workflow and +/// measures nothing, because the check's subject IS a pull request. +/// +/// `tools/check_now_entry_shape.py` is explicit about it, and deliberately so: +/// on any event that is not `pull_request` it prints "NOT APPLICABLE ... Nothing +/// was checked and nothing is claimed" and exits 0. It is honest in its log and +/// green in the checks list, and `check` is one of the four contexts the ruleset +/// requires. +/// +/// So "can be started" and "can be measured" are different, and a tool that +/// conflates them sends a reader to dispatch a gate that will decline. This is +/// a grep over the workflow text, which is a weaker instrument than reading the +/// scripts it calls -- it finds the context the WORKFLOW passes down, not every +/// way a script might depend on a pull request. It errs toward marking fewer +/// files, so the column understates rather than overstates. +fn reads_pr_context(root: &std::path::Path, rel: &str) -> bool { + match std::fs::read_to_string(root.join(rel)) { + Ok(t) => text_reads_pr_context(&t), + Err(_) => false, + } +} + +/// Split from the file read so the markers can be tested without a tree. +fn text_reads_pr_context(text: &str) -> bool { + const MARKERS: [&str; 4] = [ + "github.event.pull_request", + "PR_BASE_SHA", + "PR_HEAD_SHA", + "github.event.number", + ]; + MARKERS.iter().any(|m| text.contains(m)) +} + /// Can this workflow ever produce a default-branch run WITHOUT a human? /// /// `unmeasured` above reports staleness: how long since the last default-branch @@ -2525,7 +2562,7 @@ fn has_dispatch(root: &std::path::Path, rel: &str) -> bool { fn unmeasured(repos: &[String], stale_days: u64) -> Result<()> { let root = repo_root()?; - let mut no_auto: Vec<(String, String, bool, String)> = Vec::new(); + let mut no_auto: Vec<(String, String, bool, bool, String)> = Vec::new(); // Named once, so the printed sentence quotes the branch actually queried // rather than the word "master" hardcoded into a message. let mut default_branch_seen = String::new(); @@ -2615,6 +2652,7 @@ fn unmeasured(repos: &[String], stale_days: u64) -> Result<()> { repo.clone(), name.to_string(), has_dispatch(&root, path), + reads_pr_context(&root, path), if last.is_empty() { "never".to_string() } else { @@ -2673,22 +2711,34 @@ fn unmeasured(repos: &[String], stale_days: u64) -> Result<()> { no_auto.len(), if default_branch_seen.is_empty() { "the default branch" } else { default_branch_seen.as_str() } ); - println!(" {:<10} {:<9} {}", "LAST", "dispatch", "WORKFLOW"); - for (repo, name, dispatch, last) in &no_auto { + println!( + " {:<10} {:<9} {:<8} {}", + "LAST", "dispatch", "pr-only", "WORKFLOW" + ); + for (repo, name, dispatch, pr_only, last) in &no_auto { println!( - " {:<10} {:<9} {} ({})", + " {:<10} {:<9} {:<8} {} ({})", last, if *dispatch { "yes" } else { "NO" }, + if *pr_only { "YES" } else { "-" }, name, repo ); } println!( - "\n `dispatch: yes` means the missing reading can be TAKEN -- fire it at the\n\ - default branch rather than inferring the baseline from sibling branches.\n\ - `LAST` here is a lifetime per-workflow query, not a window over recent\n\ - runs: reading a window and reporting a lifetime is how this section came\n\ - to exist.\n" + "\n `dispatch: yes` with `pr-only: -` means the missing reading can be TAKEN:\n\ + fire it at the default branch rather than inferring a baseline from\n\ + sibling branches.\n\ + \n `pr-only: YES` means it CANNOT. Those workflows read pull-request context,\n\ + so dispatching one starts it and measures nothing -- check-now-freshness\n\ + prints \"NOT APPLICABLE ... nothing was checked and nothing is claimed\"\n\ + and exits 0, which is green in the checks list. For those the answer is\n\ + not a dispatch: either the check learns a default-branch mode, or the\n\ + context is recorded as PR-only by construction and stops being read as a\n\ + gap.\n\ + \n `LAST` is a lifetime per-workflow query, not a window over recent runs.\n\ + Reading a window and reporting a lifetime is how this section came to\n\ + exist.\n" ); } @@ -4221,3 +4271,37 @@ mod branch_pattern_tests { assert!(!branch_pattern_matches("+([a-z])", "master")); } } + +#[cfg(test)] +mod pr_context_tests { + use super::text_reads_pr_context; + + /// The three required-or-merge-critical shapes this separates. Dispatching + /// any of them at the default branch starts the workflow and measures + /// nothing, so "the reading can be taken" is false for them. + #[test] + fn the_three_marker_families_are_all_seen() { + assert!(text_reads_pr_context("env:\n PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}\n")); + assert!(text_reads_pr_context("run: echo ${{ github.event.pull_request.title }}\n")); + assert!(text_reads_pr_context("run: gh pr view ${{ github.event.number }}\n")); + } + + /// A workflow that only ever reads the ref or the SHA is measurable by + /// dispatch, and must not be marked. + #[test] + fn ordinary_context_is_not_pull_request_context() { + let y = "run: echo ${{ github.ref }} ${{ github.sha }} ${{ github.repository }}\n"; + assert!(!text_reads_pr_context(y)); + } + + /// The column understates on purpose: this grep sees the context the + /// WORKFLOW passes down, not every way a script it calls might depend on a + /// pull request. A false "-" sends someone to dispatch a gate that then + /// declines, which is recoverable; a false "YES" would tell them not to + /// bother taking a reading that was available. + #[test] + fn a_script_that_reads_the_event_json_itself_is_not_seen() { + let y = "run: python3 tools/gate.py # reads GITHUB_EVENT_PATH inside\n"; + assert!(!text_reads_pr_context(y)); + } +} From 0a99491f7fd6d309f1d4e704259b7dd1de1cd4a5 Mon Sep 17 00:00:00 2001 From: Vasilev Dmitrii Date: Sun, 30 Aug 2026 23:08:02 +0700 Subject: [PATCH 4/4] fix(readme): the CI badge pointed at a workflow that is not on master `ci.yml` does not exist on origin/master. Its registration is still active and its 23 lifetime runs are all on `dev`, the last of them 2026-04-22. With the README's own `?branch=master` the badge renders NO STATUS; drop that one parameter and it renders PASSING off April's dev history. The repository's most public claim about its own CI is one query parameter from a false green and cannot ever go red. Repointed at bootstrap-tests.yml -- the compiler's own test suite, 97 master runs. Reviewable: seal-coverage (914 master runs, 593 green), corpus-ratchet (409/182), schema-validation (428/426), secret-scan (711/707), gate-topology (351/351) and cli-tri (119/103) were the alternatives measured. cli-tri was rejected because #2903 records that it builds `-p tri` behind a `cli/**` filter, so a change to cli/tri-mcp turns a different crate green. The version badge said 0.1.0; bootstrap/Cargo.toml says 0.2.0 and the tag t27c-v0.2.0 exists. Deregistering the twelve fileless registrations appears unavailable -- GitHub keeps them after the file is deleted and exposes no delete endpoint -- so the consumer is what gets fixed. Refs #2961 Co-Authored-By: Claude Opus 5 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3c2950ad82..a5d0e74d0c 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # Trinity S³AI DNA -- t27 -- TRI-27 Spec-First Language -[![CI](https://img.shields.io/github/actions/workflow/status/gHashTag/t27/ci.yml?branch=master&logo=github&label=CI)](https://github.com/gHashTag/t27/actions/workflows/ci.yml) +[![CI](https://img.shields.io/github/actions/workflow/status/gHashTag/t27/bootstrap-tests.yml?branch=master&logo=github&label=CI)](https://github.com/gHashTag/t27/actions/workflows/bootstrap-tests.yml) [![Zenodo](https://zenodo.org/badge/DOI/10.5281/zenodo.19456875.svg)](https://doi.org/10.5281/zenodo.19456875) [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) -[![Version: 0.1.0](https://img.shields.io/badge/version-0.1.0-orange.svg)](https://github.com/gHashTag/t27/releases) +[![Version: 0.2.0](https://img.shields.io/badge/version-0.2.0-orange.svg)](https://github.com/gHashTag/t27/releases) > **Canonical Zenodo SOT:** [zenodo.org/communities/trinity-s3ai](https://zenodo.org/communities/trinity-s3ai/). The GoldenFloat badge above (19456875) is a legitimate Vasilev deposit but lives **outside** the curated S³AI v5.0 record set; see [docs/ZENODO.md](docs/ZENODO.md) for the canonical 12-record bundle and aliases.