diff --git a/.trinity/seals/verified_VerifiedFrontierScope.json b/.trinity/seals/verified_VerifiedFrontierScope.json new file mode 100644 index 0000000000..bc07e9d6a4 --- /dev/null +++ b/.trinity/seals/verified_VerifiedFrontierScope.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+b48fe679", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:40867ce38648d37e21102575b6f3c59d7fd1a451de76c69faff143f2bb2ffc46", + "gen_hash_rust": "sha256:2a4bd91803cb4b164eda7a7c7686ca6a5abb91410b003c4418bcb1de95bd66b3", + "gen_hash_verilog": "sha256:e6c2cf0de35e57ceb70dcf4c8817ac42a9298094c462fe86aa0345e17044a0ec", + "gen_hash_zig": "sha256:51f505425fc868ec1a368eb5aa3922514ea7c16718caafd9ca14a8bd015f7a5c", + "module": "VerifiedFrontierScope", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T05:28:40Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:d0ddbc204ec423a718165f62512a3720d94b628ee6aa40a74609796994af7a9a", + "spec_path": "specs/verified/frontier_scope.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/bootstrap/gen/rust/ci/affected.rs b/bootstrap/gen/rust/ci/affected.rs new file mode 100644 index 0000000000..e2c8e7b731 --- /dev/null +++ b/bootstrap/gen/rust/ci/affected.rs @@ -0,0 +1,411 @@ +// Generated from .t27 spec +// DO NOT EDIT — generated by t27c +// NOT LOWERED BY THIS BACKEND: 15 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. + +pub const RUN_ALL: u32 = 0xFFFFFFFF; + +pub const C_SEED: u8 = 1; + +pub const C_NOTHING: u8 = 2; + +pub const C_ALL_OUTSIDE: u8 = 16; + +pub const C_ALL_UNDER_SPECS: u8 = 17; + +pub const C_ALL_UNREADABLE: u8 = 18; + +pub const ALL_FROM: u8 = 16; + +pub const SPEC_EXTS: &'static str = "t27,tri,$"; + +pub const PROSE_EXTS: &'static str = "md,txt,$"; + +pub const SPECS_ROOT: &'static str = "specs/,$"; + +pub const PROSE_ANYWHERE_EXTS: &'static str = "md,$"; + +pub const SEALS_ROOT: &'static str = ".trinity/seals/,$"; + +pub const SEAL_EXTS: &'static str = "json,$"; + +pub const KINDS: &'static str = "ACDMRT$"; + +pub const MARK_GRAPH: &'static str = "--graph$"; + +pub const MARK_CORPUS: &'static str = "--corpus$"; + +pub const LIST_END: u8 = 36; + +pub const LIST_SEP: u8 = 44; + +pub fn lower(c: u8) -> u8 { + if ((c >= b'A') && (c <= b'Z')) { + return (c + 32); + } + return c; +} + +pub fn in_list(list: &[u8], s: &[u8], from: usize, to: usize) -> bool { + let mut i: usize = 0; + while (list[(i) as usize] != LIST_END) { + let mut k: usize = from; + let mut ok: bool = true; + while ((list[(i) as usize] != LIST_SEP) && (list[(i) as usize] != LIST_END)) { + if (k >= to) { + ok = false; + } else { + if (lower(s[(k) as usize]) != lower(list[(i) as usize])) { + ok = false; + } + } + k += 1; + i += 1; + } + if (ok && (k == to)) { + return true; + } + if (list[(i) as usize] == LIST_SEP) { + i += 1; + } + } + return false; +} + +pub fn ext_dot(s: &[u8], from: usize, to: usize) -> usize { + let mut b: usize = from; + let mut i: usize = from; + while (i < to) { + if (s[(i) as usize] == b'/') { + b = (i + 1); + } + i += 1; + } + let mut d: usize = to; + i = (b + 1); + while (i < to) { + if (s[(i) as usize] == b'.') { + d = i; + } + i += 1; + } + return d; +} + +pub fn path_class(s: &[u8], from: usize, to: usize) -> u8 { + if (to <= from) { + return C_ALL_UNREADABLE; + } + if (s[(from) as usize] == b'"') { + return C_ALL_UNREADABLE; + } + let d: usize = ext_dot(s, from, to); + if (has_prefix(SPECS_ROOT.as_bytes(), s, from, to) == false) { + if (d < to) { + if in_list(PROSE_ANYWHERE_EXTS.as_bytes(), s, (d + 1), to) { + return C_NOTHING; + } + if (has_prefix(SEALS_ROOT.as_bytes(), s, from, to) && in_list(SEAL_EXTS.as_bytes(), s, (d + 1), to)) { + return C_NOTHING; + } + } + return C_ALL_OUTSIDE; + } + if (d < to) { + if in_list(SPEC_EXTS.as_bytes(), s, (d + 1), to) { + return C_SEED; + } + if in_list(PROSE_EXTS.as_bytes(), s, (d + 1), to) { + return C_NOTHING; + } + } + return C_ALL_UNDER_SPECS; +} + +pub fn line_is(buf: &[u8], s: usize, t: usize, lit: &[u8]) -> bool { + let mut k: usize = 0; + while (lit[(k) as usize] != LIST_END) { + if ((s + k) >= t) { + return false; + } + if (buf[((s + k)) as usize] != lit[(k) as usize]) { + return false; + } + k += 1; + } + return ((s + k) == t); +} + +pub fn marker_at(buf: &[u8], n: usize, from: usize, lit: &[u8]) -> usize { + let mut s: usize = from; + while (s < n) { + let e: usize = line_end(buf, s, n); + if line_is(buf, s, text_end(buf, s, e), lit) { + return s; + } + s = (e + 1); + } + return n; +} + +pub fn first_tab(buf: &[u8], s: usize, t: usize) -> usize { + let mut i: usize = s; + while (i < t) { + if (buf[(i) as usize] == b'\t') { + return i; + } + i += 1; + } + return t; +} + +pub fn last_tab(buf: &[u8], s: usize, t: usize) -> usize { + let mut p: usize = t; + let mut i: usize = s; + while (i < t) { + if (buf[(i) as usize] == b'\t') { + p = i; + } + i += 1; + } + return p; +} + +pub fn diff_class(buf: &[u8], s: usize, t: usize) -> u8 { + if (t <= s) { + return C_NOTHING; + } + if (is_letter_of(KINDS.as_bytes(), buf[(s) as usize]) == false) { + return C_ALL_UNREADABLE; + } + let p1: usize = first_tab(buf, s, t); + if (p1 >= t) { + return C_ALL_UNREADABLE; + } + let p2: usize = last_tab(buf, s, t); + let mut c: u8 = path_class(buf, (p2 + 1), t); + if (p2 > p1) { + let old: u8 = path_class(buf, (p1 + 1), p2); + if (old > c) { + c = old; + } + } + return c; +} + +pub fn run_all(buf: &[u8], n: usize) -> bool { + let g: usize = marker_at(buf, n, 0, MARK_GRAPH.as_bytes()); + if (g >= n) { + return true; + } + let c: usize = marker_at(buf, n, g, MARK_CORPUS.as_bytes()); + if (c >= n) { + return true; + } + let mut s: usize = 0; + while (s < g) { + let e: usize = line_end(buf, s, g); + if (diff_class(buf, s, text_end(buf, s, e)) >= ALL_FROM) { + return true; + } + s = (e + 1); + } + return false; +} + +pub fn same(a: &[u8], af: usize, at: usize, b: &[u8], bf: usize, bt: usize) -> bool { + if ((at - af) != (bt - bf)) { + return false; + } + let mut k: usize = 0; + while ((af + k) < at) { + if (a[((af + k)) as usize] != b[((bf + k)) as usize]) { + return false; + } + k += 1; + } + return true; +} + +pub fn in_set(set: &[u8], sn: usize, s: &[u8], f: usize, t: usize) -> bool { + let mut i: usize = 0; + while (i < sn) { + let e: usize = line_end(set, i, sn); + if same(set, i, e, s, f, t) { + return true; + } + i = (e + 1); + } + return false; +} + +pub fn set_add(set: &mut [u8], sn: usize, scap: usize, s: &[u8], f: usize, t: usize) -> usize { + if (sn > scap) { + return sn; + } + if in_set(set, sn, s, f, t) { + return sn; + } + if (((sn + (t - f)) + 1) > scap) { + return (scap + 1); + } + let mut w: usize = sn; + let mut i: usize = f; + while (i < t) { + set[(w) as usize] = s[(i) as usize]; + w += 1; + i += 1; + } + set[(w) as usize] = b'\n'; + return (w + 1); +} + +pub fn closure(buf: &[u8], n: usize, set: &mut [u8], scap: usize) -> usize { + let g: usize = marker_at(buf, n, 0, MARK_GRAPH.as_bytes()); + let c: usize = marker_at(buf, n, g, MARK_CORPUS.as_bytes()); + let mut sn: usize = 0; + let mut s: usize = 0; + while (s < g) { + let e: usize = line_end(buf, s, g); + let t: usize = text_end(buf, s, e); + if (t > s) { + let p1: usize = first_tab(buf, s, t); + let p2: usize = last_tab(buf, s, t); + if (path_class(buf, (p2 + 1), t) == C_SEED) { + sn = set_add(set, sn, scap, buf, (p2 + 1), t); + } + if (p2 > p1) { + if (path_class(buf, (p1 + 1), p2) == C_SEED) { + sn = set_add(set, sn, scap, buf, (p1 + 1), p2); + } + } + } + s = (e + 1); + } + let gs: usize = (line_end(buf, g, n) + 1); + let mut grew: bool = true; + while (grew && (sn <= scap)) { + grew = false; + s = gs; + while ((s < c) && (sn <= scap)) { + let e: usize = line_end(buf, s, c); + let t: usize = text_end(buf, s, e); + let p: usize = first_tab(buf, s, t); + if ((p < t) && (p > s)) { + if (in_set(set, sn, buf, (p + 1), t) && (in_set(set, sn, buf, s, p) == false)) { + sn = set_add(set, sn, scap, buf, s, p); + grew = true; + } + } + s = (e + 1); + } + } + return sn; +} + +pub fn put(out: &mut [u8], w: usize, cap: usize, c: u8) -> usize { + if (w < cap) { + out[(w) as usize] = c; + } + return (w + 1); +} + +pub fn select_affected(buf: &[u8], n: usize, set: &mut [u8], scap: usize, out: &mut [u8], cap: usize) -> u32 { + if run_all(buf, n) { + return RUN_ALL; + } + let sn: usize = closure(buf, n, set, scap); + if (sn > scap) { + return RUN_ALL; + } + let g: usize = marker_at(buf, n, 0, MARK_GRAPH.as_bytes()); + let c: usize = marker_at(buf, n, g, MARK_CORPUS.as_bytes()); + let mut count: u32 = 0; + let mut w: usize = 0; + let mut s: usize = (line_end(buf, c, n) + 1); + while (s < n) { + let e: usize = line_end(buf, s, n); + let t: usize = text_end(buf, s, e); + if ((t > s) && in_set(set, sn, buf, s, t)) { + let mut i: usize = s; + while (i < t) { + w = put(out, w, cap, buf[(i) as usize]); + i += 1; + } + w = put(out, w, cap, b'\n'); + count += 1; + } + s = (e + 1); + } + if (w > cap) { + return RUN_ALL; + } + return count; +} + +pub const ALLOW: u8 = 0; + +pub const DENY: u8 = 1; + +pub const DENY_FROM: u8 = 16; + +pub fn has_prefix(list: &[u8], s: &[u8], from: usize, to: usize) -> bool { + let mut i: usize = 0; + while (list[(i) as usize] != LIST_END) { + let mut k: usize = from; + let mut ok: bool = true; + while ((list[(i) as usize] != LIST_SEP) && (list[(i) as usize] != LIST_END)) { + if (k >= to) { + ok = false; + } else { + if (s[(k) as usize] != list[(i) as usize]) { + ok = false; + } + } + k += 1; + i += 1; + } + if ok { + return true; + } + if (list[(i) as usize] == LIST_SEP) { + i += 1; + } + } + return false; +} + +pub fn is_letter_of(list: &[u8], c: u8) -> bool { + let mut i: usize = 0; + while (list[(i) as usize] != LIST_END) { + if (list[(i) as usize] == c) { + return true; + } + i += 1; + } + return false; +} + +pub fn verdict(r: u8) -> u8 { + if (r >= DENY_FROM) { + return DENY; + } + return ALLOW; +} + +pub fn line_end(buf: &[u8], s: usize, n: usize) -> usize { + let mut e: usize = s; + while ((e < n) && (buf[(e) as usize] != b'\n')) { + e += 1; + } + return e; +} + +pub fn text_end(buf: &[u8], s: usize, e: usize) -> usize { + if ((e > s) && (buf[((e - 1)) as usize] == b'\r')) { + return (e - 1); + } + return e; +} + diff --git a/bootstrap/gen/rust/verified/frontier_scope.rs b/bootstrap/gen/rust/verified/frontier_scope.rs new file mode 100644 index 0000000000..7d38094961 --- /dev/null +++ b/bootstrap/gen/rust/verified/frontier_scope.rs @@ -0,0 +1,14 @@ +// Generated from .t27 spec +// DO NOT EDIT — generated by t27c +// NOT LOWERED BY THIS BACKEND: 4 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. + +pub fn scope_wanted(has_only: bool, has_since: bool) -> bool { + return (has_only || has_since); +} + +pub fn scope_narrows(picked: u32, all: u32) -> bool { + return (picked != all); +} + diff --git a/bootstrap/src/main.rs b/bootstrap/src/main.rs index 6729b8f12b..4d520bff07 100644 --- a/bootstrap/src/main.rs +++ b/bootstrap/src/main.rs @@ -133,6 +133,8 @@ mod suite_jobs; #[path = "../gen/rust/verified/watch.rs"] #[allow(dead_code, unused_parens, unused_mut, unused_assignments, unused_variables, non_snake_case, non_upper_case_globals, clippy::all)] mod watch; +#[path = "../gen/rust/verified/frontier_scope.rs"] #[allow(dead_code, unused_parens, unused_mut, unused_assignments, unused_variables, non_snake_case, non_upper_case_globals, clippy::all)] mod frontier_scope; // specs/verified/frontier_scope.t27 (#10236): whether, and how far, a scoped frontier narrows its walk. +#[path = "../gen/rust/ci/affected.rs"] #[allow(dead_code, unused_parens, unused_mut, unused_assignments, unused_variables, non_snake_case, non_upper_case_globals, clippy::all)] mod affected; // specs/ci/affected.t27 (#7565): the specs a change can move; CI's rule, here the selection behind `frontier --only/--since`. // specs/verified/silicon_queue.t27 (#8095 step 5): the bench queue and the reseal guard. #[path = "../gen/rust/verified/silicon_queue.rs"] #[allow(dead_code, unused_parens, unused_mut, unused_assignments, unused_variables, non_snake_case, non_upper_case_globals, clippy::all)] @@ -387,16 +389,12 @@ enum Commands { #[arg(long, value_delimiter = ',', default_value = "1,7,42")] seeds: Vec, }, - /// THE SERVICE (R2-4): read the receipts a spec's hardware runs wrote - /// (.trinity/receipts) and judge, by specs/verified/run_record.t27, whether - /// they are ONE verified run a verdict record may cite as its run - /// reference. Collects the facts, never repairs the record. /// #8095: every spec, REUSE or the reason to rebuild (specs/verified/seal_identity.t27). Frontier { /// Also print each spec that must be rebuilt, with its reason code. #[arg(long)] list: bool, /// Seal again each spec with a stale part; keep only a seal remint_keeps allows. #[arg(long)] reseal: bool, /// Rerun the reused specs verdict_audit.t27 picks for this round; exit 1 on a disagreement. #[arg(long)] audit: Option, /// Keep running: rerun each spec whose closure changed since the last look (watch.t27). - #[arg(long)] watch: bool }, + #[arg(long)] watch: bool, #[arg(long, num_args = 1.., conflicts_with = "since", help = "Walk only these specs and the specs that import them (frontier_scope.t27)")] only: Vec, #[arg(long, help = "Walk only the specs `git diff --name-status REV` names and the specs that import them")] since: Option }, /// #8095 step 5: what the bench does about each spec with a silicon run (specs/verified/silicon_queue.t27). SiliconQueue, /// #8153: the agent next to a board (specs/verified/bench_agent.t27). Runs owner-approved jobs (open @@ -11980,7 +11978,7 @@ async fn main() -> anyhow::Result<()> { service::run_prove(&std::env::current_dir()?, &input, mutate)? } Commands::Seal { input, save, verify, force } => run_seal(&input, save, verify, force)?, - Commands::Frontier { list, reseal, audit, watch } => service::run_frontier(list, reseal, audit, watch)?, + Commands::Frontier { list, reseal, audit, watch, only, since } => service::run_frontier(list, reseal, audit, watch, only, since)?, Commands::SiliconQueue => service::run_silicon_queue()?, Commands::BenchAgent { busdev_num, wrong_part, once } => service::run_bench_agent(&std::env::current_dir()?, busdev_num, wrong_part, once)?, Commands::Compile { input, backend, output } => { @@ -12417,7 +12415,7 @@ fn main() -> anyhow::Result<()> { service::run_prove(&std::env::current_dir()?, &input, mutate)? } Commands::Seal { input, save, verify, force } => run_seal(&input, save, verify, force)?, - Commands::Frontier { list, reseal, audit, watch } => service::run_frontier(list, reseal, audit, watch)?, + Commands::Frontier { list, reseal, audit, watch, only, since } => service::run_frontier(list, reseal, audit, watch, only, since)?, Commands::SiliconQueue => service::run_silicon_queue()?, Commands::BenchAgent { busdev_num, wrong_part, once } => service::run_bench_agent(&std::env::current_dir()?, busdev_num, wrong_part, once)?, Commands::Compile { input, backend, output } => { diff --git a/bootstrap/src/service.rs b/bootstrap/src/service.rs index 884c1756cf..d91d7c2f09 100644 --- a/bootstrap/src/service.rs +++ b/bootstrap/src/service.rs @@ -5602,8 +5602,8 @@ mod unresolved_is_not_a_rejection { /// #8095 step 3: every spec under specs/, REUSE or why it must be rebuilt. Each decision is the generated /// specs/verified/seal_identity.t27 node_decision(); this walks the `use` graph only (#8102 blocks frontier.t27). -pub fn run_frontier(list: bool, reseal: bool, audit: Option, watch: bool) -> anyhow::Result<()> { - use crate::{gen_hash_cache as gc, seal_identity as si, verdict_audit as va, watch as wt}; +pub fn run_frontier(list: bool, reseal: bool, audit: Option, watch: bool, only: Vec, since: Option) -> anyhow::Result<()> { + use crate::{affected, frontier_scope as sc, gen_hash_cache as gc, seal_identity as si, verdict_audit as va, watch as wt}; let (mut tc, mut memo, mut stack, mut specs) = (crate::seal_toolchain(), std::collections::HashMap::new(), vec![std::path::PathBuf::from("specs")], Vec::new()); tc["gen_cache_bin"] = std::fs::read(std::env::current_exe()?).map(|b| crate::sha256_hex(&b)).unwrap_or_default().into(); // #8737: the generator's identity // #8928: one directory per binary; keep the current one and the newest others (gen_cache_keep), drop the rest and the old flat files @@ -5648,13 +5648,14 @@ pub fn run_frontier(list: bool, reseal: bool, audit: Option, watch: bool) - memo.insert(p.to_string(), d); Some(d) } - let mut counts = [0u32; 7]; - specs.sort(); + let mut counts = [0u32; 7]; specs.sort(); + // #10236: --only / --since walk the specs frontier_scope.t27 picks (affected.t27: the seeds and their transitive importers), not the corpus; each closure is still read in full + let diff = sc::scope_wanted(!only.is_empty(), since.is_some()).then(|| -> anyhow::Result { Ok(match &since { Some(r) => { let o = std::process::Command::new("git").args(["diff", "--name-status", r]).output()?; anyhow::ensure!(o.status.success(), "git diff --name-status {r} failed"); String::from_utf8_lossy(&o.stdout).into_owned() } None => only.iter().map(|o| format!("M\t{o}\n")).collect() }) }).transpose()?; + if let Some(d) = diff { let (all, edges) = (specs.len(), specs.iter().flat_map(|p| std::fs::read_to_string(p).unwrap_or_default().lines().filter_map(|l| l.trim().strip_prefix("use ")).map(|u| format!("{p}\t{}\n", crate::use_spec_path(u))).collect::>()).collect::()); let buf = format!("{d}--graph\n{edges}--corpus\n{}", specs.iter().map(|p| format!("{p}\n")).collect::()); let (mut set, mut out) = (vec![0u8; buf.len() + 1], vec![0u8; buf.len() + 1]); let k = affected::select_affected(buf.as_bytes(), buf.len(), &mut set, buf.len() + 1, &mut out, buf.len() + 1); let narrows = sc::scope_narrows(k, affected::RUN_ALL); if narrows { let keep: std::collections::HashSet<&str> = std::str::from_utf8(&out).unwrap_or("").lines().collect(); specs.retain(|p| keep.contains(p.as_str())) } println!("frontier: scope {}", if narrows { format!("{k} of {all} specs") } else { "the whole corpus (a changed path outside specs/ cannot be judged)".into() }) } // #8846: a cold walk first fills the gen-hash cache with suite_jobs() workers (8 MiB stacks); decide() reads it crate::suite::par_in_order(&specs, |p: &String| { gen_cached(p, &tc); }); for p in &specs { let d = decide(p, &tc, &mut memo, 0).unwrap_or(si::REBUILD_MISSING); counts[d as usize] += 1; if list && d != si::REUSE { println!("{d} {p}") } } - let total = specs.len() as u32; - println!("frontier: reused {} of {} ({} permille); rebuild: spec {} closure {} toolchain {} config {} verdict {} missing {}", counts[0], total, si::reused_permille(counts[0], total), counts[1], counts[2], counts[3], counts[4], counts[5], counts[6]); + println!("frontier: reused {} of {} ({} permille); rebuild: spec {} closure {} toolchain {} config {} verdict {} missing {}", counts[0], specs.len(), si::reused_permille(counts[0], specs.len() as u32), counts[1], counts[2], counts[3], counts[4], counts[5], counts[6]); // --reseal (#8095): seal again what remint_wants names; keep the new seal only where remint_keeps allows, // copied onto every twin, else write the old seal back. `seal` exits on a refusal, so it runs as a child. let mut twins: std::collections::HashMap> = Default::default(); diff --git a/docs/now/2026-10-11-tri-hot.md b/docs/now/2026-10-11-tri-hot.md index d237319d83..9e98fb3e67 100644 --- a/docs/now/2026-10-11-tri-hot.md +++ b/docs/now/2026-10-11-tri-hot.md @@ -25,6 +25,9 @@ - `tri hot watch` runs `t27c frontier --watch`: a save reruns that spec and its importers (#8751, #8825, #8856). - `tri hot frontier` runs `t27c frontier --list`: what a build may reuse, ~1 s warm (#8741, #8847). +- `t27c frontier --only ...` and `--since ` walk only those specs and the specs that import them, by + the rule CI uses (specs/ci/affected.t27); the lab measures 0.18 s for `--only specs/verified/watch.t27`. A + changed path outside specs/ walks everything and the output says so (#10236, specs/verified/frontier_scope.t27). - `tri hot prebuild --top [--pnr-seed N]` runs `t27c silicon ... --skip-hardware`: the bitstream goes into the reuse cache before a board run, and the cable is never touched (#8797, #8808, #8814). - `tri hot suite` runs `t27c suite --repo-root . --fast --corpus-only`, every per-spec phase in parallel diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index c412449960..4c7d7e97c5 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -1780,6 +1780,7 @@ {"path": "specs/verified/evidence_chain.t27", "t27b": "pass"}, {"path": "specs/verified/failure_loop.t27", "t27b": "pass"}, {"path": "specs/verified/frontier.t27", "t27b": "pass"}, + {"path": "specs/verified/frontier_scope.t27", "t27b": "pass"}, {"path": "specs/verified/gen_hash_cache.t27", "t27b": "pass"}, {"path": "specs/verified/independence.t27", "t27b": "pass"}, {"path": "specs/verified/payout.t27", "t27b": "pass"}, diff --git a/specs/verified/frontier_scope.t27 b/specs/verified/frontier_scope.t27 new file mode 100644 index 0000000000..f368dc6fac --- /dev/null +++ b/specs/verified/frontier_scope.t27 @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/verified/frontier_scope.t27 -- which specs `t27c frontier --only ... | --since ` walks. +; Epic gHashTag/t27#8095, issue #10236 (hot reload, ledger #8737). +; -- +; Why. `t27c frontier` decides every spec under specs/ (2227 on 2026-10-18; 0.75 s warm, 5.7 s cold on the +; lab). An author who saved one spec wants the verdict for that spec and for what imports it, not for the +; corpus. The flags narrow the walk; every spec still gets the decision seal_identity.t27 gives it, because +; the closure of a spec is read in full whether or not the walk is narrowed. +; -- +; One rule, not two. "Which specs can this change move" is specs/ci/affected.t27 -- the rule CI already uses +; to test only the affected specs (#7565). The scope is that selection, called as it is: the seeds are the +; named specs (--only: one "Mpath" line each) or the lines of `git diff --name-status ` (--since); +; the graph is the `use` edges of the corpus; the corpus is the spec list frontier already walked. The Rust +; gathers those three texts and hands them to select_affected, the way CI's C stub does (spec-guards.yml). +; -- +; Why this module does not `use ci::affected`. The compiler cannot build a spec that imports it: affected.t27 +; declares its own in_list, ext_dot, KINDS and PROSE_EXTS and imports policy::own_language, which declares the +; same four names differently, so the resolver reports them ambiguous and leaves them unspliced (t27c +; test-report: "BLOCKED does not compile ... use of undeclared identifier 'own_language'"). So the selection +; runs from affected's own generated code (bootstrap/gen/rust/ci/affected.rs), exactly as in CI, and this +; module holds the two decisions around it. The selection's own tests are in affected.t27. +; -- +; Fail closed. affected.t27 answers RUN_ALL for a change it cannot judge: a path outside specs/ (the compiler, +; a tool, a workflow), a status letter it cannot read, a working set that does not fit. Then the walk is the +; whole corpus, exactly as without a flag. An answer of 0 is not a failure: a change to prose or a seal +; moves no verdict, and the scoped walk is empty. scope_narrows takes the sentinel as an argument, so the +; value is affected's and is not copied here. +; -- +; The flags are exclusive: --only names the seeds, --since derives them, and a command line that gives both +; has no single answer to "which seeds". The clap layer refuses it; scope_wanted() is the decision it feeds. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module VerifiedFrontierScope; + +/// Is the walk narrowed at all? Neither flag: every spec, as before. +pub fn scope_wanted(has_only: bool, has_since: bool) -> bool { + return has_only or has_since; +} + +/// Did the selection narrow the walk? `all` is the sentinel the selection answers when it cannot judge the +/// change; any other answer, 0 included, is a scope. +pub fn scope_narrows(picked: u32, all: u32) -> bool { + return picked != all; +} + +test scope_is_wanted_only_when_a_flag_is_given { + assert scope_wanted(false, false) == false; + assert scope_wanted(true, false) == true; + assert scope_wanted(false, true) == true; +} + +test a_count_is_a_scope_and_the_sentinel_is_the_whole_corpus { + assert scope_narrows(3, 0xFFFFFFFF) == true; + assert scope_narrows(1, 0xFFFFFFFF) == true; +} + +test negative_control_the_sentinel_walks_everything { + assert scope_narrows(0xFFFFFFFF, 0xFFFFFFFF) == false; +} + +test prose_is_an_empty_scope_not_a_failure { + // a change to a .md file or a seal selects no spec: 0 narrows to nothing, it does not widen to everything + assert scope_narrows(0, 0xFFFFFFFF) == true; +} + +invariant no_flag_never_narrows_by_itself { + // without a flag the glue never asks the selection; the decision that gates it is false + assert scope_wanted(false, false) == false; + assert scope_narrows(0xFFFFFFFF, 0xFFFFFFFF) == false; +}