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/.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/.trinity/seals/claw_ClawMutKnown.json b/.trinity/seals/claw_ClawMutKnown.json new file mode 100644 index 0000000000..5c1557591a --- /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: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-19T05:23:05Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:6a2f2e78d54f8492572f9b962da69fbd7099ebeba42f96b6ef6aaeab803c889b", + "spec_path": "specs/claw/mut_known.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 28, + "total": 28, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file 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 b6e85ff7b3..d977a2a48b 100644 --- a/docs/claw/README.md +++ b/docs/claw/README.md @@ -24,6 +24,10 @@ 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 | +| 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 7e3363a205..8dd6bd2826 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -197,14 +197,18 @@ {"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"}, {"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"}, {"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/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; +} 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; +} diff --git a/specs/claw/mut_known.t27 b/specs/claw/mut_known.t27 new file mode 100644 index 0000000000..98772f5802 --- /dev/null +++ b/specs/claw/mut_known.t27 @@ -0,0 +1,724 @@ +// 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 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. +; 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. +; 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 C_COMMENT : u8 = 7; +pub const N_GONE : usize = 8; +pub const N_COUNTS : usize = 9; + +/// 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 or an edit inside a comment\n" +]; +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) {\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. +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; +} + +/// 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; +} + +/// 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 : [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' 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'; +} + +/// 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 : [9]u32 = undefined; + assert sweep_counts(SWEEP43, SWEEP43_LEN, KNOWN_CLAW, KNOWN_CLAW_LEN, &c) == 18; + 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; + // 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; + 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 : [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] == 23; + assert sweep_reason(&c) == R_PRUNE; + assert verdict_of(R_PRUNE) == V_PASS; +} + +test moved_line_numbers_keep_a_survivor_known { + 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; + 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 : [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] == 25; + assert sweep_reason(&c) == R_NEW; + assert verdict_of(R_NEW) == V_HOLD; +} + +test a_full_known_list_passes_clean { + 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; + 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 : [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; +} + +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 : [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; + 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 : [9]u32 = undefined; + assert sweep_counts("", 0, KNOWN_CLAW, KNOWN_CLAW_LEN, &c) == 0; + 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; + assert verdict_of(R_SILENT) == V_HOLD; +} + +test empty_known_lines_are_never_gone_and_match_nothing { + 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; + 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 : [9]u32 = undefined; + assert sweep_counts("x 1 [a->b] CAUGHT :: t\n", 23, "", 0, &c) == 1; + 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; + 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 : [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; + 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'; +} 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; +}