From 5778fb7dd98dff674c1113641ef9896672ccf0ce Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 09:32:40 +0700 Subject: [PATCH 1/4] test(claw): ledger_union sweep -- two tests kill the past-the-buffer reads at a full buffer A sweep of 177 mutants caught 164. Lines 148 and 410 survived because no test gave lu_key_end or lu_find a text that fills its buffer to the last byte. Two tests now do, and the t27c bounds check catches both mutants. The other 11 survivors are equivalent (table in #10787). Production code is unchanged; gen/c/claw/ledger_union.c is regenerated by gen-c and the seal is resealed. Closes #10787. Co-Authored-By: Claude Opus 5.5 --- .trinity/seals/claw_ClawLedgerUnion.json | 16 +++++----- gen/c/claw/ledger_union.c | 34 +++++++++++++++++++++- specs/claw/ledger_union.t27 | 37 ++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 9 deletions(-) diff --git a/.trinity/seals/claw_ClawLedgerUnion.json b/.trinity/seals/claw_ClawLedgerUnion.json index c3c8e2171d..2ce7a049a7 100644 --- a/.trinity/seals/claw_ClawLedgerUnion.json +++ b/.trinity/seals/claw_ClawLedgerUnion.json @@ -7,23 +7,23 @@ } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:f5f2bdf2bfdd9ef25d0ba280851b3504149efd01b36191943b9ec731eddfec99", - "gen_hash_rust": "sha256:0c4d2ff8d06ea85b4c205e68dedc8081b1c3768ce0fdcfad6d948cd72b952254", - "gen_hash_verilog": "sha256:7beda094f46814f7db05772cd52e22de60e4ba6f845b94b01a8d761b3596ffab", - "gen_hash_zig": "sha256:86d1598c1805d89624d659204e8a37c5409e3b1e2745ad9c32947b97a5bcda11", + "gen_hash_c": "sha256:6e4d4e7407cdeecf34c806c9d05c1f6e8479407cf18fdb585a23aa30a4042ca4", + "gen_hash_rust": "sha256:99e922aa7fb4f02998b67236fb27238bc5927b2edf8a3afb83b636ba2a4dda4d", + "gen_hash_verilog": "sha256:6986ba1f836a966d913f14eba98349a316b4e30777e8acd9574ed6395ec242f7", + "gen_hash_zig": "sha256:93f4349b59f44e9690da2253337c4f3e3d73702fd063c7af57959fb1d5de36d3", "module": "ClawLedgerUnion", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-19T00:13:54Z", + "sealed_at": "2026-10-19T02:30:28Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:38a31c67e68321f02ef60d23f2e563f51e528921112a22dc6cfe9c5d1ccead69", + "spec_hash": "sha256:2b782d0001cc5d38c834673891cc303785677dbbac028b1e24e34e68ee1a35e3", "spec_path": "specs/claw/ledger_union.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 25, - "total": 25, + "passed": 27, + "total": 27, "vacuous": 0 }, "toolchain": { diff --git a/gen/c/claw/ledger_union.c b/gen/c/claw/ledger_union.c index 2a51808913..ef56a223db 100644 --- a/gen/c/claw/ledger_union.c +++ b/gen/c/claw/ledger_union.c @@ -1149,6 +1149,36 @@ void test_the_reason_tables_match_their_lengths(void) { assert((buf_has(&r, s, "same pull request", 17) == true)); } +void test_a_path_with_no_closing_quote_at_the_end_of_a_full_buffer_is_refused_without_reading_past_it(void) { + uint8_t x[30] = {0}; + size_t n = put_str(&x, 30, 0, FIX_HEAD, FIX_HEAD_LEN); + n = put_str(&x, 30, n, ENTRY_HEAD, ENTRY_HEAD_LEN); + n = put_str(&x, 30, n, "aa", 2); + assert((n == 30)); + size_t fr[2] = {0}; + assert((lu_frame(&x, 30, &fr) == U_SHAPE)); + uint8_t out[128] = {0}; + uint32_t st[4] = {0}; + size_t m = ledger_union(&x, 30, &x, 30, &x, 30, &out, 128, &st); + assert((st[0] == U_SHAPE)); + assert((st[3] == 0)); + assert((m == 52)); +} + +void test_a_lone_tag_byte_at_the_end_of_a_full_record_buffer_is_not_read_past(void) { + uint8_t x[25] = {0}; + size_t n = put_str(&x, 25, 0, "F\t.trinity/seals/a.json\n", 24); + n = put_str(&x, 25, n, "S", 1); + assert((n == 25)); + assert((lu_find(&x, 25, 'S', 2, 23) == 25)); + uint8_t out[128] = {0}; + uint32_t st[2] = {0}; + size_t m = pr_scope(&x, 25, &out, 128, &st); + assert((st[0] == SC_SEAL)); + assert((st[1] == 1)); + assert((m == 100)); +} + /* ------------------------------------------------------- Test runner (compile with -DT27_TEST_MAIN to execute) @@ -1182,7 +1212,9 @@ int main(void) { test_an_empty_or_unreadable_file_list_is_refused(); test_the_scope_rule_reads_each_record_exactly(); test_the_reason_tables_match_their_lengths(); - printf("All %d tests passed.\n", 25); + test_a_path_with_no_closing_quote_at_the_end_of_a_full_buffer_is_refused_without_reading_past_it(); + test_a_lone_tag_byte_at_the_end_of_a_full_record_buffer_is_not_read_past(); + printf("All %d tests passed.\n", 27); return 0; } #endif /* T27_TEST_MAIN */ diff --git a/specs/claw/ledger_union.t27 b/specs/claw/ledger_union.t27 index a773303e58..5e5ed0225d 100644 --- a/specs/claw/ledger_union.t27 +++ b/specs/claw/ledger_union.t27 @@ -1107,3 +1107,40 @@ test the_reason_tables_match_their_lengths { const s : usize = put_str(&r, 128, 0, SCOPE_REASONS[2], SCOPE_REASON_LENS[2]); assert buf_has(&r, s, "same pull request", 17) == true; } + +// Written against the mutation sweep of this file: two readers stopped at their range end only +// because no test gave them a text that fills its buffer to the last byte. + +// `while (i < e)` to `<=` in lu_key_end read x[e] when a path had no closing quote. On the last +// line of a side that fills its buffer, x[e] is past the buffer. +test a_path_with_no_closing_quote_at_the_end_of_a_full_buffer_is_refused_without_reading_past_it { + var x : [30]u8 = undefined; + var n : usize = put_str(&x, 30, 0, FIX_HEAD, FIX_HEAD_LEN); + n = put_str(&x, 30, n, ENTRY_HEAD, ENTRY_HEAD_LEN); + n = put_str(&x, 30, n, "aa", 2); + assert n == 30; + var fr : [2]usize = undefined; + assert lu_frame(&x, 30, &fr) == U_SHAPE; + var out : [128]u8 = undefined; + var st : [4]u32 = undefined; + const m : usize = ledger_union(&x, 30, &x, 30, &x, 30, &out, 128, &st); + assert st[0] == U_SHAPE; + assert st[3] == 0; + assert m == 52; +} + +// `e > s + 1` to `>=` in lu_find read the byte after a one-byte last line, which is past the +// record text when that text fills its buffer. +test a_lone_tag_byte_at_the_end_of_a_full_record_buffer_is_not_read_past { + var x : [25]u8 = undefined; + var n : usize = put_str(&x, 25, 0, "F\t.trinity/seals/a.json\n", 24); + n = put_str(&x, 25, n, "S", 1); + assert n == 25; + assert lu_find(&x, 25, 'S', 2, 23) == 25; + var out : [128]u8 = undefined; + var st : [2]u32 = undefined; + const m : usize = pr_scope(&x, 25, &out, 128, &st); + assert st[0] == SC_SEAL; + assert st[1] == 1; + assert m == 100; +} From 54c58ca0e53c68f897bb4b55bbdefc28f4a8017b Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 09:43:48 +0700 Subject: [PATCH 2/4] test(claw): sweep the seven never-swept claw specs -- 5 real survivors killed, 51/51 reel_still, visual_output and the five pin modules had no mutation sweep. 51 mutants, 46 caught. The 5 survivors were real: topic_reel looked one item past the lesson ids (the course title resolved to a 28th lesson), id_is compared every other code point, still_ms_ok took the first ms of the CTA hold, and same_as_lit took a literal with no dollar sign or another first byte. Four tests kill all five under t27c; t27b agrees. Production code is unchanged; the three seals are resealed. Closes #10791. Co-Authored-By: Claude Opus 5.5 --- .trinity/seals/claw_ClawReelStill.json | 22 +++++++++++----------- .trinity/seals/claw_ClawT27fGatePins.json | 16 ++++++++-------- .trinity/seals/claw_ClawVisualOutput.json | 20 ++++++++++---------- specs/claw/reel_still.t27 | 18 ++++++++++++++++++ specs/claw/t27f_gate_pins.t27 | 9 +++++++++ specs/claw/visual_output.t27 | 8 ++++++++ 6 files changed, 64 insertions(+), 29 deletions(-) diff --git a/.trinity/seals/claw_ClawReelStill.json b/.trinity/seals/claw_ClawReelStill.json index b4ee1a2cf2..feefc5ee2a 100644 --- a/.trinity/seals/claw_ClawReelStill.json +++ b/.trinity/seals/claw_ClawReelStill.json @@ -1,9 +1,9 @@ { - "built_by": "t27c-bootstrap@0.5.2+5e4d3b998", + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", "closure": [ { "spec": "specs/claw/visual_output.t27", - "spec_hash": "sha256:17137afe136e39910f38d7b031c659a672e009bfeef1bf4f9b7af7e2cb6cc628" + "spec_hash": "sha256:3ef926be1f8a2aa1e00ca66265cdc78b6f8d3418460cbc252bb7319827c5b634" }, { "spec": "specs/t27f/frame_shard.t27", @@ -23,27 +23,27 @@ } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:d32a87a4218806616b04b727d7e3f1bc76d6d33440f304f7beb42b566bb50437", - "gen_hash_rust": "sha256:5a1adebb39cdeb948aae1da51a64f43b93b1cd845f715389c30cf6785b701c30", - "gen_hash_verilog": "sha256:62df937cda4af6426ae5e1170bafa4cf124f5d952dea6b87ccf19e451968dc9f", - "gen_hash_zig": "sha256:b4e20621dcef2e6517948018e7f6aeb788da068df27beb9b6c2a485af15b492e", + "gen_hash_c": "sha256:51697502da51ccf2bb293226da8e748eef3e173bfd43c691cca61178fb578746", + "gen_hash_rust": "sha256:fac7c009707b7e567b2e083d5a501af6755c9f3283dfde21536ff5c3b23f8b69", + "gen_hash_verilog": "sha256:d941117df0e0e9c8edd763f66e1b20659e10a2cdd8cb3b1bc7c1c6b6862311e0", + "gen_hash_zig": "sha256:253da955678ba32a238d17402ffeecfecc6ec87ed7b7bd181a7469f7e6af3b97", "module": "ClawReelStill", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-18T17:06:53Z", + "sealed_at": "2026-10-19T02:42:47Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:4fc5fc3c835193faae7b312194e4f31bc6baa8b7da3f08b12b809cb4e5e293e5", + "spec_hash": "sha256:13b018d77d82c4af644eaf00334e89fa91f8b19a7fc0719fbfdfd2801090a65b", "spec_path": "specs/claw/reel_still.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 15, - "total": 15, + "passed": 17, + "total": 17, "vacuous": 0 }, "toolchain": { - "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", "zig": "0.16.0" } } \ No newline at end of file diff --git a/.trinity/seals/claw_ClawT27fGatePins.json b/.trinity/seals/claw_ClawT27fGatePins.json index e9a22c3f4a..d47c701e3c 100644 --- a/.trinity/seals/claw_ClawT27fGatePins.json +++ b/.trinity/seals/claw_ClawT27fGatePins.json @@ -11,23 +11,23 @@ } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:e35b884c19d8d6c37ed63beac053d40a5e86f9c73fa2941ff6cd0039c81bda84", - "gen_hash_rust": "sha256:4dc02033cea4ae56d81cca417e99faf450bfd97ffcbd7fed702b56b30119ce25", - "gen_hash_verilog": "sha256:a5a99dff99da4b4ebd3c15c210a92a3cb487039215158b2bb3c89a83eab168df", - "gen_hash_zig": "sha256:e39bb7edf0cb96766984159a1d7c9f3b79f432094cd7c74cd6a9b660a3195efb", + "gen_hash_c": "sha256:3444dc60e17562308a148efa48edbb08a64e39772ac7dcca53df2baaddfc1ef0", + "gen_hash_rust": "sha256:192a449bd415c91098e71355fd74b69fc95019bebbaea06f68b178e86f48fabf", + "gen_hash_verilog": "sha256:7ff62489f3e9c6bf55e9f0fdcf8c0e7f0da8967e25ae52cd9655c04117336769", + "gen_hash_zig": "sha256:ef5c3ac7378603f9afc1fa1352349bc6fb9965ce1ee8dbe0095da81f50a84398", "module": "ClawT27fGatePins", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-19T01:03:00Z", + "sealed_at": "2026-10-19T02:42:50Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:1a4fc11a3ee61e990b6226621b4379fe34a727f0d1c830004bfddb5640836666", + "spec_hash": "sha256:b623f7c2fb338e388d43730e156cf4d652230321305990494556c057cc70442b", "spec_path": "specs/claw/t27f_gate_pins.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 4, - "total": 4, + "passed": 5, + "total": 5, "vacuous": 0 }, "toolchain": { diff --git a/.trinity/seals/claw_ClawVisualOutput.json b/.trinity/seals/claw_ClawVisualOutput.json index 3556c08782..9beaa60177 100644 --- a/.trinity/seals/claw_ClawVisualOutput.json +++ b/.trinity/seals/claw_ClawVisualOutput.json @@ -7,31 +7,31 @@ }, { "spec": "specs/t27mcp/visual_output.t27", - "spec_hash": "sha256:06c1c5e73142e2f79fa5d7be037eace63c1f20597a6e33274ebf8e1b428b7584" + "spec_hash": "sha256:2c44da44a1f662108a7b3558e763ff86e89db4bc9f49fe749dd7617051343960" } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:195d1b6aa754842316d3c8becc34ce6a0a90e5f7d4ddaebd490823103ed06c33", - "gen_hash_rust": "sha256:93ecd121c7efadf95e4ca54e1e750fb5c7a8159284cebe1cbb61d7065d1b7875", - "gen_hash_verilog": "sha256:794e86d5753efad530cd7732b2638ad3c3a5c4e6d8c9a851bd686663aa097282", - "gen_hash_zig": "sha256:d6204f6e99bc281a0163177b92ee4abf096ea32cb23cb3b817913d16c8d072b1", + "gen_hash_c": "sha256:0047efb32fc909b0eb6c3436a03157799b149869004e6511fc6a170287a685a0", + "gen_hash_rust": "sha256:f0c069590af49c8c2d2d281016bbe0a486000907a87b88e67062c2a6e97c270a", + "gen_hash_verilog": "sha256:0147f34a1b6ab52cac77fdab5f904031798315e084059317656eb0b7bb3497a4", + "gen_hash_zig": "sha256:596073936f65114907ad7dea76a3a3205855b154940c8a3564092f5c8d6cd9fb", "module": "ClawVisualOutput", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-18T16:42:30Z", + "sealed_at": "2026-10-19T02:42:50Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:17137afe136e39910f38d7b031c659a672e009bfeef1bf4f9b7af7e2cb6cc628", + "spec_hash": "sha256:3ef926be1f8a2aa1e00ca66265cdc78b6f8d3418460cbc252bb7319827c5b634", "spec_path": "specs/claw/visual_output.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 6, - "total": 6, + "passed": 7, + "total": 7, "vacuous": 0 }, "toolchain": { - "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", "zig": "0.16.0" } } \ No newline at end of file diff --git a/specs/claw/reel_still.t27 b/specs/claw/reel_still.t27 index 506224df07..56b8046ec7 100644 --- a/specs/claw/reel_still.t27 +++ b/specs/claw/reel_still.t27 @@ -301,3 +301,21 @@ test the_file_name_is_prefix_id_lang_aspect { assert name_len(REEL_PHYSICS, LANG_RU, ASPECT_WIDE) == 29; assert name_len(1, LANG_EN, ASPECT_TALL) != name_len(0, LANG_EN, ASPECT_TALL); } + +// Written against the mutation sweep of this file (tick 37): two lookups were only ever given ids +// that differ at an even index or ids that stop at the last lesson. + +// `t < TOPICS` to `<=` in topic_reel looked one item past the lesson ids. Item ID_ITEM0 + TOPICS is +// the English course title, so the title resolved to a 28th lesson. +test negative_control_the_course_title_is_not_a_lesson_id { + assert topic_reel("t27 basics: the language in 27 lessons") == REEL_NONE; + assert verdict("t27 basics: the language in 27 lessons", LESSON_NONE, LANG_EN, ASPECT_TALL, true) == V_UNKNOWN_TOPIC; +} + +// `k + 1` to `k + 2` in id_is compared every other code point, so a topic that differs from an id +// only at an odd index was taken for that id. +test a_topic_that_differs_from_an_id_at_one_odd_index_is_unknown { + assert topic_reel("wXat-a-spec-is") == REEL_NONE; + assert topic_reel("what-a-spec-iS") == REEL_NONE; + assert id_is(0, "what-a-spec-is"); +} diff --git a/specs/claw/t27f_gate_pins.t27 b/specs/claw/t27f_gate_pins.t27 index 4b28b75133..c4a5de4938 100644 --- a/specs/claw/t27f_gate_pins.t27 +++ b/specs/claw/t27f_gate_pins.t27 @@ -62,3 +62,12 @@ test negative_control_a_wrong_label_is_caught { assert same_as_lit("reuse", "reuse") == false; assert same_as_lit("reuse", "reusE$") == false; } + +// Written against the mutation sweep of this file (tick 37): `return false` to `return true` on the +// '$' check, and `0..` to `1..` in the byte loop, both let a wrong literal pass, because no test gave +// a literal of the right length with no '$', or one that differs only in its first byte. +test negative_control_a_literal_without_its_dollar_or_with_another_first_byte_is_not_the_same { + assert same_as_lit("claw/t27f", "claw/t27fx") == false; + assert same_as_lit("xlaw/t27f", t27f_gate::CHECK_NAME) == false; + assert same_as_lit("claw/t27f", "claw/t27f$") == true; +} diff --git a/specs/claw/visual_output.t27 b/specs/claw/visual_output.t27 index c275dc4276..6cfefd7305 100644 --- a/specs/claw/visual_output.t27 +++ b/specs/claw/visual_output.t27 @@ -134,3 +134,11 @@ test a_missing_still_is_said_never_skipped { assert still_report_ok(K_SPEC, false, false); assert still_report_ok(K_ONE_SENTENCE, false, false); } + +// Written against the mutation sweep of this file (tick 37): `ms < REEL_MIN_MS - REEL_CTA_MIN_MS` +// to `<=` let the first millisecond of the CTA hold count as a still, and no test stood on that edge. +test negative_control_the_first_ms_of_the_cta_hold_is_not_a_still { + assert still_ms_ok(REEL_MIN_MS - REEL_CTA_MIN_MS) == false; + assert still_ms_ok(REEL_MIN_MS - REEL_CTA_MIN_MS - 1); + assert still_ms_ok(REEL_HOOK_MS + 1); +} From 3de5be4ebdd4474e9d59bca38b79e93934b6e743 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 09:56:32 +0700 Subject: [PATCH 3/4] feat(claw): move_gate.t27 -- check a Railway service move before creating anything On 2026-10-19 a move of TRI CLAW (vibee-render) into a new Railway project met three blockers, each found by hand: 12 of 139 variables name a *.railway.internal host that resolves only inside the old project, three outbound loops would run twice beside the old copy, and the target refused to create a service (trial expired). move_reason decides go or hold from those readings first. A Railway project transfer keeps the network and the volumes, so it goes once the target has a plan. Lab: t27c 16/16, 0 vacuous; t27b arm64/qemu 16/16; sweep 19/19 caught (the k >= R_COUNT bound survived once and is now pinned at exactly 7); seal saved and verified, spec sha equal on both sides. Refs #10794 Co-Authored-By: Claude Opus 5.5 --- .trinity/seals/claw_ClawMoveGate.json | 33 ++++ docs/claw/README.md | 1 + docs/reports/t27b_expectations.json | 1 + specs/claw/move_gate.t27 | 263 ++++++++++++++++++++++++++ 4 files changed, 298 insertions(+) create mode 100644 .trinity/seals/claw_ClawMoveGate.json create mode 100644 specs/claw/move_gate.t27 diff --git a/.trinity/seals/claw_ClawMoveGate.json b/.trinity/seals/claw_ClawMoveGate.json new file mode 100644 index 0000000000..c82e436620 --- /dev/null +++ b/.trinity/seals/claw_ClawMoveGate.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:9516cf037c00f79d0e80c430bafe58d4b260621c69f3701cfcb0c9d68264a7ee", + "gen_hash_rust": "sha256:2e849cf39914c540719fa038f5447a244f21bba5196524c55b882586ba84bb20", + "gen_hash_verilog": "sha256:97b887bfeb0f84b184c9aa94df3c4a4325aadedb6e8e4f62c5890cc33aad8803", + "gen_hash_zig": "sha256:945070a7d79625831e84e0b8af560c94fcf5242dc9101c27bf95698af28c9782", + "module": "ClawMoveGate", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-19T02:56:22Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:3c459b5fd23c4b534e59cdd752b69a2e214657a59566b8e266be32541a9f336b", + "spec_path": "specs/claw/move_gate.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 16, + "total": 16, + "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 0d9108c211..b6e85ff7b3 100644 --- a/docs/claw/README.md +++ b/docs/claw/README.md @@ -1,3 +1,4 @@ +| Move gate: whether a Railway service can move to another project, checked before anything is created there (private hosts, doubled outbound loops, state volumes, a target with no plan) | `specs/claw/move_gate.t27` (move kind and five readings in, one `V` line out: go or hold; not wired to a job yet) | #10794 | # TRI CLAW TRI CLAW — TRI-27 — Trinity S3AI: every CI gate of this repository, packaged diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 7d279383b4..a3515ae118 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -200,6 +200,7 @@ {"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/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/move_gate.t27 b/specs/claw/move_gate.t27 new file mode 100644 index 0000000000..ff32e28bf9 --- /dev/null +++ b/specs/claw/move_gate.t27 @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/claw/move_gate.t27 -- whether a Railway service can move to another project, decided +; before anything is created there. +; -- +; On 2026-10-19 the owner asked to move TRI CLAW (Railway service vibee-render, project 999) into a +; new project. The variables were read first, names only. Of 139 variables, 12 named a +; *.railway.internal host: the database, telegram-bot-api, browser-pod, whisper, vision, the voice +; services and zep. Railway resolves those names only inside the same project environment, so a +; copy in another project would boot with no database. Three variables switch outbound loops on +; (AUTOPILOT_LOOP, ROUNDS_ENABLED, REDDIT_DESK_ENABLED). A second copy running beside the old one +; would post twice. Then the target project refused the first service it was asked for: its trial +; had expired. Each of the three would have been found later, by a broken deploy, a doubled post or +; a stuck CLI prompt. This module is the check to run first. +; -- +; move_reason(kind, can_create, internal_refs, loops_on, old_running, state_volumes), first match +; wins: +; kind is neither M_TRANSFER nor M_REBUILD -> hold R_BAD +; the target cannot create a service -> hold R_TARGET_CLOSED (both kinds) +; kind M_TRANSFER (Railway "Transfer Project") -> go R_TRANSFER_OK +; kind M_REBUILD (a new service in another project): +; a variable names a *.railway.internal host -> hold R_INTERNAL +; an outbound loop is on and the old copy runs -> hold R_DOUBLE +; a volume holds state a rebuild cannot recreate -> hold R_DATA +; otherwise -> go R_REBUILD_OK +; A transfer keeps the project whole, so the network and the volumes go with it +; (docs.railway.com/projects#transferring-between-workspaces; private networking is scoped to one +; project environment, docs.railway.com/guides/isolate-staging-production). The caller counts only +; volumes whose loss matters: a cache the image rebuilds, such as the t27c lab's /data, is 0. +; move_line writes V TAB TAB \n for one reason. +; is_internal_value and is_builtin_name read one variable: a value that names a private host, and +; a RAILWAY_* name Railway sets itself and that is never copied. +; The honest limit: the counts are the caller's reading of the service. This module decides from +; them; it does not call Railway. +; ASCII only (L3). phi^2 + 1/phi^2 = 3 | TRINITY + +module ClawMoveGate; + +use claw::text; + +/// What kind of move. +pub const M_TRANSFER : u8 = 0; +pub const M_REBUILD : u8 = 1; + +/// Verdicts. +pub const V_GO : u8 = 0; +pub const V_HOLD : u8 = 1; + +/// Reasons, one per verdict line. +pub const R_TRANSFER_OK : u8 = 0; +pub const R_TARGET_CLOSED : u8 = 1; +pub const R_INTERNAL : u8 = 2; +pub const R_DOUBLE : u8 = 3; +pub const R_DATA : u8 = 4; +pub const R_REBUILD_OK : u8 = 5; +pub const R_BAD : u8 = 6; +pub const R_COUNT : usize = 7; + +/// The verdict of each reason, in reason order. +pub const REASON_VERDICT : [7]u8 = [0, 1, 1, 1, 1, 0, 1]; + +pub const VERDICT_TEXTS : [2]str = ["V\tgo\t", "V\thold\t"]; +pub const VERDICT_LENS : [2]usize = [5, 7]; + +pub const WHY_TEXTS : [7]str = [ + "a project transfer moves the private network and the volumes with it\n", + "the target workspace refused to create a service (no plan); a person must choose one\n", + "a variable names a *.railway.internal host, which resolves only inside the old project environment\n", + "an outbound loop is on while the old copy still runs; two copies would both act\n", + "a volume holds state that a rebuild does not recreate; copy it at a cutover the owner approves\n", + "nothing ties the service to the old project; rebuild it in the new one\n", + "unknown move kind; hold\n" +]; +pub const WHY_LENS : [7]usize = [69, 85, 99, 80, 95, 71, 24]; + +/// The private-network suffix, and the prefix of the variables Railway sets itself. +pub const INTERNAL_HOST : str = ".railway.internal"; +pub const INTERNAL_HOST_LEN : usize = 17; +pub const BUILTIN_PREFIX : str = "RAILWAY_"; +pub const BUILTIN_PREFIX_LEN : usize = 8; + +/// The reason for one move; its verdict is REASON_VERDICT[reason]. +pub fn move_reason(kind : u8, can_create : bool, internal_refs : u32, loops_on : u32, old_running : bool, state_volumes : u32) -> u8 { + if (kind != M_TRANSFER and kind != M_REBUILD) { return R_BAD; } + if (!can_create) { return R_TARGET_CLOSED; } + if (kind == M_TRANSFER) { return R_TRANSFER_OK; } + if (internal_refs > 0) { return R_INTERNAL; } + if (loops_on > 0 and old_running) { return R_DOUBLE; } + if (state_volumes > 0) { return R_DATA; } + return R_REBUILD_OK; +} + +pub fn move_verdict(kind : u8, can_create : bool, internal_refs : u32, loops_on : u32, old_running : bool, state_volumes : u32) -> u8 { + return REASON_VERDICT[move_reason(kind, can_create, internal_refs, loops_on, old_running, state_volumes)]; +} + +/// The verdict 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_BAD line. +pub fn move_line(r : u8, out : []u8, cap : usize, w : usize) -> usize { + var k : usize = r as usize; + if (k >= R_COUNT) { k = R_BAD as usize; } + const v : usize = REASON_VERDICT[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], WHY_LENS[k]); +} + +/// Does the value v (v_len bytes) name a private Railway host anywhere in it? +pub fn is_internal_value(v : str, v_len : usize) -> bool { + if (v_len < INTERNAL_HOST_LEN) { return false; } + var start : usize = 0; + while (start + INTERNAL_HOST_LEN <= v_len) { + var k : usize = 0; + var diff : usize = 0; + while (k < INTERNAL_HOST_LEN) { + if (v[start + k] != INTERNAL_HOST[k]) { diff += 1; } + k += 1; + } + if (diff == 0) { return true; } + start += 1; + } + return false; +} + +/// Is n (n_len bytes) a variable Railway sets itself? Those are never copied to the new service. +pub fn is_builtin_name(n : str, n_len : usize) -> bool { + return has_prefix(n, n_len, BUILTIN_PREFIX, BUILTIN_PREFIX_LEN); +} + +// ---- tests: the move the lane met on 2026-10-19 ---- + +test the_target_whose_trial_expired_holds_every_move { + // carefree-liberation answered "Your trial has expired" to the first service it was asked for. + assert move_reason(M_REBUILD, false, 12, 3, true, 0) == R_TARGET_CLOSED; + assert move_reason(M_TRANSFER, false, 0, 0, true, 0) == R_TARGET_CLOSED; + assert move_verdict(M_REBUILD, false, 0, 0, false, 0) == V_HOLD; +} + +test tri_claw_rebuilt_in_another_project_loses_its_twelve_private_hosts { + // vibee-render: 139 variables, 12 name a *.railway.internal host, 3 loop switches, no volume. + assert move_reason(M_REBUILD, true, 12, 3, true, 0) == R_INTERNAL; + assert move_reason(M_REBUILD, true, 1, 0, false, 0) == R_INTERNAL; +} + +test a_second_copy_with_its_loops_on_beside_the_old_one_holds { + assert move_reason(M_REBUILD, true, 0, 3, true, 0) == R_DOUBLE; + assert move_reason(M_REBUILD, true, 0, 1, true, 2) == R_DOUBLE; +} + +test loops_are_no_bar_once_the_old_copy_is_stopped { + assert move_reason(M_REBUILD, true, 0, 3, false, 0) == R_REBUILD_OK; + assert move_verdict(M_REBUILD, true, 0, 3, false, 0) == V_GO; +} + +test a_database_volume_holds_a_rebuild_until_a_cutover { + // Postgres: one volume whose data no image recreates. + assert move_reason(M_REBUILD, true, 0, 0, true, 1) == R_DATA; + assert move_verdict(M_REBUILD, true, 0, 0, false, 1) == V_HOLD; +} + +test the_t27c_lab_rebuilds_because_its_volume_is_a_cache { + // t27c-lab: no private host, no outbound loop, /data is a cargo and checkout cache: 0 state volumes. + assert move_reason(M_REBUILD, true, 0, 0, true, 0) == R_REBUILD_OK; +} + +test a_transfer_with_a_plan_goes_whatever_the_service_depends_on { + assert move_reason(M_TRANSFER, true, 12, 3, true, 19) == R_TRANSFER_OK; + assert move_verdict(M_TRANSFER, true, 12, 3, true, 19) == V_GO; +} + +test an_unknown_move_kind_holds_even_with_everything_clear { + assert move_reason(2, true, 0, 0, false, 0) == R_BAD; + assert move_reason(255, false, 0, 0, false, 0) == R_BAD; + assert move_verdict(2, true, 0, 0, false, 0) == V_HOLD; +} + +test only_the_two_ok_reasons_write_a_go_line { + var out : [128]u8 = undefined; + var r : u8 = 0; + var gos : u32 = 0; + while (r < 7) { + const w : usize = move_line(r, &out, 128, 0); + if (out[2] == 'g') { + gos += 1; + assert r == R_TRANSFER_OK or r == R_REBUILD_OK; + } else { + assert out[2] == 'h'; + } + assert out[w - 1] == '\n'; + r += 1; + } + assert gos == 2; +} + +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 INTERNAL_HOST_LEN == INTERNAL_HOST.len; + assert BUILTIN_PREFIX_LEN == BUILTIN_PREFIX.len; + assert is_internal_value(INTERNAL_HOST, INTERNAL_HOST_LEN); + assert is_internal_value(INTERNAL_HOST, INTERNAL_HOST_LEN - 1) == false; + assert is_builtin_name(BUILTIN_PREFIX, BUILTIN_PREFIX_LEN); +} + +test the_move_line_spells_the_verdict_and_the_reason { + var out : [128]u8 = undefined; + const w : usize = move_line(R_INTERNAL, &out, 128, 0); + assert w == 7 + 99; + assert out[0] == 'V'; + assert out[2] == 'h'; + assert out[7] == 'a'; + assert out[w - 1] == '\n'; + const g : usize = move_line(R_REBUILD_OK, &out, 128, 0); + assert g == 5 + 71; + assert out[2] == 'g'; +} + +test a_reason_out_of_range_writes_the_hold_line { + var out : [64]u8 = undefined; + const w : usize = move_line(9, &out, 64, 0); + assert w == 7 + 24; + assert out[2] == 'h'; + assert out[7] == 'u'; + // Exactly R_COUNT: the first reason past the table, not one that only lands far from it. + out[7] = 'x'; + const e : usize = move_line(7, &out, 64, 0); + assert e == 7 + 24; + assert out[7] == 'u'; +} + +test a_clipped_line_still_advances_the_cursor { + var out : [4]u8 = undefined; + assert move_line(R_DATA, &out, 4, 0) == 7 + 95; +} + +test a_private_host_anywhere_in_the_value_is_found { + assert is_internal_value("postgresql://u:p@postgres.railway.internal:5432/railway", 55); + assert is_internal_value("http://zep.railway.internal", 27); + assert is_internal_value(".railway.internal", 17); +} + +test negative_control_public_hosts_and_near_misses_are_not_private { + assert is_internal_value("https://inngest-production.up.railway.app", 41) == false; + assert is_internal_value("railway.internal", 16) == false; + assert is_internal_value("x.railway.internaL", 18) == false; + assert is_internal_value("x-railway.internal", 18) == false; + assert is_internal_value("", 0) == false; +} + +test railway_names_are_builtin_and_others_are_not { + assert is_builtin_name("RAILWAY_PROJECT_ID", 18); + assert is_builtin_name("RAILWAY_", 8); + assert is_builtin_name("RAILWAY", 7) == false; + assert is_builtin_name("railway_project_id", 18) == false; + assert is_builtin_name("DATABASE_URL", 12) == false; + assert is_builtin_name("XRAILWAY_A", 10) == false; +} From 6ec764151bfef493bef8170d08353c6544d2cb68 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 10:13:09 +0700 Subject: [PATCH 4/4] fix(claw): move_gate states its verdict as a rule, not a copy of push_gate's line writer (#10794) dupe_scan found move_line's body equal to push_gate.push_line. The verdict is now verdict_of(r): the two OK reasons go, everything else holds. t27c and t27b 17/17, 0 vacuous; 21/21 mutants caught; dupe_scan clean; seals 35/35. Co-Authored-By: Claude Opus 5.5 --- .trinity/seals/claw_ClawMoveGate.json | 16 ++++++++-------- specs/claw/move_gate.t27 | 26 ++++++++++++++++++++------ 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/.trinity/seals/claw_ClawMoveGate.json b/.trinity/seals/claw_ClawMoveGate.json index c82e436620..0779a0f1b7 100644 --- a/.trinity/seals/claw_ClawMoveGate.json +++ b/.trinity/seals/claw_ClawMoveGate.json @@ -7,23 +7,23 @@ } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:9516cf037c00f79d0e80c430bafe58d4b260621c69f3701cfcb0c9d68264a7ee", - "gen_hash_rust": "sha256:2e849cf39914c540719fa038f5447a244f21bba5196524c55b882586ba84bb20", - "gen_hash_verilog": "sha256:97b887bfeb0f84b184c9aa94df3c4a4325aadedb6e8e4f62c5890cc33aad8803", - "gen_hash_zig": "sha256:945070a7d79625831e84e0b8af560c94fcf5242dc9101c27bf95698af28c9782", + "gen_hash_c": "sha256:cb40a79b66fe668a64d2a2c12c2517435f3c1197b8a65c13e0ba5db5d2732648", + "gen_hash_rust": "sha256:27f70c20627453be0d36ac43f5159fc14fee0c0a74c25faaae9f2726ab5b0e45", + "gen_hash_verilog": "sha256:fd7b0e8658131c8bf55763d40a1a19ed65e9b28d280ff5075202a18a89b302e6", + "gen_hash_zig": "sha256:6ecca30250f05ea00e7dc5df0dbc994b8e9d98550fd3a77fcf25b987d1144505", "module": "ClawMoveGate", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-19T02:56:22Z", + "sealed_at": "2026-10-19T03:12:51Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:3c459b5fd23c4b534e59cdd752b69a2e214657a59566b8e266be32541a9f336b", + "spec_hash": "sha256:6fe7791e9d0a834195514dcf4ab926ed609503213e0c316e80cbcf7e14d1bd1c", "spec_path": "specs/claw/move_gate.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 16, - "total": 16, + "passed": 17, + "total": 17, "vacuous": 0 }, "toolchain": { diff --git a/specs/claw/move_gate.t27 b/specs/claw/move_gate.t27 index ff32e28bf9..50ff4a3247 100644 --- a/specs/claw/move_gate.t27 +++ b/specs/claw/move_gate.t27 @@ -55,9 +55,6 @@ pub const R_REBUILD_OK : u8 = 5; pub const R_BAD : u8 = 6; pub const R_COUNT : usize = 7; -/// The verdict of each reason, in reason order. -pub const REASON_VERDICT : [7]u8 = [0, 1, 1, 1, 1, 0, 1]; - pub const VERDICT_TEXTS : [2]str = ["V\tgo\t", "V\thold\t"]; pub const VERDICT_LENS : [2]usize = [5, 7]; @@ -78,7 +75,7 @@ pub const INTERNAL_HOST_LEN : usize = 17; pub const BUILTIN_PREFIX : str = "RAILWAY_"; pub const BUILTIN_PREFIX_LEN : usize = 8; -/// The reason for one move; its verdict is REASON_VERDICT[reason]. +/// The reason for one move; its verdict is verdict_of(reason). pub fn move_reason(kind : u8, can_create : bool, internal_refs : u32, loops_on : u32, old_running : bool, state_volumes : u32) -> u8 { if (kind != M_TRANSFER and kind != M_REBUILD) { return R_BAD; } if (!can_create) { return R_TARGET_CLOSED; } @@ -89,8 +86,14 @@ pub fn move_reason(kind : u8, can_create : bool, internal_refs : u32, loops_on : return R_REBUILD_OK; } +/// The two OK reasons go; every other reason, and any value out of range, holds. +pub fn verdict_of(r : u8) -> u8 { + if (r == R_TRANSFER_OK or r == R_REBUILD_OK) { return V_GO; } + return V_HOLD; +} + pub fn move_verdict(kind : u8, can_create : bool, internal_refs : u32, loops_on : u32, old_running : bool, state_volumes : u32) -> u8 { - return REASON_VERDICT[move_reason(kind, can_create, internal_refs, loops_on, old_running, state_volumes)]; + return verdict_of(move_reason(kind, can_create, internal_refs, loops_on, old_running, state_volumes)); } /// The verdict line for reason r into out at w, clipped at cap; returns the new cursor, which still @@ -98,7 +101,7 @@ pub fn move_verdict(kind : u8, can_create : bool, internal_refs : u32, loops_on pub fn move_line(r : u8, out : []u8, cap : usize, w : usize) -> usize { var k : usize = r as usize; if (k >= R_COUNT) { k = R_BAD as usize; } - const v : usize = REASON_VERDICT[k] as usize; + const v : usize = verdict_of(k as u8) 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], WHY_LENS[k]); } @@ -172,6 +175,17 @@ test an_unknown_move_kind_holds_even_with_everything_clear { assert move_verdict(2, true, 0, 0, false, 0) == V_HOLD; } +test only_the_two_ok_reasons_go_and_out_of_range_holds { + var r : u32 = 0; + var gos : u32 = 0; + while (r < 256) { + if (verdict_of(r as u8) == V_GO) { gos += 1; assert r == 0 or r == 5; } + r += 1; + } + assert gos == 2; + assert verdict_of(7) == V_HOLD; +} + test only_the_two_ok_reasons_write_a_go_line { var out : [128]u8 = undefined; var r : u8 = 0;