diff --git a/docs/now/2026-09-21-published-port-tools-check-seal-currency-py-python-5-functions-to-spec.md b/docs/now/2026-09-21-published-port-tools-check-seal-currency-py-python-5-functions-to-spec.md new file mode 100644 index 0000000000..1f2a57e84f --- /dev/null +++ b/docs/now/2026-09-21-published-port-tools-check-seal-currency-py-python-5-functions-to-spec.md @@ -0,0 +1,11 @@ +# NOW -- Port tools/check_seal_currency.py (Python, 5 functions) to specs/port/tools/check_seal_currency.t27 (published 2026-09-21) + +## A bee's work on #4387, published from `queen-4387` (Closes #4387) + +- The branch changes 1 file(s): `specs/port/tools/check_seal_currency.t27`. +- `git diff --stat origin/master...queen-4387` reads: 1 file changed, 308 insertions(+) +- This entry is written by the publisher, not by the bee. A pull request must + add exactly one `docs/now/` entry and a bee has no way to know that: its brief + names a boundary file and acceptance criteria, and `docs/now/` is neither. +- What this entry does NOT establish: that the work is correct. The gates on the + pull request judge that, and they are the same gates every other change meets. diff --git a/specs/port/tools/check_seal_currency.t27 b/specs/port/tools/check_seal_currency.t27 new file mode 100644 index 0000000000..b41e969f10 --- /dev/null +++ b/specs/port/tools/check_seal_currency.t27 @@ -0,0 +1,308 @@ +// specs/port/tools/check_seal_currency.t27 +// Port of tools/check_seal_currency.py functions +// Port of t27c(), current_hashes(), scan(), self_check(), and main() from the original Python implementation + +module check_seal_currency; + +// Constants from the original implementation +pub const SEALS : str = ".trinity/seals"; +pub const BACKENDS : [4]str = ["rust", "zig", "c", "verilog"]; + +// Port of t27c() function from line 49 of the original +fn t27c() -> str { + // Check for built t27c in common locations + if @fileExists("target/release/t27c") && @isExecutable("target/release/t27c") { + return "target/release/t27c"; + } + if @fileExists("bootstrap/target/release/t27c") && @isExecutable("bootstrap/target/release/t27c") { + return "bootstrap/target/release/t27c"; + } + + // Check environment variable + let env = @env("TRI_T27C"); + if env.len() > 0 && @fileExists(env) && @isExecutable(env) { + return env; + } + + // Exit 2, not 0: a check that could not run has not passed. + @printErr("check_seal_currency: t27c not built. Exit 2 = COULD NOT RUN."); + @printErr(" cargo build --release -p t27c, or set TRI_T27C."); + @exit(2); + // Unreachable but needed for type checking + return ""; +} + +// Port of current_hashes() function from line 62 of the original +fn current_hashes(binary: str, spec: str) -> [str]str { + let out = @runCommand([binary, "seal", spec]); + let mut result : [str]str = []; + + // Parse key=value lines + for line in out.split("\n") { + if line.contains("=") { + let parts = line.split("=", 1); + if parts.len() == 2 { + result[parts[0]] = parts[1]; + } + } + } + + return result; +} + +// Port of scan() function from line 69 of the original +fn scan(binary: str, seal_dir: str) -> ([(str, str, [(str, str, str)])], i32, i32, i32) { + let mut stale : [(str, str, [(str, str, str)])] = []; + let mut none_sealed = 0; + let mut missing = 0; + let mut current = 0; + + // Get all seal files + let seal_files = @glob(seal_dir + "/*.json"); + + for sp in seal_files { + let mut ok = true; + let spec : str = ""; + + // Try to read and parse the seal file + let content = @readFile(sp); + if content.len() == 0 { + missing += 1; + continue; + } + + let d = @parseJson(content); + if d == null { + missing += 1; + continue; + } + + spec = @get(d, "spec_path", ""); + if spec.len() == 0 || !@fileExists(spec) { + missing += 1; + continue; + } + + let cur = current_hashes(binary, spec); + let mut bad : [(str, str, str)] = []; + let mut saw_none = false; + + for b in BACKENDS { + let k = "gen_hash_" + b; + let a = @get(cur, k, ""); + let stored = @get(d, k, ""); + + if a.len() == 0 || stored.len() == 0 { + continue; + } + + if a.contains("none") || stored.contains("none") { + saw_none = true; + continue; + } + + if a != stored { + bad.push((b, stored, a)); + } + } + + if bad.len() > 0 { + stale.push((sp, spec, bad)); + } else if saw_none { + none_sealed += 1; + } else { + current += 1; + } + } + + return (stale, none_sealed, missing, current); +} + +// Port of self_check() function from line 102 of the original +fn self_check() -> i32 { + /* A seal deliberately given a wrong hash must be reported. + + Without this the check could return "0 stale" because it looks in the wrong + place, reads the wrong field, or silently skips every file -- and a zero from + a check that cannot see is indistinguishable from a zero that means healthy. + */ + let binary = t27c(); + let specs = @glob("specs/**/*.t27"); + + if specs.len() == 0 { + @printErr("self-check: no specs to work with. Exit 2."); + return 2; + } + + let mut spec : str = ""; + for s in specs { + let cur = current_hashes(binary, s); + let rust_hash = @get(cur, "gen_hash_rust", ""); + if rust_hash.len() > 0 && !rust_hash.contains("none") { + spec = s; + break; + } + } + + if spec.len() == 0 { + @printErr("self-check: no spec generates Rust. Exit 2."); + return 2; + } + + let cur = current_hashes(binary, spec); + let tmp_dir = @tempDir(); + let d = tmp_dir + "/seals"; + @createDir(d, true); + + // Create good seal + let mut good : [str]str = cur; + good["spec_path"] = spec; + let good_json = @stringifyJson(good, true, true); // pretty print, sort keys + @writeFile(d + "/good.json", good_json); + + // Create bad seal with wrong rust hash + let mut bad : [str]str = good; + bad["gen_hash_rust"] = "sha256:" + "0".repeat(64); + let bad_json = @stringifyJson(bad, true, true); + @writeFile(d + "/bad.json", bad_json); + + let (stale, _, _, current) = scan(binary, d); + + let ok = stale.len() == 1 && + @get(stale[0], 0, "") == "bad.json" && + current == 1; + + @print(" self-check: 2 seals scanned; stale reported " + @toString(stale.len()) + " (want 1), "); + @print("current " + @toString(current) + " (want 1) -- " + (if ok { "PASS" } else { "FAIL" })); + + return if ok { 0 } else { 1 }; +} + +// Port of main() function from line 142 of the original +pub fn main() -> i32 { + let args = @getArgs(); + + if args.contains("--self-check") { + return self_check(); + } + + if !@dirExists(SEALS) { + @printErr("check_seal_currency: " + SEALS + " is not a directory. Exit 2."); + return 2; + } + + let binary = t27c(); + let (stale, none_sealed, missing, current) = scan(binary, SEALS); + + if args.contains("--stale-specs") { + // Collect unique spec paths from stale entries + let mut spec_set : [str]str = []; + for entry in stale { + let spec = @get(entry, 1, ""); + if spec.len() > 0 && !spec_set.contains(spec) { + spec_set.push(spec); + } + } + // Sort the spec set + let mut i = 0; + while i < spec_set.len() { + let mut j = i + 1; + while j < spec_set.len() { + if spec_set[i] > spec_set[j] { + let temp = spec_set[i]; + spec_set[i] = spec_set[j]; + spec_set[j] = temp; + } + j += 1; + } + i += 1; + } + + for spec in spec_set { + @print(spec); + } + return if stale.len() > 0 { 1 } else { 0 }; + } + + let total = stale.len() + none_sealed + missing + current; + @print("seals scanned: " + @toString(total)); + @print(" current : " + @toString(current)); + @print(" spec file no longer present : " + @toString(missing)); + @print(" sealed with gen_hash=none : " + @toString(none_sealed)); + @print(" STALE generated-code hash : " + @toString(stale.len())); + + // Print up to 20 stale entries + let mut i = 0; + while i < stale.len() && i < 20 { + let entry = @get(stale, i, null); + if entry != null { + let name = @get(entry, 0, ""); + let spec = @get(entry, 1, ""); + let bad_list = @get(entry, 2, []); + + for j in 0..bad_list.len() { + let bad = @get(bad_list, j, null); + if bad != null { + let b = @get(bad, 0, ""); + let stored = @get(bad, 1, ""); + let now = @get(bad, 2, ""); + + // Extract substring after "sha256:" for display (positions 7-18 inclusive = 12 chars) + let stored_short = if stored.len() >= 19 { @slice(stored, 7, 19) } else { stored }; + let now_short = if now.len() >= 19 { @slice(now, 7, 19) } else { now }; + + @print(" " + name + ": gen_hash_" + b + " sealed=" + stored_short + " current=" + now_short + " (" + spec + ")"); + } + } + } + i += 1; + } + + if stale.len() > 20 { + @print(" ... and " + @toString(stale.len() - 20) + " more"); + } + + return if stale.len() > 0 { 1 } else { 0 }; +} + +// Test block for t27c function +test "t27c_function" { + // This test would require mocking the filesystem and environment + // For now, we just check that the function exists and returns a string + let result = t27c(); + assert!(result.len() >= 0, "t27c should return a string (possibly empty)"); +} + +// Test block for current_hashes function +test "current_hashes_function" { + // This test would require mocking the subprocess call + // For now, we just check that the function exists and returns a map + let binary = "echo"; // dummy binary + let spec = "dummy.spec"; + let result = current_hashes(binary, spec); + assert!(true, "current_hashes function exists and returns a map"); +} + +// Test block for scan function +test "scan_function" { + // This test would require mocking the filesystem and subprocess + // For now, we just check that the function exists + let binary = "echo"; + let seal_dir = "/tmp"; + let result = scan(binary, seal_dir); + assert!(true, "scan function exists"); +} + +// Test block for self_check function +test "self_check_function" { + let result = self_check(); + // self_check can return 0 (success), 1 (failure), or 2 (could not run) + assert!(result == 0 || result == 1 || result == 2, "self_check should return 0, 1, or 2"); +} + +// Test block for main function +test "main_function" { + let result = main(); + // main can return 0 (success) or 1 (failure) or 2 (could not run) + assert!(result == 0 || result == 1 || result == 2, "main should return 0, 1, or 2"); +} \ No newline at end of file