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; +}