From 5ce6652ffd79774cdcb1f2af1c346811ddc11adb Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 10:15:33 +0700 Subject: [PATCH 1/6] feat(claw): lab_receipt -- a hand-run lab check holds unless the patch applied and the report ran specs/claw/lab_receipt.t27 (ClawLabReceipt) judges one hand-run lab check: applied files must equal the diff's file count, and the t27c test-report must carry tests, FAIL and vacuous counts with tests > 0 and both others 0. Anything else holds, with one reason. The case is tick 38, where a failed git apply still printed seals 33 of 33 on master's files. t27c 18/18, 0 vacuous; t27b 18/18; 28/28 mutants caught; sealed. Refs #10810 Co-Authored-By: Claude Opus 5.5 --- .trinity/seals/claw_ClawLabReceipt.json | 33 +++ docs/claw/README.md | 1 + docs/reports/t27b_expectations.json | 1 + specs/claw/lab_receipt.t27 | 318 ++++++++++++++++++++++++ 4 files changed, 353 insertions(+) create mode 100644 .trinity/seals/claw_ClawLabReceipt.json create mode 100644 specs/claw/lab_receipt.t27 diff --git a/.trinity/seals/claw_ClawLabReceipt.json b/.trinity/seals/claw_ClawLabReceipt.json new file mode 100644 index 0000000000..d0c05696fd --- /dev/null +++ b/.trinity/seals/claw_ClawLabReceipt.json @@ -0,0 +1,33 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/claw/text.t27", + "spec_hash": "sha256:968755ec1917f766eb2cdd0ab4c0c48aef028a0e7d69e191911695e925b286ad" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:2acc9111cc283136a731bf7d06118ceaca105af33587643a6936f1b0c0516304", + "gen_hash_rust": "sha256:87d52527a2ad710b42de89eb29ef582e5abe11aa4a644516ac0b666950bc8858", + "gen_hash_verilog": "sha256:8a6938e577ce38d1c18a28c97d9c448961f941c368db939703cdff653158f0e3", + "gen_hash_zig": "sha256:9a8adacb105c2e3754029cdb6288e11421368ec5d5641adec41aa1f271cfd454", + "module": "ClawLabReceipt", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-19T03:11:31Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:cd1a19fbf10edaff4f258cf5265a5fc28243062e58d9987a56f214545e569560", + "spec_path": "specs/claw/lab_receipt.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 18, + "total": 18, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/claw/README.md b/docs/claw/README.md index b6e85ff7b3..7bf6b70856 100644 --- a/docs/claw/README.md +++ b/docs/claw/README.md @@ -24,6 +24,7 @@ behind one front door, with the gate logic in `.t27` specs. Epic | Required red: push_gate's req_red flag from the rollup, newest run per required check, so a red check that is not required never triggers a fix push | `specs/claw/required_red.t27` (check_latest rows in; an R count line and an `F req_red true/false/unknown` line out; not wired to a job yet) | #10606 | | Merge lane: is the merger starved, and does it merge anything? Counts its runs, the oldest open age, the PRs each run merged, and its share of the whole queue, so an hour-long wait, a zero yield or a tenth of the queue shows up as data | `specs/claw/merge_lane.t27` (rows per merger run or per queued run; `F merger`, `F merger-yield` and `F merger-share low/high/unknown/none` out) | #10644 | | Shape race: the issue-shape gate judges the event payload, which can be twenty minutes old; replays a gate timeline under the payload reading and a live reading, so an issue closed with a passing body shows up as data | `specs/claw/shape_race.t27` (rows `created TAB ran TAB ok` per gate event; an `R` line and `F shape-race clean/race/unknown/none` out; #10644 is the fixture; the workflow change is the owner's decision) | #10662 | +| Lab receipt: whether a hand-run lab check of a patch really ran on the patch. A `git apply` that changed fewer files than the diff, a blank or BLOCKED test report, zero tests, a failure or a vacuous pass all hold | `specs/claw/lab_receipt.t27` (expected and applied file counts and the t27c `test-report` text in; one `L pass/hold` line with the reason out; not wired to a job yet) | #10810 | | Model routing: Opus 5.5 orchestrates, Sonnet 5.5 runs sub-tasks | `specs/claw/model_route.t27`, skill `model-routing`, `.claude/settings.json` + `.claude/agents/*` | owner directive 2026-10-18 | | A T27F visual in every answer that needs one | rule `specs/t27mcp/visual_output.t27`, particle-still layer `specs/claw/visual_output.t27`, procedure in skill `t27f`, "Visual in every output" | owner directive 2026-10-18 | | `reel_still`: one particle still (PNG at STILL_MS of a token_floor reel) from TRI CLAW's `/mcp` | `specs/claw/reel_still.t27`, rendered on the Railway lab only | follow-up of the row above | diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 7e3363a205..5ca5f5922d 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -197,6 +197,7 @@ {"path": "specs/claw/gates.t27", "t27b": "pass"}, {"path": "specs/claw/gates_pins.t27", "t27b": "pass"}, {"path": "specs/claw/github_surface.t27", "t27b": "pass"}, + {"path": "specs/claw/lab_receipt.t27", "t27b": "pass"}, {"path": "specs/claw/ledger_union.t27", "t27b": "pass"}, {"path": "specs/claw/merge_lane.t27", "t27b": "pass"}, {"path": "specs/claw/model_route.t27", "t27b": "pass"}, diff --git a/specs/claw/lab_receipt.t27 b/specs/claw/lab_receipt.t27 new file mode 100644 index 0000000000..56a17fa4ab --- /dev/null +++ b/specs/claw/lab_receipt.t27 @@ -0,0 +1,318 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/claw/lab_receipt.t27 -- whether a Railway lab check is evidence about the change it was run +; for, read from what the lab printed. +; -- +; On 2026-10-19 (tick 38) the lane checked a rebased branch on the lab in one shell line: +; git clean specs/claw && git apply && ...; seal --verify every spec; test-report each +; The clean left a stale .trinity/seals/*.json behind, so `git apply` stopped with "already exists" +; and changed nothing. The checks after the `;` still ran, on master's files: seals 33 of 33 and +; four specs green. The fifth spec, the new one, was not in the tree, and its summary line came out +; blank. The output read like a pass. Only the missing line gave it away. Re-run on a clean +; tree, the same check applied 13 files and showed 34 seals. A blank line is not a zero, and a pass +; on code the patch never reached is not a pass. +; -- +; lab_reason(expected_files, applied_files, tests, fails, vacuous), first match wins: +; applied_files != expected_files -> hold R_NOT_APPLIED +; tests, fails or vacuous is NONE (no such line) -> hold R_SILENT +; tests == 0 -> hold R_EMPTY +; fails > 0 -> hold R_RED +; vacuous > 0 -> hold R_VACUOUS +; otherwise -> pass R_PASS +; expected_files is the number of files in the local diff (`git diff --name-only | wc -l`); +; applied_files is what the lab echoed after `git apply --index` (`git status --short | wc -l`). +; count_after reads one number out of t27c's test-report text. It looks for these keys: +; "\n tests " "\n FAIL " "\n vacuous passes " +; Each key is anchored at a line start with exactly two spaces, so a test name in the per-test +; assert table (indented by a right-aligned count) never matches. The number is the first run of +; digits after the spaces that follow the key. It returns NONE when the key is absent, when no +; digit follows, or when the number has more than 9 digits. +; receipt_line writes L TAB TAB \n for one reason. +; Not specs/tri/lab/receipt.t27: that one reads the t27b lab's published run of a master commit. +; This one reads a hand-run lab check of a patch that is not on master yet. +; The honest limit: the counts are what the caller read off the lab. This module decides from them; +; it does not run the lab. +; ASCII only (L3). phi^2 + 1/phi^2 = 3 | TRINITY + +module ClawLabReceipt; + +use claw::text; + +/// "No such line in the report." +pub const NONE : u32 = 4294967295; + +/// Verdicts. +pub const V_PASS : u8 = 0; +pub const V_HOLD : u8 = 1; + +/// Reasons, one per receipt line. +pub const R_PASS : u8 = 0; +pub const R_NOT_APPLIED : u8 = 1; +pub const R_SILENT : u8 = 2; +pub const R_EMPTY : u8 = 3; +pub const R_RED : u8 = 4; +pub const R_VACUOUS : u8 = 5; +pub const R_COUNT : usize = 6; + +pub const VERDICT_TEXTS : [2]str = ["L\tpass\t", "L\thold\t"]; +pub const VERDICT_LENS : [2]usize = [7, 7]; + +pub const WHY_TEXTS : [6]str = [ + "the patch landed and every test ran an assert and passed\n", + "the lab applied a different number of files than the diff holds; the checks ran on other code\n", + "the report has no tests, FAIL or vacuous line; the compiler stopped or a filter hid it\n", + "the report ran no test; nothing was checked\n", + "a test failed\n", + "a test passed without running an assert; it checks nothing\n" +]; +pub const WHY_LENS : [6]usize = [57, 94, 87, 44, 14, 59]; + +/// The three summary keys of t27c test-report. +pub const KEY_TESTS : str = "\n tests "; +pub const KEY_TESTS_LEN : usize = 9; +pub const KEY_FAIL : str = "\n FAIL "; +pub const KEY_FAIL_LEN : usize = 8; +pub const KEY_VACUOUS : str = "\n vacuous passes "; +pub const KEY_VACUOUS_LEN : usize = 18; + +/// The most digits a count may have. +pub const MAX_DIGITS : u32 = 9; + +/// The reason for one lab check; its verdict is verdict_of(reason). +pub fn lab_reason(expected_files : u32, applied_files : u32, tests : u32, fails : u32, vacuous : u32) -> u8 { + if (applied_files != expected_files) { return R_NOT_APPLIED; } + if (tests == NONE or fails == NONE or vacuous == NONE) { return R_SILENT; } + if (tests == 0) { return R_EMPTY; } + if (fails > 0) { return R_RED; } + if (vacuous > 0) { return R_VACUOUS; } + return R_PASS; +} + +/// Only R_PASS passes; every other reason, and any value out of range, holds. +pub fn verdict_of(r : u8) -> u8 { + if (r == R_PASS) { return V_PASS; } + return V_HOLD; +} + +pub fn lab_verdict(expected_files : u32, applied_files : u32, tests : u32, fails : u32, vacuous : u32) -> u8 { + return verdict_of(lab_reason(expected_files, applied_files, tests, fails, vacuous)); +} + +/// Where the first match of key starts in b[0..b_len); b_len when there is none. +pub fn key_at(b : str, b_len : usize, key : str, key_len : usize) -> usize { + var start : usize = 0; + while (start + key_len <= b_len) { + var k : usize = 0; + var diff : usize = 0; + while (k < key_len) { + if (b[start + k] != key[k]) { diff += 1; } + k += 1; + } + if (diff == 0) { return start; } + start += 1; + } + return b_len; +} + +/// The number after the first match of key in b[0..b_len), past any spaces; NONE when the key is +/// absent, no digit follows, or the number has more than MAX_DIGITS digits. +pub fn count_after(b : str, b_len : usize, key : str, key_len : usize) -> u32 { + const at : usize = key_at(b, b_len, key, key_len); + if (at == b_len) { return NONE; } + var i : usize = at + key_len; + while (i < b_len and b[i] == ' ') { i += 1; } + var v : u32 = 0; + var digits : u32 = 0; + while (i < b_len and b[i] >= '0' and b[i] <= '9') { + if (digits == MAX_DIGITS) { return NONE; } + v = v * 10 + ((b[i] - '0') as u32); + digits += 1; + i += 1; + } + if (digits == 0) { return NONE; } + return v; +} + +/// The reason for a whole test-report text, given the file counts. +pub fn report_reason(expected_files : u32, applied_files : u32, b : str, b_len : usize) -> u8 { + const t : u32 = count_after(b, b_len, KEY_TESTS, KEY_TESTS_LEN); + const f : u32 = count_after(b, b_len, KEY_FAIL, KEY_FAIL_LEN); + const v : u32 = count_after(b, b_len, KEY_VACUOUS, KEY_VACUOUS_LEN); + return lab_reason(expected_files, applied_files, t, f, v); +} + +/// The receipt line for reason r into out at w, clipped at cap; returns the new cursor, which still +/// advances when clipped. A reason out of range writes the R_SILENT line. +pub fn receipt_line(r : u8, out : []u8, cap : usize, w : usize) -> usize { + var k : u8 = r; + if ((k as usize) >= R_COUNT) { k = R_SILENT; } + const v : usize = verdict_of(k) as usize; + const x : usize = put_str(out, cap, w, VERDICT_TEXTS[v], VERDICT_LENS[v]); + return put_str(out, cap, x, WHY_TEXTS[k as usize], WHY_LENS[k as usize]); +} + +// ---- tests: the reports the lane met ---- + +/// move_gate's report on 2026-10-19, trimmed to the summary and two table rows. +pub const GOOD : str = "--- test report: specs/claw/move_gate.t27 ---\n\n tests 16\n pass 16\n FAIL 0\n rate 100.0%\n\n runtime asserts executed, per test (#6509; a pass with 0 is vacuous, T730):\n 3 the_target_whose_trial_expired_holds_every_move\n 15 only_the_two_ok_reasons_write_a_go_line\n vacuous passes 0 of 16 (passed with 0 runtime asserts executed)\n"; +pub const GOOD_LEN : usize = 369; + +/// What t27c printed when zig was not on PATH: no summary at all. +pub const BLOCKED : str = "t27c: BLOCKED: zig not on PATH\n"; +pub const BLOCKED_LEN : usize = 31; + +test the_tick_38_apply_that_changed_nothing_holds_even_though_the_checks_were_green { + // The diff held 13 files; the failed apply left 0 staged. The other specs' reports were green. + assert lab_reason(13, 0, 25, 0, 0) == R_NOT_APPLIED; + assert lab_verdict(13, 0, 25, 0, 0) == V_HOLD; + assert report_reason(13, 0, GOOD, GOOD_LEN) == R_NOT_APPLIED; + assert lab_reason(13, 14, 25, 0, 0) == R_NOT_APPLIED; +} + +test the_new_spec_whose_line_came_out_blank_is_silent_not_zero { + assert lab_reason(13, 13, NONE, NONE, NONE) == R_SILENT; + assert report_reason(13, 13, "", 0) == R_SILENT; + assert report_reason(13, 13, BLOCKED, BLOCKED_LEN) == R_SILENT; +} + +test one_missing_count_is_enough_to_be_silent { + assert lab_reason(1, 1, NONE, 0, 0) == R_SILENT; + assert lab_reason(1, 1, 16, NONE, 0) == R_SILENT; + assert lab_reason(1, 1, 16, 0, NONE) == R_SILENT; +} + +test the_clean_rerun_passes { + assert report_reason(13, 13, GOOD, GOOD_LEN) == R_PASS; + assert lab_verdict(13, 13, 16, 0, 0) == V_PASS; + assert lab_reason(0, 0, 1, 0, 0) == R_PASS; +} + +test no_test_red_and_vacuous_each_hold { + assert lab_reason(1, 1, 0, 0, 0) == R_EMPTY; + assert lab_reason(1, 1, 16, 1, 0) == R_RED; + assert lab_reason(1, 1, 16, 2, 2) == R_RED; + assert lab_reason(1, 1, 16, 0, 1) == R_VACUOUS; + assert lab_verdict(1, 1, 16, 0, 1) == V_HOLD; +} + +test the_summary_numbers_are_read_from_the_report { + assert count_after(GOOD, GOOD_LEN, KEY_TESTS, KEY_TESTS_LEN) == 16; + assert count_after(GOOD, GOOD_LEN, KEY_FAIL, KEY_FAIL_LEN) == 0; + assert count_after(GOOD, GOOD_LEN, KEY_VACUOUS, KEY_VACUOUS_LEN) == 0; + assert count_after("x\n FAIL 3\n", 11, KEY_FAIL, KEY_FAIL_LEN) == 3; + assert count_after("\n tests 120", 12, KEY_TESTS, KEY_TESTS_LEN) == 120; +} + +test negative_control_a_test_named_tests_in_the_table_is_not_the_summary { + // Table rows are indented by a right-aligned count, so "\n tests " cannot match inside one. + assert count_after("\n 3 tests 7 pass\n", 24, KEY_TESTS, KEY_TESTS_LEN) == NONE; + assert count_after("\n tests 7\n", 10, KEY_TESTS, KEY_TESTS_LEN) == NONE; + assert count_after("\n testsx 7\n", 12, KEY_TESTS, KEY_TESTS_LEN) == NONE; +} + +test a_key_with_no_number_after_it_reads_as_none { + assert count_after("\n FAIL ", 8, KEY_FAIL, KEY_FAIL_LEN) == NONE; + assert count_after("\n FAIL x\n", 13, KEY_FAIL, KEY_FAIL_LEN) == NONE; + assert count_after("\n FAIL ", 11, KEY_FAIL, KEY_FAIL_LEN) == NONE; +} + +test a_count_stops_at_the_buffer_end_not_past_it { + // b_len cuts "12" to "1": the digit past b_len is not read. + assert count_after("\n FAIL 12", 9, KEY_FAIL, KEY_FAIL_LEN) == 1; + assert count_after("\n FAIL ", 9, KEY_FAIL, KEY_FAIL_LEN) == NONE; + // A key that ends exactly at b_len. + assert count_after("\n FAIL 4", 8, KEY_FAIL, KEY_FAIL_LEN) == NONE; +} + +test nine_digits_read_and_ten_are_refused { + assert count_after("\n FAIL 123456789\n", 18, KEY_FAIL, KEY_FAIL_LEN) == 123456789; + assert count_after("\n FAIL 1234567890\n", 19, KEY_FAIL, KEY_FAIL_LEN) == NONE; +} + +test a_key_is_found_where_it_ends_exactly_at_the_buffer_end { + assert key_at("\n FAIL ", 8, KEY_FAIL, KEY_FAIL_LEN) == 0; + assert key_at("x\n FAIL ", 9, KEY_FAIL, KEY_FAIL_LEN) == 1; + assert key_at("\n FAI", 6, KEY_FAIL, KEY_FAIL_LEN) == 6; + assert key_at("x\n FAIL ", 8, KEY_FAIL, KEY_FAIL_LEN) == 8; +} + +test the_first_match_wins { + assert count_after("\n FAIL 2\n FAIL 5\n", 20, KEY_FAIL, KEY_FAIL_LEN) == 2; +} + +test only_r_pass_passes_and_out_of_range_holds { + var r : u32 = 0; + var passes : u32 = 0; + while (r < 256) { + if (verdict_of(r as u8) == V_PASS) { passes += 1; assert r == 0; } + r += 1; + } + assert passes == 1; + assert verdict_of(6) == V_HOLD; +} + +test only_the_pass_reason_writes_a_pass_line { + var out : [128]u8 = undefined; + var r : u8 = 0; + var passes : u32 = 0; + while (r < 6) { + const w : usize = receipt_line(r, &out, 128, 0); + if (out[2] == 'p') { + passes += 1; + assert r == R_PASS; + } else { + assert out[2] == 'h'; + } + assert out[w - 1] == '\n'; + r += 1; + } + assert passes == 1; +} + +test the_lens_are_the_lengths_of_the_texts { + var k : usize = 0; + while (k < R_COUNT) { + assert WHY_LENS[k] == WHY_TEXTS[k].len; + k += 1; + } + var v : usize = 0; + while (v < 2) { + assert VERDICT_LENS[v] == VERDICT_TEXTS[v].len; + v += 1; + } + assert KEY_TESTS_LEN == KEY_TESTS.len; + assert KEY_FAIL_LEN == KEY_FAIL.len; + assert KEY_VACUOUS_LEN == KEY_VACUOUS.len; + assert GOOD_LEN == GOOD.len; + assert BLOCKED_LEN == BLOCKED.len; +} + +test the_receipt_line_spells_the_verdict_and_the_reason { + var out : [128]u8 = undefined; + const w : usize = receipt_line(R_NOT_APPLIED, &out, 128, 0); + assert w == 7 + 94; + assert out[0] == 'L'; + assert out[2] == 'h'; + assert out[7] == 't'; + assert out[w - 1] == '\n'; + const p : usize = receipt_line(R_PASS, &out, 128, 0); + assert p == 7 + 57; + assert out[2] == 'p'; +} + +test a_reason_out_of_range_writes_the_silent_line { + var out : [128]u8 = undefined; + const w : usize = receipt_line(9, &out, 128, 0); + assert w == 7 + 87; + assert out[2] == 'h'; + assert out[11] == 'r'; + // Exactly R_COUNT: the first reason past the table. + out[11] = 'x'; + const e : usize = receipt_line(6, &out, 128, 0); + assert e == 7 + 87; + assert out[11] == 'r'; +} + +test a_clipped_line_still_advances_the_cursor { + var out : [4]u8 = undefined; + assert receipt_line(R_RED, &out, 4, 0) == 7 + 14; +} From b3ec2a86b3e60700b073277bd7cd9e45a22be0f7 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 10:22:40 +0700 Subject: [PATCH 2/6] feat(claw): lab_disk -- hold a lab check when the lab volume has no room specs/claw/lab_disk.t27 (ClawLabDisk) reads df -Pk text for one mount: under 1 GiB free holds, under 5 GiB or under 10% free warns, a mount df does not list or a line with no readable size holds. The case is the t27c lab's /data at 0 KB free of 47.7 GB on 2026-10-19. t27c 18/18, 0 vacuous; t27b 18/18; 49/50 mutants caught (the survivor is equivalent); sealed. Refs #10813 Co-Authored-By: Claude Opus 5.5 --- .trinity/seals/claw_ClawLabDisk.json | 33 +++ docs/claw/README.md | 1 + docs/reports/t27b_expectations.json | 1 + specs/claw/lab_disk.t27 | 321 +++++++++++++++++++++++++++ 4 files changed, 356 insertions(+) create mode 100644 .trinity/seals/claw_ClawLabDisk.json create mode 100644 specs/claw/lab_disk.t27 diff --git a/.trinity/seals/claw_ClawLabDisk.json b/.trinity/seals/claw_ClawLabDisk.json new file mode 100644 index 0000000000..9ef1ed3401 --- /dev/null +++ b/.trinity/seals/claw_ClawLabDisk.json @@ -0,0 +1,33 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/claw/text.t27", + "spec_hash": "sha256:968755ec1917f766eb2cdd0ab4c0c48aef028a0e7d69e191911695e925b286ad" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:a5832b28b521ad179e99fee0b263f599b5a8c82e42366cb24c595b8a4a750924", + "gen_hash_rust": "sha256:cfc04e8965f0f5164765cb597ca99533e4ec134bcbf451cd889698f6fa25f58b", + "gen_hash_verilog": "sha256:3a90cbd4b4ad92a271bf63c7ae8da58fa4a453b1ea26518f4ca05d653746ff63", + "gen_hash_zig": "sha256:a13391de0f3672d872614e98bfe16e7022515df16a09cdd28e5f0320e1430e7b", + "module": "ClawLabDisk", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-19T03:21:48Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:e6f72ecf00aec2c63dce43b3cce803db99b676ea4bf258e03689df9eb20b6018", + "spec_path": "specs/claw/lab_disk.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 18, + "total": 18, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/claw/README.md b/docs/claw/README.md index 7bf6b70856..af4b070894 100644 --- a/docs/claw/README.md +++ b/docs/claw/README.md @@ -25,6 +25,7 @@ behind one front door, with the gate logic in `.t27` specs. Epic | Merge lane: is the merger starved, and does it merge anything? Counts its runs, the oldest open age, the PRs each run merged, and its share of the whole queue, so an hour-long wait, a zero yield or a tenth of the queue shows up as data | `specs/claw/merge_lane.t27` (rows per merger run or per queued run; `F merger`, `F merger-yield` and `F merger-share low/high/unknown/none` out) | #10644 | | Shape race: the issue-shape gate judges the event payload, which can be twenty minutes old; replays a gate timeline under the payload reading and a live reading, so an issue closed with a passing body shows up as data | `specs/claw/shape_race.t27` (rows `created TAB ran TAB ok` per gate event; an `R` line and `F shape-race clean/race/unknown/none` out; #10644 is the fixture; the workflow change is the owner's decision) | #10662 | | Lab receipt: whether a hand-run lab check of a patch really ran on the patch. A `git apply` that changed fewer files than the diff, a blank or BLOCKED test report, zero tests, a failure or a vacuous pass all hold | `specs/claw/lab_receipt.t27` (expected and applied file counts and the t27c `test-report` text in; one `L pass/hold` line with the reason out; not wired to a job yet) | #10810 | +| Lab disk: whether a lab volume has room for a check, read from `df -Pk` before it runs. Under 1 GiB free holds; under 5 GiB or under 10% free warns; a mount df does not list, or a line with no readable size, holds | `specs/claw/lab_disk.t27` (`df -Pk` text and a mount in; one `D go/warn/hold` line with the reason out; not wired to a job yet) | #10813 | | Model routing: Opus 5.5 orchestrates, Sonnet 5.5 runs sub-tasks | `specs/claw/model_route.t27`, skill `model-routing`, `.claude/settings.json` + `.claude/agents/*` | owner directive 2026-10-18 | | A T27F visual in every answer that needs one | rule `specs/t27mcp/visual_output.t27`, particle-still layer `specs/claw/visual_output.t27`, procedure in skill `t27f`, "Visual in every output" | owner directive 2026-10-18 | | `reel_still`: one particle still (PNG at STILL_MS of a token_floor reel) from TRI CLAW's `/mcp` | `specs/claw/reel_still.t27`, rendered on the Railway lab only | follow-up of the row above | diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 5ca5f5922d..8b2c117b12 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -197,6 +197,7 @@ {"path": "specs/claw/gates.t27", "t27b": "pass"}, {"path": "specs/claw/gates_pins.t27", "t27b": "pass"}, {"path": "specs/claw/github_surface.t27", "t27b": "pass"}, + {"path": "specs/claw/lab_disk.t27", "t27b": "pass"}, {"path": "specs/claw/lab_receipt.t27", "t27b": "pass"}, {"path": "specs/claw/ledger_union.t27", "t27b": "pass"}, {"path": "specs/claw/merge_lane.t27", "t27b": "pass"}, diff --git a/specs/claw/lab_disk.t27 b/specs/claw/lab_disk.t27 new file mode 100644 index 0000000000..01899c43a2 --- /dev/null +++ b/specs/claw/lab_disk.t27 @@ -0,0 +1,321 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/claw/lab_disk.t27 -- whether a Railway lab volume has room for a check, read from +; `df -Pk` before the check runs. +; -- +; On 2026-10-19 (tick 40) the t27c lab printed this for its volume: +; /dev/zd31792 47745772 47729388 0 100% /data +; 0 KB free on 47.7 GB. The lab's poller and every cache kept there write to /data. A check +; that writes to a full volume either fails on its first write or prints nothing, and +; specs/claw/lab_receipt.t27 then reads the blank as silent. This gate asks first: is there +; room? It holds a check before it runs instead of explaining a blank after. +; -- +; disk_reason(blocks, avail), sizes in KiB, first match wins: +; blocks or avail is NONE (no such field) -> hold R_UNREADABLE +; blocks == 0, or avail > blocks -> hold R_UNREADABLE (df does not say that) +; avail < FULL_KB (1 GiB) -> hold R_FULL +; avail < LOW_KB (5 GiB), or avail*10 < blocks (under 10% free) -> warn R_LOW +; otherwise -> go R_OK +; The 10% line is the kubelet's default hard-eviction threshold for a node's root volume +; (nodefs.available<10%). The 1 GiB and 5 GiB floors are ours: a zig build of one spec +; writes a few hundred MB of cache. +; line_for finds the df line whose last field is the mount, byte for byte: "/data" does not +; match "/data2", "/dat" or "/". field reads field n of that line (0 is the filesystem, 1 the +; size, 3 the free count) as digits that end at a space, a newline or the buffer's end. +; `df -P` keeps each filesystem on one line; without -P a long device name wraps, and the +; numbers move to the next line. Inode use (`df -Pi`) is not judged here. +; disk_line writes D TAB TAB \n for one reason. +; The honest limit: the text is what the caller read off the lab. This module decides from it; +; it does not run df, and it deletes nothing. +; ASCII only (L3). phi^2 + 1/phi^2 = 3 | TRINITY + +module ClawLabDisk; + +use claw::text; + +/// "No such field." +pub const NONE : u64 = 999999999999999999; + +/// The most digits a size may have (KiB; 15 digits is about 1 EiB). +pub const MAX_DIGITS : u32 = 15; + +/// 1 GiB and 5 GiB, in KiB. +pub const FULL_KB : u64 = 1048576; +pub const LOW_KB : u64 = 5242880; + +/// Verdicts. +pub const V_GO : u8 = 0; +pub const V_WARN : u8 = 1; +pub const V_HOLD : u8 = 2; + +/// Reasons, one per disk line. +pub const R_OK : u8 = 0; +pub const R_LOW : u8 = 1; +pub const R_FULL : u8 = 2; +pub const R_NO_MOUNT : u8 = 3; +pub const R_UNREADABLE : u8 = 4; +pub const R_COUNT : usize = 5; + +/// df -Pk fields. +pub const F_SIZE : usize = 1; +pub const F_FREE : usize = 3; + +pub const VERDICT_TEXTS : [3]str = ["D\tgo\t", "D\twarn\t", "D\thold\t"]; +pub const VERDICT_LENS : [3]usize = [5, 7, 7]; + +pub const WHY_TEXTS : [5]str = [ + "the volume has room: at least 5 GiB and at least 10% free\n", + "the volume is low: under 5 GiB or under 10% free; a check may still run\n", + "the volume is full: under 1 GiB free; a build that writes there fails or prints nothing\n", + "no df line names this mount; the path is not a mount point or df failed\n", + "the df line for this mount has no readable size or free count\n" +]; +pub const WHY_LENS : [5]usize = [58, 72, 88, 72, 62]; + +/// The reason for one volume, sizes in KiB; its verdict is verdict_of(reason). +pub fn disk_reason(blocks : u64, avail : u64) -> u8 { + if (blocks == NONE or avail == NONE) { return R_UNREADABLE; } + if (blocks == 0 or avail > blocks) { return R_UNREADABLE; } + if (avail < FULL_KB) { return R_FULL; } + if (avail < LOW_KB or avail * 10 < blocks) { return R_LOW; } + return R_OK; +} + +/// R_OK goes, R_LOW warns; every other reason, and any value out of range, holds. +pub fn verdict_of(r : u8) -> u8 { + if (r == R_OK) { return V_GO; } + if (r == R_LOW) { return V_WARN; } + return V_HOLD; +} + +/// Where the first line of b[0..b_len) whose last field is mount starts; b_len when none is. +pub fn line_for(b : str, b_len : usize, mount : str, mount_len : usize) -> usize { + var s : usize = 0; + while (s < b_len) { + var e : usize = s; + while (e < b_len and b[e] != '\n') { e += 1; } + var j : usize = e; + while (j > s and b[j - 1] != ' ') { j -= 1; } + if (e - j == mount_len) { + var k : usize = 0; + var same : bool = true; + while (k < mount_len) { + if (b[j + k] != mount[k]) { same = false; } + k += 1; + } + if (same) { return s; } + } + s = e + 1; + } + return b_len; +} + +/// Field n of the line that starts at s, as a number; NONE when the line has fewer fields, the +/// field is not all digits, or it has more than MAX_DIGITS digits. +pub fn field(b : str, b_len : usize, s : usize, n : usize) -> u64 { + var i : usize = s; + var f : usize = 0; + while (f < n) { + while (i < b_len and b[i] != ' ' and b[i] != '\n') { i += 1; } + while (i < b_len and b[i] == ' ') { i += 1; } + if (i >= b_len or b[i] == '\n') { return NONE; } + f += 1; + } + var v : u64 = 0; + var digits : u32 = 0; + while (i < b_len and b[i] >= '0' and b[i] <= '9') { + if (digits == MAX_DIGITS) { return NONE; } + v = v * 10 + ((b[i] - '0') as u64); + digits += 1; + i += 1; + } + if (digits == 0) { return NONE; } + if (i < b_len and b[i] != ' ' and b[i] != '\n') { return NONE; } + return v; +} + +/// The reason for mount in a whole `df -Pk` text. +pub fn df_reason(b : str, b_len : usize, mount : str, mount_len : usize) -> u8 { + const s : usize = line_for(b, b_len, mount, mount_len); + if (s == b_len) { return R_NO_MOUNT; } + return disk_reason(field(b, b_len, s, F_SIZE), field(b, b_len, s, F_FREE)); +} + +/// The disk line for reason r into out at w, clipped at cap; returns the new cursor, which still +/// advances when clipped. A reason out of range writes the R_UNREADABLE line. +pub fn disk_line(r : u8, out : []u8, cap : usize, w : usize) -> usize { + var k : u8 = r; + if ((k as usize) >= R_COUNT) { k = R_UNREADABLE; } + const v : usize = verdict_of(k) as usize; + const x : usize = put_str(out, cap, w, VERDICT_TEXTS[v], VERDICT_LENS[v]); + return put_str(out, cap, x, WHY_TEXTS[k as usize], WHY_LENS[k as usize]); +} + +// ---- tests: the volumes the lane met ---- + +/// `df -Pk /data /root` on the t27c lab, 2026-10-19. +pub const LAB : str = "Filesystem 1024-blocks Used Available Capacity Mounted on\n/dev/zd31792 47745772 47729388 0 100% /data\noverlay 2030337424 1507827364 522493676 75% /\n"; +pub const LAB_LEN : usize = 190; + +test the_full_lab_volume_holds_a_check { + assert df_reason(LAB, LAB_LEN, "/data", 5) == R_FULL; + assert verdict_of(df_reason(LAB, LAB_LEN, "/data", 5)) == V_HOLD; + assert disk_reason(47745772, 0) == R_FULL; +} + +test the_overlay_root_has_room { + assert df_reason(LAB, LAB_LEN, "/", 1) == R_OK; + assert verdict_of(df_reason(LAB, LAB_LEN, "/", 1)) == V_GO; +} + +test the_numbers_are_read_from_the_right_fields { + const d : usize = line_for(LAB, LAB_LEN, "/data", 5); + assert d == 68; + assert field(LAB, LAB_LEN, d, F_SIZE) == 47745772; + assert field(LAB, LAB_LEN, d, 2) == 47729388; + assert field(LAB, LAB_LEN, d, F_FREE) == 0; + const r : usize = line_for(LAB, LAB_LEN, "/", 1); + assert field(LAB, LAB_LEN, r, F_SIZE) == 2030337424; + assert field(LAB, LAB_LEN, r, F_FREE) == 522493676; +} + +test negative_control_a_mount_matches_only_byte_for_byte { + assert df_reason(LAB, LAB_LEN, "/dat", 4) == R_NO_MOUNT; + assert df_reason(LAB, LAB_LEN, "/data2", 6) == R_NO_MOUNT; + assert df_reason(LAB, LAB_LEN, "data", 4) == R_NO_MOUNT; + assert df_reason(LAB, LAB_LEN, "/root", 5) == R_NO_MOUNT; + assert df_reason("", 0, "/data", 5) == R_NO_MOUNT; + assert verdict_of(R_NO_MOUNT) == V_HOLD; +} + +test negative_control_the_header_line_is_not_a_volume { + // The header's last field is "on"; its fields are words, not numbers. + assert line_for(LAB, LAB_LEN, "on", 2) == 0; + assert df_reason(LAB, LAB_LEN, "on", 2) == R_UNREADABLE; +} + +test a_last_line_without_a_newline_is_read_to_the_buffer_end { + const b : str = "x 20000000 1 15000000 1% /m"; + assert line_for(b, 27, "/m", 2) == 0; + assert field(b, 27, 0, F_FREE) == 15000000; + assert df_reason(b, 27, "/m", 2) == R_OK; + assert df_reason(b, 26, "/m", 2) == R_NO_MOUNT; +} + +test a_line_that_ends_early_or_carries_a_word_is_unreadable { + assert field("x 10\n", 5, 0, F_FREE) == NONE; + assert field("x 10", 4, 0, F_SIZE) == 10; + assert field("x 10a 3", 7, 0, F_SIZE) == NONE; + assert field("x 10 3", 6, 0, 2) == 3; + assert field("x ", 3, 0, F_SIZE) == NONE; + assert df_reason("x 10a 1 2 3% /m\n", 16, "/m", 2) == R_UNREADABLE; +} + +test a_line_that_is_only_the_mount_is_found_and_unreadable { + // No space on the line: its one field is both the first and the last. + assert line_for("/m", 2, "/m", 2) == 0; + assert line_for("x 1\n/m", 6, "/m", 2) == 4; + assert df_reason("x 1\n/m", 6, "/m", 2) == R_UNREADABLE; +} + +test a_one_field_line_at_the_end_of_a_full_buffer_is_not_read_past { + var one : [1]u8 = undefined; + one[0] = 'x'; + assert field(&one, 1, 0, F_SIZE) == NONE; + var two : [3]u8 = undefined; + two[0] = '7'; + two[1] = ' '; + two[2] = 'x'; + assert field(&two, 3, 2, F_SIZE) == NONE; + assert field(&two, 3, 0, 0) == 7; +} + +test fifteen_digits_read_and_sixteen_are_refused { + assert field("x 999999999999999", 17, 0, F_SIZE) == 999999999999999; + assert field("x 1000000000000000", 18, 0, F_SIZE) == NONE; +} + +test the_full_line_is_exactly_one_gib { + assert disk_reason(100000000, FULL_KB - 1) == R_FULL; + assert disk_reason(2000000, FULL_KB) == R_LOW; +} + +test the_low_line_is_five_gib_and_ten_percent { + assert disk_reason(LOW_KB * 10, LOW_KB - 1) == R_LOW; + assert disk_reason(LOW_KB * 10, LOW_KB) == R_OK; + assert disk_reason(LOW_KB * 10 + 1, LOW_KB) == R_LOW; + assert disk_reason(LOW_KB, LOW_KB) == R_OK; + assert verdict_of(R_LOW) == V_WARN; +} + +test sizes_df_does_not_print_are_unreadable { + assert disk_reason(0, 0) == R_UNREADABLE; + assert disk_reason(LOW_KB, LOW_KB + 1) == R_UNREADABLE; + assert disk_reason(NONE, LOW_KB) == R_UNREADABLE; + assert disk_reason(LOW_KB, NONE) == R_UNREADABLE; +} + +test only_ok_goes_only_low_warns_and_out_of_range_holds { + var gos : u32 = 0; + var warns : u32 = 0; + var r : u32 = 0; + while (r < 256) { + const v : u8 = verdict_of(r as u8); + if (v == V_GO) { gos += 1; assert r == 0; } + if (v == V_WARN) { warns += 1; assert r == 1; } + r += 1; + } + assert gos == 1; + assert warns == 1; + assert verdict_of(5) == V_HOLD; +} + +test the_lens_are_the_text_lengths { + var i : usize = 0; + while (i < 3) { + assert VERDICT_TEXTS[i][0] == 'D'; + assert VERDICT_TEXTS[i][1] == '\t'; + assert VERDICT_TEXTS[i][VERDICT_LENS[i] - 1] == '\t'; + i += 1; + } + var k : usize = 0; + while (k < R_COUNT) { + assert WHY_TEXTS[k][WHY_LENS[k] - 1] == '\n'; + assert WHY_TEXTS[k][WHY_LENS[k] - 2] != '\n'; + k += 1; + } +} + +test the_full_line_is_spelled_out { + var out : [128]u8 = undefined; + const w : usize = disk_line(R_FULL, &out, 128, 0); + assert w == 95; + assert str_eq(&out, 7, "D\thold\t", 7); + assert out[7] == 't'; + assert out[94] == '\n'; + const g : usize = disk_line(R_OK, &out, 128, 0); + assert g == 63; + assert out[2] == 'g'; + assert out[4] == '\t'; +} + +test out_of_range_reasons_write_the_unreadable_line { + var out : [128]u8 = undefined; + var r : u32 = 5; + while (r < 256) { + assert disk_line(r as u8, &out, 128, 0) == 69; + // The bytes, not only the length: this is the unreadable line. + assert out[2] == 'h'; + assert out[7] == 't'; + assert out[11] == 'd'; + r += 1; + } + assert disk_line(R_UNREADABLE, &out, 128, 0) == 69; + assert disk_line(R_NO_MOUNT, &out, 128, 0) == 79; +} + +test a_clipped_line_still_advances_the_cursor { + var out : [8]u8 = undefined; + assert disk_line(R_LOW, &out, 8, 0) == 79; + assert out[2] == 'w'; + assert disk_line(R_LOW, &out, 8, 3) == 82; +} From bf8f9c954ccd27ffaa81d35dcfcc845e6f17d75a Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 10:41:54 +0700 Subject: [PATCH 3/6] feat(claw): red_origin -- whose red a failing check is, the PR's or master's When Auto-fix reports a failing check, red_origin reads the PR's changed paths, the paths the failing log names, the paths master changed since the PR's base, and master's state on the same check, and says own, inherited or unknown. PR #10797 had four reds and none was its own; each took a log read and a git log by hand, and a literal "fix and push" would have put other lanes' files into the PR. t27c 18/18 with 0 vacuous, t27b 18/18, mutants 28/30 caught (two loop bounds are equivalent), gen-c ok, sealed. Lines are tagged O, apart from required_red.t27's R. Closes #10821 Co-Authored-By: Claude Opus 5.5 --- .trinity/seals/claw_ClawRedOrigin.json | 33 +++ docs/claw/README.md | 1 + docs/reports/t27b_expectations.json | 1 + specs/claw/red_origin.t27 | 320 +++++++++++++++++++++++++ 4 files changed, 355 insertions(+) create mode 100644 .trinity/seals/claw_ClawRedOrigin.json create mode 100644 specs/claw/red_origin.t27 diff --git a/.trinity/seals/claw_ClawRedOrigin.json b/.trinity/seals/claw_ClawRedOrigin.json new file mode 100644 index 0000000000..bbc62ee96d --- /dev/null +++ b/.trinity/seals/claw_ClawRedOrigin.json @@ -0,0 +1,33 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/claw/text.t27", + "spec_hash": "sha256:968755ec1917f766eb2cdd0ab4c0c48aef028a0e7d69e191911695e925b286ad" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:9aadf5a0c6fb059a4c986f2e4ded6c8d4a5e2837b7d31b014d9ba03eb9d3fd9b", + "gen_hash_rust": "sha256:515a25e36579d2525c44bd2b5d1f982f0fb37ba16350cbd5b0d42e9c2c676678", + "gen_hash_verilog": "sha256:ad520108d7b60f60ab6d3257a44ee992e61a5b62dbb3ab36a91ba64628e58f6b", + "gen_hash_zig": "sha256:3d2b2ba9bfa9c1109c4ab5a09135df2e121d2f1e6df45d58ab32e29dcf09333a", + "module": "ClawRedOrigin", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-19T03:41:21Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:ca93d2b89d539b2f811f8d42465df012dc2c664791fe2121f0809214b576bcbe", + "spec_path": "specs/claw/red_origin.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 18, + "total": 18, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/claw/README.md b/docs/claw/README.md index af4b070894..3991e9b032 100644 --- a/docs/claw/README.md +++ b/docs/claw/README.md @@ -26,6 +26,7 @@ behind one front door, with the gate logic in `.t27` specs. Epic | Shape race: the issue-shape gate judges the event payload, which can be twenty minutes old; replays a gate timeline under the payload reading and a live reading, so an issue closed with a passing body shows up as data | `specs/claw/shape_race.t27` (rows `created TAB ran TAB ok` per gate event; an `R` line and `F shape-race clean/race/unknown/none` out; #10644 is the fixture; the workflow change is the owner's decision) | #10662 | | Lab receipt: whether a hand-run lab check of a patch really ran on the patch. A `git apply` that changed fewer files than the diff, a blank or BLOCKED test report, zero tests, a failure or a vacuous pass all hold | `specs/claw/lab_receipt.t27` (expected and applied file counts and the t27c `test-report` text in; one `L pass/hold` line with the reason out; not wired to a job yet) | #10810 | | Lab disk: whether a lab volume has room for a check, read from `df -Pk` before it runs. Under 1 GiB free holds; under 5 GiB or under 10% free warns; a mount df does not list, or a line with no readable size, holds | `specs/claw/lab_disk.t27` (`df -Pk` text and a mount in; one `D go/warn/hold` line with the reason out; not wired to a job yet) | #10813 | +| Red origin: whose red a failing check is. A file the log names that the PR changes, or a green master, makes it the PR's; a red master, or a named file master changed since the PR's base, makes it inherited, and the PR gets no push. Ask required_red first | `specs/claw/red_origin.t27` (the PR's paths, the log's named paths, master's moved paths and master's state in; one `O own/inherited/unknown` line with the reason out; not wired to a job yet) | #10821 | | Model routing: Opus 5.5 orchestrates, Sonnet 5.5 runs sub-tasks | `specs/claw/model_route.t27`, skill `model-routing`, `.claude/settings.json` + `.claude/agents/*` | owner directive 2026-10-18 | | A T27F visual in every answer that needs one | rule `specs/t27mcp/visual_output.t27`, particle-still layer `specs/claw/visual_output.t27`, procedure in skill `t27f`, "Visual in every output" | owner directive 2026-10-18 | | `reel_still`: one particle still (PNG at STILL_MS of a token_floor reel) from TRI CLAW's `/mcp` | `specs/claw/reel_still.t27`, rendered on the Railway lab only | follow-up of the row above | diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 8b2c117b12..70831c8704 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -207,6 +207,7 @@ {"path": "specs/claw/push_gate.t27", "t27b": "pass"}, {"path": "specs/claw/queue_cost.t27", "t27b": "pass"}, {"path": "specs/claw/recipes.t27", "t27b": "pass"}, + {"path": "specs/claw/red_origin.t27", "t27b": "pass"}, {"path": "specs/claw/reel_still.t27", "t27b": "pass"}, {"path": "specs/claw/refresh.t27", "t27b": "pass"}, {"path": "specs/claw/required_red.t27", "t27b": "pass"}, diff --git a/specs/claw/red_origin.t27 b/specs/claw/red_origin.t27 new file mode 100644 index 0000000000..eab82f5e54 --- /dev/null +++ b/specs/claw/red_origin.t27 @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/claw/red_origin.t27 -- whose red is it: when a check fails on a PR, does the fix belong in +; the PR, or did the PR inherit a failure master already has? +; -- +; On 2026-10-19 Auto-fix reported four failing checks on gHashTag/t27 PR #10797 (claw mutation +; sweeps). Not one was the PR's: t27b-native-ratchet named 9 analog, fpga, port and t27f specs +; that master fails too; "Documented t27c subcommands exist" counted a dead `tri t27t` that +; master's #10790 had added to AGENTS.md; coverage and spec-guards named specs/t27f/frontend.t27, +; changed on master by #10786 without a re-seal. Each one took a log read, a diff and a git log +; by hand, and the reflex the event invites -- "fix, commit, and push" -- would have put other +; lanes' files into the PR and restarted every check while the queue stood above 300. This module +; is that triage as a rule. +; -- +; red_reason(pr, named, moved, master), first match wins: +; a path the failing log names is a path the PR changes -> own R_OWN_PATH +; master's last finished run of the same check is red -> inherited R_INHERITED_RED +; master's last finished run of the same check is green -> own R_OWN_MERGE +; master has no finished run (cancelled, none) and a named +; path changed on master since the PR's merge base -> inherited R_INHERITED_MOVED +; otherwise -> unknown R_UNSEEN +; pr, named and moved are newline-separated path lists: the PR's changed files (gh pr diff +; --name-only), the files the failing log names, and the files master changed since the PR's +; merge base (git diff --name-only base origin/master). A path matches only as a whole line, +; byte for byte; an empty line matches nothing. master is M_GREEN, M_RED or M_UNSEEN; any other +; value reads as M_UNSEEN. +; The own-path rule comes first: when master is red too, a PR that touches a named file still +; fixes its own part. A green master makes any red the PR's, even on files the PR never +; touched -- that is the PR's merge with master failing, and only a rebase in the PR fixes it. +; R_INHERITED_MOVED is evidence, not proof: a counted ratchet ("dead mentions rose 138 -> 139") +; names old entries beside the new one, so one moved path is enough to say inherited, and the +; caller confirms on master before it calls the matter settled. +; Ask required_red.t27 first: it says whether a required check is red at all. None of #10797's +; four was required, so no push was due on any of them. This module answers the next question, +; for a red that does block: whose is it. Its lines are tagged O, apart from required_red's R. +; red_line writes O TAB TAB \n for one reason. +; The honest limits: the lists are what the caller read off GitHub and git; a flaky check that +; went red once on a green master reads as own here. This module decides; it pushes nothing. +; ASCII only (L3). phi^2 + 1/phi^2 = 3 | TRINITY + +module ClawRedOrigin; + +use claw::text; + +/// The state of master's last finished run of the same check. +pub const M_GREEN : u8 = 0; +pub const M_RED : u8 = 1; +pub const M_UNSEEN : u8 = 2; + +/// Verdicts. +pub const V_OWN : u8 = 0; +pub const V_INHERITED : u8 = 1; +pub const V_UNKNOWN : u8 = 2; + +/// Reasons, one per red line. +pub const R_OWN_PATH : u8 = 0; +pub const R_OWN_MERGE : u8 = 1; +pub const R_INHERITED_RED : u8 = 2; +pub const R_INHERITED_MOVED : u8 = 3; +pub const R_UNSEEN : u8 = 4; +pub const R_COUNT : usize = 5; + +pub const VERDICT_TEXTS : [3]str = ["O\town\t", "O\tinherited\t", "O\tunknown\t"]; +pub const VERDICT_LENS : [3]usize = [6, 12, 10]; + +pub const WHY_TEXTS : [5]str = [ + "the log names a file this PR changes: fix it here and push\n", + "master passes this check, so the PR's merge with master fails it: fix it here\n", + "master fails this check too: do not push here; find or propose master's fix\n", + "a file the log names changed on master since the PR's base, and the PR changes none: confirm on master, do not push here\n", + "master has no finished run of this check and no named file settles it: read the log before you push\n" +]; +pub const WHY_LENS : [5]usize = [59, 78, 76, 121, 100]; + +/// Whether b[s..e) is one whole line of list[0..list_len). An empty slice is on no list. +pub fn line_in(list : str, list_len : usize, b : str, s : usize, e : usize) -> bool { + if (e <= s) { return false; } + const n : usize = e - s; + var i : usize = 0; + while (i < list_len) { + var j : usize = i; + while (j < list_len and list[j] != '\n') { j += 1; } + if (j - i == n) { + var k : usize = 0; + var same : bool = true; + while (k < n) { + if (list[i + k] != b[s + k]) { same = false; } + k += 1; + } + if (same) { return true; } + } + i = j + 1; + } + return false; +} + +/// How many lines of b[0..b_len) are lines of list. +pub fn hits(b : str, b_len : usize, list : str, list_len : usize) -> u32 { + var n : u32 = 0; + var s : usize = 0; + while (s < b_len) { + var e : usize = s; + while (e < b_len and b[e] != '\n') { e += 1; } + if (line_in(list, list_len, b, s, e)) { n += 1; } + s = e + 1; + } + return n; +} + +/// The reason for one failing check; its verdict is verdict_of(reason). +pub fn red_reason(pr : str, pr_len : usize, named : str, named_len : usize, moved : str, moved_len : usize, master : u8) -> u8 { + if (hits(named, named_len, pr, pr_len) > 0) { return R_OWN_PATH; } + if (master == M_RED) { return R_INHERITED_RED; } + if (master == M_GREEN) { return R_OWN_MERGE; } + if (hits(named, named_len, moved, moved_len) > 0) { return R_INHERITED_MOVED; } + return R_UNSEEN; +} + +/// The two own reasons are own, the two inherited reasons are inherited; every other reason, and +/// any value out of range, is unknown. +pub fn verdict_of(r : u8) -> u8 { + if (r == R_OWN_PATH or r == R_OWN_MERGE) { return V_OWN; } + if (r == R_INHERITED_RED or r == R_INHERITED_MOVED) { return V_INHERITED; } + return V_UNKNOWN; +} + +/// The red line for reason r into out at w, clipped at cap; returns the new cursor, which still +/// advances when clipped. A reason out of range writes the R_UNSEEN line. +pub fn red_line(r : u8, out : []u8, cap : usize, w : usize) -> usize { + var k : u8 = r; + if ((k as usize) >= R_COUNT) { k = R_UNSEEN; } + const v : usize = verdict_of(k) as usize; + const x : usize = put_str(out, cap, w, VERDICT_TEXTS[v], VERDICT_LENS[v]); + return put_str(out, cap, x, WHY_TEXTS[k as usize], WHY_LENS[k as usize]); +} + +// ---- tests: the four reds of PR #10797 ---- + +/// Five of the thirteen paths PR #10797 changed. +pub const PR : str = "specs/claw/move_gate.t27\nspecs/claw/reel_still.t27\n.trinity/seals/claw_ClawMoveGate.json\ndocs/claw/README.md\ndocs/reports/t27b_expectations.json\n"; +pub const PR_LEN : usize = 145; + +/// coverage and spec-guards named one spec; #10786 changed it on master. +pub const FRONT : str = "specs/t27f/frontend.t27\n"; +pub const FRONT_LEN : usize = 24; +pub const MOVED_10786 : str = "specs/t27f/frontend.t27\nspecs/t27f/petal_menu.t27\n"; +pub const MOVED_10786_LEN : usize = 50; + +/// t27b-native-ratchet named nine specs; two of them here. +pub const RATCHET : str = "specs/analog/sar_switch.t27\nspecs/t27f/petal_menu.t27"; +pub const RATCHET_LEN : usize = 53; + +/// The dead-mention count names old entries beside the new one; #10790 changed AGENTS.md. +pub const DEAD : str = "docs/tri-ssot-integration.md\nAGENTS.md\ndocs/nona-03-manifest/SOUL.md\n"; +pub const DEAD_LEN : usize = 69; +pub const MOVED_10790 : str = "AGENTS.md\nscripts/tri\nspecs/t27t/t27t.t27\n"; +pub const MOVED_10790_LEN : usize = 42; + +test the_stale_frontend_seal_was_inherited_from_master { + // Master's runs of coverage were cancelled; the named spec moved on master. + const r : u8 = red_reason(PR, PR_LEN, FRONT, FRONT_LEN, MOVED_10786, MOVED_10786_LEN, M_UNSEEN); + assert r == R_INHERITED_MOVED; + assert verdict_of(r) == V_INHERITED; +} + +test the_ratchet_red_was_inherited_because_master_fails_it_too { + const r : u8 = red_reason(PR, PR_LEN, RATCHET, RATCHET_LEN, "", 0, M_RED); + assert r == R_INHERITED_RED; + assert verdict_of(r) == V_INHERITED; +} + +test the_dead_tri_count_was_inherited_through_one_moved_file { + assert hits(DEAD, DEAD_LEN, MOVED_10790, MOVED_10790_LEN) == 1; + const r : u8 = red_reason(PR, PR_LEN, DEAD, DEAD_LEN, MOVED_10790, MOVED_10790_LEN, M_UNSEEN); + assert r == R_INHERITED_MOVED; +} + +test a_named_file_the_pr_changes_is_own_even_when_master_is_red { + const named : str = "specs/analog/sar_switch.t27\nspecs/claw/reel_still.t27\n"; + assert hits(named, 54, PR, PR_LEN) == 1; + assert red_reason(PR, PR_LEN, named, 54, "", 0, M_RED) == R_OWN_PATH; + assert red_reason(PR, PR_LEN, named, 54, named, 54, M_UNSEEN) == R_OWN_PATH; + assert verdict_of(R_OWN_PATH) == V_OWN; +} + +test a_green_master_makes_the_red_the_prs_even_on_moved_files { + const r : u8 = red_reason(PR, PR_LEN, FRONT, FRONT_LEN, MOVED_10786, MOVED_10786_LEN, M_GREEN); + assert r == R_OWN_MERGE; + assert verdict_of(r) == V_OWN; +} + +test with_no_finished_master_run_and_nothing_moved_it_is_unknown { + assert red_reason(PR, PR_LEN, FRONT, FRONT_LEN, "", 0, M_UNSEEN) == R_UNSEEN; + assert red_reason(PR, PR_LEN, "", 0, MOVED_10786, MOVED_10786_LEN, M_UNSEEN) == R_UNSEEN; + assert verdict_of(R_UNSEEN) == V_UNKNOWN; +} + +test a_log_that_names_no_file_follows_master_alone { + assert red_reason(PR, PR_LEN, "", 0, "", 0, M_RED) == R_INHERITED_RED; + assert red_reason(PR, PR_LEN, "", 0, "", 0, M_GREEN) == R_OWN_MERGE; + assert red_reason(PR, PR_LEN, "\n\n", 2, "\n", 1, M_UNSEEN) == R_UNSEEN; +} + +test a_master_state_out_of_range_reads_as_unseen { + var m : u32 = 3; + while (m < 256) { + assert red_reason(PR, PR_LEN, FRONT, FRONT_LEN, MOVED_10786, MOVED_10786_LEN, m as u8) == R_INHERITED_MOVED; + assert red_reason(PR, PR_LEN, FRONT, FRONT_LEN, "", 0, m as u8) == R_UNSEEN; + m += 1; + } +} + +test negative_control_a_path_matches_only_as_a_whole_line { + assert hits("specs/claw/move_gate.t2", 23, PR, PR_LEN) == 0; + assert hits("specs/claw/move_gate.t27x", 25, PR, PR_LEN) == 0; + assert hits("claw/move_gate.t27", 18, PR, PR_LEN) == 0; + assert hits("pecs/claw/move_gate.t27", 23, PR, PR_LEN) == 0; + assert hits("specs/claw/move_gate.t27", 24, PR, PR_LEN) == 1; + assert hits("docs/reports/t27b_expectations.json", 35, PR, PR_LEN) == 1; +} + +test negative_control_an_empty_line_matches_nothing { + // Both lists carry an empty line; it must not count as a shared path. + assert hits("a\n\nb", 4, "x\n\ny", 4) == 0; + assert line_in("x\n\ny", 4, "a\n\nb", 2, 2) == false; + assert hits("\n", 1, "\n", 1) == 0; + assert hits("a", 1, "", 0) == 0; +} + +test the_last_line_without_a_newline_is_read_to_the_end { + assert hits(RATCHET, RATCHET_LEN, MOVED_10786, MOVED_10786_LEN) == 1; + assert hits(RATCHET, RATCHET_LEN - 1, MOVED_10786, MOVED_10786_LEN) == 0; + assert line_in("a\nbc", 4, "bc", 0, 2) == true; + assert line_in("a\nbc", 3, "bc", 0, 2) == false; +} + +test every_named_line_is_counted { + assert hits(DEAD, DEAD_LEN, DEAD, DEAD_LEN) == 3; + assert hits(PR, PR_LEN, PR, PR_LEN) == 5; + assert hits(RATCHET, RATCHET_LEN, RATCHET, RATCHET_LEN) == 2; +} + +test a_one_byte_list_at_the_end_of_a_full_buffer_is_not_read_past { + var one : [1]u8 = undefined; + one[0] = 'x'; + assert line_in(&one, 1, "x", 0, 1) == true; + assert line_in(&one, 1, "y", 0, 1) == false; + var two : [2]u8 = undefined; + two[0] = 'x'; + two[1] = '\n'; + assert hits(&two, 2, "x", 1) == 1; + assert hits(&two, 2, "xy", 2) == 0; +} + +test only_two_reasons_are_own_and_two_inherited { + var owns : u32 = 0; + var inherits : u32 = 0; + var r : u32 = 0; + while (r < 256) { + const v : u8 = verdict_of(r as u8); + if (v == V_OWN) { owns += 1; assert r < 2; } + if (v == V_INHERITED) { inherits += 1; assert r == 2 or r == 3; } + r += 1; + } + assert owns == 2; + assert inherits == 2; + assert verdict_of(R_UNSEEN) == V_UNKNOWN; +} + +test the_lens_are_the_text_lengths { + var i : usize = 0; + while (i < 3) { + assert VERDICT_TEXTS[i][0] == 'O'; + assert VERDICT_TEXTS[i][1] == '\t'; + assert VERDICT_TEXTS[i][VERDICT_LENS[i] - 1] == '\t'; + assert VERDICT_TEXTS[i][VERDICT_LENS[i] - 2] != '\t'; + i += 1; + } + var k : usize = 0; + while (k < R_COUNT) { + assert WHY_TEXTS[k][WHY_LENS[k] - 1] == '\n'; + assert WHY_TEXTS[k][WHY_LENS[k] - 2] != '\n'; + k += 1; + } +} + +test each_red_line_is_spelled_out { + var out : [160]u8 = undefined; + assert red_line(R_OWN_PATH, &out, 160, 0) == 65; + assert str_eq(&out, 6, "O\town\t", 6); + assert out[6] == 't'; + assert red_line(R_OWN_MERGE, &out, 160, 0) == 84; + assert out[6] == 'm'; + assert red_line(R_INHERITED_RED, &out, 160, 0) == 88; + assert str_eq(&out, 12, "O\tinherited\t", 12); + assert out[12] == 'm'; + assert red_line(R_INHERITED_MOVED, &out, 160, 0) == 133; + assert out[12] == 'a'; + assert out[132] == '\n'; + assert red_line(R_UNSEEN, &out, 160, 0) == 110; + assert str_eq(&out, 10, "O\tunknown\t", 10); +} + +test out_of_range_reasons_write_the_unseen_line { + var out : [160]u8 = undefined; + var r : u32 = 5; + while (r < 256) { + assert red_line(r as u8, &out, 160, 0) == 110; + assert out[2] == 'u'; + assert out[10] == 'm'; + assert out[109] == '\n'; + r += 1; + } +} + +test a_clipped_line_still_advances_the_cursor { + var out : [8]u8 = undefined; + assert red_line(R_INHERITED_RED, &out, 8, 0) == 88; + assert out[2] == 'i'; + assert red_line(R_INHERITED_RED, &out, 8, 5) == 93; +} From 88a7c9a7fb88eb5ea7792c1133d744bde9201d70 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 11:09:20 +0700 Subject: [PATCH 4/6] feat(claw): mut_known -- argued-equivalent mutants keyed by line text, not line number A survivor argued equivalent in an issue was named by its line number, and the first edit above it moved the number (red_origin: 76/97, then 79/100). mut_known keys a known survivor by its sweep line minus the line number, reads a sweep against the list and returns pass or hold: silent, unreadable, harness (NOAPPLY), new, prune or pass. KNOWN_CLAW carries red_origin's two, lab_disk's one and this module's own five keys. Its own sweep passes on its own list, and the drift case held live: a header line moved all seven survivors down one line and every one still matched. Lab: t27c 24/24 (0 vacuous), t27b 24/24, gen-c rc=0, sweep 104 of 111 caught (7 equivalent, all known), seal --verify 38/38, dupe scan clean. Closes #10835 Co-Authored-By: Claude Opus 5.5 --- .trinity/seals/claw_ClawMutKnown.json | 33 ++ docs/claw/README.md | 1 + docs/reports/t27b_expectations.json | 1 + specs/claw/mut_known.t27 | 610 ++++++++++++++++++++++++++ 4 files changed, 645 insertions(+) create mode 100644 .trinity/seals/claw_ClawMutKnown.json create mode 100644 specs/claw/mut_known.t27 diff --git a/.trinity/seals/claw_ClawMutKnown.json b/.trinity/seals/claw_ClawMutKnown.json new file mode 100644 index 0000000000..0ebfb7832a --- /dev/null +++ b/.trinity/seals/claw_ClawMutKnown.json @@ -0,0 +1,33 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/claw/text.t27", + "spec_hash": "sha256:968755ec1917f766eb2cdd0ab4c0c48aef028a0e7d69e191911695e925b286ad" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:e0b8837e4fdd45fec45afa4ec0a207a4b8a1c920d72083c46c596b2b3fdc98ef", + "gen_hash_rust": "sha256:99d80171de39e7a1ced7ef179e48cd97891284f4b43bf7cf0a0450f43b7c614b", + "gen_hash_verilog": "sha256:5deabda0e3d30dbc1ca2c3c548eba296e223619eb8a9fac4176102f0eac98148", + "gen_hash_zig": "sha256:518a2f4536c85265ce5e33be2c14e9cfe1f0cecab5a62b9c565693a9da8bc669", + "module": "ClawMutKnown", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-19T04:08:18Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:96b1ecb2587c641d2ff98f2c56c2046c643e3db42fe8fca252b106c64dac205c", + "spec_path": "specs/claw/mut_known.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 24, + "total": 24, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/claw/README.md b/docs/claw/README.md index 3991e9b032..d977a2a48b 100644 --- a/docs/claw/README.md +++ b/docs/claw/README.md @@ -27,6 +27,7 @@ behind one front door, with the gate logic in `.t27` specs. Epic | Lab receipt: whether a hand-run lab check of a patch really ran on the patch. A `git apply` that changed fewer files than the diff, a blank or BLOCKED test report, zero tests, a failure or a vacuous pass all hold | `specs/claw/lab_receipt.t27` (expected and applied file counts and the t27c `test-report` text in; one `L pass/hold` line with the reason out; not wired to a job yet) | #10810 | | Lab disk: whether a lab volume has room for a check, read from `df -Pk` before it runs. Under 1 GiB free holds; under 5 GiB or under 10% free warns; a mount df does not list, or a line with no readable size, holds | `specs/claw/lab_disk.t27` (`df -Pk` text and a mount in; one `D go/warn/hold` line with the reason out; not wired to a job yet) | #10813 | | Red origin: whose red a failing check is. A file the log names that the PR changes, or a green master, makes it the PR's; a red master, or a named file master changed since the PR's base, makes it inherited, and the PR gets no push. Ask required_red first | `specs/claw/red_origin.t27` (the PR's paths, the log's named paths, master's moved paths and master's state in; one `O own/inherited/unknown` line with the reason out; not wired to a job yet) | #10821 | +| Known survivors: whether a mutation sweep holds anything new. A survivor is keyed by its spec, operator and line text, not its line number, so an argued equivalent stays known when lines above it move; a new survivor, a NOAPPLY line or an unreadable line holds; a known entry no line matches asks to be pruned | `specs/claw/mut_known.t27` (the sweep text and the known list in; one `E pass/hold` line with the reason, and one `E new/harness/unreadable` line per holding sweep line, out; applied by hand, not wired to the sweep script yet) | #10835 | | Model routing: Opus 5.5 orchestrates, Sonnet 5.5 runs sub-tasks | `specs/claw/model_route.t27`, skill `model-routing`, `.claude/settings.json` + `.claude/agents/*` | owner directive 2026-10-18 | | A T27F visual in every answer that needs one | rule `specs/t27mcp/visual_output.t27`, particle-still layer `specs/claw/visual_output.t27`, procedure in skill `t27f`, "Visual in every output" | owner directive 2026-10-18 | | `reel_still`: one particle still (PNG at STILL_MS of a token_floor reel) from TRI CLAW's `/mcp` | `specs/claw/reel_still.t27`, rendered on the Railway lab only | follow-up of the row above | diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 70831c8704..8dd6bd2826 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -203,6 +203,7 @@ {"path": "specs/claw/merge_lane.t27", "t27b": "pass"}, {"path": "specs/claw/model_route.t27", "t27b": "pass"}, {"path": "specs/claw/move_gate.t27", "t27b": "pass"}, + {"path": "specs/claw/mut_known.t27", "t27b": "pass"}, {"path": "specs/claw/preview.t27", "t27b": "pass"}, {"path": "specs/claw/push_gate.t27", "t27b": "pass"}, {"path": "specs/claw/queue_cost.t27", "t27b": "pass"}, diff --git a/specs/claw/mut_known.t27 b/specs/claw/mut_known.t27 new file mode 100644 index 0000000000..e31081d534 --- /dev/null +++ b/specs/claw/mut_known.t27 @@ -0,0 +1,610 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/claw/mut_known.t27 -- which mutation survivors are known equivalent mutants, and which are +; new, keyed by the mutated line's text rather than its line number. +; -- +; On 2026-10-19 the claw lane's red_origin sweep reported its two surviving mutants at lines 76 and +; 97; a header paragraph was added, and the next run reported the same two mutants at lines 79 and +; 100. Both were argued equivalent in issue #10821, by line number. lab_disk's equivalent survivor +; was argued in #10813, at line 93, with the same text as one of red_origin's. Every later sweep was +; then read by eye against issue text: is this survivor the one we already argued, or a new one? +; Five real survivors (tick 37: reel_still, visual_output, t27f_gate_pins) stood in exactly that +; place before tests killed them. A list keyed by line numbers goes stale on the first edit above the +; line, and a reader who trusts a stale list waves a new survivor through. This module is the list, +; and the reading of a sweep against it. +; -- +; A sweep line is what the lab sweep prints for one mutant: +; SP SP [->] SP [SP :: SP ] +; is the word just before the first " :: ", or the last word when there is none: +; CAUGHT a test failed on the mutant caught +; TIMEOUT the tests ran past the time limit caught, as Stryker and PIT count it +; BLOCKED the mutant did not compile or no test ran not viable, left out of the score +; SURVIVED every test passed on the mutant known or new, by the list +; NOAPPLY the edit did not change the line the harness failed; nothing ran +; any other word, or no SP SP head unreadable +; A known entry is a sweep line with its SP field removed: +; red_origin [ < -> <= ] SURVIVED :: while (i < list_len) { +; It matches a sweep line as a whole line, byte for byte, so a mutant stays known when lines above it +; move, and becomes new when its own line's text, its operator or its spec changes. The result word is +; part of the key, so a known entry never matches a CAUGHT line. A known entry no sweep line matches +; is gone: the line changed or a test now kills the mutant, and the entry should be pruned. +; sweep_reason(counts), first match wins: +; no mutant line at all -> hold R_SILENT +; an unreadable line -> hold R_UNREADABLE +; a NOAPPLY line -> hold R_HARNESS +; a survivor not on the list -> hold R_NEW +; a known entry is gone -> pass R_PRUNE +; otherwise -> pass R_PASS +; sweep_line writes, from the counts, E TAB TAB \n and new_lines writes one +; E TAB TAB \n per line that holds the sweep. +; KNOWN_CLAW holds the claw lane's argued equivalents (#10813, #10821), and this module's own five keys +; (seven survivors: one loop-bound key covers three loops). The eleven ledger_union and four +; social_visuals equivalents of #10787 and #10773 join it when those specs are next swept, because only +; a sweep prints their exact operator text. +; The honest limits: two mutants with the same spec, operator and line text share one key, so one +; entry covers both; a known entry is the reviewer's argument, and this module trusts it. It reads the +; text a sweep printed; it runs no sweep. +; ASCII only (L3). phi^2 + 1/phi^2 = 3 | TRINITY + +module ClawMutKnown; + +use claw::text; + +/// The class of one sweep line; index N_GONE of the counts holds the gone known entries. +pub const C_CAUGHT : u8 = 0; +pub const C_TIMEOUT : u8 = 1; +pub const C_BLOCKED : u8 = 2; +pub const C_KNOWN : u8 = 3; +pub const C_NEW : u8 = 4; +pub const C_NOAPPLY : u8 = 5; +pub const C_BAD : u8 = 6; +pub const N_GONE : usize = 7; +pub const N_COUNTS : usize = 8; + +/// Verdicts. +pub const V_PASS : u8 = 0; +pub const V_HOLD : u8 = 1; + +/// Reasons. +pub const R_SILENT : u8 = 0; +pub const R_UNREADABLE : u8 = 1; +pub const R_HARNESS : u8 = 2; +pub const R_NEW : u8 = 3; +pub const R_PRUNE : u8 = 4; +pub const R_PASS : u8 = 5; +pub const R_COUNT : usize = 6; + +pub const VERDICT_TEXTS : [2]str = ["E\tpass\t", "E\thold\t"]; +pub const VERDICT_LENS : [2]usize = [7, 7]; + +pub const WHY_TEXTS : [6]str = [ + "the sweep printed no mutant line: run it again before you read a score\n", + "a sweep line is not [->] : fix the sweep before you trust its score\n", + "a mutant edit left its line unchanged, so that mutant was never tested: fix the generator\n", + "a survivor is not on the known list: kill it with a test, or argue it equivalent in the issue and list it\n", + "every survivor is known, and some known entries no longer survive: prune them from the list\n", + "every survivor is a known equivalent mutant\n" +]; +pub const WHY_LENS : [6]usize = [71, 100, 90, 106, 92, 44]; + +/// The claw lane's argued equivalent mutants: lab_disk (#10813) and red_origin (#10821). +pub const KNOWN_CLAW : str = "red_origin [ < -> <= ] SURVIVED :: while (i < list_len) {\nred_origin [ < -> <= ] SURVIVED :: while (s < b_len) {\nlab_disk [ < -> <= ] SURVIVED :: while (s < b_len) {\nmut_known [ < -> <= ] SURVIVED :: if (i < n1) { x = b[s + i]; } else { x = b[c + i - n1]; }\nmut_known [ < -> <= ] SURVIVED :: while (i < known_len) {\nmut_known [ > -> >= ] SURVIVED :: while (q > from and b[q - 1] != ' ') { q -= 1; }\nmut_known [ < -> <= ] SURVIVED :: while (s < b_len) {\nmut_known [ < -> <= ] SURVIVED :: while (ks < known_len) {\n"; +pub const KNOWN_CLAW_LEN : usize = 512; + +/// Where the line number of sweep line b[s..e) is: f[0] is the space before it, f[1] the space +/// after. False when the line has no non-empty spec name, no digits, or no space after them. +pub fn num_span(b : str, s : usize, e : usize, f : []usize) -> bool { + var i : usize = s; + while (i < e and b[i] != ' ') { i += 1; } + if (i == s or i == e) { return false; } + const d : usize = i + 1; + var j : usize = d; + while (j < e and b[j] >= '0' and b[j] <= '9') { j += 1; } + if (j == d or j == e or b[j] != ' ') { return false; } + f[0] = i; + f[1] = j; + return true; +} + +/// Whether known line k[ks..ke) is b[s..a) followed by b[c..e): the sweep line without its number. +pub fn key_eq(k : str, ks : usize, ke : usize, b : str, s : usize, a : usize, c : usize, e : usize) -> bool { + const n1 : usize = a - s; + if (ke - ks != n1 + (e - c)) { return false; } + var i : usize = 0; + while (i < ke - ks) { + var x : u8 = 0; + if (i < n1) { x = b[s + i]; } else { x = b[c + i - n1]; } + if (k[ks + i] != x) { return false; } + i += 1; + } + return true; +} + +/// Whether some line of known[0..known_len) is the sweep line b[s..e) without its number field +/// b[a..c). An empty known line matches nothing, because a key always starts with a spec name. +pub fn key_in(known : str, known_len : usize, b : str, s : usize, a : usize, c : usize, e : usize) -> bool { + var i : usize = 0; + while (i < known_len) { + var j : usize = i; + while (j < known_len and known[j] != '\n') { j += 1; } + if (key_eq(known, i, j, b, s, a, c, e)) { return true; } + i = j + 1; + } + return false; +} + +/// Whether b[s..e) is the n bytes of lit. +pub fn span_is(b : str, s : usize, e : usize, lit : str, n : usize) -> bool { + if (e - s != n) { return false; } + var i : usize = 0; + while (i < n) { + if (b[s + i] != lit[i]) { return false; } + i += 1; + } + return true; +} + +/// The class the result word of b[from..e) names: the word before the first " :: ", or the last +/// word. SURVIVED comes back as C_NEW; the caller looks it up on the list. +pub fn result_class(b : str, from : usize, e : usize) -> u8 { + var p : usize = from; + var end : usize = e; + while (p + 4 <= e and end == e) { + if (b[p] == ' ' and b[p + 1] == ':' and b[p + 2] == ':' and b[p + 3] == ' ') { end = p; } + p += 1; + } + var q : usize = end; + while (q > from and b[q - 1] != ' ') { q -= 1; } + if (span_is(b, q, end, "CAUGHT", 6)) { return C_CAUGHT; } + if (span_is(b, q, end, "TIMEOUT", 7)) { return C_TIMEOUT; } + if (span_is(b, q, end, "BLOCKED", 7)) { return C_BLOCKED; } + if (span_is(b, q, end, "SURVIVED", 8)) { return C_NEW; } + if (span_is(b, q, end, "NOAPPLY", 7)) { return C_NOAPPLY; } + return C_BAD; +} + +/// The class of sweep line b[s..e) against the known list. +pub fn line_class(b : str, s : usize, e : usize, known : str, known_len : usize) -> u8 { + var f : [2]usize = undefined; + if (num_span(b, s, e, &f) == false) { return C_BAD; } + const c : u8 = result_class(b, f[1], e); + if (c == C_NEW and key_in(known, known_len, b, s, f[0], f[1], e)) { return C_KNOWN; } + return c; +} + +/// Whether some sweep line of b[0..b_len) is known line k[ks..ke) once its number is removed. +pub fn seen(k : str, ks : usize, ke : usize, b : str, b_len : usize) -> bool { + var s : usize = 0; + while (s < b_len) { + var e : usize = s; + while (e < b_len and b[e] != '\n') { e += 1; } + var f : [2]usize = undefined; + if (num_span(b, s, e, &f) and key_eq(k, ks, ke, b, s, f[0], f[1], e)) { return true; } + s = e + 1; + } + return false; +} + +/// Counts of every class over the sweep's non-empty lines, and in counts[N_GONE] the non-empty +/// known lines no sweep line matches. Returns the number of non-empty sweep lines. +pub fn sweep_counts(b : str, b_len : usize, known : str, known_len : usize, counts : []u32) -> u32 { + var i : usize = 0; + var lines : u32 = 0; + while (i < N_COUNTS) { + counts[i] = 0; + i += 1; + } + var s : usize = 0; + while (s < b_len) { + var e : usize = s; + while (e < b_len and b[e] != '\n') { e += 1; } + if (e > s) { + counts[line_class(b, s, e, known, known_len) as usize] += 1; + lines += 1; + } + s = e + 1; + } + var ks : usize = 0; + while (ks < known_len) { + var ke : usize = ks; + while (ke < known_len and known[ke] != '\n') { ke += 1; } + if (ke > ks and seen(known, ks, ke, b, b_len) == false) { counts[N_GONE] += 1; } + ks = ke + 1; + } + return lines; +} + +/// The reason for a sweep, from its counts. +pub fn sweep_reason(counts : []u32) -> u8 { + var lines : u32 = 0; + var i : usize = 0; + while (i < N_GONE) { + lines += counts[i]; + i += 1; + } + if (lines == 0) { return R_SILENT; } + if (counts[C_BAD as usize] > 0) { return R_UNREADABLE; } + if (counts[C_NOAPPLY as usize] > 0) { return R_HARNESS; } + if (counts[C_NEW as usize] > 0) { return R_NEW; } + if (counts[N_GONE] > 0) { return R_PRUNE; } + return R_PASS; +} + +/// R_PRUNE and R_PASS pass; every other reason, and any value out of range, holds. +pub fn verdict_of(r : u8) -> u8 { + if (r == R_PRUNE or r == R_PASS) { return V_PASS; } + return V_HOLD; +} + +/// The verdict line for a sweep's counts into out at w, clipped at cap; returns the new cursor, which +/// still advances when clipped. It takes the counts, not a reason, so the line cannot name a reason +/// the sweep did not give. +pub fn sweep_line(counts : []u32, out : []u8, cap : usize, w : usize) -> usize { + const k : u8 = sweep_reason(counts); + const v : usize = verdict_of(k) as usize; + const x : usize = put_str(out, cap, w, VERDICT_TEXTS[v], VERDICT_LENS[v]); + return put_str(out, cap, x, WHY_TEXTS[k as usize], WHY_LENS[k as usize]); +} + +/// One E line per sweep line that holds the sweep (new, harness, unreadable), into out at w, +/// clipped at cap; returns the new cursor. +pub fn new_lines(b : str, b_len : usize, known : str, known_len : usize, out : []u8, cap : usize, w : usize) -> usize { + var x : usize = w; + var s : usize = 0; + while (s < b_len) { + var e : usize = s; + while (e < b_len and b[e] != '\n') { e += 1; } + if (e > s) { + const c : u8 = line_class(b, s, e, known, known_len); + var put : bool = true; + if (c == C_NEW) { + x = put_str(out, cap, x, "E\tnew\t", 6); + } else if (c == C_NOAPPLY) { + x = put_str(out, cap, x, "E\tharness\t", 10); + } else if (c == C_BAD) { + x = put_str(out, cap, x, "E\tunreadable\t", 13); + } else { + put = false; + } + if (put) { + var i : usize = s; + while (i < e) { + if (x < cap) { out[x] = b[i]; } + x += 1; + i += 1; + } + if (x < cap) { out[x] = '\n'; } + x += 1; + } + } + s = e + 1; + } + return x; +} + +// ---- tests: the claw lane's real sweeps ---- + +/// The red_origin sweep of tick 41, after the header grew: one caught, two equivalent survivors. +pub const SWEEP41 : str = "red_origin 76 [<=-><] CAUGHT :: if (e <= s) { return false; }\nred_origin 79 [ < -> <= ] SURVIVED :: while (i < list_len) {\nred_origin 100 [ < -> <= ] SURVIVED :: while (s < b_len) {\n"; +pub const SWEEP41_LEN : usize = 182; + +/// The same two survivors in the run before the header grew, three lines higher. +pub const SWEEP41_EARLY : str = "red_origin 76 [ < -> <= ] SURVIVED :: while (i < list_len) {\nred_origin 97 [ < -> <= ] SURVIVED :: while (s < b_len) {\n"; +pub const SWEEP41_EARLY_LEN : usize = 119; + +/// The five real survivors of tick 37, each later killed by a test. +pub const SWEEP37 : str = "reel_still 101 [+ 1->+ 2] SURVIVED :: k = k + 1;\nreel_still 110 [ < -> <= ] SURVIVED :: while (t < (TOPICS as u32)) {\nt27f_gate_pins 21 [return false->return true] SURVIVED :: if (b[a.len] != '$') { return false; }\nt27f_gate_pins 22 [in 0..->in 1..] SURVIVED :: for i in 0..a.len {\nvisual_output 76 [ < -> <= ] SURVIVED :: return ms > REEL_HOOK_MS and ms < REEL_MIN_MS - REEL_CTA_MIN_MS;\n"; +pub const SWEEP37_LEN : usize = 388; + +/// lab_disk's equivalent survivor (#10813): the same text as one of red_origin's. +pub const DISK93 : str = "lab_disk 93 [ < -> <= ] SURVIVED :: while (s < b_len) {\n"; +pub const DISK93_LEN : usize = 56; + +/// This module's own sweep (tick 42): its seven survivors under five keys. +pub const SWEEP42 : str = "mut_known 114 [ < -> <= ] SURVIVED :: if (i < n1) { x = b[s + i]; } else { x = b[c + i - n1]; }\nmut_known 125 [ < -> <= ] SURVIVED :: while (i < known_len) {\nmut_known 155 [ > -> >= ] SURVIVED :: while (q > from and b[q - 1] != ' ') { q -= 1; }\nmut_known 176 [ < -> <= ] SURVIVED :: while (s < b_len) {\nmut_known 196 [ < -> <= ] SURVIVED :: while (s < b_len) {\nmut_known 206 [ < -> <= ] SURVIVED :: while (ks < known_len) {\nmut_known 252 [ < -> <= ] SURVIVED :: while (s < b_len) {\n"; +pub const SWEEP42_LEN : usize = 482; + +test this_modules_own_sweep_passes_on_its_own_list { + var c : [8]u32 = undefined; + assert sweep_counts(SWEEP42, SWEEP42_LEN, KNOWN_CLAW, KNOWN_CLAW_LEN, &c) == 7; + assert c[C_KNOWN as usize] == 7; + assert c[C_NEW as usize] == 0; + assert c[C_BAD as usize] == 0; + // red_origin's two and lab_disk's one are not in this sweep. + assert c[N_GONE] == 3; + assert sweep_reason(&c) == R_PRUNE; + assert verdict_of(R_PRUNE) == V_PASS; + assert SWEEP42[SWEEP42_LEN - 1] == '\n'; +} + +test the_tick_41_sweep_passes_on_the_known_list { + var c : [8]u32 = undefined; + assert sweep_counts(SWEEP41, SWEEP41_LEN, KNOWN_CLAW, KNOWN_CLAW_LEN, &c) == 3; + assert c[C_CAUGHT as usize] == 1; + assert c[C_KNOWN as usize] == 2; + assert c[C_NEW as usize] == 0; + assert c[N_GONE] == 6; + assert sweep_reason(&c) == R_PRUNE; + assert verdict_of(R_PRUNE) == V_PASS; +} + +test moved_line_numbers_keep_a_survivor_known { + var c : [8]u32 = undefined; + assert sweep_counts(SWEEP41_EARLY, SWEEP41_EARLY_LEN, KNOWN_CLAW, KNOWN_CLAW_LEN, &c) == 2; + assert c[C_KNOWN as usize] == 2; + assert c[C_NEW as usize] == 0; + assert line_class(SWEEP41_EARLY, 0, 60, KNOWN_CLAW, KNOWN_CLAW_LEN) == C_KNOWN; + assert line_class(SWEEP41, 62, 122, KNOWN_CLAW, KNOWN_CLAW_LEN) == C_KNOWN; +} + +test the_tick_37_survivors_are_new_and_hold_the_sweep { + var c : [8]u32 = undefined; + assert sweep_counts(SWEEP37, SWEEP37_LEN, KNOWN_CLAW, KNOWN_CLAW_LEN, &c) == 5; + assert c[C_NEW as usize] == 5; + assert c[C_KNOWN as usize] == 0; + assert c[C_BAD as usize] == 0; + assert c[N_GONE] == 8; + assert sweep_reason(&c) == R_NEW; + assert verdict_of(R_NEW) == V_HOLD; +} + +test a_full_known_list_passes_clean { + var c : [8]u32 = undefined; + assert sweep_counts(DISK93, DISK93_LEN, "lab_disk [ < -> <= ] SURVIVED :: while (s < b_len) {", 52, &c) == 1; + assert c[C_KNOWN as usize] == 1; + assert c[N_GONE] == 0; + assert sweep_reason(&c) == R_PASS; +} + +test negative_control_the_same_text_in_another_spec_is_new { + // red_origin's entry has lab_disk's text; the spec name is part of the key. + const known : str = "red_origin [ < -> <= ] SURVIVED :: while (s < b_len) {\n"; + assert line_class(DISK93, 0, 55, known, 55) == C_NEW; + assert line_class(DISK93, 0, 55, KNOWN_CLAW, KNOWN_CLAW_LEN) == C_KNOWN; +} + +test negative_control_a_changed_operator_or_text_is_new { + assert line_class("lab_disk 93 [ < -> > ] SURVIVED :: while (s < b_len) {", 0, 54, KNOWN_CLAW, KNOWN_CLAW_LEN) == C_NEW; + assert line_class("lab_disk 93 [ < -> <= ] SURVIVED :: while (s < n) {", 0, 51, KNOWN_CLAW, KNOWN_CLAW_LEN) == C_NEW; + assert line_class("lab_disk 93 [ < -> <= ] SURVIVED :: while (s < b_len) { ", 0, 56, KNOWN_CLAW, KNOWN_CLAW_LEN) == C_NEW; + assert line_class("lab_disk 93 [ < -> <= ] SURVIVED :: while (s < b_len) ", 0, 54, KNOWN_CLAW, KNOWN_CLAW_LEN) == C_NEW; +} + +test negative_control_a_known_entry_never_matches_a_caught_line { + const line : str = "lab_disk 93 [ < -> <= ] CAUGHT :: while (s < b_len) {"; + assert line_class(line, 0, 53, KNOWN_CLAW, KNOWN_CLAW_LEN) == C_CAUGHT; + var c : [8]u32 = undefined; + assert sweep_counts(line, 53, "lab_disk [ < -> <= ] SURVIVED :: while (s < b_len) {", 52, &c) == 1; + assert c[C_CAUGHT as usize] == 1; + assert c[N_GONE] == 1; +} + +test every_result_word_has_its_class { + assert line_class("x 1 [a->b] CAUGHT :: t", 0, 22, "", 0) == C_CAUGHT; + assert line_class("x 1 [a->b] TIMEOUT :: t", 0, 23, "", 0) == C_TIMEOUT; + assert line_class("x 1 [a->b] BLOCKED :: t", 0, 23, "", 0) == C_BLOCKED; + assert line_class("x 1 [a->b] SURVIVED :: t", 0, 24, "", 0) == C_NEW; + assert line_class("x 1 [a->b] NOAPPLY", 0, 18, "", 0) == C_NOAPPLY; + assert line_class("x 1 [a->b] KILLED :: t", 0, 22, "", 0) == C_BAD; + assert line_class("x 1 [a->b] caught :: t", 0, 22, "", 0) == C_BAD; + assert line_class("x 1 [a->b] CAUGHTX :: t", 0, 23, "", 0) == C_BAD; +} + +test the_result_word_is_the_one_before_the_first_separator { + // The source text may hold " :: " and result words of its own. + assert line_class("x 1 [a->b] CAUGHT :: a :: SURVIVED :: b", 0, 39, "", 0) == C_CAUGHT; + assert line_class("x 1 [a->b] SURVIVED :: CAUGHT", 0, 29, "", 0) == C_NEW; + // With no separator the last word is read. + assert line_class("x 1 [a->b] CAUGHT", 0, 17, "", 0) == C_CAUGHT; + assert line_class("x 1 [a->b] CAUGHT ::", 0, 20, "", 0) == C_BAD; +} + +test lines_without_the_head_are_unreadable { + assert line_class("red_origin x [a->b] SURVIVED :: t", 0, 33, KNOWN_CLAW, KNOWN_CLAW_LEN) == C_BAD; + assert line_class(" 12 [a->b] SURVIVED :: t", 0, 24, "", 0) == C_BAD; + assert line_class("red_origin 12[a->b] SURVIVED :: t", 0, 33, "", 0) == C_BAD; + assert line_class("red_origin", 0, 10, "", 0) == C_BAD; + assert line_class("red_origin 12", 0, 13, "", 0) == C_BAD; + assert line_class("red_origin 12 ", 0, 14, "", 0) == C_BAD; + assert line_class("red_origin 12 [a->b] SURVIVED :: t", 0, 35, "", 0) == C_BAD; + var f : [2]usize = undefined; + assert num_span("ab 123 c", 0, 8, &f) == true; + assert f[0] == 2; + assert f[1] == 6; +} + +test the_hold_reasons_come_in_order { + var c : [8]u32 = undefined; + assert sweep_counts("x 1 [a->b] SURVIVED :: t\nx 2 [a->b] NOAPPLY\nx 3 bad\n", 52, "", 0, &c) == 3; + assert c[C_NEW as usize] == 1; + assert c[C_NOAPPLY as usize] == 1; + assert c[C_BAD as usize] == 1; + assert sweep_reason(&c) == R_UNREADABLE; + assert sweep_counts("x 1 [a->b] SURVIVED :: t\nx 2 [a->b] NOAPPLY\n", 44, "", 0, &c) == 2; + assert sweep_reason(&c) == R_HARNESS; + assert sweep_counts("x 1 [a->b] SURVIVED :: t\n", 25, "y [a->b] SURVIVED :: t", 22, &c) == 1; + assert sweep_reason(&c) == R_NEW; + assert sweep_counts("x 1 [a->b] TIMEOUT :: t\nx 2 [a->b] BLOCKED :: t\n", 48, "", 0, &c) == 2; + assert c[C_TIMEOUT as usize] == 1; + assert c[C_BLOCKED as usize] == 1; + assert sweep_reason(&c) == R_PASS; +} + +test an_empty_sweep_holds_as_silent { + var c : [8]u32 = undefined; + assert sweep_counts("", 0, KNOWN_CLAW, KNOWN_CLAW_LEN, &c) == 0; + assert c[N_GONE] == 8; + assert sweep_reason(&c) == R_SILENT; + assert sweep_counts("\n\n\n", 3, "", 0, &c) == 0; + assert sweep_reason(&c) == R_SILENT; + assert verdict_of(R_SILENT) == V_HOLD; +} + +test empty_known_lines_are_never_gone_and_match_nothing { + var c : [8]u32 = undefined; + assert sweep_counts(DISK93, DISK93_LEN, "\n\n", 2, &c) == 1; + assert c[C_NEW as usize] == 1; + assert c[N_GONE] == 0; + assert key_in("\n", 1, DISK93, 0, 8, 11, 55) == false; +} + +test only_prune_and_pass_pass { + var passes : u32 = 0; + var r : u32 = 0; + while (r < 256) { + if (verdict_of(r as u8) == V_PASS) { passes += 1; assert r == 4 or r == 5; } + r += 1; + } + assert passes == 2; +} + +test the_lens_are_the_text_lengths { + var i : usize = 0; + while (i < 2) { + assert VERDICT_TEXTS[i][0] == 'E'; + assert VERDICT_TEXTS[i][VERDICT_LENS[i] - 1] == '\t'; + assert VERDICT_TEXTS[i][VERDICT_LENS[i] - 2] != '\t'; + i += 1; + } + var k : usize = 0; + while (k < R_COUNT) { + assert WHY_TEXTS[k][WHY_LENS[k] - 1] == '\n'; + assert WHY_TEXTS[k][WHY_LENS[k] - 2] != '\n'; + k += 1; + } + assert KNOWN_CLAW[KNOWN_CLAW_LEN - 1] == '\n'; + assert SWEEP37[SWEEP37_LEN - 1] == '\n'; + assert SWEEP41[SWEEP41_LEN - 2] == '{'; +} + +test each_sweep_line_is_spelled_out { + var out : [128]u8 = undefined; + var c : [8]u32 = undefined; + assert sweep_counts("x 1 [a->b] CAUGHT :: t\n", 23, "", 0, &c) == 1; + assert sweep_line(&c, &out, 128, 0) == 51; + assert str_eq(&out, 7, "E\tpass\t", 7); + assert out[7] == 'e'; + assert sweep_counts("x 1 [a->b] CAUGHT :: t\n", 23, "y [a->b] SURVIVED :: t", 22, &c) == 1; + assert sweep_line(&c, &out, 128, 0) == 99; + assert out[7] == 'e'; + assert out[98] == '\n'; + assert sweep_counts("x 1 [a->b] SURVIVED :: t\n", 25, "", 0, &c) == 1; + assert sweep_line(&c, &out, 128, 0) == 113; + assert str_eq(&out, 7, "E\thold\t", 7); + assert out[9] == 's'; + assert sweep_counts("x 2 [a->b] NOAPPLY\n", 19, "", 0, &c) == 1; + assert sweep_line(&c, &out, 128, 0) == 97; + assert out[9] == 'm'; + assert sweep_counts("", 0, "", 0, &c) == 0; + assert sweep_line(&c, &out, 128, 0) == 78; + assert out[7] == 't'; + assert sweep_counts("x y\n", 4, "", 0, &c) == 1; + assert sweep_line(&c, &out, 128, 0) == 107; + assert out[9] == 's'; + assert out[106] == '\n'; +} + +test the_lines_that_hold_a_sweep_are_listed { + var out : [128]u8 = undefined; + const sweep : str = "x 1 [a->b] CAUGHT :: t\nx 2 [a->b] SURVIVED :: u\nx 3 [a->b] NOAPPLY\nx y\n"; + assert new_lines(sweep, 71, "x [a->b] SURVIVED :: u", 22, &out, 128, 0) == 46; + assert str_eq(&out, 10, "E\tharness\t", 10); + assert out[10] == 'x'; + assert out[12] == '3'; + assert out[28] == '\n'; + assert out[29] == 'E'; + assert out[31] == 'u'; + assert out[42] == 'x'; + assert out[45] == '\n'; + assert new_lines(sweep, 71, "", 0, &out, 128, 0) == 77; + assert str_eq(&out, 6, "E\tnew\t", 6); + assert out[6] == 'x'; + assert out[8] == '2'; + assert out[30] == '\n'; + assert out[31] == 'E'; + assert new_lines(sweep, 71, "", 0, &out, 4, 3) == 80; + assert out[3] == 'E'; + assert new_lines(sweep, 71, "", 0, &out, 128, 5) == 82; +} + +test a_line_at_the_end_of_a_full_buffer_is_not_read_past { + var one : [24]u8 = undefined; + const src : str = "x 1 [a->b] SURVIVED :: t"; + var i : usize = 0; + while (i < 24) { + one[i] = src[i]; + i += 1; + } + assert line_class(&one, 0, 24, "", 0) == C_NEW; + assert line_class(&one, 0, 24, "x [a->b] SURVIVED :: t", 22) == C_KNOWN; + var c : [8]u32 = undefined; + assert sweep_counts(&one, 24, "x [a->b] SURVIVED :: t", 22, &c) == 1; + assert c[C_KNOWN as usize] == 1; + assert seen("x [a->b] SURVIVED :: t", 0, 22, &one, 24) == true; + var two : [4]u8 = undefined; + two[0] = 'x'; + two[1] = ' '; + two[2] = '1'; + two[3] = ' '; + assert line_class(&two, 0, 4, "", 0) == C_BAD; + var f : [2]usize = undefined; + assert num_span(&two, 0, 3, &f) == false; +} + +test num_span_needs_a_name_digits_and_a_space_after_them { + var f : [2]usize = undefined; + assert num_span("red_origin", 0, 10, &f) == false; + assert num_span(" 12 x", 0, 5, &f) == false; + assert num_span("x 12", 0, 4, &f) == false; + assert num_span("x ", 0, 2, &f) == false; + assert num_span("x 1x ", 0, 5, &f) == false; + assert num_span("x 1 ", 0, 5, &f) == false; + assert num_span("x 1 ", 0, 4, &f) == true; + assert f[0] == 1; + assert f[1] == 3; + assert num_span("ab x 1 ", 3, 7, &f) == true; + assert f[0] == 4; + assert f[1] == 6; +} + +test the_separator_is_exactly_space_colon_colon_space { + // A sweep of a blank source line ends in " :: " with no text after it. + assert line_class("x 1 [a->b] SURVIVED :: ", 0, 23, "", 0) == C_NEW; + assert line_class("x 1 [a->b] CAUGHT :: ", 0, 21, "", 0) == C_CAUGHT; + // " a: " and " :a " are not separators, so the last word is read. + assert line_class("x 1 [a->b] SURVIVED a: CAUGHT", 0, 29, "", 0) == C_CAUGHT; + assert line_class("x 1 [a->b] SURVIVED :a CAUGHT", 0, 29, "", 0) == C_CAUGHT; + assert line_class("x 1 [a->b] SURVIVED ::: CAUGHT", 0, 30, "", 0) == C_CAUGHT; +} + +test empty_sweep_lines_list_nothing { + var out : [8]u8 = undefined; + assert new_lines("\n\n", 2, "", 0, &out, 8, 0) == 0; + assert new_lines("", 0, "", 0, &out, 8, 3) == 3; +} + +test new_lines_never_write_past_a_full_out_buffer { + // The line's first byte lands exactly at cap. + var six : [6]u8 = undefined; + assert new_lines("x 2 [a->b] SURVIVED :: u", 24, "", 0, &six, 6, 0) == 31; + assert six[0] == 'E'; + assert six[5] == '\t'; + // The closing newline lands exactly at cap. + var thirty : [30]u8 = undefined; + assert new_lines("x 2 [a->b] SURVIVED :: u", 24, "", 0, &thirty, 30, 0) == 31; + assert thirty[6] == 'x'; + assert thirty[29] == 'u'; +} + +test new_lines_read_a_last_line_without_a_newline_to_the_end { + var src : [18]u8 = undefined; + const t : str = "x 3 [a->b] NOAPPLY"; + var i : usize = 0; + while (i < 18) { + src[i] = t[i]; + i += 1; + } + var out : [32]u8 = undefined; + assert new_lines(&src, 18, "", 0, &out, 32, 0) == 29; + assert out[27] == 'Y'; + assert out[28] == '\n'; +} From 69845a18861fe39b95d99cd83f9928eb1885db56 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 11:30:18 +0700 Subject: [PATCH 5/6] feat(claw): mut_known -- add ledger_union's and social_visuals' equivalents to the known list KNOWN_CLAW grows from 8 to 22 keys: ten ledger_union keys (eleven survivors, #10787) and four social_visuals keys (#10773), taken from their sweeps on the Railway lab on 2026-10-19. A new test reads the eighteen lines of those two sweeps (fifteen SURVIVED, three TIMEOUT) and passes them with no new survivor; a negative control shows a key cut at 90 characters keeps its trailing space. sweep_line now takes the counts, not a reason, so it is no longer a byte copy of lab_disk's disk_line (the duplicate-body scan caught it). Lab: t27c 26/26 (0 vacuous), t27b 26/26, gen-c rc=0, sweep 104/111 with the seven survivors all on the list, seal --verify 38/38, dupe scan ok. Refs #10835, #10787, #10773. Co-Authored-By: Claude Opus 5.5 --- .trinity/seals/claw_ClawMutKnown.json | 16 +++++----- specs/claw/mut_known.t27 | 44 +++++++++++++++++++++------ 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/.trinity/seals/claw_ClawMutKnown.json b/.trinity/seals/claw_ClawMutKnown.json index 0ebfb7832a..6660f51d9f 100644 --- a/.trinity/seals/claw_ClawMutKnown.json +++ b/.trinity/seals/claw_ClawMutKnown.json @@ -7,23 +7,23 @@ } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:e0b8837e4fdd45fec45afa4ec0a207a4b8a1c920d72083c46c596b2b3fdc98ef", - "gen_hash_rust": "sha256:99d80171de39e7a1ced7ef179e48cd97891284f4b43bf7cf0a0450f43b7c614b", - "gen_hash_verilog": "sha256:5deabda0e3d30dbc1ca2c3c548eba296e223619eb8a9fac4176102f0eac98148", - "gen_hash_zig": "sha256:518a2f4536c85265ce5e33be2c14e9cfe1f0cecab5a62b9c565693a9da8bc669", + "gen_hash_c": "sha256:e9137bde4ffe270f74350532105ddb6b82b9e413358d5cd4a496fb5df01ed93d", + "gen_hash_rust": "sha256:2af969a3d12a4bffc6fda251b8283029219dcbe615e1d827baf6bb5283068c42", + "gen_hash_verilog": "sha256:e12e6e6acce54123a0adfc6fc8a51b861d59b518017b5d08523f142b26134b7a", + "gen_hash_zig": "sha256:2ead0c11923356737367e058e0737daf3747c5766457dc9162b239429a807114", "module": "ClawMutKnown", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-19T04:08:18Z", + "sealed_at": "2026-10-19T04:27:47Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:96b1ecb2587c641d2ff98f2c56c2046c643e3db42fe8fca252b106c64dac205c", + "spec_hash": "sha256:2ebb16fa7e2be093012b84a2e708ab9fa147332843b6f3e0c931bbe4ff2c9a32", "spec_path": "specs/claw/mut_known.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 24, - "total": 24, + "passed": 26, + "total": 26, "vacuous": 0 }, "toolchain": { diff --git a/specs/claw/mut_known.t27 b/specs/claw/mut_known.t27 index e31081d534..b8211e975d 100644 --- a/specs/claw/mut_known.t27 +++ b/specs/claw/mut_known.t27 @@ -37,9 +37,9 @@ ; sweep_line writes, from the counts, E TAB TAB \n and new_lines writes one ; E TAB TAB \n per line that holds the sweep. ; KNOWN_CLAW holds the claw lane's argued equivalents (#10813, #10821), and this module's own five keys -; (seven survivors: one loop-bound key covers three loops). The eleven ledger_union and four -; social_visuals equivalents of #10787 and #10773 join it when those specs are next swept, because only -; a sweep prints their exact operator text. +; (seven survivors: one loop-bound key covers three loops), and the eleven ledger_union and four +; social_visuals equivalents of #10787 and #10773, taken from their sweeps on 2026-10-19 (fourteen keys). +; Two social_visuals survivors are edits inside a // comment: the sweep mutates comment text too. ; The honest limits: two mutants with the same spec, operator and line text share one key, so one ; entry covers both; a known entry is the reviewer's argument, and this module trusts it. It reads the ; text a sweep printed; it runs no sweep. @@ -87,8 +87,8 @@ pub const WHY_TEXTS : [6]str = [ pub const WHY_LENS : [6]usize = [71, 100, 90, 106, 92, 44]; /// The claw lane's argued equivalent mutants: lab_disk (#10813) and red_origin (#10821). -pub const KNOWN_CLAW : str = "red_origin [ < -> <= ] SURVIVED :: while (i < list_len) {\nred_origin [ < -> <= ] SURVIVED :: while (s < b_len) {\nlab_disk [ < -> <= ] SURVIVED :: while (s < b_len) {\nmut_known [ < -> <= ] SURVIVED :: if (i < n1) { x = b[s + i]; } else { x = b[c + i - n1]; }\nmut_known [ < -> <= ] SURVIVED :: while (i < known_len) {\nmut_known [ > -> >= ] SURVIVED :: while (q > from and b[q - 1] != ' ') { q -= 1; }\nmut_known [ < -> <= ] SURVIVED :: while (s < b_len) {\nmut_known [ < -> <= ] SURVIVED :: while (ks < known_len) {\n"; -pub const KNOWN_CLAW_LEN : usize = 512; +pub const KNOWN_CLAW : str = "red_origin [ < -> <= ] SURVIVED :: while (i < list_len) {\nred_origin [ < -> <= ] SURVIVED :: while (s < b_len) {\nlab_disk [ < -> <= ] SURVIVED :: while (s < b_len) {\nmut_known [ < -> <= ] SURVIVED :: if (i < n1) { x = b[s + i]; } else { x = b[c + i - n1]; }\nmut_known [ < -> <= ] SURVIVED :: while (i < known_len) {\nmut_known [ > -> >= ] SURVIVED :: while (q > from and b[q - 1] != ' ') { q -= 1; }\nmut_known [ < -> <= ] SURVIVED :: while (s < b_len) {\nmut_known [ < -> <= ] SURVIVED :: while (ks < known_len) {\nledger_union [ < -> <= ] SURVIVED :: while (s < n and hs == n) {\nledger_union [- 1->- 0] SURVIVED :: if (lu_at(x, hs - 1, hs - 1 - ENTRIES_OPEN_LEN, ENTRIES_OPEN, ENTRIES_OPEN_LEN) == false) \nledger_union [ < -> <= ] SURVIVED :: const hb : bool = pb < fb[1];\nledger_union [ and -> or ] SURVIVED :: if (same_tb and in_b) { same_tb = lu_cmp(t, pt, vte, b, pb, vbe) == 0; }\nledger_union [ and -> or ] SURVIVED :: if (same_ot and in_t) { same_ot = lu_cmp(o, po, voe, t, pt, vte) == 0; }\nledger_union [ < -> <= ] SURVIVED :: while (s < n) {\nledger_union [ < -> <= ] SURVIVED :: while (i + 1 < e) {\nledger_union [+ 1->+ 2] SURVIVED :: while (i + 1 < e) {\nledger_union [+ 1->+ 2] SURVIVED :: if (e > s + 1 and x[s] == g and x[s + 1] == '\\t') {\nledger_union [ < -> <= ] SURVIVED :: while (s < n and code == SC_OK) {\nsocial_visuals [ and -> or ] SURVIVED :: pub const KIND_MOVING : usize = 0; // a reel: video with the voice and the music\nsocial_visuals [>=->>] SURVIVED :: if (a >= ARCHS) {\nsocial_visuals [ > -> >= ] SURVIVED :: return content(t, a) > 0;\nsocial_visuals [ or -> and ] SURVIVED :: pub const G_NOT_MEASURED : usize = 3; // the comparison did not run, or its answer is no\n"; +pub const KNOWN_CLAW_LEN : usize = 1695; /// Where the line number of sweep line b[s..e) is: f[0] is the space before it, f[1] the space /// after. False when the line has no non-empty spec name, no digits, or no space after them. @@ -309,20 +309,44 @@ test this_modules_own_sweep_passes_on_its_own_list { assert c[C_KNOWN as usize] == 7; assert c[C_NEW as usize] == 0; assert c[C_BAD as usize] == 0; - // red_origin's two and lab_disk's one are not in this sweep. - assert c[N_GONE] == 3; + // The other specs' seventeen keys are not in this sweep. + assert c[N_GONE] == 17; assert sweep_reason(&c) == R_PRUNE; assert verdict_of(R_PRUNE) == V_PASS; assert SWEEP42[SWEEP42_LEN - 1] == '\n'; } +/// ledger_union's and social_visuals' sweeps (tick 43, #10787 and #10773): every line that was not +/// CAUGHT, so 15 survivors under 14 keys and 3 timeouts. +pub const SWEEP43 : str = "ledger_union 168 [ < -> <= ] SURVIVED :: while (s < n and hs == n) {\nledger_union 168 [ and -> or ] TIMEOUT :: while (s < n and hs == n) {\nledger_union 174 [- 1->- 0] SURVIVED :: if (lu_at(x, hs - 1, hs - 1 - ENTRIES_OPEN_LEN, ENTRIES_OPEN, ENTRIES_OPEN_LEN) == false) \nledger_union 181 [ and -> or ] TIMEOUT :: while (p < n and fe == n) {\nledger_union 268 [ < -> <= ] SURVIVED :: const hb : bool = pb < fb[1];\nledger_union 328 [ and -> or ] SURVIVED :: if (same_tb and in_b) { same_tb = lu_cmp(t, pt, vte, b, pb, vbe) == 0; }\nledger_union 330 [ and -> or ] SURVIVED :: if (same_ot and in_t) { same_ot = lu_cmp(o, po, voe, t, pt, vte) == 0; }\nledger_union 364 [ < -> <= ] SURVIVED :: while (s < n) {\nledger_union 390 [ < -> <= ] SURVIVED :: while (i + 1 < e) {\nledger_union 390 [+ 1->+ 2] SURVIVED :: while (i + 1 < e) {\nledger_union 408 [ < -> <= ] SURVIVED :: while (s < n) {\nledger_union 410 [+ 1->+ 2] SURVIVED :: if (e > s + 1 and x[s] == g and x[s + 1] == '\\t') {\nledger_union 443 [ < -> <= ] SURVIVED :: while (s < n and code == SC_OK) {\nledger_union 443 [ and -> or ] TIMEOUT :: while (s < n and code == SC_OK) {\nsocial_visuals 72 [ and -> or ] SURVIVED :: pub const KIND_MOVING : usize = 0; // a reel: video with the voice and the music\nsocial_visuals 152 [>=->>] SURVIVED :: if (a >= ARCHS) {\nsocial_visuals 155 [ > -> >= ] SURVIVED :: return content(t, a) > 0;\nsocial_visuals 163 [ or -> and ] SURVIVED :: pub const G_NOT_MEASURED : usize = 3; // the comparison did not run, or its answer is no\n"; +pub const SWEEP43_LEN : usize = 1511; + +test the_ledger_union_and_social_visuals_sweeps_pass_on_the_list { + var c : [8]u32 = undefined; + assert sweep_counts(SWEEP43, SWEEP43_LEN, KNOWN_CLAW, KNOWN_CLAW_LEN, &c) == 18; + assert c[C_KNOWN as usize] == 15; + assert c[C_TIMEOUT as usize] == 3; + assert c[C_NEW as usize] == 0; + assert c[C_BAD as usize] == 0; + // 22 keys, 14 of them matched here. + assert c[N_GONE] == 8; + assert sweep_reason(&c) == R_PRUNE; + assert SWEEP43[SWEEP43_LEN - 1] == '\n'; +} + +test negative_control_a_cut_line_keeps_its_trailing_space { + // The sweep cuts the source text at 90 bytes, and this cut ends in a space; the key holds it. + assert line_class("ledger_union 174 [- 1->- 0] SURVIVED :: if (lu_at(x, hs - 1, hs - 1 - ENTRIES_OPEN_LEN, ENTRIES_OPEN, ENTRIES_OPEN_LEN) == false)", 0, 129, KNOWN_CLAW, KNOWN_CLAW_LEN) == C_NEW; + assert line_class("ledger_union 174 [- 1->- 0] SURVIVED :: if (lu_at(x, hs - 1, hs - 1 - ENTRIES_OPEN_LEN, ENTRIES_OPEN, ENTRIES_OPEN_LEN) == false) ", 0, 130, KNOWN_CLAW, KNOWN_CLAW_LEN) == C_KNOWN; +} + test the_tick_41_sweep_passes_on_the_known_list { var c : [8]u32 = undefined; assert sweep_counts(SWEEP41, SWEEP41_LEN, KNOWN_CLAW, KNOWN_CLAW_LEN, &c) == 3; assert c[C_CAUGHT as usize] == 1; assert c[C_KNOWN as usize] == 2; assert c[C_NEW as usize] == 0; - assert c[N_GONE] == 6; + assert c[N_GONE] == 20; assert sweep_reason(&c) == R_PRUNE; assert verdict_of(R_PRUNE) == V_PASS; } @@ -342,7 +366,7 @@ test the_tick_37_survivors_are_new_and_hold_the_sweep { assert c[C_NEW as usize] == 5; assert c[C_KNOWN as usize] == 0; assert c[C_BAD as usize] == 0; - assert c[N_GONE] == 8; + assert c[N_GONE] == 22; assert sweep_reason(&c) == R_NEW; assert verdict_of(R_NEW) == V_HOLD; } @@ -432,7 +456,7 @@ test the_hold_reasons_come_in_order { test an_empty_sweep_holds_as_silent { var c : [8]u32 = undefined; assert sweep_counts("", 0, KNOWN_CLAW, KNOWN_CLAW_LEN, &c) == 0; - assert c[N_GONE] == 8; + assert c[N_GONE] == 22; assert sweep_reason(&c) == R_SILENT; assert sweep_counts("\n\n\n", 3, "", 0, &c) == 0; assert sweep_reason(&c) == R_SILENT; From b7e42e3b4e2b33b4edd6e1fc5b159e0d0ac384d3 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 12:24:15 +0700 Subject: [PATCH 6/6] feat(claw): mut_known -- a survivor that only edits a // comment needs no known entry The lab sweep mutates trailing // comment text as well as code, and two of social_visuals' four survivors (#10773) changed only a comment. A new class C_COMMENT passes such a survivor without a list entry. A line is a comment edit when its text appears after the first " :: ", every match starts after the //, and no quote byte comes before the //. When in doubt the line stays new. The two social_visuals comment keys leave KNOWN_CLAW; five in_comment loop-guard keys join it, each argued in the doc comment, for 25 keys. Negative controls keep new: code before the //, a quote, a string, a the cut hid, an edit of or across the //, an empty , no separator, no bracket. A CAUGHT line in a comment stays CAUGHT. Lab: t27c 28/28 (0 vacuous), t27b 28/28, gen-c rc=0, sweep 140/152 with all twelve survivors on the list, seal --verify 38/38, dupe scan ok. Refs #10846, #10835, #10773. Co-Authored-By: Claude Opus 5.5 --- .trinity/seals/claw_ClawMutKnown.json | 16 +-- specs/claw/mut_known.t27 | 156 ++++++++++++++++++++------ 2 files changed, 131 insertions(+), 41 deletions(-) diff --git a/.trinity/seals/claw_ClawMutKnown.json b/.trinity/seals/claw_ClawMutKnown.json index 6660f51d9f..5c1557591a 100644 --- a/.trinity/seals/claw_ClawMutKnown.json +++ b/.trinity/seals/claw_ClawMutKnown.json @@ -7,23 +7,23 @@ } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:e9137bde4ffe270f74350532105ddb6b82b9e413358d5cd4a496fb5df01ed93d", - "gen_hash_rust": "sha256:2af969a3d12a4bffc6fda251b8283029219dcbe615e1d827baf6bb5283068c42", - "gen_hash_verilog": "sha256:e12e6e6acce54123a0adfc6fc8a51b861d59b518017b5d08523f142b26134b7a", - "gen_hash_zig": "sha256:2ead0c11923356737367e058e0737daf3747c5766457dc9162b239429a807114", + "gen_hash_c": "sha256:4d8214dd7ec4721c48dfc9fff85291cd3ae826c50f6fb1bc8194610a8ab0bd02", + "gen_hash_rust": "sha256:933124083d9a6e92e554585447af7cccd1b0c375bb48066b9d91cd23060c9c90", + "gen_hash_verilog": "sha256:2c5ea6b85656230964260b2d750a28fd4db1c878b1450b2e4b06982e1f2ba0b9", + "gen_hash_zig": "sha256:d62cd56e4f7d36a85737fafa40cf973e97fc541deb44459a5e2a5f4d2116cb8f", "module": "ClawMutKnown", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-19T04:27:47Z", + "sealed_at": "2026-10-19T05:23:05Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:2ebb16fa7e2be093012b84a2e708ab9fa147332843b6f3e0c931bbe4ff2c9a32", + "spec_hash": "sha256:6a2f2e78d54f8492572f9b962da69fbd7099ebeba42f96b6ef6aaeab803c889b", "spec_path": "specs/claw/mut_known.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 26, - "total": 26, + "passed": 28, + "total": 28, "vacuous": 0 }, "toolchain": { diff --git a/specs/claw/mut_known.t27 b/specs/claw/mut_known.t27 index b8211e975d..98772f5802 100644 --- a/specs/claw/mut_known.t27 +++ b/specs/claw/mut_known.t27 @@ -31,15 +31,17 @@ ; no mutant line at all -> hold R_SILENT ; an unreadable line -> hold R_UNREADABLE ; a NOAPPLY line -> hold R_HARNESS -; a survivor not on the list -> hold R_NEW +; a survivor not listed, not a comment -> hold R_NEW ; a known entry is gone -> pass R_PRUNE ; otherwise -> pass R_PASS ; sweep_line writes, from the counts, E TAB TAB \n and new_lines writes one ; E TAB TAB \n per line that holds the sweep. -; KNOWN_CLAW holds the claw lane's argued equivalents (#10813, #10821), and this module's own five keys -; (seven survivors: one loop-bound key covers three loops), and the eleven ledger_union and four -; social_visuals equivalents of #10787 and #10773, taken from their sweeps on 2026-10-19 (fourteen keys). -; Two social_visuals survivors are edits inside a // comment: the sweep mutates comment text too. +; The sweep mutates the text of a trailing // comment too, so a survivor whose text appears only +; after the // (and with no quote before it) is a comment edit, C_COMMENT, and needs no entry. +; KNOWN_CLAW holds the claw lane's argued equivalents (#10813, #10821), this module's own ten keys +; (one loop-bound key covers three loops; five are in_comment's line-end guards, #10846), and ten +; ledger_union and two social_visuals keys for the code survivors of #10787 and #10773, from their +; sweeps on 2026-10-19. ; The honest limits: two mutants with the same spec, operator and line text share one key, so one ; entry covers both; a known entry is the reviewer's argument, and this module trusts it. It reads the ; text a sweep printed; it runs no sweep. @@ -57,8 +59,9 @@ pub const C_KNOWN : u8 = 3; pub const C_NEW : u8 = 4; pub const C_NOAPPLY : u8 = 5; pub const C_BAD : u8 = 6; -pub const N_GONE : usize = 7; -pub const N_COUNTS : usize = 8; +pub const C_COMMENT : u8 = 7; +pub const N_GONE : usize = 8; +pub const N_COUNTS : usize = 9; /// Verdicts. pub const V_PASS : u8 = 0; @@ -82,13 +85,13 @@ pub const WHY_TEXTS : [6]str = [ "a mutant edit left its line unchanged, so that mutant was never tested: fix the generator\n", "a survivor is not on the known list: kill it with a test, or argue it equivalent in the issue and list it\n", "every survivor is known, and some known entries no longer survive: prune them from the list\n", - "every survivor is a known equivalent mutant\n" + "every survivor is a known equivalent mutant or an edit inside a comment\n" ]; -pub const WHY_LENS : [6]usize = [71, 100, 90, 106, 92, 44]; +pub const WHY_LENS : [6]usize = [71, 100, 90, 106, 92, 72]; /// The claw lane's argued equivalent mutants: lab_disk (#10813) and red_origin (#10821). -pub const KNOWN_CLAW : str = "red_origin [ < -> <= ] SURVIVED :: while (i < list_len) {\nred_origin [ < -> <= ] SURVIVED :: while (s < b_len) {\nlab_disk [ < -> <= ] SURVIVED :: while (s < b_len) {\nmut_known [ < -> <= ] SURVIVED :: if (i < n1) { x = b[s + i]; } else { x = b[c + i - n1]; }\nmut_known [ < -> <= ] SURVIVED :: while (i < known_len) {\nmut_known [ > -> >= ] SURVIVED :: while (q > from and b[q - 1] != ' ') { q -= 1; }\nmut_known [ < -> <= ] SURVIVED :: while (s < b_len) {\nmut_known [ < -> <= ] SURVIVED :: while (ks < known_len) {\nledger_union [ < -> <= ] SURVIVED :: while (s < n and hs == n) {\nledger_union [- 1->- 0] SURVIVED :: if (lu_at(x, hs - 1, hs - 1 - ENTRIES_OPEN_LEN, ENTRIES_OPEN, ENTRIES_OPEN_LEN) == false) \nledger_union [ < -> <= ] SURVIVED :: const hb : bool = pb < fb[1];\nledger_union [ and -> or ] SURVIVED :: if (same_tb and in_b) { same_tb = lu_cmp(t, pt, vte, b, pb, vbe) == 0; }\nledger_union [ and -> or ] SURVIVED :: if (same_ot and in_t) { same_ot = lu_cmp(o, po, voe, t, pt, vte) == 0; }\nledger_union [ < -> <= ] SURVIVED :: while (s < n) {\nledger_union [ < -> <= ] SURVIVED :: while (i + 1 < e) {\nledger_union [+ 1->+ 2] SURVIVED :: while (i + 1 < e) {\nledger_union [+ 1->+ 2] SURVIVED :: if (e > s + 1 and x[s] == g and x[s + 1] == '\\t') {\nledger_union [ < -> <= ] SURVIVED :: while (s < n and code == SC_OK) {\nsocial_visuals [ and -> or ] SURVIVED :: pub const KIND_MOVING : usize = 0; // a reel: video with the voice and the music\nsocial_visuals [>=->>] SURVIVED :: if (a >= ARCHS) {\nsocial_visuals [ > -> >= ] SURVIVED :: return content(t, a) > 0;\nsocial_visuals [ or -> and ] SURVIVED :: pub const G_NOT_MEASURED : usize = 3; // the comparison did not run, or its answer is no\n"; -pub const KNOWN_CLAW_LEN : usize = 1695; +pub const KNOWN_CLAW : str = "red_origin [ < -> <= ] SURVIVED :: while (i < list_len) {\nred_origin [ < -> <= ] SURVIVED :: while (s < b_len) {\nlab_disk [ < -> <= ] SURVIVED :: while (s < b_len) {\nmut_known [ < -> <= ] SURVIVED :: if (i < n1) { x = b[s + i]; } else { x = b[c + i - n1]; }\nmut_known [ < -> <= ] SURVIVED :: while (i < known_len) {\nmut_known [ > -> >= ] SURVIVED :: while (q > from and b[q - 1] != ' ') { q -= 1; }\nmut_known [ < -> <= ] SURVIVED :: while (s < b_len) {\nmut_known [ < -> <= ] SURVIVED :: while (ks < known_len) {\nmut_known [>=->>] SURVIVED :: if (c + 2 >= e or b[c + 1] != '[') { return false; }\nmut_known [+ 1->+ 2] SURVIVED :: while (p + 1 < e and (b[p] != '-' or b[p + 1] != '>')) { p += 1; }\nmut_known [<=-><] SURVIVED :: while (t + 4 <= e and ts == e) {\nmut_known [<=-><] SURVIVED :: while (q + 2 <= e and cs == e) {\nmut_known [ < -> <= ] SURVIVED :: while (p + 1 < e and (b[p] != '-' or b[p + 1] != '>')) { p += 1; }\nledger_union [ < -> <= ] SURVIVED :: while (s < n and hs == n) {\nledger_union [- 1->- 0] SURVIVED :: if (lu_at(x, hs - 1, hs - 1 - ENTRIES_OPEN_LEN, ENTRIES_OPEN, ENTRIES_OPEN_LEN) == false) \nledger_union [ < -> <= ] SURVIVED :: const hb : bool = pb < fb[1];\nledger_union [ and -> or ] SURVIVED :: if (same_tb and in_b) { same_tb = lu_cmp(t, pt, vte, b, pb, vbe) == 0; }\nledger_union [ and -> or ] SURVIVED :: if (same_ot and in_t) { same_ot = lu_cmp(o, po, voe, t, pt, vte) == 0; }\nledger_union [ < -> <= ] SURVIVED :: while (s < n) {\nledger_union [ < -> <= ] SURVIVED :: while (i + 1 < e) {\nledger_union [+ 1->+ 2] SURVIVED :: while (i + 1 < e) {\nledger_union [+ 1->+ 2] SURVIVED :: if (e > s + 1 and x[s] == g and x[s + 1] == '\\t') {\nledger_union [ < -> <= ] SURVIVED :: while (s < n and code == SC_OK) {\nsocial_visuals [>=->>] SURVIVED :: if (a >= ARCHS) {\nsocial_visuals [ > -> >= ] SURVIVED :: return content(t, a) > 0;\n"; +pub const KNOWN_CLAW_LEN : usize = 1848; /// Where the line number of sweep line b[s..e) is: f[0] is the space before it, f[1] the space /// after. False when the line has no non-empty spec name, no digits, or no space after them. @@ -162,12 +165,61 @@ pub fn result_class(b : str, from : usize, e : usize) -> u8 { return C_BAD; } -/// The class of sweep line b[s..e) against the known list. +/// Whether the mutant of sweep line b[..e), whose number field ends at c, can only have edited text +/// inside a // comment. Its text is b after the [ up to the first ->. That text must appear in +/// the source text after the first " :: ", every time it appears there it must start after the // +/// (an edit that touches the // itself changes the code), and no quote byte may come before the //, +/// so the // is not inside a string or a char. A line cut before its comment, or a text the +/// cut hid, is not in a comment: when in doubt the survivor stays new. No -> needs no guard: +/// then runs to the line's end, longer than any text after the //, so it never matches; an empty +/// first matches where the source text starts, before the //. The -> search may look one byte +/// further (p + 1 <= e) with the same result: past the last -> it only lengthens a that already +/// runs to the line's end. +pub fn in_comment(b : str, c : usize, e : usize) -> bool { + if (c + 2 >= e or b[c + 1] != '[') { return false; } + const o : usize = c + 2; + var p : usize = o; + while (p + 1 < e and (b[p] != '-' or b[p + 1] != '>')) { p += 1; } + const n : usize = p - o; + var t : usize = c; + var ts : usize = e; + while (t + 4 <= e and ts == e) { + if (b[t] == ' ' and b[t + 1] == ':' and b[t + 2] == ':' and b[t + 3] == ' ') { ts = t + 4; } + t += 1; + } + var cs : usize = e; + var q : usize = ts; + while (q + 2 <= e and cs == e) { + if (b[q] == '/' and b[q + 1] == '/') { + cs = q; + } else if (b[q] == '"' or b[q] == '\'') { + return false; + } + q += 1; + } + if (cs == e) { return false; } + var found : bool = false; + var r : usize = ts; + while (r + n <= e) { + var k : usize = 0; + while (k < n and b[r + k] == b[o + k]) { k += 1; } + if (k == n) { + if (r < cs + 2) { return false; } + found = true; + } + r += 1; + } + return found; +} + +/// The class of sweep line b[s..e) against the known list: a survivor on the list is known, and one +/// whose edit lies inside a comment needs no entry. pub fn line_class(b : str, s : usize, e : usize, known : str, known_len : usize) -> u8 { var f : [2]usize = undefined; if (num_span(b, s, e, &f) == false) { return C_BAD; } const c : u8 = result_class(b, f[1], e); if (c == C_NEW and key_in(known, known_len, b, s, f[0], f[1], e)) { return C_KNOWN; } + if (c == C_NEW and in_comment(b, f[1], e)) { return C_COMMENT; } return c; } @@ -304,13 +356,13 @@ pub const SWEEP42 : str = "mut_known 114 [ < -> <= ] SURVIVED :: if (i < n1) { x pub const SWEEP42_LEN : usize = 482; test this_modules_own_sweep_passes_on_its_own_list { - var c : [8]u32 = undefined; + var c : [9]u32 = undefined; assert sweep_counts(SWEEP42, SWEEP42_LEN, KNOWN_CLAW, KNOWN_CLAW_LEN, &c) == 7; assert c[C_KNOWN as usize] == 7; assert c[C_NEW as usize] == 0; assert c[C_BAD as usize] == 0; - // The other specs' seventeen keys are not in this sweep. - assert c[N_GONE] == 17; + // The other specs' fifteen keys and the five in_comment keys are not in this sweep. + assert c[N_GONE] == 20; assert sweep_reason(&c) == R_PRUNE; assert verdict_of(R_PRUNE) == V_PASS; assert SWEEP42[SWEEP42_LEN - 1] == '\n'; @@ -322,18 +374,56 @@ pub const SWEEP43 : str = "ledger_union 168 [ < -> <= ] SURVIVED :: while (s < n pub const SWEEP43_LEN : usize = 1511; test the_ledger_union_and_social_visuals_sweeps_pass_on_the_list { - var c : [8]u32 = undefined; + var c : [9]u32 = undefined; assert sweep_counts(SWEEP43, SWEEP43_LEN, KNOWN_CLAW, KNOWN_CLAW_LEN, &c) == 18; - assert c[C_KNOWN as usize] == 15; + assert c[C_KNOWN as usize] == 13; + assert c[C_COMMENT as usize] == 2; assert c[C_TIMEOUT as usize] == 3; assert c[C_NEW as usize] == 0; assert c[C_BAD as usize] == 0; - // 22 keys, 14 of them matched here. - assert c[N_GONE] == 8; + // 25 keys, 12 of them matched here; the two comment edits need no key. + assert c[N_GONE] == 13; assert sweep_reason(&c) == R_PRUNE; assert SWEEP43[SWEEP43_LEN - 1] == '\n'; } +test social_visuals_comment_survivors_need_no_key { + // Lines 72 and 163 of the tick 43 sweep: " and " and " or " appear only in the trailing comment. + assert line_class(SWEEP43, 1121, 1248, "", 0) == C_COMMENT; + assert line_class(SWEEP43, 1375, 1510, "", 0) == C_COMMENT; + assert line_class("x 1 [ < -> <= ] SURVIVED :: x = 0; // a < b", 0, 43, "", 0) == C_COMMENT; + assert line_class("x 1 [ and -> or ] SURVIVED :: /// a and b", 0, 41, "", 0) == C_COMMENT; + assert line_class("x 1 [<-><=] SURVIVED :: x = 0; // a<", 0, 36, "", 0) == C_COMMENT; + // A that holds - or >, an edit just after the //, and a [ x: ] that is not the separator. + assert line_class("x 1 [- 1->- 0] SURVIVED :: x = 0; // a - 1", 0, 42, "", 0) == C_COMMENT; + assert line_class("x 1 [<-><=] SURVIVED :: x = 0; //<", 0, 34, "", 0) == C_COMMENT; + assert line_class("x 1 [ x: ->'] SURVIVED :: a; // x: ", 0, 35, "", 0) == C_COMMENT; + var out : [64]u8 = undefined; + assert new_lines("x 1 [ < -> <= ] SURVIVED :: x = 0; // a < b\n", 44, "", 0, &out, 64, 0) == 0; +} + +test negative_control_code_outside_the_comment_keeps_a_survivor_new { + // The text also appears before the //. + assert line_class("x 1 [ and -> or ] SURVIVED :: if (a and b) { } // a and b", 0, 57, "", 0) == C_NEW; + // A quote before the //: the // may sit in a char or a string. + assert line_class("x 1 [ < -> <= ] SURVIVED :: if (c == '/') { } // a < b", 0, 54, "", 0) == C_NEW; + assert line_class("x 1 [ < -> <= ] SURVIVED :: s = \"a\"; // a < b", 0, 45, "", 0) == C_NEW; + // The text is not visible after the //: the cut may have hidden it. + assert line_class("x 1 [ < -> <= ] SURVIVED :: x = 0; // none", 0, 42, "", 0) == C_NEW; + // An edit of the // itself, or one that overlaps it, changes the code. + assert line_class("x 1 [//->#] SURVIVED :: x = 0; // a", 0, 35, "", 0) == C_NEW; + assert line_class("x 1 [/ ->x] SURVIVED :: x = 0; // a", 0, 35, "", 0) == C_NEW; + // An empty , no separator, no bracket. + assert line_class("x 1 [->x] SURVIVED :: a; // ->x", 0, 31, "", 0) == C_NEW; + assert line_class("x 1 [ < -> <= ] SURVIVED", 0, 24, "", 0) == C_NEW; + assert line_class("x 1 x< -> <= ] SURVIVED :: // a < b", 0, 35, "", 0) == C_NEW; + assert in_comment("x 1 [", 3, 5) == false; + // No -> at all: runs to the line's last byte, longer than the text after the ::, so it never matches. + assert in_comment("x 1 [ab] SURVIVED :: // ab", 3, 26) == false; + // Only a survivor can be a comment edit. + assert line_class("x 1 [ and -> or ] CAUGHT :: x = 0; // a and b", 0, 45, "", 0) == C_CAUGHT; +} + test negative_control_a_cut_line_keeps_its_trailing_space { // The sweep cuts the source text at 90 bytes, and this cut ends in a space; the key holds it. assert line_class("ledger_union 174 [- 1->- 0] SURVIVED :: if (lu_at(x, hs - 1, hs - 1 - ENTRIES_OPEN_LEN, ENTRIES_OPEN, ENTRIES_OPEN_LEN) == false)", 0, 129, KNOWN_CLAW, KNOWN_CLAW_LEN) == C_NEW; @@ -341,18 +431,18 @@ test negative_control_a_cut_line_keeps_its_trailing_space { } test the_tick_41_sweep_passes_on_the_known_list { - var c : [8]u32 = undefined; + var c : [9]u32 = undefined; assert sweep_counts(SWEEP41, SWEEP41_LEN, KNOWN_CLAW, KNOWN_CLAW_LEN, &c) == 3; assert c[C_CAUGHT as usize] == 1; assert c[C_KNOWN as usize] == 2; assert c[C_NEW as usize] == 0; - assert c[N_GONE] == 20; + assert c[N_GONE] == 23; assert sweep_reason(&c) == R_PRUNE; assert verdict_of(R_PRUNE) == V_PASS; } test moved_line_numbers_keep_a_survivor_known { - var c : [8]u32 = undefined; + var c : [9]u32 = undefined; assert sweep_counts(SWEEP41_EARLY, SWEEP41_EARLY_LEN, KNOWN_CLAW, KNOWN_CLAW_LEN, &c) == 2; assert c[C_KNOWN as usize] == 2; assert c[C_NEW as usize] == 0; @@ -361,18 +451,18 @@ test moved_line_numbers_keep_a_survivor_known { } test the_tick_37_survivors_are_new_and_hold_the_sweep { - var c : [8]u32 = undefined; + var c : [9]u32 = undefined; assert sweep_counts(SWEEP37, SWEEP37_LEN, KNOWN_CLAW, KNOWN_CLAW_LEN, &c) == 5; assert c[C_NEW as usize] == 5; assert c[C_KNOWN as usize] == 0; assert c[C_BAD as usize] == 0; - assert c[N_GONE] == 22; + assert c[N_GONE] == 25; assert sweep_reason(&c) == R_NEW; assert verdict_of(R_NEW) == V_HOLD; } test a_full_known_list_passes_clean { - var c : [8]u32 = undefined; + var c : [9]u32 = undefined; assert sweep_counts(DISK93, DISK93_LEN, "lab_disk [ < -> <= ] SURVIVED :: while (s < b_len) {", 52, &c) == 1; assert c[C_KNOWN as usize] == 1; assert c[N_GONE] == 0; @@ -396,7 +486,7 @@ test negative_control_a_changed_operator_or_text_is_new { test negative_control_a_known_entry_never_matches_a_caught_line { const line : str = "lab_disk 93 [ < -> <= ] CAUGHT :: while (s < b_len) {"; assert line_class(line, 0, 53, KNOWN_CLAW, KNOWN_CLAW_LEN) == C_CAUGHT; - var c : [8]u32 = undefined; + var c : [9]u32 = undefined; assert sweep_counts(line, 53, "lab_disk [ < -> <= ] SURVIVED :: while (s < b_len) {", 52, &c) == 1; assert c[C_CAUGHT as usize] == 1; assert c[N_GONE] == 1; @@ -437,7 +527,7 @@ test lines_without_the_head_are_unreadable { } test the_hold_reasons_come_in_order { - var c : [8]u32 = undefined; + var c : [9]u32 = undefined; assert sweep_counts("x 1 [a->b] SURVIVED :: t\nx 2 [a->b] NOAPPLY\nx 3 bad\n", 52, "", 0, &c) == 3; assert c[C_NEW as usize] == 1; assert c[C_NOAPPLY as usize] == 1; @@ -454,9 +544,9 @@ test the_hold_reasons_come_in_order { } test an_empty_sweep_holds_as_silent { - var c : [8]u32 = undefined; + var c : [9]u32 = undefined; assert sweep_counts("", 0, KNOWN_CLAW, KNOWN_CLAW_LEN, &c) == 0; - assert c[N_GONE] == 22; + assert c[N_GONE] == 25; assert sweep_reason(&c) == R_SILENT; assert sweep_counts("\n\n\n", 3, "", 0, &c) == 0; assert sweep_reason(&c) == R_SILENT; @@ -464,7 +554,7 @@ test an_empty_sweep_holds_as_silent { } test empty_known_lines_are_never_gone_and_match_nothing { - var c : [8]u32 = undefined; + var c : [9]u32 = undefined; assert sweep_counts(DISK93, DISK93_LEN, "\n\n", 2, &c) == 1; assert c[C_NEW as usize] == 1; assert c[N_GONE] == 0; @@ -502,9 +592,9 @@ test the_lens_are_the_text_lengths { test each_sweep_line_is_spelled_out { var out : [128]u8 = undefined; - var c : [8]u32 = undefined; + var c : [9]u32 = undefined; assert sweep_counts("x 1 [a->b] CAUGHT :: t\n", 23, "", 0, &c) == 1; - assert sweep_line(&c, &out, 128, 0) == 51; + assert sweep_line(&c, &out, 128, 0) == 79; assert str_eq(&out, 7, "E\tpass\t", 7); assert out[7] == 'e'; assert sweep_counts("x 1 [a->b] CAUGHT :: t\n", 23, "y [a->b] SURVIVED :: t", 22, &c) == 1; @@ -560,7 +650,7 @@ test a_line_at_the_end_of_a_full_buffer_is_not_read_past { } assert line_class(&one, 0, 24, "", 0) == C_NEW; assert line_class(&one, 0, 24, "x [a->b] SURVIVED :: t", 22) == C_KNOWN; - var c : [8]u32 = undefined; + var c : [9]u32 = undefined; assert sweep_counts(&one, 24, "x [a->b] SURVIVED :: t", 22, &c) == 1; assert c[C_KNOWN as usize] == 1; assert seen("x [a->b] SURVIVED :: t", 0, 22, &one, 24) == true;