From cbd79c776943e1ea548e7a9c758d3e3a3a9539a1 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 06:00:30 +0700 Subject: [PATCH 1/3] t27f: deps view writes the use lines t27c does not resolve as unknown (Closes #10702) deps_unknown reads the focus spec's source next to the use-edges list and writes one `unknownpath` line per use line with no edge to its whole path or its single fallback, mirroring bootstrap/src/use_resolve.rs. Measured on the Railway lab, master f33f269d5: 2489 use lines, 23 unknown in 13 specs, byte for byte the `t27c gen` "resolves to no spec" warnings; the drop-one and empty controls both fail. Spec 25/25 tests, 0 vacuous; corpus ratchet CLEAN. The t27f skill gains the oracle recipe. Co-Authored-By: Claude Opus 5.5 --- .claude/skills/t27f/SKILL.md | 20 ++- .trinity/seals/t27f_t27f_spec_deps.json | 16 +- specs/t27f/spec_deps.t27 | 222 +++++++++++++++++++++++- 3 files changed, 247 insertions(+), 11 deletions(-) diff --git a/.claude/skills/t27f/SKILL.md b/.claude/skills/t27f/SKILL.md index 41202f3b4d..918e37c92f 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); 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 | +| 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. A `use` line t27c does not resolve is not in the edge list, so `deps_unknown` (t27#10702) reads the focus source and writes each such line as `unknown\tpath`, mirroring `use_resolve.rs` (whole path, then one fallback level) without looking for a file; on master f33f269d5 that was 23 lines in 13 specs, byte for byte the `t27c gen` warnings. Limits: 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 | @@ -366,6 +366,24 @@ Railway lab. Do not commit it. 4. Remove the scratch spec, and `git checkout` any spec you swapped in, before you seal anything. +### Check that the unknown imports match t27c (t27#10702) + +`deps_unknown` has an oracle. `t27c gen ` prints `warning: :: use X resolves +to no spec` on stderr for every `use` line it drops (#7176), so the two lists must agree byte +for byte. + +1. On the lab, before any scratch spec is in `specs/`, write three files: + - the gen warnings over every spec (`t27c gen $f > o.zig 2>> warn.txt` over + `git ls-files 'specs/*.t27'`; never write to or read from `/dev/null` there); + - the `use-edges` list; + - every `use` line as `specline` (`grep -E '^[[:space:]]*use '`). + Check that none of them holds `"`, `\` or `$` before you embed it. +2. The scratch spec copies each use line into a small buffer and calls `deps_unknown` with that + line's spec as the focus. It prefixes each output line with the spec and expects + `specunknownX` in `git ls-files` order, which is the order gen ran in. +3. Controls: the same list minus one line must FAIL, and so must `w == 0`. On master f33f269d5 + it was 23 lines in 13 specs (#7176 had counted 119 in 70), and both controls failed. + ## 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 a711a4adc5..861fdba625 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:24377e650e9303759aa6b0b805604ae6d9529c45c10e33242c8be73da902067e", - "gen_hash_rust": "sha256:1753610b6d2287d2dec15cd904813eeb22db4208ecfb98b90cec0e34b76274e9", - "gen_hash_verilog": "sha256:ca51cd614290ebde62e75fa09618241eb221062882dde6d86248a1c28666469b", - "gen_hash_zig": "sha256:add0c9a74743b291f6e2ff110a93868e2a777a7ba99d3126745dc07922ac444e", + "gen_hash_c": "sha256:a827ac501c5bc3a6cb34fb9eed1a765efaf6c65dcaef25c64ff4ab34e053f47f", + "gen_hash_rust": "sha256:ecd73ef257fce64599a99a522559ac91b0fe6a1a364863f163584bdd253e0673", + "gen_hash_verilog": "sha256:47a568b333eb16e44c62b907f678ab98e42ea629611d36a2d52edd91a95e4023", + "gen_hash_zig": "sha256:8082c63e31383157aaa1771ec8d4feaf6b1090da42127267c41b8a8afad06251", "module": "t27f_spec_deps", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-18T22:30:43Z", + "sealed_at": "2026-10-18T22:58:06Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:b4a87ecfd782bbd552d561fbb39a43b23d8ab4f174b30e1045d52c5cef16d2bb", + "spec_hash": "sha256:c7b44f43776b8a7b0d21aee8a73502e996ec9fb35cd56935bd300b4358f8d4be", "spec_path": "specs/t27f/spec_deps.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 19, - "total": 19, + "passed": 25, + "total": 25, "vacuous": 0 }, "toolchain": { diff --git a/specs/t27f/spec_deps.t27 b/specs/t27f/spec_deps.t27 index e9323116b3..ab0659e6c0 100644 --- a/specs/t27f/spec_deps.t27 +++ b/specs/t27f/spec_deps.t27 @@ -17,12 +17,16 @@ ; 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. ; deps_cycles asks the same question of every importer and lists each spec flagged, once; +; - a `use` line of the focus is unknown when the focus has no edge to the spec its path names, nor +; to the one module an item form falls back to (a brace list's module, or the path one level up); ; - 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); +; - a `use` line t27c does not resolve is not in the edge list. deps_unknown (#10702) reads the +; focus's own source beside the list and writes each such line as `unknownpath`: an import +; whose target nobody knows, not left out. It reads a line the way bootstrap/src/use_resolve.rs +; does, and never looks for a file; ; - 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; @@ -37,6 +41,10 @@ ; 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. +; Measured for deps_unknown, master f33f269d5 (2466 edge lines, 2489 `use` lines), in the same kind of +; scratch test: over every spec it writes 23 unknown lines in 13 specs, byte for byte the +; `use X resolves to no spec` warnings `t27c gen` prints for every spec (#7176 counted 119 in 70). +; The control that drops one expected line fails. ; phi^2 + 1/phi^2 = 3 | TRINITY module t27f_spec_deps; @@ -73,6 +81,10 @@ pub const DEPS_EXACT : str = "\texact\n$"; pub const DEPS_REFUSED : str = "refused\t$"; pub const DEPS_WHY_FULL : str = "full\t$"; pub const DEPS_WHY_DEEP : str = "deep\t$"; +pub const DEPS_UNKNOWN : str = "unknown\t$"; + +/// The directory `t27c use-edges --specs-dir specs` keys its paths under. +pub const DEPS_ROOT : str = "specs$"; /// Is s[f..t] one of the newline-terminated lines of set[lo..hi]? pub fn deps_in_range(set: []const u8, lo: usize, hi: usize, s: []const u8, f: usize, t: usize) -> bool { @@ -321,6 +333,151 @@ pub fn deps_view(buf: []const u8, n: usize, focus: []const u8, ff: usize, ft: us return w; } +// ---- use lines the edge list does not resolve (#10702) ---- +// The resolver is bootstrap/src/use_resolve.rs. These functions read a `use` line the way its +// use_path_expr, use_path, brace_module and use_target do. They never look for a file: a line counts +// as resolved when the edge list has an edge from the focus to the spec the line names. + +/// Is c a byte the resolver trims? +pub fn deps_space(c: u8) -> bool { + return c == ' ' or c == '\t' or c == '\r'; +} + +/// Where the module path of the line src[s..t] starts, or t when the line is not `use ` after spaces. +pub fn deps_use_from(src: []const u8, s: usize, t: usize) -> usize { + var i : usize = s; + while (i < t and deps_space(src[i])) { i += 1; } + if (i + 4 > t) { return t; } + if (src[i] != 'u' or src[i + 1] != 's' or src[i + 2] != 'e' or src[i + 3] != ' ') { return t; } + i += 4; + while (i < t and deps_space(src[i])) { i += 1; } + return i; +} + +/// Where the module path from f ends on the line ending at t: before a `//` comment, then before +/// trailing spaces, the trailing ';'s and the spaces before them. +pub fn deps_use_to(src: []const u8, f: usize, t: usize) -> usize { + var e : usize = t; + var j : usize = f; + while (j + 1 < t and e == t) { + if (src[j] == '/' and src[j + 1] == '/') { e = j; } + j += 1; + } + while (e > f and deps_space(src[e - 1])) { e -= 1; } + while (e > f and src[e - 1] == ';') { e -= 1; } + while (e > f and deps_space(src[e - 1])) { e -= 1; } + return e; +} + +/// Does src[f..e] read as an import: not empty, and a path with `::` or a word with no space? +pub fn deps_use_ok(src: []const u8, f: usize, e: usize) -> bool { + if (e <= f) { return false; } + var sep : bool = false; + var sp : bool = false; + var i : usize = f; + while (i < e) { + if (src[i] == ':' and i + 1 < e and src[i + 1] == ':') { sep = true; } + if (src[i] == ' ') { sp = true; } + i += 1; + } + return sep or sp == false; +} + +/// Is buf[ts..te] the path root[0..rn]/.t27? +pub fn deps_mod_eq(buf: []const u8, ts: usize, te: usize, root: []const u8, rn: usize, src: []const u8, ps: usize, pe: usize) -> bool { + if (te < ts + rn + 5) { return false; } + if (same(buf, ts, ts + rn, root, 0, rn) == false) { return false; } + if (buf[ts + rn] != '/') { return false; } + var b : usize = ts + rn + 1; + var i : usize = ps; + while (i < pe) { + var c : u8 = src[i]; + var step : usize = 1; + if (c == ':' and i + 1 < pe and src[i + 1] == ':') { c = '/'; step = 2; } + if (c == '.') { c = '/'; } + if (b >= te) { return false; } + if (buf[b] != c) { return false; } + b += 1; + i += step; + } + return same(buf, b, te, ".t27", 0, 4); +} + +/// The end of the module the line's item form falls back to, from ps: the path before a brace list +/// (trailing spaces, then `::`s, then '.'s cut off), or else the path before its last `::` or '.'. +/// Returns ps when there is none, so nothing is matched: one level only, never two. +pub fn deps_fallback_to(src: []const u8, ps: usize, pe: usize) -> usize { + var i : usize = ps; + while (i < pe) { + if (src[i] == '{') { + var e : usize = i; + while (e > ps and deps_space(src[e - 1])) { e -= 1; } + while (e >= ps + 2 and src[e - 1] == ':' and src[e - 2] == ':') { e -= 2; } + while (e > ps and src[e - 1] == '.') { e -= 1; } + return e; + } + i += 1; + } + var last : usize = ps; + i = ps; + while (i < pe) { + if (src[i] == ':' and i + 1 < pe and src[i + 1] == ':') { + last = i; + i += 2; + } else { + if (src[i] == '.') { last = i; } + i += 1; + } + } + return last; +} + +/// Does the edge list buf[0..n] hold an edge from focus[ff..ft] to the module src[ps..pe]? +pub fn deps_imports_mod(buf: []const u8, n: usize, focus: []const u8, ff: usize, ft: usize, root: []const u8, rn: usize, src: []const u8, ps: usize, pe: usize) -> bool { + if (pe <= ps) { return false; } + 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) { + if (same(buf, s, p, focus, ff, ft) and deps_mod_eq(buf, p + 1, t, root, rn, src, ps, pe)) { return true; } + } + s = e + 1; + } + return false; +} + +/// One `unknownpath` line for each `use` line of the focus's source src[0..sn] that the edge list +/// buf[0..n] does not resolve, in source order, into out[0..cap]; root[0..rn] is the directory the edge +/// list's paths start with (DEPS_ROOT). A line is resolved when the focus has an edge to the spec its +/// whole path names, or to its one fallback (deps_fallback_to). Returns the bytes written, or cap + 1 +/// when they do not fit: refused whole. +pub fn deps_unknown(buf: []const u8, n: usize, focus: []const u8, ff: usize, ft: usize, root: []const u8, rn: usize, src: []const u8, sn: usize, out: []u8, cap: usize) -> usize { + var w : usize = 0; + var s : usize = 0; + while (s < sn) { + const e : usize = line_end(src, s, sn); + const f : usize = deps_use_from(src, s, e); + if (f < e) { + const pe : usize = deps_use_to(src, f, e); + if (deps_use_ok(src, f, pe)) { + const fb : usize = deps_fallback_to(src, f, pe); + const whole : bool = deps_imports_mod(buf, n, focus, ff, ft, root, rn, src, f, pe); + const part : bool = deps_imports_mod(buf, n, focus, ff, ft, root, rn, src, f, fb); + if (whole == false and part == false) { + w = deps_put_lit(out, w, cap, DEPS_UNKNOWN); + w = deps_put_span(out, w, cap, src, f, pe); + w = put(out, w, cap, '\n'); + } + } + } + s = e + 1; + } + if (w > cap) { return cap + 1; } + return w; +} + // ---- tests ---- // A diamond under the focus a (a imports b and c, both import d), two importers above it (e imports @@ -541,3 +698,64 @@ test negative_control_a_list_that_does_not_fit_is_refused_whole { 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; } + +// The focus specs/f.t27 imports specs/a/b.t27 and specs/m.t27. specs/g.t27 imports specs/z.t27, an +// edge that is not the focus's. +pub const DEPS_UNK_EDGES : str = "specs/f.t27\tspecs/a/b.t27\nspecs/f.t27\tspecs/m.t27\nspecs/g.t27\tspecs/z.t27\n$"; + +test every_form_that_resolves_writes_nothing { + var out : [256]u8 = undefined; + const g : str = DEPS_UNK_EDGES; + const src : str = "module f;\nuse a::b;\nuse a::b::Item;\nuse m::{X, Y};\nuse m.k;\nuse a::b ;; // whole, with a comment\n$"; + assert deps_unknown(g, lit_len(g), "specs/f.t27", 0, 11, DEPS_ROOT, 5, src, lit_len(src), &out, 256) == 0; +} + +test each_line_that_resolves_to_no_spec_is_one_unknown_line { + var out : [256]u8 = undefined; + const g : str = DEPS_UNK_EDGES; + const src : str = "module f;\nuse q::r; // a note\nuse a::b;\nuse a::b::c::Item;\nuse z;\nuse n::{P};\n use base::types;\nlet use_count = 1;\n; use q::s;\n$"; + const w : usize = deps_unknown(g, lit_len(g), "specs/f.t27", 0, 11, DEPS_ROOT, 5, src, lit_len(src), &out, 256); + const want : str = "unknown\tq::r\nunknown\ta::b::c::Item\nunknown\tz\nunknown\tn::{P}\nunknown\tbase::types\n$"; + assert w == lit_len(want); + assert same(&out, 0, w, want, 0, lit_len(want)) == true; +} + +test negative_control_an_item_form_falls_back_one_level_only { + var out : [64]u8 = undefined; + const g : str = DEPS_UNK_EDGES; + const one : str = "use a::b::Item;\n$"; + const two : str = "use a::b::c::Item;\n$"; + assert deps_unknown(g, lit_len(g), "specs/f.t27", 0, 11, DEPS_ROOT, 5, one, lit_len(one), &out, 64) == 0; + assert deps_unknown(g, lit_len(g), "specs/f.t27", 0, 11, DEPS_ROOT, 5, two, lit_len(two), &out, 64) == 22; + assert deps_fallback_to(two, 4, 17) == 11; + assert deps_fallback_to("use m::{X}\n$", 4, 10) == 5; + assert deps_fallback_to("use z\n$", 4, 5) == 4; +} + +test negative_control_another_spec_edge_does_not_resolve_the_focus_line { + var out : [64]u8 = undefined; + const g : str = DEPS_UNK_EDGES; + const src : str = "use z;\n$"; + assert deps_unknown(g, lit_len(g), "specs/g.t27", 0, 11, DEPS_ROOT, 5, src, lit_len(src), &out, 64) == 0; + assert deps_unknown(g, lit_len(g), "specs/f.t27", 0, 11, DEPS_ROOT, 5, src, lit_len(src), &out, 64) == 10; +} + +test negative_control_an_unknown_list_that_does_not_fit_is_refused_whole { + var out : [64]u8 = undefined; + const g : str = DEPS_UNK_EDGES; + const src : str = "use q::r;\nuse z;\n$"; + assert deps_unknown(g, lit_len(g), "specs/f.t27", 0, 11, DEPS_ROOT, 5, src, lit_len(src), &out, 23) == 23; + assert deps_unknown(g, lit_len(g), "specs/f.t27", 0, 11, DEPS_ROOT, 5, src, lit_len(src), &out, 22) == 23; +} + +test the_path_reader_matches_the_resolver { + const l : str = " use a::b.c ;; // x\n$"; + const f : usize = deps_use_from(l, 0, 21); + assert f == 7; + assert deps_use_to(l, f, 21) == 13; + assert deps_use_ok(l, f, 13) == true; + assert deps_use_ok("use a b\n$", 4, 7) == false; + assert deps_use_from("use\tx\n$", 0, 5) == 5; + assert deps_mod_eq("specs/a/b/c.t27$", 0, 15, DEPS_ROOT, 5, l, 7, 13) == true; + assert deps_mod_eq("specs/a/b/c.t2$", 0, 14, DEPS_ROOT, 5, l, 7, 13) == false; +} From 0ccec9c80dcbcb6445cf7bdbbe2868924e24b8f9 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 06:19:00 +0700 Subject: [PATCH 2/3] t27f: deps view answers why A imports B -- one shortest use chain (Closes #10705) deps_why a b writes one shortest chain of edge-list lines from a to b, each exact, a tie taken in edge-list order; `none` when a does not reach b; refused whole when the walk or the output does not fit. It answers the question cargo tree -i, npm explain and pnpm why answer. deps_find is split out of deps_in_range so the backtrack can pick the first edge line in a range whose importer sits one layer closer. Six tests: the shortest chain over DEPS_FIXTURE, a tie in edge-list order, a spec to itself in no hops, none against the edges and off the graph, a cycle crossed both ways, and an output one byte short refused whole. Lab: test-report 31/31, 0 vacuous; a scratch corpus check over 2466 edge lines held hop count = layer and joined hops for every spec reached from lesson_reel, spec_deps and t27f_gate; the one-hop-more control and the last-edge mutation fail. Seal re-saved and verified; corpus ratchet CLEAN. The t27f skill gets the why recipe, the finding that all 13 specs with a dropped use line fail for other reasons first, and the pkill self-match trap. Co-Authored-By: Claude Opus 5.5 --- .claude/skills/t27f/SKILL.md | 24 ++- .trinity/seals/t27f_t27f_spec_deps.json | 16 +- specs/t27f/spec_deps.t27 | 201 +++++++++++++++++++++++- 3 files changed, 228 insertions(+), 13 deletions(-) diff --git a/.claude/skills/t27f/SKILL.md b/.claude/skills/t27f/SKILL.md index 918e37c92f..a77a6ab080 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. A `use` line t27c does not resolve is not in the edge list, so `deps_unknown` (t27#10702) reads the focus source and writes each such line as `unknown\tpath`, mirroring `use_resolve.rs` (whole path, then one fallback level) without looking for a file; on master f33f269d5 that was 23 lines in 13 specs, byte for byte the `t27c gen` warnings. Limits: 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 | +| 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. A `use` line t27c does not resolve is not in the edge list, so `deps_unknown` (t27#10702) reads the focus source and writes each such line as `unknown\tpath`, mirroring `use_resolve.rs` (whole path, then one fallback level) without looking for a file; on master f33f269d5 that was 23 lines in 13 specs, byte for byte the `t27c gen` warnings. `deps_why a b` (t27#10705) writes one shortest chain of edge-list lines from a to b, each `exact`, a tie taken in edge-list order, `none` when a does not reach b, refused whole when it does not fit. Limits: 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 | @@ -166,6 +166,7 @@ Traps from this tick (ffmpeg on the lab): - The lab cgroup has `pids.max` 1000, and a graph with many `drawtext` filters starts 400 to 700 threads. Cap them with `-threads 2 -filter_threads 2 -filter_complex_threads 2` and `-threads 1` on the output. Never run two renders at once. "ff_frame_thread_encoder_init failed" means the cap was hit. Look for a stuck ffmpeg and kill it by pid. - lavfi rejects `d=.900`. Print the duration as `$((m/1000)).$(printf %03d $((m%1000)))`. - `pkill -f` on the loop script leaves its child renders running. Find them with `ps -eo pid,args | grep '[s]h xf'` and kill them by pid. +- Through `fs-lab.sh`, `pkill -f 'why_seal'` also matches the ssh command line that carries it and kills your own session; the output comes back empty. Bracket the first letter (`pkill -f '[w]hy_seal'`) so the pattern does not match itself. A killed session leaves the detached ratchets it started running, so several end up writing one log; list them with `ps -eo pid,etimes,args | grep '[c]orpus-only'` before you read a verdict. **Holding the address when the voice runs long (t27#10629).** If an overlay must stay on screen for a minimum time and must also stay off a crossfade, then the crossfade has to move; the overlay cannot. Work out in frames the latest cut that still leaves the last scene its minimum: cut 1 is capped at `BEAT_CUT1_LAST_MS` = 13249. Prove it is the latest with a one-ms-later control. Only the voices the reel speeds up are affected, so the `cmp` diff is limited to those voices and to crossfade 1's offset. The cost is real: the crossfade starts up to 184 ms under the task's last word, and lesson 1's measured voice (1300/4200/6800) is one of the affected voices. Write a test that bounds that overlap. @@ -383,6 +384,27 @@ for byte. `specunknownX` in `git ls-files` order, which is the order gen ran in. 3. Controls: the same list minus one line must FAIL, and so must `w == 0`. On master f33f269d5 it was 23 lines in 13 specs (#7176 had counted 119 in 70), and both controls failed. +4. All 13 of those specs fail `t27c test-report` for other reasons before the dropped `use` + matters (`std::*` imports, an undeclared `gf32_from_f64`, `Float`, Rust syntax). Repairing a + dropped import alone therefore fixes none of them. Classify a spec before you promise a fix. + +### Check one chain per reached spec (t27#10705) + +`deps_why a b` answers the question that `cargo tree -i`, `npm explain` and `pnpm why` answer: +why does a import b. It writes one shortest chain of edge-list lines. The corpus check holds it +to three properties: + +1. Over the `use-edges` list, call `deps_why focus x` for every spec x the focus reaches. The + chain must have exactly `layer(x)` hops, every hop must be an edge-list line, and each hop + must start where the last one ended: at the focus first, at x last. Count the specs checked + and compare with `deps_count` of the walk, so no spec is skipped. +2. Control: expect one hop more (`zz_check(1)`). It must FAIL. +3. Mutation: drop the last edge from the chain. Two unit tests must FAIL. + +Build the scratch spec by concatenation (head of the template, the escaped edges, then the +tail). A `sed s#@EDGES@#...#` with 2466 lines fails with "Argument list too long". A focus with +no outgoing edge, such as `verified/reuse`, checks only itself, so `> 1` fails for it; that is +the probe, not the function. ## What this skill does not claim diff --git a/.trinity/seals/t27f_t27f_spec_deps.json b/.trinity/seals/t27f_t27f_spec_deps.json index 861fdba625..e9a0a1a744 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:a827ac501c5bc3a6cb34fb9eed1a765efaf6c65dcaef25c64ff4ab34e053f47f", - "gen_hash_rust": "sha256:ecd73ef257fce64599a99a522559ac91b0fe6a1a364863f163584bdd253e0673", - "gen_hash_verilog": "sha256:47a568b333eb16e44c62b907f678ab98e42ea629611d36a2d52edd91a95e4023", - "gen_hash_zig": "sha256:8082c63e31383157aaa1771ec8d4feaf6b1090da42127267c41b8a8afad06251", + "gen_hash_c": "sha256:cf159312ea60356819669f1af08181d3fa7698777fc89621d64814413c9335f6", + "gen_hash_rust": "sha256:912f179d20bcb6f7af05cb7688e3b095fb2ced9c9b1449edb6322d2d3c413e61", + "gen_hash_verilog": "sha256:4950c28e69b7b88e812544714f55a9814f40eb4e94296ec5dc981759630b2750", + "gen_hash_zig": "sha256:963538cd84850fba1814350998a496856a3ba35709ab952b5f9b2965602fdcda", "module": "t27f_spec_deps", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-18T22:58:06Z", + "sealed_at": "2026-10-18T23:14:31Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:c7b44f43776b8a7b0d21aee8a73502e996ec9fb35cd56935bd300b4358f8d4be", + "spec_hash": "sha256:c6e6c03537dd7238ce7cceb937dfe307294ab59d4946de95f7d96e40333cb070", "spec_path": "specs/t27f/spec_deps.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 25, - "total": 25, + "passed": 31, + "total": 31, "vacuous": 0 }, "toolchain": { diff --git a/specs/t27f/spec_deps.t27 b/specs/t27f/spec_deps.t27 index ab0659e6c0..5add6471c0 100644 --- a/specs/t27f/spec_deps.t27 +++ b/specs/t27f/spec_deps.t27 @@ -17,6 +17,8 @@ ; 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. ; deps_cycles asks the same question of every importer and lists each spec flagged, once; +; - deps_why a b is one shortest chain of edge-list lines from a to b, each exact, a tie taken in +; edge-list order; `none` when a does not reach b; ; - a `use` line of the focus is unknown when the focus has no edge to the spec its path names, nor ; to the one module an item form falls back to (a brace list's module, or the path one level up); ; - a neighbourhood that does not fit the caller's room, or is deeper than the depth asked for, is @@ -45,6 +47,11 @@ ; scratch test: over every spec it writes 23 unknown lines in 13 specs, byte for byte the ; `use X resolves to no spec` warnings `t27c gen` prints for every spec (#7176 counted 119 in 70). ; The control that drops one expected line fails. +; Measured for deps_why on the same kind of scratch test, 2466 edge lines: from specs/t27f/lesson_reel.t27, +; specs/t27f/spec_deps.t27 and specs/claw/t27f_gate.t27 to every spec each reaches, the chain is as +; long as the spec's layer, every hop is a line of the list, and the hops join from the focus to the +; spec. The control that expects one hop more fails. A mutation that takes the last edge into a spec +; instead of the first fails two of the tests below. ; phi^2 + 1/phi^2 = 3 | TRINITY module t27f_spec_deps; @@ -86,15 +93,20 @@ pub const DEPS_UNKNOWN : str = "unknown\t$"; /// The directory `t27c use-edges --specs-dir specs` keys its paths under. pub const DEPS_ROOT : str = "specs$"; -/// Is s[f..t] one of the newline-terminated lines of set[lo..hi]? -pub fn deps_in_range(set: []const u8, lo: usize, hi: usize, s: []const u8, f: usize, t: usize) -> bool { +/// Where s[f..t] starts as one of the newline-terminated lines of set[lo..hi], or hi when it is none. +pub fn deps_find(set: []const u8, lo: usize, hi: usize, s: []const u8, f: usize, t: usize) -> usize { var i : usize = lo; while (i < hi) { const e : usize = line_end(set, i, hi); - if (same(set, i, e, s, f, t)) { return true; } + if (same(set, i, e, s, f, t)) { return i; } i = e + 1; } - return false; + return hi; +} + +/// Is s[f..t] one of the newline-terminated lines of set[lo..hi]? +pub fn deps_in_range(set: []const u8, lo: usize, hi: usize, s: []const u8, f: usize, t: usize) -> bool { + return deps_find(set, lo, hi, s, f, t) < hi; } /// How many lines set[lo..hi] holds. @@ -333,6 +345,91 @@ pub fn deps_view(buf: []const u8, n: usize, focus: []const u8, ff: usize, ft: us return w; } +// ---- why one spec imports another (#10705) ---- +// cargo tree -i, npm explain, pnpm why and Bazel somepath answer the question a layer cannot: through +// which chain of imports does a reach b. + +pub const DEPS_WHY : str = "why\t$"; +pub const DEPS_NONE : str = "\tnone\n$"; + +/// One shortest import chain from a[af..at] to b[bf..bt] over the edge list buf[0..n], into out[0..cap]: +/// whyabhops=N, then N lines edgexyexact, in order from a to b; +/// whyabnone when nothing a imports, transitively, is b; +/// refusedfull|deepa when the walk from a does not fit set[0..scap] or DEPS_MAX_DEPTH. +/// The walk is deps_walk's, so b's layer is the length of its shortest chain. Each step back from b +/// takes the first edge-list line into it from the layer before. Every spec of a layer k > 0 has such +/// a line, since the walk added it from one. hs/he hold the chain's specs as spans of set, one per +/// layer (DEPS_BOUNDS entries). Returns the length, or cap + 1 when the answer does not fit. +pub fn deps_why(buf: []const u8, n: usize, a: []const u8, af: usize, at: usize, b: []const u8, bf: usize, bt: usize, set: []u8, scap: usize, bounds: []usize, hs: []usize, he: []usize, out: []u8, cap: usize) -> usize { + const st : u8 = deps_walk(buf, n, a, af, at, DEPS_IMPORTS, DEPS_MAX_DEPTH, set, scap, bounds); + var w : usize = 0; + if (st != DEPS_OK) { + w = deps_put_lit(out, w, cap, DEPS_REFUSED); + if (st == DEPS_ERR_FULL) { + w = deps_put_lit(out, w, cap, DEPS_WHY_FULL); + } else { + w = deps_put_lit(out, w, cap, DEPS_WHY_DEEP); + } + w = deps_put_span(out, w, cap, a, af, at); + w = put(out, w, cap, '\n'); + if (w > cap) { return cap + 1; } + return w; + } + w = deps_put_lit(out, w, cap, DEPS_WHY); + w = deps_put_span(out, w, cap, a, af, at); + w = put(out, w, cap, '\t'); + w = deps_put_span(out, w, cap, b, bf, bt); + const d : usize = deps_depth(bounds, DEPS_MAX_DEPTH); + var l : usize = 0; + var found : bool = false; + while (l <= d and found == false) { + if (deps_in_range(set, bounds[l], bounds[l + 1], b, bf, bt)) { found = true; } else { l += 1; } + } + if (found == false) { + w = deps_put_lit(out, w, cap, DEPS_NONE); + if (w > cap) { return cap + 1; } + return w; + } + w = deps_put_lit(out, w, cap, "\thops=$"); + w = deps_put_dec(out, w, cap, l); + w = put(out, w, cap, '\n'); + hs[l] = deps_find(set, bounds[l], bounds[l + 1], b, bf, bt); + he[l] = line_end(set, hs[l], bounds[l + 1]); + var k : usize = l; + while (k >= 1) { + var s : usize = 0; + var got : bool = false; + while (s < n and got == false) { + 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) { + if (same(buf, p + 1, t, set, hs[k], he[k])) { + const x : usize = deps_find(set, bounds[k - 1], bounds[k], buf, s, p); + if (x < bounds[k]) { + hs[k - 1] = x; + he[k - 1] = line_end(set, x, bounds[k]); + got = true; + } + } + } + s = e + 1; + } + k -= 1; + } + k = 1; + while (k <= l) { + w = deps_put_lit(out, w, cap, DEPS_EDGE); + w = deps_put_span(out, w, cap, set, hs[k - 1], he[k - 1]); + w = put(out, w, cap, '\t'); + w = deps_put_span(out, w, cap, set, hs[k], he[k]); + w = deps_put_lit(out, w, cap, DEPS_EXACT); + k += 1; + } + if (w > cap) { return cap + 1; } + return w; +} + // ---- use lines the edge list does not resolve (#10702) ---- // The resolver is bootstrap/src/use_resolve.rs. These functions read a `use` line the way its // use_path_expr, use_path, brace_module and use_target do. They never look for a file: a line counts @@ -759,3 +856,99 @@ test the_path_reader_matches_the_resolver { assert deps_mod_eq("specs/a/b/c.t27$", 0, 15, DEPS_ROOT, 5, l, 7, 13) == true; assert deps_mod_eq("specs/a/b/c.t2$", 0, 14, DEPS_ROOT, 5, l, 7, 13) == false; } + +// ---- why tests (#10705) ---- + +test why_walks_the_shortest_chain_from_a_spec_to_one_it_imports { + var set : [64]u8 = undefined; + var b : [35]usize = undefined; + var hs : [35]usize = undefined; + var he : [35]usize = undefined; + var out : [256]u8 = undefined; + const g : str = DEPS_FIXTURE; + const w : usize = deps_why(g, lit_len(g), "f", 0, 1, "d", 0, 1, &set, 64, &b, &hs, &he, &out, 256); + const want : str = "why\tf\td\thops=4\nedge\tf\te\texact\nedge\te\ta\texact\nedge\ta\tb\texact\nedge\tb\td\texact\n$"; + assert w == lit_len(want); + assert same(&out, 0, w, want, 0, lit_len(want)) == true; +} + +test why_takes_a_tie_in_edge_list_order { + var set : [64]u8 = undefined; + var b : [35]usize = undefined; + var hs : [35]usize = undefined; + var he : [35]usize = undefined; + var out : [256]u8 = undefined; + const g : str = "a\tc\na\tb\nb\td\nc\td\n$"; + const w : usize = deps_why(g, lit_len(g), "a", 0, 1, "d", 0, 1, &set, 64, &b, &hs, &he, &out, 256); + const want : str = "why\ta\td\thops=2\nedge\ta\tb\texact\nedge\tb\td\texact\n$"; + assert w == lit_len(want); + assert same(&out, 0, w, want, 0, lit_len(want)) == true; + const g2 : str = "a\tc\na\tb\nc\td\nb\td\n$"; + const w2 : usize = deps_why(g2, lit_len(g2), "a", 0, 1, "d", 0, 1, &set, 64, &b, &hs, &he, &out, 256); + const want2 : str = "why\ta\td\thops=2\nedge\ta\tc\texact\nedge\tc\td\texact\n$"; + assert w2 == lit_len(want2); + assert same(&out, 0, w2, want2, 0, lit_len(want2)) == true; +} + +test why_a_spec_reaches_itself_in_no_hops { + var set : [64]u8 = undefined; + var b : [35]usize = undefined; + var hs : [35]usize = undefined; + var he : [35]usize = undefined; + var out : [256]u8 = undefined; + const g : str = DEPS_FIXTURE; + const w : usize = deps_why(g, lit_len(g), "a", 0, 1, "a", 0, 1, &set, 64, &b, &hs, &he, &out, 256); + const want : str = "why\ta\ta\thops=0\n$"; + assert w == lit_len(want); + assert same(&out, 0, w, want, 0, lit_len(want)) == true; +} + +test negative_control_why_says_none_against_the_edges_and_off_the_graph { + var set : [64]u8 = undefined; + var b : [35]usize = undefined; + var hs : [35]usize = undefined; + var he : [35]usize = undefined; + var out : [256]u8 = undefined; + const g : str = DEPS_FIXTURE; + const w : usize = deps_why(g, lit_len(g), "d", 0, 1, "a", 0, 1, &set, 64, &b, &hs, &he, &out, 256); + const want : str = "why\td\ta\tnone\n$"; + assert w == lit_len(want); + assert same(&out, 0, w, want, 0, lit_len(want)) == true; + const w2 : usize = deps_why(g, lit_len(g), "a", 0, 1, "x", 0, 1, &set, 64, &b, &hs, &he, &out, 256); + const want2 : str = "why\ta\tx\tnone\n$"; + assert w2 == lit_len(want2); + assert same(&out, 0, w2, want2, 0, lit_len(want2)) == true; +} + +test why_crosses_a_cycle_both_ways { + var set : [64]u8 = undefined; + var b : [35]usize = undefined; + var hs : [35]usize = undefined; + var he : [35]usize = undefined; + var out : [256]u8 = undefined; + const g : str = "p\tq\nq\tp\n$"; + const w : usize = deps_why(g, lit_len(g), "p", 0, 1, "q", 0, 1, &set, 64, &b, &hs, &he, &out, 256); + const want : str = "why\tp\tq\thops=1\nedge\tp\tq\texact\n$"; + assert w == lit_len(want); + assert same(&out, 0, w, want, 0, lit_len(want)) == true; + const w2 : usize = deps_why(g, lit_len(g), "q", 0, 1, "p", 0, 1, &set, 64, &b, &hs, &he, &out, 256); + const want2 : str = "why\tq\tp\thops=1\nedge\tq\tp\texact\n$"; + assert w2 == lit_len(want2); + assert same(&out, 0, w2, want2, 0, lit_len(want2)) == true; +} + +test negative_control_a_why_that_does_not_fit_is_refused_whole { + var set : [64]u8 = undefined; + var b : [35]usize = undefined; + var hs : [35]usize = undefined; + var he : [35]usize = undefined; + var out : [256]u8 = undefined; + const g : str = DEPS_FIXTURE; + const full : usize = deps_why(g, lit_len(g), "f", 0, 1, "d", 0, 1, &set, 64, &b, &hs, &he, &out, 256); + assert deps_why(g, lit_len(g), "f", 0, 1, "d", 0, 1, &set, 64, &b, &hs, &he, &out, full - 1) == full; + assert deps_why(g, lit_len(g), "f", 0, 1, "d", 0, 1, &set, 64, &b, &hs, &he, &out, full) == full; + const w : usize = deps_why(g, lit_len(g), "f", 0, 1, "d", 0, 1, &set, 5, &b, &hs, &he, &out, 256); + const want : str = "refused\tfull\tf\n$"; + assert w == lit_len(want); + assert same(&out, 0, w, want, 0, lit_len(want)) == true; +} From 7071f97024719a6563253729c24f95323f961d01 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 06:43:36 +0700 Subject: [PATCH 3/3] t27f: why card -- draw one deps_why chain as a social card, box per spec (Closes #10716) specs/t27f/why_card.t27 reads the chain deps_why writes and returns an ffmpeg filter graph for a 1080x1350 card: one box per spec in chain order, every box in spec_graph_view's exact style, a `uses` connector per edge, names without specs/ and .t27 at the largest size that fits. The column is centred in 260..1150 with gaps of at least 30 px for every length from 2 to 7 boxes. Nothing is drawn for none, refused, 0 hops, a broken join, a non-exact edge, an unquotable name or font, a name over 58 bytes, or more than 7 specs; a graph that does not fit is refused whole (cap + 1). The font is a parameter (WHY_FONT): an import of lesson_reel for FONT pulls its scene tree in and does not compile. Skill card t27/why-card (social-visual) with its seal. Lab: test-report 10/10, 0 vacuous; 5 mutants (count, join, step cap, font quote, font cap) each fail one test. Corpus: 20 chains from why_card drew, the longest 7 boxes to verified/device_dna; renders of that card and a 3-box one held 62 pixel checks and white text in every box, and a deliberately wrong expectation missed. Seals verify; corpus ratchet CLEAN. The t27f skill gets the card recipe and three traps: the import of a print module, the open slice t27c gen drops (#6250), and a count check that hides behind a later check. Co-Authored-By: Claude Opus 5.5 --- .claude/skills/t27f/SKILL.md | 30 +- .trinity/seals/skills_skill_t27_why_card.json | 28 + .trinity/seals/t27f_t27f_why_card.json | 49 ++ specs/skills/t27-why-card.t27 | 49 ++ specs/t27f/why_card.t27 | 573 ++++++++++++++++++ 5 files changed, 728 insertions(+), 1 deletion(-) create mode 100644 .trinity/seals/skills_skill_t27_why_card.json create mode 100644 .trinity/seals/t27f_t27f_why_card.json create mode 100644 specs/skills/t27-why-card.t27 create mode 100644 specs/t27f/why_card.t27 diff --git a/.claude/skills/t27f/SKILL.md b/.claude/skills/t27f/SKILL.md index a77a6ab080..7c9107c91a 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. A `use` line t27c does not resolve is not in the edge list, so `deps_unknown` (t27#10702) reads the focus source and writes each such line as `unknown\tpath`, mirroring `use_resolve.rs` (whole path, then one fallback level) without looking for a file; on master f33f269d5 that was 23 lines in 13 specs, byte for byte the `t27c gen` warnings. `deps_why a b` (t27#10705) writes one shortest chain of edge-list lines from a to b, each `exact`, a tie taken in edge-list order, `none` when a does not reach b, refused whole when it does not fit. Limits: 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 | +| 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. A `use` line t27c does not resolve is not in the edge list, so `deps_unknown` (t27#10702) reads the focus source and writes each such line as `unknown\tpath`, mirroring `use_resolve.rs` (whole path, then one fallback level) without looking for a file; on master f33f269d5 that was 23 lines in 13 specs, byte for byte the `t27c gen` warnings. `deps_why a b` (t27#10705) writes one shortest chain of edge-list lines from a to b, each `exact`, a tie taken in edge-list order, `none` when a does not reach b, refused whole when it does not fit; `specs/t27f/why_card.t27` (t27#10716) draws that chain as a 1080x1350 social card, one exact box per spec, and draws nothing for a chain that is not one. Limits: 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 | @@ -406,6 +406,34 @@ tail). A `sed s#@EDGES@#...#` with 2466 lines fails with "Argument list too long no outgoing edge, such as `verified/reuse`, checks only itself, so `> 1` fails for it; that is the probe, not the function. +### Draw the chain as a card (t27#10716) + +`specs/t27f/why_card.t27` turns the chain into an ffmpeg graph. Its main reads the edge list on +stdin, plus `WHY_A`, `WHY_B` and `WHY_FONT`. Skill card: `t27/why-card`. On the lab: + +1. `t27c gen specs/t27f/why_card.t27 > why_card.zig`, then compile it with zig and `-lc`. +2. `t27c use-edges --specs-dir specs | WHY_A=... WHY_B=... WHY_FONT=/root/tf/fonts/jbm-all-800.ttf ./why_card 2> g.txt`. +3. Render with `ffmpeg -nostdin -threads 2 -filter_threads 1 -filter_complex_script g.txt -map '[v]' -frames:v 1 card.png`. +4. Check pixels, never by eye. Crop one pixel (`crop=1:1:x:y`) to rgb24 and compare: box fill + at (106, top+6), stroke at (101, top+48), fill at both inner edges (110 and 970) so a name + never reaches the stroke, backdrop in each gap, the green connector at x=540. Count + `ff ff ff` in each box's text band so every name is drawn, with 0 below the column. Add one + deliberately wrong expectation that must MISS. + +On 8aec8e259 (2474 edge lines): from `why_card` 20 chains drew, up to 7 boxes (to `verified/device_dna`, 6 hops), +and all 62 pixel checks held. + +Traps: + +- **Do not import a print module for one constant.** `use t27f::lesson_reel::FONT` pulls + lesson_reel's whole scene tree into the build, and zig stops on `FLOOR_KEY` undeclared. Pass + the value in (`WHY_FONT`) instead of copying it. +- **`t27c gen` drops an open slice end** (t27#6250): `buf[n..].ptr` becomes `buf[n].ptr`, and + zig says "type 'u8' does not support field access". Write `buf[n..cap].ptr`. +- **A count check can hide behind a later check.** `k != hops` mutated to `k > hops` survived + until a fixture had fewer edges than `hops=` *and* still ended at B. Give every refusal a + fixture that only that refusal catches. + ## 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/skills_skill_t27_why_card.json b/.trinity/seals/skills_skill_t27_why_card.json new file mode 100644 index 0000000000..8b585001c6 --- /dev/null +++ b/.trinity/seals/skills_skill_t27_why_card.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:3e2cb9c5de2ae2c30b6e20883edcd206910c33c77de68d94040bf860d73635ad", + "gen_hash_rust": "sha256:5781a115f9a47ae898623d893de3925ac3cbea9958d7b14f5f5a27aedb735240", + "gen_hash_verilog": "sha256:b2abd22fabe5a482094b79436f85ce56578c8727c910536ce8cfbfbe3f7a240b", + "gen_hash_zig": "sha256:02488bd498760018d2e80d5d7da2719ffa4cd8a031f2033b1945b8cc321a58d7", + "module": "skill_t27_why_card", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T23:40:56Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:7a8a7d317b6876db1c66fc3a2ebb03cde5d21a931feea30e36d80926d26682e5", + "spec_path": "specs/skills/t27-why-card.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 1, + "total": 1, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_t27f_why_card.json b/.trinity/seals/t27f_t27f_why_card.json new file mode 100644 index 0000000000..47ea880754 --- /dev/null +++ b/.trinity/seals/t27f_t27f_why_card.json @@ -0,0 +1,49 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/ci/affected.t27", + "spec_hash": "sha256:38a7bdc245f1974f0d5f1a55a45966decd70098afdb89e882f568da3650ee9f9" + }, + { + "spec": "specs/claw/t27f_gate.t27", + "spec_hash": "sha256:856be049f9ef3de20cce2f7d18ca3b78a5f4a8ad660dddd8f2cf1aa97bc61ee7" + }, + { + "spec": "specs/policy/text_lines.t27", + "spec_hash": "sha256:13c90895a4a44c760cce6b3b65f72f937298633062f040f8489486108ab0c2d1" + }, + { + "spec": "specs/t27f/spec_deps.t27", + "spec_hash": "sha256:c6e6c03537dd7238ce7cceb937dfe307294ab59d4946de95f7d96e40333cb070" + }, + { + "spec": "specs/t27f/spec_graph_view.t27", + "spec_hash": "sha256:d4c318162bf52405208d4016d0505c8037cd648b58feb3ad5240a2695c6730fe" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:8be8c42a1cdddd18fcb9c024e3f3567b9cfc1d01bcf36f3b982698bd6b339678", + "gen_hash_rust": "sha256:b2ec6788661eec77f37b5a37a99a2c6d9d63f9187233e23de67963c8f33cfdbb", + "gen_hash_verilog": "sha256:8e8a23e07c446ddcc960f96865756bd2804f9a0f0001364b41c5b810d1a8add3", + "gen_hash_zig": "sha256:b1db9ea3c5fcf9f466564a67281d6de3616306101f19d76a3561eef484d65eeb", + "module": "t27f_why_card", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T23:40:53Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:01381c902fe4b5702dc26505a0d081faa0895bfaf60ef51a529cfef894a8ae68", + "spec_path": "specs/t27f/why_card.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 10, + "total": 10, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/specs/skills/t27-why-card.t27 b/specs/skills/t27-why-card.t27 new file mode 100644 index 0000000000..7789e9d11a --- /dev/null +++ b/specs/skills/t27-why-card.t27 @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/skills/t27-why-card.t27 -- skill t27/why-card +; Source of truth for the skill card on t27.ai (#/skills?skill=t27/why-card). +; How TRI CLAW answers "why does spec A import spec B" with a picture: pipe +; `t27c use-edges` into why_card with WHY_A, WHY_B and WHY_FONT set, and render the +; graph it prints with ffmpeg as a 1080x1350 card, one box per spec of the shortest +; use chain, each hop an exact edge. The law lives once, in specs/t27f/why_card.t27 +; (card_* functions and their tests), and the chain in specs/t27f/spec_deps.t27 +; (deps_why); this card points at them and copies none. Posting a card is the owner's +; call. There is no SKILL.md behind it yet, so the card is spec-only (see "Witness +; labels" in specs/skills/README.md). Issues gHashTag/t27#10705 and +; gHashTag/t27#10716, epic gHashTag/trinity#1648. +; The JSON the site serves is generated from this file by the real compiler; +; nothing below is parsed with a regex. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module skill_t27_why_card; + +pub const KIND : str = "skill"; +pub const ID : str = "t27/why-card"; +pub const NAME : str = "Why Card"; +pub const REPO : str = "t27"; +; No SKILL.md exists for this card. +pub const SOURCE : str = ""; +pub const SUMMARY_EN : str = "Draw why one spec imports another as a social card: the shortest use chain t27c resolved, one box per spec, every edge exact, and no card at all when the chain does not exist or does not fit"; +pub const COMMAND : str = "/why-card"; +; Specs this skill stands on: the card layout and the chain it draws. +pub const SPECS : [2]str = ["specs/t27f/why_card.t27", "specs/t27f/spec_deps.t27"]; +pub const TAGS : [4]str = ["social-visual", "t27f", "deps", "card"]; +pub const ENABLED : bool = true; +; Default budget; no SKILL.md declares a timeout. +pub const TIMEOUT_MIN : u16 = 30; + +test basic_constants { + assert KIND == "skill"; + assert ID == "t27/why-card"; + assert NAME == "Why Card"; + assert REPO == "t27"; + assert SOURCE == ""; + assert COMMAND == "/why-card"; + assert ENABLED == true; + assert TIMEOUT_MIN == 30; + assert TAGS.len == 4; + assert TAGS[0] == "social-visual"; + assert TAGS[2] == "deps"; + assert SPECS.len == 2; + assert SPECS[0] == "specs/t27f/why_card.t27"; + assert SPECS[1] == "specs/t27f/spec_deps.t27"; +} diff --git a/specs/t27f/why_card.t27 b/specs/t27f/why_card.t27 new file mode 100644 index 0000000000..389148e8dd --- /dev/null +++ b/specs/t27f/why_card.t27 @@ -0,0 +1,573 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/t27f/why_card.t27 -- a social card for one import chain: why spec A imports spec B +; (gHashTag/t27#10716; epic gHashTag/trinity#1648). T27F is the visual spec of .t27 +; (.claude/skills/t27f/SKILL.md, "What T27F is"). specs/t27f/spec_deps.t27's deps_why writes the +; shortest chain of `t27c use-edges` lines from A to B as text; this file lays that chain out as a +; 1080x1350 card, one box per spec from A at the top to B at the bottom, and writes the ffmpeg +; filter graph that draws it. +; -- +; The rules, each with a test below: +; - one box per spec of the chain, in chain order, joined by a `uses` connector per edge. Every +; edge is drawn in the exact style (spec_graph_view's green stroke on its dark tint), because +; every edge of deps_why is a line t27c resolved; +; - a name is the spec's path without `specs/` and `.t27`, in the box at the largest size up to +; CARD_FONT_MAX that fits the box: the lab font is monospace, 600/1000 em per character; +; - the column is centred between CARD_TOP and CARD_BOTTOM, boxes at most CARD_STEP_MAX apart and +; never closer than CARD_GAP_MIN; +; - a chain is drawn only when it is one: a `why ... hops=N` line, then exactly N edge lines that +; start at A, join hop to hop and end at B. `none`, `refused`, 0 hops, a broken join, a name the +; graph cannot quote or that does not fit, and more than CARD_MAX_BOXES specs are not drawn +; (card_graph returns 0). An answer that does not fit the caller's room is refused whole. +; -- +; HOW (main): t27c use-edges --specs-dir specs | WHY_A=specs/x.t27 WHY_B=specs/y.t27 +; WHY_FONT= ./why_card 2> graph.txt, where why_card is +; this file through `t27c gen` and `zig build-exe -lc`. Then ffmpeg -filter_complex_script +; graph.txt -map '[v]' -frames:v 1 card.png. The graph goes to stderr, as lesson_reel's does. The +; font is a parameter, not an import: importing lesson_reel for FONT pulls its whole scene tree +; into this build and does not compile (FLOOR_KEY undeclared). +; -- +; The honest limits: +; - the card shows one chain, the one deps_why picks. Other chains of the same length exist when +; the edge list has ties; the card does not say how many; +; - nothing posts the card. Publishing it is the owner's call, through the social-visual cards +; (specs/skills/t27-social-visual-*.t27); +; - the 600/1000 em advance is the font's, not measured per glyph here; the lab render below +; checks the longest name against the box edge. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module t27f_why_card; + +use policy::text_lines; +use ci::affected; +use claw::t27f_gate; +use t27f::spec_deps; +use t27f::spec_graph_view::VIEW_LABEL_FILL; +use t27f::spec_graph_view::VIEW_LABEL_STROKE; +use t27f::spec_graph_view::VIEW_BACKDROP; +use t27f::spec_graph_view::VIEW_TEXT; + +pub const CARD_W : usize = 1080; +pub const CARD_H : usize = 1350; +pub const CARD_TOP : usize = 260; +pub const CARD_BOTTOM : usize = 1150; +pub const CARD_BOX_X : usize = 100; +pub const CARD_BOX_W : usize = 880; +pub const CARD_BOX_H : usize = 96; +pub const CARD_BORDER : usize = 4; +pub const CARD_PAD : usize = 20; +pub const CARD_STEP_MAX : usize = 190; +pub const CARD_GAP_MIN : usize = 30; +pub const CARD_MAX_BOXES : usize = 7; +pub const CARD_FONT_MAX : usize = 44; +pub const CARD_FONT_MIN : usize = 24; +pub const CARD_NAME_MAX : usize = 58; +pub const CARD_LINE_W : usize = 4; +pub const CARD_LABEL_DX : usize = 20; +pub const CARD_LABEL_FONT : usize = 24; +pub const CARD_HEAD_Y : usize = 110; +pub const CARD_HEAD_FONT : usize = 56; +pub const CARD_SUB_Y : usize = 180; +pub const CARD_SUB_FONT : usize = 32; +pub const CARD_FOOT_Y : usize = 1230; +pub const CARD_FOOT_FONT : usize = 32; +pub const CARD_SITE_Y : usize = 1285; +pub const CARD_SITE_FONT : usize = 36; + +pub const CARD_HEX : str = "0123456789ABCDEF$"; +pub const CARD_HEAD : str = "WHY IT IMPORTS$"; +pub const CARD_SUB : str = "the shortest use chain t27c resolved$"; +pub const CARD_HOP : str = " hop$"; +pub const CARD_HOPS : str = " hops$"; +pub const CARD_FOOT : str = " / every edge exact / t27c use-edges$"; +pub const CARD_SITE : str = "t27.ai$"; +pub const CARD_USES : str = "uses$"; +pub const CARD_PREFIX : str = "specs/$"; +pub const CARD_SUFFIX : str = ".t27$"; +pub const CARD_HOPS_EQ : str = "hops=$"; +pub const CARD_EXACT_TAIL : str = "\texact$"; + +// ---- geometry ---- + +/// How far apart the boxes of an nb-box column are, top to top. +pub fn card_step(nb: usize) -> usize { + if (nb < 2) { return 0; } + const s : usize = (CARD_BOTTOM - CARD_TOP - CARD_BOX_H) / (nb - 1); + if (s > CARD_STEP_MAX) { return CARD_STEP_MAX; } + return s; +} + +/// The top of the first box: the column is centred between CARD_TOP and CARD_BOTTOM. +pub fn card_top(nb: usize) -> usize { + const col : usize = CARD_BOX_H + card_step(nb) * (nb - 1); + return CARD_TOP + (CARD_BOTTOM - CARD_TOP - col) / 2; +} + +/// The top of box k. +pub fn card_box_y(nb: usize, k: usize) -> usize { + return card_top(nb) + k * card_step(nb); +} + +/// The largest text size up to CARD_FONT_MAX at which len characters, 3/5 em each, fit the box +/// between its pads. +pub fn card_font(len: usize) -> usize { + if (len == 0) { return CARD_FONT_MAX; } + const f : usize = (CARD_BOX_W - 2 * CARD_PAD) * 5 / (3 * len); + if (f > CARD_FONT_MAX) { return CARD_FONT_MAX; } + return f; +} + +// ---- names ---- + +/// A byte the graph can carry inside text='...' with expansion=none: letters, digits, _ / . - +pub fn card_quotable(c: u8) -> bool { + if (c >= 'a' and c <= 'z') { return true; } + if (c >= 'A' and c <= 'Z') { return true; } + if (c >= '0' and c <= '9') { return true; } + return c == '_' or c == '/' or c == '.' or c == '-'; +} + +/// Where the shown name of s[f..t] begins: after a leading `specs/`. +pub fn card_name_from(s: []const u8, f: usize, t: usize) -> usize { + const pn : usize = lit_len(CARD_PREFIX); + if (t - f > pn and same(s, f, f + pn, CARD_PREFIX, 0, pn)) { return f + pn; } + return f; +} + +/// Where the shown name of s[f..t] ends: before a trailing `.t27`. +pub fn card_name_to(s: []const u8, f: usize, t: usize) -> usize { + const sn : usize = lit_len(CARD_SUFFIX); + if (t - f > sn and same(s, t - sn, t, CARD_SUFFIX, 0, sn)) { return t - sn; } + return t; +} + +/// Can s[f..t] be drawn: shown name 1..CARD_NAME_MAX bytes, every one quotable. +pub fn card_name_ok(s: []const u8, f: usize, t: usize) -> bool { + const a : usize = card_name_from(s, f, t); + const b : usize = card_name_to(s, a, t); + if (b <= a or b - a > CARD_NAME_MAX) { return false; } + var i : usize = a; + while (i < b) { + if (card_quotable(s[i]) == false) { return false; } + i += 1; + } + return true; +} + +// ---- writing the graph ---- + +/// Write v as 0xRRGGBB. +pub fn card_put_hex(out: []u8, w: usize, cap: usize, v: u32) -> usize { + var p : usize = deps_put_lit(out, w, cap, "0x$"); + var sh : u32 = 20; + var k : usize = 0; + while (k < 6) { + p = put(out, p, cap, CARD_HEX[@as(usize, @intCast((v >> @as(u5, @intCast(sh))) & 15))]); + if (sh >= 4) { sh = sh - 4; } + k += 1; + } + return p; +} + +/// Write one drawbox: thick 0 fills it. +pub fn card_put_box(out: []u8, w: usize, cap: usize, x: usize, y: usize, bw: usize, bh: usize, color: u32, thick: usize) -> usize { + var p : usize = deps_put_lit(out, w, cap, ",drawbox=x=$"); + p = deps_put_dec(out, p, cap, x); + p = deps_put_lit(out, p, cap, ":y=$"); + p = deps_put_dec(out, p, cap, y); + p = deps_put_lit(out, p, cap, ":w=$"); + p = deps_put_dec(out, p, cap, bw); + p = deps_put_lit(out, p, cap, ":h=$"); + p = deps_put_dec(out, p, cap, bh); + p = deps_put_lit(out, p, cap, ":color=$"); + p = card_put_hex(out, p, cap, color); + p = deps_put_lit(out, p, cap, ":t=$"); + if (thick == 0) { + p = deps_put_lit(out, p, cap, "fill$"); + } else { + p = deps_put_dec(out, p, cap, thick); + } + return p; +} + +/// Open a drawtext up to the text's first byte. +pub fn card_text_open(out: []u8, w: usize, cap: usize, font: []const u8, fnl: usize, fs: usize) -> usize { + var p : usize = deps_put_lit(out, w, cap, ",drawtext=fontfile=$"); + p = deps_put_span(out, p, cap, font, 0, fnl); + p = deps_put_lit(out, p, cap, ":expansion=none:fontsize=$"); + p = deps_put_dec(out, p, cap, fs); + p = deps_put_lit(out, p, cap, ":text='$"); + return p; +} + +/// Close a drawtext: its colour, x (0 centres it on the card) and the y of its middle. +pub fn card_text_close(out: []u8, w: usize, cap: usize, color: u32, x: usize, y: usize) -> usize { + var p : usize = deps_put_lit(out, w, cap, "':fontcolor=$"); + p = card_put_hex(out, p, cap, color); + p = deps_put_lit(out, p, cap, ":x=$"); + if (x == 0) { + p = deps_put_lit(out, p, cap, "(w-text_w)/2$"); + } else { + p = deps_put_dec(out, p, cap, x); + } + p = deps_put_lit(out, p, cap, ":y=$"); + p = deps_put_dec(out, p, cap, y); + p = deps_put_lit(out, p, cap, "-text_h/2$"); + return p; +} + +/// Read the chain deps_why wrote into chain[0..cn]: its specs as spans ns/ne (CARD_MAX_BOXES + 1 +/// entries). Returns how many specs, or 0 when the text is not one drawable chain. +pub fn card_chain(chain: []const u8, cn: usize, ns: []usize, ne: []usize) -> usize { + const wn : usize = lit_len(DEPS_WHY); + if (cn < wn or same(chain, 0, wn, DEPS_WHY, 0, wn) == false) { return 0; } + const e0 : usize = line_end(chain, 0, cn); + if (e0 >= cn) { return 0; } + const ta : usize = first_tab(chain, wn, e0); + if (ta >= e0) { return 0; } + const tb : usize = first_tab(chain, ta + 1, e0); + if (tb >= e0) { return 0; } + const hn : usize = lit_len(CARD_HOPS_EQ); + if (e0 - (tb + 1) <= hn or same(chain, tb + 1, tb + 1 + hn, CARD_HOPS_EQ, 0, hn) == false) { return 0; } + var hops : usize = 0; + var i : usize = tb + 1 + hn; + while (i < e0) { + if (chain[i] < '0' or chain[i] > '9') { return 0; } + hops = hops * 10 + @as(usize, @intCast(chain[i] - '0')); + if (hops > CARD_MAX_BOXES) { return 0; } + i += 1; + } + if (hops == 0 or hops + 1 > CARD_MAX_BOXES) { return 0; } + const en : usize = lit_len(DEPS_EDGE); + const xn : usize = lit_len(CARD_EXACT_TAIL); + var s : usize = e0 + 1; + var k : usize = 0; + while (s < cn) { + const e : usize = line_end(chain, s, cn); + if (k >= hops) { return 0; } + if (e - s <= en + xn or same(chain, s, s + en, DEPS_EDGE, 0, en) == false) { return 0; } + if (same(chain, e - xn, e, CARD_EXACT_TAIL, 0, xn) == false) { return 0; } + const p : usize = first_tab(chain, s + en, e - xn); + if (p >= e - xn or p == s + en) { return 0; } + if (k == 0) { + if (same(chain, s + en, p, chain, wn, ta) == false) { return 0; } + ns[0] = s + en; + ne[0] = p; + } else { + if (same(chain, s + en, p, chain, ns[k], ne[k]) == false) { return 0; } + } + ns[k + 1] = p + 1; + ne[k + 1] = e - xn; + k += 1; + s = e + 1; + } + if (k != hops) { return 0; } + if (same(chain, ns[k], ne[k], chain, ta + 1, tb) == false) { return 0; } + var j : usize = 0; + while (j <= k) { + if (card_name_ok(chain, ns[j], ne[j]) == false) { return 0; } + j += 1; + } + return k + 1; +} + +/// The ffmpeg filter graph of the card for chain[0..cn], into out[0..cap]: a CARD_W x CARD_H +/// backdrop, the headline, the column, the footer, ending `[v]`. Returns its length, 0 when the +/// chain is not drawn (card_chain) or the font path font[0..fnl] is empty or holds a byte +/// card_quotable refuses, or cap + 1 when the graph does not fit. +pub fn card_graph(chain: []const u8, cn: usize, font: []const u8, fnl: usize, out: []u8, cap: usize) -> usize { + var ns : [8]usize = undefined; + var ne : [8]usize = undefined; + const nb : usize = card_chain(chain, cn, &ns, &ne); + if (nb == 0 or fnl == 0) { return 0; } + var fi : usize = 0; + while (fi < fnl) { + if (card_quotable(font[fi]) == false) { return 0; } + fi += 1; + } + const ok : u32 = VIEW_LABEL_STROKE[0]; + const tint : u32 = VIEW_LABEL_FILL[0]; + const muted : u32 = VIEW_LABEL_STROKE[3]; + var w : usize = deps_put_lit(out, 0, cap, "color=c=$"); + w = card_put_hex(out, w, cap, VIEW_BACKDROP); + w = deps_put_lit(out, w, cap, ":s=$"); + w = deps_put_dec(out, w, cap, CARD_W); + w = put(out, w, cap, 'x'); + w = deps_put_dec(out, w, cap, CARD_H); + w = deps_put_lit(out, w, cap, ":d=1$"); + w = card_text_open(out, w, cap, font, fnl, CARD_HEAD_FONT); + w = deps_put_lit(out, w, cap, CARD_HEAD); + w = card_text_close(out, w, cap, ok, 0, CARD_HEAD_Y); + w = card_text_open(out, w, cap, font, fnl, CARD_SUB_FONT); + w = deps_put_lit(out, w, cap, CARD_SUB); + w = card_text_close(out, w, cap, muted, 0, CARD_SUB_Y); + const step : usize = card_step(nb); + var k : usize = 0; + while (k < nb) { + const y : usize = card_box_y(nb, k); + w = card_put_box(out, w, cap, CARD_BOX_X, y, CARD_BOX_W, CARD_BOX_H, tint, 0); + w = card_put_box(out, w, cap, CARD_BOX_X, y, CARD_BOX_W, CARD_BOX_H, ok, CARD_BORDER); + const a : usize = card_name_from(chain, ns[k], ne[k]); + const b : usize = card_name_to(chain, a, ne[k]); + w = card_text_open(out, w, cap, font, fnl, card_font(b - a)); + w = deps_put_span(out, w, cap, chain, a, b); + w = card_text_close(out, w, cap, VIEW_TEXT, 0, y + CARD_BOX_H / 2); + if (k + 1 < nb) { + const gap : usize = step - CARD_BOX_H; + w = card_put_box(out, w, cap, CARD_W / 2 - CARD_LINE_W / 2, y + CARD_BOX_H, CARD_LINE_W, gap, ok, 0); + w = card_text_open(out, w, cap, font, fnl, CARD_LABEL_FONT); + w = deps_put_lit(out, w, cap, CARD_USES); + w = card_text_close(out, w, cap, ok, CARD_W / 2 + CARD_LABEL_DX, y + CARD_BOX_H + gap / 2); + } + k += 1; + } + w = card_text_open(out, w, cap, font, fnl, CARD_FOOT_FONT); + w = deps_put_dec(out, w, cap, nb - 1); + if (nb == 2) { + w = deps_put_lit(out, w, cap, CARD_HOP); + } else { + w = deps_put_lit(out, w, cap, CARD_HOPS); + } + w = deps_put_lit(out, w, cap, CARD_FOOT); + w = card_text_close(out, w, cap, muted, 0, CARD_FOOT_Y); + w = card_text_open(out, w, cap, font, fnl, CARD_SITE_FONT); + w = deps_put_lit(out, w, cap, CARD_SITE); + w = card_text_close(out, w, cap, ok, 0, CARD_SITE_Y); + w = deps_put_lit(out, w, cap, "[v]\n$"); + if (w > cap) { return cap + 1; } + return w; +} + +/// How often lit occurs in out[0..w]. +pub fn card_count(out: []const u8, w: usize, lit: []const u8) -> usize { + const ln : usize = lit_len(lit); + var c : usize = 0; + var i : usize = 0; + while (i + ln <= w) { + if (same(out, i, i + ln, lit, 0, ln)) { c += 1; } + i += 1; + } + return c; +} + +// ---- main ---- + +// The environment variable name copied into dst[0..cap]; its length, 0 when unset. Only main calls +// this, so the tests, which never link libc, do not reach getenv. +fn env_copy(name: [*:0]const u8, dst: []u8, cap: usize) -> usize { + const p = std.c.getenv(name); + if (p == null) { + return 0; + } + const s = p.?; + var i : usize = 0; + while (i < cap and s[i] != 0) { + dst[i] = s[i]; + i += 1; + } + return i; +} + +pub fn main() -> void { + var edges : [262144]u8 = undefined; + var n : usize = 0; + var r : isize = 1; + while (r > 0 and n < 262144) { + r = std.c.read(0, edges[n..262144].ptr, 262144 - n); + if (r > 0) { + n += @as(usize, @intCast(r)); + } + } + var a : [256]u8 = undefined; + var b : [256]u8 = undefined; + const an : usize = env_copy("WHY_A", &a, 256); + const bn : usize = env_copy("WHY_B", &b, 256); + var font : [256]u8 = undefined; + const fnl : usize = env_copy("WHY_FONT", &font, 256); + var set : [16384]u8 = undefined; + var bounds : [35]usize = undefined; + var hs : [35]usize = undefined; + var he : [35]usize = undefined; + var chain : [4096]u8 = undefined; + var g : [32768]u8 = undefined; + if (n >= 262144 or an == 0 or bn == 0 or fnl == 0) { + std.debug.print("why_card: need the edge list on stdin (under 256 KiB), WHY_A, WHY_B and WHY_FONT\n", .{}); + return; + } + const cw : usize = deps_why(&edges, n, &a, 0, an, &b, 0, bn, &set, 16384, &bounds, &hs, &he, &chain, 4096); + if (cw > 4096) { + std.debug.print("why_card: the chain does not fit\n", .{}); + return; + } + const gw : usize = card_graph(&chain, cw, &font, fnl, &g, 32768); + if (gw == 0 or gw > 32768) { + std.debug.print("why_card: not drawn\n{s}", .{chain[0..cw]}); + return; + } + std.debug.print("{s}", .{g[0..gw]}); +} + +// ---- tests ---- + +pub const CARD_LONG : str = "a\tb\nb\tc\nc\td\nd\te\ne\tf\nf\tg\ng\th\n$"; +pub const CARD_TEST_FONT : str = "/fonts/mono-800.ttf$"; + +test card_draws_one_box_per_spec_of_the_chain { + var set : [256]u8 = undefined; + var b : [35]usize = undefined; + var hs : [35]usize = undefined; + var he : [35]usize = undefined; + var chain : [256]u8 = undefined; + var out : [8192]u8 = undefined; + const g : str = DEPS_FIXTURE; + const cw : usize = deps_why(g, lit_len(g), "f$", 0, 1, "d$", 0, 1, &set, 256, &b, &hs, &he, &chain, 256); + const w : usize = card_graph(&chain, cw, CARD_TEST_FONT, lit_len(CARD_TEST_FONT), &out, 8192); + assert w > 0; + assert w <= 8192; + assert card_count(&out, w, ",drawbox=$") == 14; + assert card_count(&out, w, ",drawtext=$") == 13; + assert card_count(&out, w, "text='f'$") == 1; + assert card_count(&out, w, "text='e'$") == 1; + assert card_count(&out, w, "text='a'$") == 1; + assert card_count(&out, w, "text='b'$") == 1; + assert card_count(&out, w, "text='d'$") == 1; + assert card_count(&out, w, "text='c'$") == 0; + assert card_count(&out, w, "text='uses'$") == 4; + assert card_count(&out, w, "text='4 hops / every edge exact / t27c use-edges'$") == 1; + assert card_count(&out, w, "color=c=0x0B0D0C:s=1080x1350:d=1$") == 1; + assert card_count(&out, w, ":color=0x00FF88:t=4$") == 5; + assert card_count(&out, w, ":color=0x0C2A1B:t=fill$") == 5; + assert card_count(&out, w, "[v]\n$") == 1; +} + +test card_names_drop_the_specs_prefix_and_suffix { + var out : [8192]u8 = undefined; + const c : str = "why\tspecs/a/b.t27\tspecs/c.t27\thops=1\nedge\tspecs/a/b.t27\tspecs/c.t27\texact\n$"; + const w : usize = card_graph(c, lit_len(c), CARD_TEST_FONT, lit_len(CARD_TEST_FONT), &out, 8192); + assert w > 0; + assert card_count(&out, w, "text='a/b'$") == 1; + assert card_count(&out, w, "text='c'$") == 1; + assert card_count(&out, w, "text='1 hop / $") == 1; + assert card_count(&out, w, "specs/$") == 0; + assert card_count(&out, w, ",drawbox=$") == 5; +} + +test card_every_column_fits_the_card_without_overlap { + var nb : usize = 2; + while (nb <= CARD_MAX_BOXES) { + const top : usize = card_box_y(nb, 0); + const bottom : usize = card_box_y(nb, nb - 1) + CARD_BOX_H; + assert top >= CARD_TOP; + assert bottom <= CARD_BOTTOM; + assert card_step(nb) - CARD_BOX_H >= CARD_GAP_MIN; + assert card_step(nb) <= CARD_STEP_MAX; + assert (top - CARD_TOP) + 1 >= CARD_BOTTOM - bottom; + assert (CARD_BOTTOM - bottom) + 1 >= top - CARD_TOP; + assert CARD_FOOT_Y > bottom + CARD_FOOT_FONT; + assert CARD_SUB_Y + CARD_SUB_FONT < top; + nb += 1; + } + assert CARD_BOX_X * 2 + CARD_BOX_W == CARD_W; +} + +test card_font_fits_the_box_for_every_name_length { + var len : usize = 1; + while (len <= CARD_NAME_MAX) { + const fs : usize = card_font(len); + assert fs >= CARD_FONT_MIN; + assert fs <= CARD_FONT_MAX; + assert fs * len * 3 <= (CARD_BOX_W - 2 * CARD_PAD) * 5; + assert (fs + 1) * len * 3 > (CARD_BOX_W - 2 * CARD_PAD) * 5 or fs == CARD_FONT_MAX; + len += 1; + } + assert card_font(CARD_NAME_MAX + 1) < CARD_FONT_MIN; +} + +test negative_control_a_chain_with_no_answer_is_not_drawn { + var set : [256]u8 = undefined; + var b : [35]usize = undefined; + var hs : [35]usize = undefined; + var he : [35]usize = undefined; + var chain : [256]u8 = undefined; + var out : [8192]u8 = undefined; + const g : str = DEPS_FIXTURE; + const cw : usize = deps_why(g, lit_len(g), "d$", 0, 1, "a$", 0, 1, &set, 256, &b, &hs, &he, &chain, 256); + assert cw > 0; + assert card_graph(&chain, cw, CARD_TEST_FONT, lit_len(CARD_TEST_FONT), &out, 8192) == 0; + const sw : usize = deps_why(g, lit_len(g), "a$", 0, 1, "a$", 0, 1, &set, 256, &b, &hs, &he, &chain, 256); + assert sw > 0; + assert card_graph(&chain, sw, CARD_TEST_FONT, lit_len(CARD_TEST_FONT), &out, 8192) == 0; + const r : str = "refused\tfull\ta\n$"; + assert card_graph(r, lit_len(r), CARD_TEST_FONT, lit_len(CARD_TEST_FONT), &out, 8192) == 0; + assert card_graph(r, 0, CARD_TEST_FONT, lit_len(CARD_TEST_FONT), &out, 8192) == 0; +} + +test negative_control_a_chain_that_does_not_join_is_not_drawn { + var out : [8192]u8 = undefined; + const good : str = "why\ta\tc\thops=2\nedge\ta\tb\texact\nedge\tb\tc\texact\n$"; + assert card_graph(good, lit_len(good), CARD_TEST_FONT, lit_len(CARD_TEST_FONT), &out, 8192) > 0; + const gap : str = "why\ta\tc\thops=2\nedge\ta\tb\texact\nedge\tx\tc\texact\n$"; + assert card_graph(gap, lit_len(gap), CARD_TEST_FONT, lit_len(CARD_TEST_FONT), &out, 8192) == 0; + const start : str = "why\ta\tc\thops=2\nedge\tz\tb\texact\nedge\tb\tc\texact\n$"; + assert card_graph(start, lit_len(start), CARD_TEST_FONT, lit_len(CARD_TEST_FONT), &out, 8192) == 0; + const end : str = "why\ta\tc\thops=2\nedge\ta\tb\texact\nedge\tb\ty\texact\n$"; + assert card_graph(end, lit_len(end), CARD_TEST_FONT, lit_len(CARD_TEST_FONT), &out, 8192) == 0; + const short : str = "why\ta\tc\thops=2\nedge\ta\tb\texact\n$"; + assert card_graph(short, lit_len(short), CARD_TEST_FONT, lit_len(CARD_TEST_FONT), &out, 8192) == 0; + const fewer : str = "why\ta\tc\thops=2\nedge\ta\tc\texact\n$"; + assert card_graph(fewer, lit_len(fewer), CARD_TEST_FONT, lit_len(CARD_TEST_FONT), &out, 8192) == 0; + const long : str = "why\ta\tc\thops=2\nedge\ta\tb\texact\nedge\tb\tc\texact\nedge\tc\td\texact\n$"; + assert card_graph(long, lit_len(long), CARD_TEST_FONT, lit_len(CARD_TEST_FONT), &out, 8192) == 0; + const label : str = "why\ta\tc\thops=2\nedge\ta\tb\texact\nedge\tb\tc\tderived\n$"; + assert card_graph(label, lit_len(label), CARD_TEST_FONT, lit_len(CARD_TEST_FONT), &out, 8192) == 0; +} + +test negative_control_a_name_the_graph_cannot_quote_is_not_drawn { + var out : [8192]u8 = undefined; + const colon : str = "why\ta\tb:c\thops=1\nedge\ta\tb:c\texact\n$"; + assert card_graph(colon, lit_len(colon), CARD_TEST_FONT, lit_len(CARD_TEST_FONT), &out, 8192) == 0; + const quote : str = "why\ta'\tb\thops=1\nedge\ta'\tb\texact\n$"; + assert card_graph(quote, lit_len(quote), CARD_TEST_FONT, lit_len(CARD_TEST_FONT), &out, 8192) == 0; + const fits : str = "why\ta\tspecs/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.t27\thops=1\nedge\ta\tspecs/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.t27\texact\n$"; + assert card_graph(fits, lit_len(fits), CARD_TEST_FONT, lit_len(CARD_TEST_FONT), &out, 8192) > 0; + const over : str = "why\ta\tspecs/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.t27\thops=1\nedge\ta\tspecs/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.t27\texact\n$"; + assert card_graph(over, lit_len(over), CARD_TEST_FONT, lit_len(CARD_TEST_FONT), &out, 8192) == 0; +} + +test negative_control_a_chain_longer_than_the_card_is_not_drawn { + var set : [256]u8 = undefined; + var b : [35]usize = undefined; + var hs : [35]usize = undefined; + var he : [35]usize = undefined; + var chain : [512]u8 = undefined; + var out : [8192]u8 = undefined; + const g : str = CARD_LONG; + const six : usize = deps_why(g, lit_len(g), "a$", 0, 1, "g$", 0, 1, &set, 256, &b, &hs, &he, &chain, 512); + const w6 : usize = card_graph(&chain, six, CARD_TEST_FONT, lit_len(CARD_TEST_FONT), &out, 8192); + assert w6 > 0; + assert card_count(&out, w6, ",drawbox=$") == 20; + const seven : usize = deps_why(g, lit_len(g), "a$", 0, 1, "h$", 0, 1, &set, 256, &b, &hs, &he, &chain, 512); + assert seven > 0; + assert card_graph(&chain, seven, CARD_TEST_FONT, lit_len(CARD_TEST_FONT), &out, 8192) == 0; +} + +test negative_control_a_card_that_does_not_fit_is_refused_whole { + var out : [8192]u8 = undefined; + const c : str = "why\ta\tc\thops=2\nedge\ta\tb\texact\nedge\tb\tc\texact\n$"; + const full : usize = card_graph(c, lit_len(c), CARD_TEST_FONT, lit_len(CARD_TEST_FONT), &out, 8192); + assert full > 0; + assert card_graph(c, lit_len(c), CARD_TEST_FONT, lit_len(CARD_TEST_FONT), &out, full) == full; + assert card_graph(c, lit_len(c), CARD_TEST_FONT, lit_len(CARD_TEST_FONT), &out, full - 1) == full; +} + +test negative_control_a_font_path_the_graph_cannot_quote_is_not_drawn { + var out : [8192]u8 = undefined; + const c : str = "why\ta\tb\thops=1\nedge\ta\tb\texact\n$"; + const w : usize = card_graph(c, lit_len(c), CARD_TEST_FONT, lit_len(CARD_TEST_FONT), &out, 8192); + assert w > 0; + assert card_count(&out, w, "fontfile=/fonts/mono-800.ttf:expansion=none$") == 7; + const bad : str = "/fonts/a b.ttf$"; + assert card_graph(c, lit_len(c), bad, lit_len(bad), &out, 8192) == 0; + const colon : str = "/fonts/a:b.ttf$"; + assert card_graph(c, lit_len(c), colon, lit_len(colon), &out, 8192) == 0; + assert card_graph(c, lit_len(c), CARD_TEST_FONT, 0, &out, 8192) == 0; +}