diff --git a/.claude/skills/t27f/SKILL.md b/.claude/skills/t27f/SKILL.md index eb5da0af3a..755443143e 100644 --- a/.claude/skills/t27f/SKILL.md +++ b/.claude/skills/t27f/SKILL.md @@ -81,7 +81,7 @@ the diagram core in detail, then the other surfaces and the topic-map applicatio | Spec -> graph IR schema (the core) | `specs/t27f/spec_graph.t27` (t27#10020) | spec with tests: node / edge kinds, labels, spans, `validate_graph`, line -> node mapping, node -> ui_plan widget, one hand-read fixture (`specs/tutorial/08_modules_and_arrays.t27`) | | Graph view: layout of a spec graph | `specs/t27f/spec_graph_view.t27` (t27#10034) | spec with tests: one column per node kind, size / colour / dash / row order from the inference label, UNKNOWN nodes alone in a band, every edge with a source line, node -> widget through `node_widget`; numbers only, laid out on the hand-read fixture; drawn by the MCP tool `t27f_graph` (gHashTag/t27-mcp#9) | | Graph view for big specs: wrap, fold, expand, hit | `specs/t27f/spec_graph_fold.t27` (t27#10218) | spec with tests: a kind column taller than N = 12 rows wraps into sub-columns so the canvas aspect is near phi (identical to the view when nothing wraps); a fold to one box per kind with the exact count and one arc per pair of kinds with the exact edge count, each labelled by its weakest member; `expand_graph` = one kind plus its direct neighbours, copied verbatim with an origin map; `wrap_hit` / `wrap_hit_line_start` / `wrap_hit_line_end` take a click to a node and its lines; numbers only, the view's colours are not touched | -| Dependencies view: the specs around one spec | `specs/t27f/spec_deps.t27` (t27#10680) | spec with tests: reads `t27c use-edges --specs-dir specs`; layer 0 is the focus, +k the specs k imports away (right), -k the specs k importers away (left), each spec once in its shortest-path layer; every edge-list line between two drawn specs is one `exact` edge, a repeated line drawn once; `cycle=1` when the focus imports itself or is imported by a spec in its own import closure; a walk past the caller's room or depth is refused whole (`refused\tfull` / `refused\tdeep`), never cut. Limits: `use` lines t27c does not resolve are not drawn (#7176); cycles that miss the focus are not reported; a spec on a cycle through the focus is drawn on both sides; no host draws it yet. Corpus on master e607228f7: 806 specs, 2461 edge lines, nothing refused at depth 32, deepest walks 6, one real cycle (igla race `backend` <-> `rtl`) | +| Dependencies view: the specs around one spec | `specs/t27f/spec_deps.t27` (t27#10680) | spec with tests: reads `t27c use-edges --specs-dir specs`; layer 0 is the focus, +k the specs k imports away (right), -k the specs k importers away (left), each spec once in its shortest-path layer; every edge-list line between two drawn specs is one `exact` edge, a repeated line drawn once; `cycle=1` when the focus imports itself or is imported by a spec in its own import closure; a walk past the caller's room or depth is refused whole (`refused\tfull` / `refused\tdeep`), never cut. Limits: `use` lines t27c does not resolve are not drawn (#7176); the view flags only the focus, and `deps_cycles` (t27#10693) lists every spec on a cycle, each once, refused whole (`cap + 1`) if anything does not fit; a spec on a cycle through the focus is drawn on both sides; no host draws it yet. Corpus on master e607228f7: 806 specs, 2461 edge lines, nothing refused at depth 32, deepest walks 6, one real cycle (igla race `backend` <-> `rtl`), which `deps_cycles` lists exactly on master f7efbc232 and lists as empty with #10689 | | Spec -> graph IR exporter | `specs/t27f/spec_graph_export.t27` (t27#10162) | spec with tests: `export_graph(path, src)` is a deterministic byte scanner over the source, `export_verdict` is `validate_graph` of the result. One node per top-level declaration (a leading `pub ` is allowed), spans to the matching closing brace, every node and edge EXACT or (unclassifiable) OPAQUE + UNKNOWN, CALLS / USES / CONSTRAINS edges with the line of the use, refusal past 1024 nodes or 4096 edges. On the tutorial 08 snapshot it equals the hand-read fixture on all 14 declaration nodes and all 9 EXACT edges; it does not emit the fixture's `std.testing` UNKNOWN node, nor its 7 DERIVED edges (4 data flow, 3 tested_by), because a scanner never derives; the derive pass below adds them. Limits: textual matching without scope or type resolution, no block comments, no struct/enum bodies, no cross-spec `use` resolution, capacity 1024 nodes and 4096 edges, u16 indices (all 2225 specs in specs/ fit, largest 775 nodes and 3194 edges after derive; anything past the cap is refused, never truncated), takes a `[]const u8` slice rather than `[65536]u8` plus a length. AST/IR audit is trinity#1650, pilot choice #1651 | | Graph IR derive pass | `specs/t27f/spec_graph_derive.t27` (t27#10191) | spec with tests: `derive_graph(g, src)` appends what the exporter must not claim and changes nothing it holds. R1 DERIVED `tested_by` = every EXACT CALLS edge from a test, invariant or bench to a fn read backwards (no line). R2 DERIVED `data_flow` = a const or `use` that an EXACT USES edge ties to a fn and that is named inside one of the fn's return statements (from `return` to the next `;`), on the line of the first such name. R3 UNKNOWN opaque node = an undeclared qualified call target of three or more segments (`std.testing.expectEqual(` names `std.testing`), spanning the lines that call through it. Exporter plus derive equal the tutorial 08 fixture as sets: 15 of 15 nodes, 16 of 16 edges, no special case. Limits: names matched as text; call results and flow through locals are not derived; R3 needs 3 or more segments (two cannot be told from a method call); a graph that does not validate, or a source of another line count, comes back unchanged; past 1024 nodes or 4096 edges the result is refused with the too-many verdict. Not wired to a host: the MCP tool does not call it | | UI plan IR + trusted widget catalog | `specs/t27f/ui_plan.t27` (t27#9962) | spec with tests; no renderer draws a plan yet | @@ -325,6 +325,30 @@ Before you change a uniqueness rule, measure what it can draw from: shell heredoc, such as a PR body that mentions tests. Write the body with the Write tool, then pass `--body-file`. +## Check the whole corpus for import cycles (t27#10693) + +dependency-cruiser's `no-circular` and madge's `--circular` list every module on a cycle. +`deps_cycles` in `specs/t27f/spec_deps.t27` gives the same answer for the `use-edges` list. +No t27c subcommand runs a main (t27#10692), so the only host today is a scratch test on the +Railway lab. Do not commit it. + +1. On the lab, write `t27c use-edges --specs-dir specs` to a file **before** you put the + scratch spec in `specs/`. Otherwise its own `use` lines join the list, which is harmless but + changes the count. +2. Build the scratch spec from three parts: + - a head: `use policy::text_lines; use ci::affected; use claw::t27f_gate; use + t27f::spec_deps;`, then a test that opens `const g : str = "`; + - the edges, converted with `sed 's/\t/\\t/g; s/$/\\n/' | tr -d '\n'` (in an `fs-lab.sh` + string the backslashes are doubled again); + - a tail that closes with `$";` and asserts on the result. + Room: the 2462-line corpus fits `set` 16384, `done` 65536 and `out` 4096. +3. Assert `w <= cap` first, so that a refusal is told apart from a wrong list. Then run a + **negative control**: the same edges with the opposite expectation must FAIL. On master + f7efbc232 the list was exactly `backend.t27` and `rtl.t27`. With #10689 it was empty, and + the control that expected the pair failed. +4. Remove the scratch spec, and `git checkout` any spec you swapped in, before you seal + anything. + ## What this skill does not claim The graph IR is a schema (`specs/t27f/spec_graph.t27`) and `specs/t27f/spec_graph_export.t27` turns source into it, but only exact declarations, spans and edges within the IR's capacity, with `specs/t27f/spec_graph_derive.t27` adding the derived and unknown part by stated rules, and no host calls either yet; no renderer draws a T27F view in the Trinity UI, no diff --git a/.trinity/seals/t27f_t27f_spec_deps.json b/.trinity/seals/t27f_t27f_spec_deps.json index 0b91f608f1..a711a4adc5 100644 --- a/.trinity/seals/t27f_t27f_spec_deps.json +++ b/.trinity/seals/t27f_t27f_spec_deps.json @@ -15,23 +15,23 @@ } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:8dfb9c21bc40906375fafd873fc6a53cbc80726a04beaf7ef3a7248ef6e92b5e", - "gen_hash_rust": "sha256:c80e910314b1eb91dff11da6a8b637bcfd411926c341e48b556caa26b394022e", - "gen_hash_verilog": "sha256:80dc78cf2dfdbf055cb38f149f2986575b0453994e2db9f9defdb288b3287be4", - "gen_hash_zig": "sha256:2a4a4580c07b6aee3d5d235f18adac2eb3ae03a04e03a38d59c25c9184e299ba", + "gen_hash_c": "sha256:24377e650e9303759aa6b0b805604ae6d9529c45c10e33242c8be73da902067e", + "gen_hash_rust": "sha256:1753610b6d2287d2dec15cd904813eeb22db4208ecfb98b90cec0e34b76274e9", + "gen_hash_verilog": "sha256:ca51cd614290ebde62e75fa09618241eb221062882dde6d86248a1c28666469b", + "gen_hash_zig": "sha256:add0c9a74743b291f6e2ff110a93868e2a777a7ba99d3126745dc07922ac444e", "module": "t27f_spec_deps", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-18T22:12:14Z", + "sealed_at": "2026-10-18T22:30:43Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:75aafa2bc9e6f0c54b721e34d216a498814a097585ba353f72a6aeb813fa4014", + "spec_hash": "sha256:b4a87ecfd782bbd552d561fbb39a43b23d8ab4f174b30e1045d52c5cef16d2bb", "spec_path": "specs/t27f/spec_deps.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 13, - "total": 13, + "passed": 19, + "total": 19, "vacuous": 0 }, "toolchain": { diff --git a/specs/t27f/spec_deps.t27 b/specs/t27f/spec_deps.t27 index c2234c881d..e9323116b3 100644 --- a/specs/t27f/spec_deps.t27 +++ b/specs/t27f/spec_deps.t27 @@ -15,15 +15,17 @@ ; away. Each spec is in one layer per side, the layer of its shortest path, in edge-list order; ; - every edge drawn is a line of the edge list between two drawn specs, drawn once however often ; the list repeats it, and labelled exact: t27c resolved it, nothing here is inferred; -; - the focus is flagged as on a cycle when it imports itself or a spec it imports imports it; +; - the focus is flagged as on a cycle when it imports itself or a spec it imports imports it. +; deps_cycles asks the same question of every importer and lists each spec flagged, once; ; - a neighbourhood that does not fit the caller's room, or is deeper than the depth asked for, is ; refused whole. Nothing is cut off. ; -- ; The honest limits: ; - a `use` line t27c does not resolve is not in the edge list, so it is not drawn (#7176 counted ; 119 such lines in 70 specs); -; - the flag says only whether the focus is on a cycle. A cycle among other specs is not reported, -; and a spec on a cycle through the focus is drawn on both sides; +; - the view flags only the focus. deps_cycles (#10693) lists every spec on a cycle, but nothing runs +; it on every PR yet: no t27c subcommand runs a main (#10692). A spec on a cycle through the focus +; is drawn on both sides; ; - x and y are numbers for a host, in the spacing of spec_graph_view.t27. No host draws them yet. ; -- ; Measured on the Railway lab, master e607228f7 plus this file: 2461 edge lines over 806 specs. Every @@ -32,6 +34,9 @@ ; specs/base/types.t27, with 400 importers. Two specs are flagged on a cycle, and both flags are ; true: specs/igla/race/backend.t27 and specs/igla/race/rtl.t27 import each other. `tsort` reports ; the same loop on stderr. +; Measured again for deps_cycles, master f7efbc232 (2462 edge lines), with the list embedded in a +; scratch test that was not committed: it lists exactly those two specs. With #10689's rtl.t27, +; which drops its `use igla::race::backend;`, it lists none. ; phi^2 + 1/phi^2 = 3 | TRINITY module t27f_spec_deps; @@ -154,6 +159,33 @@ pub fn deps_on_cycle(buf: []const u8, n: usize, focus: []const u8, ff: usize, ft return false; } +/// Every spec of the edge list buf[0..n] that is on a cycle, one per line, into out[0..cap], in the +/// order of its first line as an importer. A spec that imports nothing is on no cycle, so only +/// importers are walked, each once (done[0..dcap] holds the ones walked). Returns the bytes written, +/// or cap + 1 when the list, a walk's set or the done list does not fit, or a walk is deeper than +/// DEPS_MAX_DEPTH: refused whole, nothing is cut off. +pub fn deps_cycles(buf: []const u8, n: usize, set: []u8, scap: usize, bounds: []usize, done: []u8, dcap: usize, out: []u8, cap: usize) -> usize { + var w : usize = 0; + var dn : usize = 0; + var s : usize = 0; + while (s < n) { + const e : usize = line_end(buf, s, n); + const t : usize = text_end(buf, s, e); + const p : usize = first_tab(buf, s, t); + if (p < t and p > s and in_set(done, dn, buf, s, p) == false) { + dn = set_add(done, dn, dcap, buf, s, p); + if (dn > dcap) { return cap + 1; } + if (deps_walk(buf, n, buf, s, p, DEPS_IMPORTS, DEPS_MAX_DEPTH, set, scap, bounds) != DEPS_OK) { return cap + 1; } + if (deps_on_cycle(buf, n, buf, s, p, set, bounds[deps_depth(bounds, DEPS_MAX_DEPTH) + 1])) { + w = set_add(out, w, cap, buf, s, p); + if (w > cap) { return cap + 1; } + } + } + s = e + 1; + } + return w; +} + /// Write buf[f..t]. pub fn deps_put_span(out: []u8, w: usize, cap: usize, buf: []const u8, f: usize, t: usize) -> usize { var p : usize = w; @@ -439,3 +471,73 @@ test negative_control_a_view_that_does_not_fit_returns_more_than_cap { assert deps_view(g, lit_len(g), "a", 0, 1, 8, &imp, 64, &ib, &rev, 64, &rb, &es, 128, &out, 32) > 32; assert deps_view(g, lit_len(g), "a", 0, 1, 8, &imp, 64, &ib, &rev, 64, &rb, &es, 4, &out, 32) > 32; } + +// The corpus question #10682 asked one spec at a time: which specs are on a cycle? +test the_old_igla_pair_lists_both_specs_and_not_their_importer { + var set : [64]u8 = undefined; + var b : [35]usize = undefined; + var done : [64]u8 = undefined; + var out : [64]u8 = undefined; + const g : str = "a\tt\na\tb\nb\tt\nb\ta\nc\tb\n$"; + const w : usize = deps_cycles(g, lit_len(g), &set, 64, &b, &done, 64, &out, 64); + const want : str = "a\nb\n$"; + assert w == lit_len(want); + assert same(&out, 0, w, want, 0, lit_len(want)) == true; +} + +test negative_control_a_dag_lists_no_spec { + var set : [64]u8 = undefined; + var b : [35]usize = undefined; + var done : [64]u8 = undefined; + var out : [64]u8 = undefined; + const g : str = DEPS_FIXTURE; + assert deps_cycles(g, lit_len(g), &set, 64, &b, &done, 64, &out, 64) == 0; +} + +test a_cycle_the_first_spec_is_not_on_lists_only_the_cycle { + var set : [64]u8 = undefined; + var b : [35]usize = undefined; + var done : [64]u8 = undefined; + var out : [64]u8 = undefined; + const g : str = "a\tb\nb\tc\nc\tb\n$"; + const w : usize = deps_cycles(g, lit_len(g), &set, 64, &b, &done, 64, &out, 64); + const want : str = "b\nc\n$"; + assert w == lit_len(want); + assert same(&out, 0, w, want, 0, lit_len(want)) == true; +} + +test a_self_import_is_a_cycle_of_one { + var set : [64]u8 = undefined; + var b : [35]usize = undefined; + var done : [64]u8 = undefined; + var out : [64]u8 = undefined; + const g : str = "a\ta\n$"; + const w : usize = deps_cycles(g, lit_len(g), &set, 64, &b, &done, 64, &out, 64); + const want : str = "a\n$"; + assert w == lit_len(want); + assert same(&out, 0, w, want, 0, lit_len(want)) == true; +} + +test two_cycles_and_a_repeated_line_list_each_spec_once { + var set : [64]u8 = undefined; + var b : [35]usize = undefined; + var done : [64]u8 = undefined; + var out : [64]u8 = undefined; + const g : str = "p\tq\nq\tp\nx\ty\ny\tz\nz\tx\np\tq\n$"; + const w : usize = deps_cycles(g, lit_len(g), &set, 64, &b, &done, 64, &out, 64); + const want : str = "p\nq\nx\ny\nz\n$"; + assert w == lit_len(want); + assert same(&out, 0, w, want, 0, lit_len(want)) == true; +} + +test negative_control_a_list_that_does_not_fit_is_refused_whole { + var set : [64]u8 = undefined; + var b : [35]usize = undefined; + var done : [64]u8 = undefined; + var out : [64]u8 = undefined; + const g : str = "p\tq\nq\tp\nx\ty\ny\tz\nz\tx\n$"; + assert deps_cycles(g, lit_len(g), &set, 64, &b, &done, 64, &out, 9) == 10; + assert deps_cycles(g, lit_len(g), &set, 64, &b, &done, 64, &out, 10) == 10; + assert deps_cycles(g, lit_len(g), &set, 64, &b, &done, 9, &out, 64) == 65; + assert deps_cycles(g, lit_len(g), &set, 3, &b, &done, 64, &out, 64) == 65; +}