from __future__ import annotations
import re
import sys
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True)
class Rule:
name: str
guarantee: str
pattern: re.Pattern[str]
hint: str
RULES: list[Rule] = [
Rule(
"wall-clock",
"G6",
re.compile(r"\b[A-Za-z_]\w*::now(?:_[a-z_]+)?\b|\.elapsed\s*\("),
"the timestamp arrives in the frame as `now_ms` — reading the wall clock "
"makes every replay of the same input non-reproducible",
),
Rule(
"filesystem",
"G4",
re.compile(
r"\bfs::(?:read|write|copy|remove|create|rename|metadata|canonicalize|File)\w*"
r"|\bFile::(?:open|create)\b"
r"|\bOpenOptions\b"
r"|\bread_to_string\s*\("
r"|\bread_dir\s*\("
r"|\bcanonicalize\s*\("
r"|\.(?:exists|metadata|symlink_metadata|is_file|is_dir)\s*\(\s*\)"
r"|\binclude_str!|\binclude_bytes!"
),
"the path classifier reads DECLARED STRINGS ONLY — the hook path walks up "
"from the payload's working directory and this one must not. Reading stdin "
"is the command layer's job: stream it with `BufRead::lines()`",
),
Rule(
"environment",
"G8",
re.compile(
r"\benv::(?:var|var_os|vars|vars_os|current_dir|set_current_dir|home_dir|temp_dir)\b"
r"|\bdirs::|\bdirectories::|\betcetera::|\bkeyring::|\bsecret_service::"
),
"no config read, no keyring read, no data-directory resolution — a keychain "
"prompt on a headless box is a hang, not an error",
),
Rule(
"network",
"G5",
re.compile(
r"\breqwest::|\bureq::|\bhyper::|\bsocket2::|\brustls::"
r"|\bstd::net\b|\bTcpStream\b|\bTcpListener\b|\bUdpSocket\b"
r"|\bUnixStream\b|\bUnixListener\b|\bToSocketAddrs\b"
),
"no poller, no ingest, no telemetry, no update check — evaluation reads its "
"inputs and returns",
),
Rule(
"randomness",
"G7",
re.compile(
r"\brand::|\brand_core::|\bthread_rng\b|\bOsRng\b|\bgetrandom\b"
r"|\bUuid::|\buuid::|\bnew_v4\b|\bnanoid\b|\bulid::"
),
"determinism is the requirement: the same row must yield the same bytes on "
"every replay, so no randomness and no identifier minting",
),
Rule(
"async-runtime",
"G5",
re.compile(
r"\btokio::|\bfutures::|\basync_std::"
r"|\basync\s+(?:fn|move|\{)|\.await\b|\bblock_on\s*\(|\bspawn_blocking\b"
),
"the entry point is a synchronous function of its arguments — an async "
"signature here is the seam a network call arrives through later",
),
Rule(
"global-state",
"G9",
re.compile(
r"\blazy_static!|\bthread_local!|\bstatic\s+mut\b"
r"|\bOnceLock\b|\bOnceCell\b|\bLazyLock\b|\bLazy::|\bArcSwap\b"
r"|\bAtomic(?:Bool|Usize|Isize|U8|U16|U32|U64|I8|I16|I32|I64|Ptr)\b"
r"|\bstatic\s+[A-Z_][A-Z0-9_]*\s*:\s*(?:Mutex|RwLock)\b"
),
"no resident bundle, no shared application state. The process may cache what "
"is derived from its INPUTS; it may never retain what is derived from its "
"HISTORY — and a global is how the second one gets built by accident",
),
Rule(
"client-subsystem",
"G1/G2/G3/G8",
re.compile(
r"\bcrate::(?:daemon|app|hooks|hook_output|boundary)\b"
r"|\bcrate::core::(?:cloud|config|auth|telemetry|update|crash_report"
r"|hook_state|logging|egress|net|supervision|install_state)\b"
),
"the audit line, the outbox spool, the dedup map and the session registry all "
"hang off these modules — the outbox in particular is the DEFAULT when no "
"cloud credential is present, which is exactly the platform's situation",
),
Rule(
"subprocess",
"G5/G9",
re.compile(r"\bprocess::(?:Command|exit|abort)\b|\bthread::spawn\b"),
"a verdict is computed in this process, from these bytes — spawning anything "
"moves the state and the failure mode somewhere the corpus cannot see",
),
]
WAIVER = re.compile(r"engine-purity-ok:\s*\S")
ATTR = re.compile(r"^\s*#!?\[")
CFG_TEST = re.compile(r"^\s*#!?\[\s*cfg\s*\(.*\btest\b")
NON_CODE = re.compile(
r'r(?P<hashes>#*)"(?:.|\n)*?"(?P=hashes)' r'|"(?:\\.|[^"\\\n])*"' r"|'(?:\\.|[^'\\\n])'" r"|/\*(?:.|\n)*?\*/" r"|//[^\n]*" )
@dataclass
class Violation:
path: Path
line: int
rule: Rule
text: str
def code_lines(text: str) -> list[str]:
def blank(match: re.Match[str]) -> str:
return re.sub(r"[^\n]", " ", match.group(0))
return NON_CODE.sub(blank, text).splitlines()
def cfg_test_lines(lines: list[str]) -> set[int]:
skip: set[int] = set()
for i, raw in enumerate(lines):
if not CFG_TEST.match(raw):
continue
if raw.lstrip().startswith("#!["): skip.update(range(i, len(lines)))
continue
j = i + 1
while j < len(lines) and (ATTR.match(lines[j]) or lines[j].lstrip().startswith("///")):
j += 1
depth, opened = 0, False
for k in range(j, len(lines)):
depth += lines[k].count("{") - lines[k].count("}")
opened = opened or "{" in lines[k]
if opened and depth <= 0:
skip.update(range(i, k + 1))
break
if not opened and lines[k].rstrip().endswith(";"):
skip.update(range(i, k + 1))
break
else:
skip.update(range(i, len(lines)))
return skip
def scan(path: Path) -> list[Violation]:
text = path.read_text(encoding="utf-8")
raw_lines = text.splitlines()
lines = code_lines(text)
skip = cfg_test_lines(lines)
out: list[Violation] = []
for i, line in enumerate(lines):
if i in skip or WAIVER.search(raw_lines[i]):
continue
for rule in RULES:
if rule.pattern.search(line):
out.append(Violation(path, i + 1, rule, raw_lines[i].strip()))
break
return out
def targets(root: Path) -> tuple[list[Path], list[Path]]:
candidates = [
root / "src" / "zone_eval",
root / "src" / "cli" / "commands" / "evaluate",
root / "src" / "cli" / "commands" / "evaluate.rs",
]
present = [p for p in candidates if p.exists()]
files: set[Path] = set()
for path in present:
if path.is_dir():
files.update(path.rglob("*.rs"))
elif path.suffix == ".rs":
files.add(path)
return sorted(files), present
def main(argv: list[str]) -> int:
root = Path(argv[1]) if len(argv) > 1 else Path.cwd()
if not root.is_dir():
print(f"error: {root} is not a directory", file=sys.stderr)
return 2
files, present = targets(root)
if not present:
print(
"engine purity: src/zone_eval/ and src/cli/commands/evaluate.rs are both "
"absent — the engine has not landed yet, so there is nothing to scan.\n"
"This gate is green by construction until the module exists, and red the "
"moment it exists and reaches for a clock, a file or the network."
)
return 0
if not files:
here = ", ".join(str(p.relative_to(root)) for p in present)
print(f"::error::engine purity: {here} exists but holds no .rs file.\n")
print(
"A gate that passes because it found nothing is worse than no gate. Either "
"the engine moved and this scanner's target list is stale, or the directory "
"is an empty leftover — fix whichever it is."
)
return 1
violations = [v for path in files for v in scan(path)]
if not violations:
print(f"engine purity: clean ({len(files)} file(s) scanned)")
return 0
print("::error::The evaluator must run on a box holding nothing but the binary:\n")
for v in violations:
rel = v.path.relative_to(root)
print(f" {rel}:{v.line} [{v.rule.name}] {v.rule.guarantee}")
print(f" {v.text}")
print(f" → {v.rule.hint}\n")
print(f"{len(violations)} violation(s).")
print(
"Move the read into the command layer above the engine and hand the bytes "
"down, or append `// engine-purity-ok: <reason>` when the call really is "
"input-derived and side-effect free."
)
return 1
if __name__ == "__main__":
sys.exit(main(sys.argv))