#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Scope {
InScope,
OutNested,
OutRetired,
}
const RETIRED_MARKERS: &[&str] = &[
"SUPERSEDED",
"STRUCK",
"retained below",
"RETAINED BELOW",
"DISCHARGED",
];
fn depth(line: &str) -> usize {
let mut d = 0;
for ch in line.chars() {
match ch {
'>' => d += 1,
c if c.is_whitespace() => {}
_ => break,
}
}
d
}
fn strip_quote_prefix(line: &str) -> &str {
line.trim_start_matches(|c: char| c == '>' || c.is_whitespace())
}
pub fn classify(lines: &[&str]) -> Vec<Scope> {
let d: Vec<usize> = lines.iter().map(|l| depth(l)).collect();
let mut out = vec![Scope::InScope; lines.len()];
let mut i = 0;
while i < lines.len() {
if d[i] == 0 {
i += 1;
continue;
}
let start = i;
while i < lines.len() && d[i] >= 1 {
i += 1;
}
let joined = lines[start..i]
.iter()
.map(|l| strip_quote_prefix(l))
.collect::<Vec<_>>()
.join(" ");
let retired = RETIRED_MARKERS.iter().any(|m| joined.contains(m));
for k in start..i {
out[k] = if d[k] >= 2 {
Scope::OutNested
} else if retired {
Scope::OutRetired
} else {
Scope::InScope
};
}
}
out
}
#[test]
fn depth_zero_live_prose_is_in_scope() {
let f = ["Ordinary prose naming `Gap302FixtureAlpha`.", "More prose."];
assert_eq!(classify(&f), vec![Scope::InScope, Scope::InScope]);
}
#[test]
fn a_nested_quote_is_out_of_scope() {
let f = [
"> Outer block, live.",
">",
"> > **As-built note (2026-07-29): `fuel-nn` is a planned crate.**",
];
let got = classify(&f);
assert_eq!(got[2], Scope::OutNested, "depth 2 must be OUT: {got:?}");
}
#[test]
fn a_block_marked_superseded_is_out_of_scope() {
let f = [
"> ⚠️ **AS-BUILT NOTE SUPERSEDED 2026-08-27 — `fuel-nn` HAS SHIPPED.**",
">",
"> The original note is retained below.",
];
assert_eq!(
classify(&f),
vec![Scope::OutRetired, Scope::OutRetired, Scope::OutRetired]
);
}
#[test]
fn depth_one_without_a_marker_is_in_scope() {
let f = [
"> ⚠️ **AS-BUILT 2026-08-28 — GENUINELY UNBUILT. FILE AS WORK.**",
"> Measured at head: ZERO `fn` definitions for `Gap302FixtureDelta`.",
];
assert_eq!(classify(&f), vec![Scope::InScope, Scope::InScope]);
}
#[test]
fn retirement_does_not_leak_into_the_next_block() {
let f = [
"> **SUPERSEDED — retained below.**",
"> > the quoted history",
"",
"> A LATER, UNRELATED quoted block naming `Gap302FixtureBeta`.",
];
let got = classify(&f);
assert_eq!(got[0], Scope::OutRetired);
assert_eq!(got[1], Scope::OutNested);
assert_eq!(
got[3],
Scope::InScope,
"a blank line ends the block; retirement must not carry over: {got:?}"
);
}
#[test]
fn retirement_does_not_leak_into_following_prose() {
let f = [
"> **SUPERSEDED 2026-08-27.**",
"> > the quoted history",
"",
"Ordinary prose naming `Gap302FixtureGamma`, which the gate must read.",
];
assert_eq!(classify(&f)[3], Scope::InScope);
}
#[test]
fn a_marker_below_still_retires_the_block_it_is_in() {
let f = [
"> An opening line with no marker at all.",
"> **SUPERSEDED 2026-08-27.**",
];
assert_eq!(classify(&f), vec![Scope::OutRetired, Scope::OutRetired]);
}
#[test]
fn a_marker_split_across_a_hard_wrap_still_retires_the_block() {
let f = [
"> **The original note is retained",
"> below because its recorded injury is the point.**",
"> > **As-built note (2026-07-29): `fuel-nn` is a planned crate.**",
];
let got = classify(&f);
assert_eq!(
got[0],
Scope::OutRetired,
"wrapped marker must still retire: {got:?}"
);
assert_eq!(got[1], Scope::OutRetired);
assert_eq!(got[2], Scope::OutNested);
}
#[test]
fn depth_counts_markers_not_indentation() {
assert_eq!(depth("no quote"), 0);
assert_eq!(depth("> one"), 1);
assert_eq!(depth("> > two"), 2);
assert_eq!(depth(">> two, unspaced"), 2);
assert_eq!(depth(" > still one, indented"), 1);
}
const SELF_PATH: &str = "fuel-ir/tests/doc_block_scope.rs";
const SELF_EXCLUSION_PROBE: &str = "Gap302SelfExclusionProbe";
fn known_absent() -> Vec<&'static str> {
include_str!("gap302_known_absent.txt")
.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.collect()
}
fn repo_root() -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("fuel-ir must sit under the workspace root")
.to_path_buf()
}
fn rel(p: &std::path::Path, root: &std::path::Path) -> String {
p.strip_prefix(root)
.unwrap_or(p)
.to_string_lossy()
.replace('\\', "/")
}
fn is_pruned_dir(p: &std::path::Path) -> bool {
p.file_name()
.is_some_and(|n| n == "target" || n == ".git" || n == "node_modules")
}
fn is_scanned_source(p: &std::path::Path, root: &std::path::Path, skip_self: bool) -> bool {
p.extension().is_some_and(|e| e == "rs") && !(skip_self && rel(p, root) == SELF_PATH)
}
fn rust_files(skip_self: bool) -> Vec<std::path::PathBuf> {
let root = repo_root();
let mut out = Vec::new();
let mut stack = vec![root.clone()];
while let Some(dir) = stack.pop() {
let rd = std::fs::read_dir(&dir)
.unwrap_or_else(|e| panic!("read_dir({}) failed: {e}", dir.display()));
for entry in rd.flatten() {
let p = entry.path();
if p.is_dir() {
if !is_pruned_dir(&p) {
stack.push(p);
}
} else if is_scanned_source(&p, &root, skip_self) {
out.push(p);
}
}
}
out
}
fn rust_tokens(skip_self: bool) -> std::collections::BTreeSet<String> {
let mut out = std::collections::BTreeSet::new();
for p in rust_files(skip_self) {
if let Ok(src) = std::fs::read_to_string(&p) {
for t in idents(&src) {
out.insert(t);
}
}
}
out
}
fn idents(src: &str) -> Vec<String> {
let b = src.as_bytes();
let mut out = Vec::new();
let mut i = 0;
while i < b.len() {
if b[i].is_ascii_alphabetic() || b[i] == b'_' {
let s = i;
while i < b.len() && (b[i].is_ascii_alphanumeric() || b[i] == b'_') {
i += 1;
}
out.push(src[s..i].to_string());
} else {
i += 1;
}
}
out
}
fn is_identifier_claim(t: &str) -> bool {
t.len() > 1
&& t.starts_with(|c: char| c.is_ascii_uppercase())
&& t.chars().all(|c| c.is_ascii_alphanumeric())
&& t.chars()
.any(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
}
fn backtick_parity_is_inside(line: &str, byte_at: usize) -> bool {
line[..byte_at].chars().filter(|c| *c == BACKTICK).count() % 2 == 1
}
const BACKTICK: char = '`';
fn in_scope_absent() -> std::collections::BTreeSet<String> {
let root = repo_root();
let present = rust_tokens(true);
let mut out = std::collections::BTreeSet::new();
let dir = root.join("docs/architecture");
let rd = std::fs::read_dir(&dir)
.unwrap_or_else(|e| panic!("read_dir({}) failed: {e}", dir.display()));
for entry in rd.flatten() {
let p = entry.path();
if !p.extension().is_some_and(|e| e == "md") {
continue;
}
let Ok(src) = std::fs::read_to_string(&p) else {
continue;
};
let lines: Vec<&str> = src.lines().collect();
let scope = classify(&lines);
for (i, line) in lines.iter().enumerate() {
if scope[i] != Scope::InScope {
continue;
}
let b = line.as_bytes();
let mut j = 0;
while j < b.len() {
if b[j].is_ascii_alphabetic() || b[j] == b'_' {
let s = j;
while j < b.len() && (b[j].is_ascii_alphanumeric() || b[j] == b'_') {
j += 1;
}
let t = &line[s..j];
if backtick_parity_is_inside(line, s)
&& is_identifier_claim(t)
&& !present.contains(t)
{
out.insert(t.to_string());
}
} else {
j += 1;
}
}
}
}
out
}
#[test]
fn the_absent_population_is_exactly_the_adjudicated_set() {
let found = in_scope_absent();
let known: std::collections::BTreeSet<String> =
known_absent().iter().map(|s| (*s).to_string()).collect();
let new: Vec<&String> = found.difference(&known).collect();
let gone: Vec<&String> = known.difference(&found).collect();
assert!(
new.is_empty(),
"NEW doc/code drift -- {} backticked name(s) in docs/architecture/ with no \
referent in *.rs and no entry here: {new:?}. Adjudicate each in \
docs/gaps.md (GAP-302), then add it to gap302_known_absent.txt.",
new.len()
);
assert!(
gone.is_empty(),
"{} name(s) in the adjudicated set now HAVE a referent, or left the corpus: \
{gone:?}. If the drift was fixed, delete the entry. A ratchet reddens in \
BOTH directions on purpose -- a stale entry is a claim nobody re-derives.",
gone.len()
);
}
#[test]
fn excluding_this_file_is_load_bearing_and_changes_the_population() {
let with_self = rust_tokens(false);
let without = rust_tokens(true);
assert!(
with_self.contains(SELF_EXCLUSION_PROBE),
"the probe must appear in *.rs when THIS file is scanned; if not, it \
was renamed and this arm no longer tests anything"
);
assert!(
!without.contains(SELF_EXCLUSION_PROBE),
"the probe must vanish once this file is excluded; if it survives, \
SELF_PATH is not matching this file and the exclusion is inert"
);
assert!(
without.len() < with_self.len(),
"excluding one file must remove tokens; it removed none, so SELF_PATH no \
longer matches any file on disk"
);
}
#[test]
fn parity_membership_rejects_a_name_outside_every_span() {
let line = "a `Foo` and a ```rust``` marker. Refines an already-stated claim.";
let at = line.find("Refines").unwrap();
assert!(
!backtick_parity_is_inside(line, at),
"an even backtick count before the token means OUTSIDE every span"
);
let at2 = line.find("Foo").unwrap();
assert!(
backtick_parity_is_inside(line, at2),
"control: a genuinely backticked name must read as INSIDE"
);
}
#[test]
fn this_file_names_none_of_the_population() {
let path = repo_root().join(SELF_PATH);
let src = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("read {} failed: {e}", path.display()));
let mut offenders: Vec<&str> = Vec::new();
for name in known_absent() {
if idents(&src).iter().any(|t| t == name) {
offenders.push(name);
}
}
assert!(
offenders.is_empty(),
"{} adjudicated name(s) are written into this *.rs file: {offenders:?}. Every one of them RESOLVES in any *.rs scan, which falsifies the very property that makes them population members -- and SELF_PATH hides that from this gate while leaving every other instrument wrong. Put the name in gap302_known_absent.txt, where prose may reference it freely.",
offenders.len()
);
assert!(
idents(&src).iter().any(|t| t == SELF_EXCLUSION_PROBE),
"the tokenizer cannot see a token known to be in this file, so the emptiness above is a reader defect, not a finding"
);
}