Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion .claude/skills/t27f/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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
Expand Down
16 changes: 8 additions & 8 deletions .trinity/seals/t27f_t27f_spec_deps.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
108 changes: 105 additions & 3 deletions specs/t27f/spec_deps.t27
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Loading