From d6f9478d69918eedc6affc1143d44f0dc773a702 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 18 Oct 2026 22:32:10 +0700 Subject: [PATCH 01/27] spec(t27mcp): visual_output.t27 -- every answer carries a T27F visual, by host, readable or redrawn One rule for the t27 MCP server's instructions and TRI CLAW's chat prompt: needs_visual, answer_channel (desktop inline widget, TRI CLAW image URL, markdown table fallback), inline_readable (font * 680 >= 11 * width), inline_source (embed or redraw from t27f_outline), answer_verdict. Lab: t27c test-report 9/9, 0 vacuous; a mutant readability rule fails 3. Co-Authored-By: Claude Opus 5.5 --- .trinity/seals/t27mcp_T27mcpVisualOutput.json | 28 ++ specs/t27mcp/visual_output.t27 | 243 ++++++++++++++++++ 2 files changed, 271 insertions(+) create mode 100644 .trinity/seals/t27mcp_T27mcpVisualOutput.json create mode 100644 specs/t27mcp/visual_output.t27 diff --git a/.trinity/seals/t27mcp_T27mcpVisualOutput.json b/.trinity/seals/t27mcp_T27mcpVisualOutput.json new file mode 100644 index 0000000000..53bca11e6d --- /dev/null +++ b/.trinity/seals/t27mcp_T27mcpVisualOutput.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:076a0683dcc4e8487922bd6ff561422a1dfa707069e639fffaafea9d70ad8e2d", + "gen_hash_rust": "sha256:6267998500edaecfe3378c6ff7ed7482d1b0c239e1b8480fa697a8fd05b0d7b8", + "gen_hash_verilog": "sha256:241e62e3030919b4f540ff38480f8d726a4cc8301fbc30443369e7978adbbb1b", + "gen_hash_zig": "sha256:e7446390463513c4496f484f375ef59ae2f260172c8e46326f21008d84860411", + "module": "T27mcpVisualOutput", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T15:31:37Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:06c1c5e73142e2f79fa5d7be037eace63c1f20597a6e33274ebf8e1b428b7584", + "spec_path": "specs/t27mcp/visual_output.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 9, + "total": 9, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/specs/t27mcp/visual_output.t27 b/specs/t27mcp/visual_output.t27 new file mode 100644 index 0000000000..95a2fd98a1 --- /dev/null +++ b/specs/t27mcp/visual_output.t27 @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/t27mcp/visual_output.t27 -- every answer about a spec, a flow, numbers or a piece of work carries +; one visual, and the visual is the T27F way: drawn from tool rows, labelled, with the file it came from. +; Where it is drawn depends on the host. The rule is said to Claude Code through the MCP server's +; `instructions` (gHashTag/t27-mcp) and to TRI CLAW's chat agent through its prompt +; (gHashTag/999-multibots-telegraf, apps/vibee-editor/render): both read RULE_* from this spec, so the rule +; has one home. See the epic #10066 and T27F, gHashTag/trinity#1648. +; -- +; What was measured (2026-10-18, t27-mcp db98153): +; - t27f_graph of the reel fixture: an SVG 1860 wide, text at 13 px, 5923 bytes. Shown inline at 680 px it +; scales to 0.37, so its text is about 4.75 px: unreadable. t27f_view deck-evidence-wide: 1280 wide, body +; text 26 px (13.8 px inline, readable), a caption at 15 px (8 px inline, not readable). +; - The Claude Code desktop app draws an inline SVG or HTML visual with its visual tool (show_widget), at a +; container 680 wide, text at least 11 px. The CLI draws no image; it renders markdown tables. +; - An MCP server's `instructions` (the initialize result) reach the client model's system prompt; a tool +; result does not reach the user as an image on these hosts (reel.t27 header, 2026-10-17). +; - TRI CLAW's chat shows an image for an https URL ending in .svg on its own line or for ![title](url) +; (player/src/components/Chat/ChatAssets.tsx IMAGE_RE). The t27f tools write their SVG to the server's +; temporary directory, so TRI CLAW serves that directory by URL. +; -- +; Hosts 0..3 carry the numbers of reel.t27's HOST_*, and channels 0..3 its CHANNEL_*; this spec adds TRI +; CLAW, the inline widget, the image URL and the markdown table. reel.t27 channel_for() stays the channel +; of a reel still; answer_channel() here is the channel of an answer's visual. +; Readability: a drawing of width W shown at INLINE_W shows a font of F px at F * INLINE_W / W px, so it is +; readable when F * INLINE_W >= MIN_FONT_PX * W for its smallest text. A tool SVG is embedded as it is only +; when readable and at most INLINE_MAX_BYTES (the model re-emits it, so its bytes are output); otherwise it +; is redrawn at INLINE_W from t27f_outline rows. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module T27mcpVisualOutput; + +/// Hosts. 0..3 are reel.t27's numbers. +pub const HOST_CLAUDE_CODE_CLI : u32 = 0; +pub const HOST_CLAUDE_CODE_DESKTOP : u32 = 1; +pub const HOST_MCP_APPS : u32 = 2; +pub const HOST_SITE : u32 = 3; +pub const HOST_TRI_CLAW : u32 = 4; +pub const HOST_COUNT : u32 = 5; + +/// Channels. 0..3 are reel.t27's numbers. +pub const CH_FILE_PATH : u32 = 0; +pub const CH_FILE_AND_PANE : u32 = 1; +pub const CH_UI_RESOURCE : u32 = 2; +pub const CH_LIVE_PLAYER : u32 = 3; +pub const CH_INLINE_WIDGET : u32 = 4; +pub const CH_IMAGE_URL : u32 = 5; +pub const CH_MARKDOWN_TABLE : u32 = 6; +pub const CHANNEL_NAME : [7]str = [ + "file path", + "file and pane", + "ui resource", + "live player", + "inline widget", + "image url", + "markdown table" +]; + +/// Answer kinds. The first four carry a visual. +pub const K_SPEC : u32 = 0; +pub const K_FLOW : u32 = 1; +pub const K_NUMBERS : u32 = 2; +pub const K_REPORT : u32 = 3; +pub const K_ONE_SENTENCE : u32 = 4; +pub const K_QUESTION : u32 = 5; + +/// The inline container and the smallest text it may show. +pub const INLINE_W : u32 = 680; +pub const MIN_FONT_PX : u32 = 11; +pub const INLINE_MAX_BYTES : u32 = 16384; + +/// Where a tool SVG's drawing comes from. +pub const SRC_EMBED : u32 = 0; +pub const SRC_REDRAW : u32 = 1; + +/// The T27F labels, as t27f_outline gives them. +pub const LABEL_NAMES : [4]str = ["exact", "derived", "suggested", "unknown"]; + +/// answer_verdict() answers. +pub const V_OK : u32 = 0; +pub const V_NO_VISUAL : u32 = 1; +pub const V_INVENTED : u32 = 2; +pub const V_NO_LEGEND : u32 = 3; +pub const V_NO_SOURCE : u32 = 4; +pub const V_UNREADABLE : u32 = 5; +pub const VERDICT_TEXT : [6]str = [ + "ok", + "no visual: this answer needs one", + "invented: a node no tool row backs", + "no legend: labels drawn without their key", + "no source: the file or URL the drawing came from is not given", + "unreadable: its smallest text is under 11 px where it is shown" +]; + +/// The rule as Claude Code reads it, from the t27 MCP server's `instructions`. ASCII, one line each. +pub const RULE_CLAUDE_CODE : [7]str = [ + "Visual output (specs/t27mcp/visual_output.t27):", + "- Every answer that explains a spec, a flow, numbers or a piece of work carries one visual. Only a one-sentence reply or a question back goes without.", + "- In the Claude Code desktop app, draw it inline with your visual tool (show_widget) when you have one; else give the SVG path, which the app opens. In the CLI, give the path, or a markdown table when there is no SVG.", + "- Draw from tool rows, never from memory: t27f_outline, t27f_graph, t27f_view, t27_spec. Nothing is drawn that a row does not back.", + "- Label every node exact, derived, suggested or unknown as its row says, and show the legend.", + "- Embed a tool SVG as it is only when its smallest font_px * 680 >= 11 * its width and it is at most 16384 bytes; else redraw it 680 wide from t27f_outline rows.", + "- Always name the file the drawing came from: the file is the record, the drawing is a view of it." +]; + +/// The rule as TRI CLAW's chat agent reads it, from its prompt. ASCII, one line each. +pub const RULE_TRI_CLAW : [6]str = [ + "Visual output (specs/t27mcp/visual_output.t27):", + "- Every answer that explains a spec, a flow, numbers or a piece of work carries one visual. Only a one-sentence reply or a question back goes without.", + "- When a t27f tool answered, show its drawing as an image: its https URL ending in .svg on a line of its own, or ![title](url). Never paste SVG source into the chat.", + "- With no drawing, show the rows as a markdown table, not as prose.", + "- Draw from tool rows, never from memory. Label every item exact, derived, suggested or unknown as its row says, and give the legend.", + "- Name where the drawing came from: the tool and its file or URL." +]; +pub const RULE_CLAUDE_CODE_LINES : u32 = 7; +pub const RULE_TRI_CLAW_LINES : u32 = 6; + +/// Whether an answer of kind `kind` carries a visual. +pub fn needs_visual(kind: u32) -> bool { + return kind <= K_REPORT; +} + +/// How an answer's visual reaches the user on `host`. `inline_tool`: the client has a visual tool +/// (show_widget). `svg`: a t27f tool produced a drawing for this answer. +pub fn answer_channel(host: u32, inline_tool: bool, svg: bool) -> u32 { + if (host == HOST_CLAUDE_CODE_DESKTOP) { + if (inline_tool) { return CH_INLINE_WIDGET; } + if (svg) { return CH_FILE_AND_PANE; } + return CH_MARKDOWN_TABLE; + } + if (host == HOST_MCP_APPS) { return CH_UI_RESOURCE; } + if (host == HOST_SITE) { return CH_LIVE_PLAYER; } + if (host == HOST_TRI_CLAW) { + if (svg) { return CH_IMAGE_URL; } + return CH_MARKDOWN_TABLE; + } + if (svg) { return CH_FILE_PATH; } + return CH_MARKDOWN_TABLE; +} + +/// Whether text of `font_px` in a drawing `view_w` wide is at least MIN_FONT_PX when shown INLINE_W wide. +pub fn inline_readable(view_w: u32, font_px: u32) -> bool { + if (view_w == 0) { return false; } + return font_px * INLINE_W >= MIN_FONT_PX * view_w; +} + +/// Whether a tool SVG goes inline as it is or is redrawn from t27f_outline rows. `min_font_px` is its +/// smallest text. +pub fn inline_source(view_w: u32, min_font_px: u32, bytes: u32) -> u32 { + if (inline_readable(view_w, min_font_px) == false) { return SRC_REDRAW; } + if (bytes > INLINE_MAX_BYTES) { return SRC_REDRAW; } + return SRC_EMBED; +} + +/// The verdict on an answer. `drawn`: nodes in its visual; `backed`: those a tool row backs. +/// `label_kinds`: how many of the four labels it draws. `view_w` / `min_font_px`: the drawing's width +/// and its smallest text, in its own units (0 width when it is not shown inline). +pub fn answer_verdict(kind: u32, has_visual: bool, drawn: u32, backed: u32, label_kinds: u32, legend: bool, source: bool, view_w: u32, min_font_px: u32) -> u32 { + if (needs_visual(kind) == false) { return V_OK; } + if (has_visual == false) { return V_NO_VISUAL; } + if (drawn > backed) { return V_INVENTED; } + if (label_kinds > 0) { + if (legend == false) { return V_NO_LEGEND; } + } + if (source == false) { return V_NO_SOURCE; } + if (view_w > 0) { + if (inline_readable(view_w, min_font_px) == false) { return V_UNREADABLE; } + } + return V_OK; +} + +test "the tables are the size they say" { + assert CHANNEL_NAME[CH_INLINE_WIDGET] == "inline widget"; + assert CHANNEL_NAME[CH_MARKDOWN_TABLE] == "markdown table"; + assert LABEL_NAMES[3] == "unknown"; + assert VERDICT_TEXT[V_OK] == "ok"; + assert RULE_CLAUDE_CODE[0] == "Visual output (specs/t27mcp/visual_output.t27):"; + assert RULE_TRI_CLAW[0] == "Visual output (specs/t27mcp/visual_output.t27):"; + assert RULE_CLAUDE_CODE[1] == "- Every answer that explains a spec, a flow, numbers or a piece of work carries one visual. Only a one-sentence reply or a question back goes without."; + assert RULE_TRI_CLAW[1] == "- Every answer that explains a spec, a flow, numbers or a piece of work carries one visual. Only a one-sentence reply or a question back goes without."; +} + +test "needs_visual: spec, flow, numbers and a report carry one; one sentence and a question do not" { + assert needs_visual(K_SPEC); + assert needs_visual(K_FLOW); + assert needs_visual(K_NUMBERS); + assert needs_visual(K_REPORT); + assert needs_visual(K_ONE_SENTENCE) == false; + assert needs_visual(K_QUESTION) == false; +} + +test "answer_channel: the desktop app draws inline, TRI CLAW shows an image URL" { + assert answer_channel(HOST_CLAUDE_CODE_DESKTOP, true, true) == CH_INLINE_WIDGET; + assert answer_channel(HOST_CLAUDE_CODE_DESKTOP, true, false) == CH_INLINE_WIDGET; + assert answer_channel(HOST_TRI_CLAW, false, true) == CH_IMAGE_URL; + assert answer_channel(HOST_TRI_CLAW, true, true) == CH_IMAGE_URL; +} + +test "answer_channel: with no inline tool or no drawing, a path or a markdown table" { + assert answer_channel(HOST_CLAUDE_CODE_DESKTOP, false, true) == CH_FILE_AND_PANE; + assert answer_channel(HOST_CLAUDE_CODE_DESKTOP, false, false) == CH_MARKDOWN_TABLE; + assert answer_channel(HOST_CLAUDE_CODE_CLI, false, true) == CH_FILE_PATH; + assert answer_channel(HOST_CLAUDE_CODE_CLI, false, false) == CH_MARKDOWN_TABLE; + assert answer_channel(HOST_TRI_CLAW, false, false) == CH_MARKDOWN_TABLE; + assert answer_channel(HOST_MCP_APPS, false, true) == CH_UI_RESOURCE; + assert answer_channel(HOST_SITE, false, true) == CH_LIVE_PLAYER; + assert answer_channel(HOST_COUNT, false, true) == CH_FILE_PATH; +} + +test "inline_readable: the measured drawings" { + assert inline_readable(1860, 13) == false; + assert inline_readable(1280, 26); + assert inline_readable(1280, 15) == false; + assert inline_readable(680, 11); + assert inline_readable(680, 10) == false; + assert inline_readable(340, 6); +} + +test "inline_source: embed a small readable SVG, redraw the rest" { + assert inline_source(680, 12, 5923) == SRC_EMBED; + assert inline_source(1860, 13, 5923) == SRC_REDRAW; + assert inline_source(680, 12, INLINE_MAX_BYTES) == SRC_EMBED; + assert inline_source(680, 12, INLINE_MAX_BYTES + 1) == SRC_REDRAW; +} + +test "answer_verdict: a backed, labelled, sourced, readable drawing is ok" { + assert answer_verdict(K_SPEC, true, 12, 12, 3, true, true, 680, 12) == V_OK; + assert answer_verdict(K_REPORT, true, 4, 4, 0, false, true, 680, 11) == V_OK; + assert answer_verdict(K_ONE_SENTENCE, false, 0, 0, 0, false, false, 0, 0) == V_OK; + assert answer_verdict(K_NUMBERS, true, 5, 5, 1, true, true, 0, 0) == V_OK; +} + +test "negative control: a spec answer with no visual, or with a node no row backs" { + assert answer_verdict(K_SPEC, false, 0, 0, 0, false, false, 0, 0) == V_NO_VISUAL; + assert answer_verdict(K_FLOW, false, 0, 0, 0, false, true, 0, 0) == V_NO_VISUAL; + assert answer_verdict(K_SPEC, true, 13, 12, 3, true, true, 680, 12) == V_INVENTED; +} + +test "negative control: labels without a legend, a drawing without its source, unreadable text" { + assert answer_verdict(K_SPEC, true, 12, 12, 2, false, true, 680, 12) == V_NO_LEGEND; + assert answer_verdict(K_SPEC, true, 12, 12, 2, true, false, 680, 12) == V_NO_SOURCE; + assert answer_verdict(K_SPEC, true, 12, 12, 2, true, true, 1860, 13) == V_UNREADABLE; + assert inline_readable(0, 13) == false; +} From 2cdfd981b097a45a33479ee4eace5f4521d954c8 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 18 Oct 2026 22:37:17 +0700 Subject: [PATCH 02/27] claw: visual_output.t27 -- a T27F visual in every output that did work (Closes #10508) Owner order 2026-10-18: every Claude Code output carries a visual, built on the T27F visualizer and its MCP tools, written into the TRI CLAW rules. The T27F meant here is the particle video (token_floor reels). - specs/claw/visual_output.t27: which output kind carries which visual (particle still by default, reel, spec graph, check-run link), lab vs MCP host rendering, session-folder placement through T27fReel.channel_for, EXPORTED/FOLDED/MANUAL graph labels at the t27f_graph caps, and report_ok: every needed visual is made or reported missing. 8/8, 0 vacuous on the lab. - Seal written by t27c seal --save; spec_hash matches. - Skill t27f: "Visual in every output" steps pointing to the law; the particle reels are T27F too. - docs/claw/README.md: one row pointing to the law. Co-Authored-By: Claude Opus 5.5 --- .claude/skills/t27f/SKILL.md | 19 ++ .trinity/seals/claw_ClawVisualOutput.json | 41 ++++ docs/claw/README.md | 1 + specs/claw/visual_output.t27 | 232 ++++++++++++++++++++++ 4 files changed, 293 insertions(+) create mode 100644 .trinity/seals/claw_ClawVisualOutput.json create mode 100644 specs/claw/visual_output.t27 diff --git a/.claude/skills/t27f/SKILL.md b/.claude/skills/t27f/SKILL.md index 187a88c8ab..447d835169 100644 --- a/.claude/skills/t27f/SKILL.md +++ b/.claude/skills/t27f/SKILL.md @@ -49,6 +49,11 @@ executable visual diagrams"). as a navigable program, linked to source, tests, issues and real task status; each cell is both a spec and a learning entry point. +**The particle video is T27F too** (owner, 2026-10-18: "t27f is the particle video, the thing you +were doing"). The token_floor reels (`specs/t27f/token_floor*.t27`, `lesson_reel.t27`) are the +form of T27F the owner means when asking for a visual. The graph IR below is the inspectable form. +Both are T27F; neither replaces the other. + **What T27F is not:** it is not the topic-to-value engine. gHashTag/trinity#1677 (topic or client brief -> 27-lesson map -> Bee -> verified artifact) is an *application* built on T27F, and so are the reels. Describing T27F as #1677 is the error this section exists to stop (owner correction, @@ -93,6 +98,20 @@ is not progress on the core. Anything the rules do not reach (SUGGESTED explanations, call-result flow, anything larger) is still a manual reading, and no host runs either pass yet. Name that gap rather than hide it. +## Visual in every output (Claude Code and TRI CLAW) + +**The law is `specs/claw/visual_output.t27`.** This section is the order of steps; which output +carries which visual, where it is made and where the file goes are the spec's, not repeated here. + +1. Decide the output's kind (`OUT_*`). Only a reply that just talks goes without a visual. +2. Particle still or reel: render on the Railway lab (never the Mac), take the still at `STILL_MS` + with `ffmpeg -ss 9 -frames:v 1`, and bring it into the session folder (`out-reels/`, gitignored, + never `git add`). Open it in the Browser pane as `file://`, so the user sees it. +3. Spec output: also `mcp__t27__t27f_graph` on the spec. If the corpus answers 404, say it is a + manual reading (`GRAPH_MANUAL`); if it is over the caps, fold it, never cut it. +4. `t27f_view` stills: at most two headline lines per slide; pass `out` inside the session folder. +5. Anything not made: say which and why in the answer. Never show an older file as this turn's. + ## Application: topic map (gHashTag/trinity#1677) Use this part when the user gives a topic or a client brief, not a spec. diff --git a/.trinity/seals/claw_ClawVisualOutput.json b/.trinity/seals/claw_ClawVisualOutput.json new file mode 100644 index 0000000000..08e216f34b --- /dev/null +++ b/.trinity/seals/claw_ClawVisualOutput.json @@ -0,0 +1,41 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/deck.t27", + "spec_hash": "sha256:bd8ef9fc155365c805ba716d97155bed1e4017c378dbc335e252bcb244512147" + }, + { + "spec": "specs/t27f/reel.t27", + "spec_hash": "sha256:c47b21cea1d7169da6528e321cf2d2c513d3f552c0cd04fe9d37bbd5314ccacf" + }, + { + "spec": "specs/t27f/reel_marketing.t27", + "spec_hash": "sha256:ab07f6476d900cc0e089ba1d851f0b610a77b778f00fd906d1b98a29281941c7" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:2bded953aa3eb3d347630c52244a46152cbd24305f9cad12cf4e73ac0e11cc54", + "gen_hash_rust": "sha256:68e06b627cadee1d253df6776e5c5de04a9a01beef31d3d8662f8d63c05ea577", + "gen_hash_verilog": "sha256:96c0a56fe36c322ba2b59439a2e848975007626a66bf28cfbfdc609d9fa462de", + "gen_hash_zig": "sha256:6abdb83ec1646b9c5d3f6146a19d329149f8c5478b92c1aed04ea8014b105729", + "module": "ClawVisualOutput", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T15:35:23Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:2cbf82c0dad4bc18171b46d1e90bd2451c4f5fc5b3dbc83198389e55fa42571d", + "spec_path": "specs/claw/visual_output.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 8, + "total": 8, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/claw/README.md b/docs/claw/README.md index 989fe8ff50..82752902e3 100644 --- a/docs/claw/README.md +++ b/docs/claw/README.md @@ -17,6 +17,7 @@ behind one front door, with the gate logic in `.t27` specs. Epic | Suggestion review on a pull request | `specs/claw/suggest_feed.t27` over `specs/claw/suggest.t27` and `specs/tri/fmt_source.t27`, the `claw-suggest` job of `gate-topology.yml` | #10365, live once the owner steps below are done | | Service skeleton | Railway, thin I/O glue only | T5, merged (spec contract) | | Model routing: Opus 5.5 orchestrates, Sonnet 5.5 runs sub-tasks | `specs/claw/model_route.t27`, skill `model-routing`, `.claude/settings.json` + `.claude/agents/*` | owner directive 2026-10-18 | +| A T27F visual in every output that did work (particle still by default) | `specs/claw/visual_output.t27`, procedure in skill `t27f`, "Visual in every output" | owner directive 2026-10-18 | | Positioning / pricing | `docs/claw/competitors.md` | T6, started | ## The T27F check in every `.t27` repository (#10268) diff --git a/specs/claw/visual_output.t27 b/specs/claw/visual_output.t27 new file mode 100644 index 0000000000..6b03f6d9ba --- /dev/null +++ b/specs/claw/visual_output.t27 @@ -0,0 +1,232 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/claw/visual_output.t27 -- every TRI CLAW output that did work carries a T27F visual, as law. +; Owner order 2026-10-18: study the T27F visualizer through the MCP tools, take the best practices, +; make the output in Claude Code always carry a visual, and write that into the TRI CLAW rules. The +; owner's T27F here is the particle video: the token_floor world (specs/t27f/token_floor.t27, +; token_floor_scenes.t27, token_floor_mux.t27, lesson_reel.t27), rendered as a reel. +; -- +; The rule, each row tested below: +; - a reply that only talks (a short answer, a question back) needs no visual. Every other output +; needs one: a turn that changed files or ran commands, a spec added or explained, a status report, +; a reel, a TRI CLAW check-run on a pull request. +; - The default visual is a particle STILL: one PNG frame of a rendered token_floor reel, taken at +; STILL_MS, after the hook has written its statement and before the CTA hold. A reel output carries +; the reel itself (tall and wide) plus its still. A spec output also carries the spec's T27F GRAPH. +; A check-run carries a LINK (claw/t27f and the T27F preview already post one). +; - Particle frames are rendered on the Railway lab, never on the owner's Mac (owner rule +; 2026-10-09). The MCP stills (t27f_graph, t27f_view) are SVG the MCP host draws. +; - Where it goes is T27fReel.channel_for (specs/t27f/reel.t27), imported, not copied. On the +; desktop Code tab the file sits inside the session's folder (a gitignored out dir of the +; worktree): the file pane opens only files there, and the Browser pane shows a file outside the +; project folder only as a static snapshot (measured 2026-10-18). +; - Honesty: a visual that could not be made is reported missing, with the reason. It is never +; skipped silently, and an older file is never shown as this turn's. A spec graph is EXPORTED only +; when the MCP corpus served the spec and it fits the tool's caps. A larger one is FOLDED +; (specs/t27f/spec_graph_fold.t27), never truncated. An unserved one is a MANUAL reading and is +; called that. +; - A slide headline longer than T27fDeck.MAX_HEADLINE_LINES is split over more slides, not cut. +; -- +; Measured 2026-10-18 through the MCP tools (gHashTag/t27-mcp): +; - t27f_outline and t27f_graph read the published corpus at t27.ai/t27/files/. +; specs/tutorial/08_modules_and_arrays.t27 is served (14 nodes, 9 edges, exact only). But +; specs/t27f/*.t27 and specs/claw/*.t27 answer 404, so the visualizer cannot yet draw its own +; specs or TRI CLAW's: GRAPH_MANUAL until they are published. +; - t27f_view refuses a 3-line headline ("split it into a second slide"). It writes to the temp +; directory unless `out` names a path, and on the desktop that temp path is outside the session. +; - No MCP tool renders a particle still. The lab recipe (render the reel, then +; `ffmpeg -ss -frames:v 1`) is the only path today; a reel_still tool in TRI CLAW's /mcp is +; the follow-up. +; The caps GRAPH_NODE_CAP / GRAPH_EDGE_CAP are the t27f_graph tool's own (its description, +; 2026-10-18), not a spec's; the exporter itself reaches 1024 / 4096. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module ClawVisualOutput; + +use t27f::reel::channel_for; +use t27f::reel::HOST_CLAUDE_CODE_CLI; +use t27f::reel::HOST_CLAUDE_CODE_DESKTOP; +use t27f::reel::HOST_MCP_APPS; +use t27f::reel::HOST_SITE; +use t27f::reel::CHANNEL_FILE_AND_PANE; +use t27f::deck::MAX_HEADLINE_LINES; +use t27f::reel_marketing::REEL_HOOK_MS; +use t27f::reel_marketing::REEL_CTA_MIN_MS; +use t27f::reel_marketing::REEL_MIN_MS; + +/// What an output is. +pub const OUT_TALK : u8 = 0; // a short answer, a question back, conversation +pub const OUT_WORK : u8 = 1; // a turn that changed files or ran commands +pub const OUT_SPEC : u8 = 2; // a .t27 spec added, changed or explained +pub const OUT_STATUS : u8 = 3; // a status or progress report +pub const OUT_REEL : u8 = 4; // the output is a reel +pub const OUT_CHECK : u8 = 5; // a TRI CLAW check-run on a pull request +pub const OUT_KINDS : u8 = 6; + +/// What it carries, one bit each, so one output can carry several. +pub const VIS_STILL : u8 = 1; // particle still, PNG, one frame of a token_floor reel +pub const VIS_REEL : u8 = 2; // the reel, tall and wide mp4 +pub const VIS_GRAPH : u8 = 4; // the spec's T27F graph, SVG, from t27f_graph +pub const VIS_LINK : u8 = 8; // a link in the check-run summary + +/// Where a visual is made. +pub const RENDER_LAB : u8 = 0; // the Railway lab +pub const RENDER_HOST : u8 = 1; // the MCP host + +/// Where the file is put. +pub const PLACE_ANY : u8 = 0; // any path, printed in the answer +pub const PLACE_SESSION : u8 = 1; // inside the session's folder + +/// How a spec graph is labelled. +pub const GRAPH_EXPORTED : u8 = 0; +pub const GRAPH_FOLDED : u8 = 1; +pub const GRAPH_MANUAL : u8 = 2; +pub const GRAPH_NODE_CAP : u32 = 192; +pub const GRAPH_EDGE_CAP : u32 = 254; + +/// The still is this frame of a reel: past the hook's decision window, before the CTA hold. +pub const STILL_MS : i32 = 9000; + +pub fn has(v: u8, bit: u8) -> bool { + return @mod(v / bit, 2) == 1; +} + +/// The visuals an output of this kind carries. A kind this law does not know gets the default +/// still: a reader that waves through an unknown kind with no visual is not a rule. +pub fn visuals_for(kind: u8) -> u8 { + if (kind == OUT_TALK) { + return 0; + } + if (kind == OUT_SPEC) { + return VIS_STILL + VIS_GRAPH; + } + if (kind == OUT_REEL) { + return VIS_STILL + VIS_REEL; + } + if (kind == OUT_CHECK) { + return VIS_LINK; + } + return VIS_STILL; +} + +pub fn needs_visual(kind: u8) -> bool { + return visuals_for(kind) != 0; +} + +/// Particle frames come from the lab; SVG from the MCP host. +pub fn render_place(bit: u8) -> u8 { + if (bit == VIS_STILL or bit == VIS_REEL) { + return RENDER_LAB; + } + return RENDER_HOST; +} + +/// The file goes inside the session folder wherever the host opens it in a pane. +pub fn place_for(host: u8) -> u8 { + if (channel_for(host) == CHANNEL_FILE_AND_PANE) { + return PLACE_SESSION; + } + return PLACE_ANY; +} + +pub fn graph_label(served: bool, nodes: u32, edges: u32) -> u8 { + if (served == false) { + return GRAPH_MANUAL; + } + if (nodes > GRAPH_NODE_CAP or edges > GRAPH_EDGE_CAP) { + return GRAPH_FOLDED; + } + return GRAPH_EXPORTED; +} + +/// An output is honest when every visual it needs was made this turn or is reported missing. +pub fn report_ok(kind: u8, made: u8, said_missing: u8) -> bool { + const need = visuals_for(kind); + var ok = true; + var bit : u8 = 1; + while (bit <= VIS_LINK) { + if (has(need, bit) and has(made, bit) == false and has(said_missing, bit) == false) { + ok = false; + } + bit = bit * 2; + } + return ok; +} + +/// A still frame shows the statement, not the hook still writing it and not the CTA hold. +pub fn still_ms_ok(ms: i32) -> bool { + return ms > REEL_HOOK_MS and ms < REEL_MIN_MS - REEL_CTA_MIN_MS; +} + +/// Slides a headline of this many lines takes: split, never cut. +pub fn slides_for(lines: i32) -> i32 { + return (lines + MAX_HEADLINE_LINES - 1) / MAX_HEADLINE_LINES; +} + +test only_talk_goes_without_a_visual { + assert needs_visual(OUT_TALK) == false; + var k : u8 = 1; + while (k < OUT_KINDS) { + assert needs_visual(k); + k = k + 1; + } + assert visuals_for(OUT_KINDS) == VIS_STILL; + assert visuals_for(200) == VIS_STILL; +} + +test each_kind_carries_its_visuals { + assert visuals_for(OUT_WORK) == VIS_STILL; + assert visuals_for(OUT_STATUS) == VIS_STILL; + assert has(visuals_for(OUT_SPEC), VIS_STILL) and has(visuals_for(OUT_SPEC), VIS_GRAPH); + assert has(visuals_for(OUT_REEL), VIS_REEL) and has(visuals_for(OUT_REEL), VIS_STILL); + assert has(visuals_for(OUT_REEL), VIS_GRAPH) == false; + assert visuals_for(OUT_CHECK) == VIS_LINK; +} + +test particle_frames_come_from_the_lab { + assert render_place(VIS_STILL) == RENDER_LAB; + assert render_place(VIS_REEL) == RENDER_LAB; + assert render_place(VIS_GRAPH) == RENDER_HOST; +} + +test the_desktop_file_sits_in_the_session_folder { + assert place_for(HOST_CLAUDE_CODE_DESKTOP) == PLACE_SESSION; + assert place_for(HOST_CLAUDE_CODE_CLI) == PLACE_ANY; + assert place_for(HOST_MCP_APPS) == PLACE_ANY; + assert place_for(HOST_SITE) == PLACE_ANY; +} + +test a_graph_says_how_it_was_read { + // tutorial 08, as t27f_outline served it on 2026-10-18 + assert graph_label(true, 14, 9) == GRAPH_EXPORTED; + assert graph_label(true, 192, 254) == GRAPH_EXPORTED; + // the largest spec in specs/ after derive: 775 nodes, 3194 edges + assert graph_label(true, 775, 3194) == GRAPH_FOLDED; + assert graph_label(true, 193, 9) == GRAPH_FOLDED; + // specs/claw/*.t27 answered 404 + assert graph_label(false, 14, 9) == GRAPH_MANUAL; +} + +test a_missing_visual_is_said_never_skipped { + assert report_ok(OUT_TALK, 0, 0); + assert report_ok(OUT_WORK, VIS_STILL, 0); + assert report_ok(OUT_WORK, 0, 0) == false; + assert report_ok(OUT_WORK, 0, VIS_STILL); + assert report_ok(OUT_SPEC, VIS_STILL, 0) == false; + assert report_ok(OUT_SPEC, VIS_STILL, VIS_GRAPH); + assert report_ok(OUT_REEL, VIS_STILL + VIS_REEL, 0); + assert report_ok(OUT_CHECK, VIS_STILL, 0) == false; +} + +test the_still_is_past_the_hook_and_before_the_cta { + assert still_ms_ok(STILL_MS); + assert still_ms_ok(REEL_HOOK_MS) == false; + assert still_ms_ok(1000) == false; + assert still_ms_ok(14000) == false; +} + +test a_long_headline_takes_more_slides { + assert slides_for(1) == 1; + assert slides_for(2) == 1; + assert slides_for(3) == 2; + assert slides_for(5) == 3; +} From 9e9d57d401309d5259f8dc9d22d11522c790bc3c Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 18 Oct 2026 22:50:40 +0700 Subject: [PATCH 03/27] ledger: specs/claw/visual_output.t27 passes t27b (Closes #10508) The t27b-native ratchet reported the new spec as UNLISTED: the reference passes it and the ledger did not name it. A pass row; max_not_pass is unchanged. Co-Authored-By: Claude Opus 5.5 --- docs/reports/t27b_expectations.json | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 09b79809ce..c5bc8a31e0 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -202,6 +202,7 @@ {"path": "specs/claw/t27f_gate.t27", "t27b": "pass"}, {"path": "specs/claw/t27f_gate_pins.t27", "t27b": "pass"}, {"path": "specs/claw/text.t27", "t27b": "pass"}, + {"path": "specs/claw/visual_output.t27", "t27b": "pass"}, {"path": "specs/compiler/c_array_repeat_wrapper.t27", "t27b": "pass"}, {"path": "specs/compiler/c_fixed_array_return.t27", "t27b": "pass"}, {"path": "specs/compiler/codegen_conformance.t27", "t27b": "pass"}, From fb64113d8b564f87921abce59b443735ba36b32c Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 18 Oct 2026 23:11:05 +0700 Subject: [PATCH 04/27] ledger: specs/analog/linear.t27 passes t27b (Closes #10508) It landed on master without a ledger row, so the t27b-native ratchet reports it UNLISTED on every branch. A pass row; max_not_pass unchanged. Co-Authored-By: Claude Opus 5.5 --- docs/reports/t27b_expectations.json | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index c5bc8a31e0..8a76efd6da 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -43,6 +43,7 @@ {"path": "specs/agents/x.t27", "t27b": "pass"}, {"path": "specs/agents/y.t27", "t27b": "pass"}, {"path": "specs/agents/z.t27", "t27b": "pass"}, + {"path": "specs/analog/linear.t27", "t27b": "pass"}, {"path": "specs/api/c_abi.t27", "t27b": "pass"}, {"path": "specs/api/tri_api_context.t27", "t27b": "pass"}, {"path": "specs/api/tri_api_loop.t27", "t27b": "pass"}, From 2759272a223bf868152b14279f37c6298c6f2201 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 18 Oct 2026 23:22:53 +0700 Subject: [PATCH 05/27] ledger: specs/analog/mos_table.t27 passes t27b (Closes #10508) Co-Authored-By: Claude Opus 5.5 --- docs/reports/t27b_expectations.json | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 8a76efd6da..61f30b1327 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -44,6 +44,7 @@ {"path": "specs/agents/y.t27", "t27b": "pass"}, {"path": "specs/agents/z.t27", "t27b": "pass"}, {"path": "specs/analog/linear.t27", "t27b": "pass"}, + {"path": "specs/analog/mos_table.t27", "t27b": "pass"}, {"path": "specs/api/c_abi.t27", "t27b": "pass"}, {"path": "specs/api/tri_api_context.t27", "t27b": "pass"}, {"path": "specs/api/tri_api_loop.t27", "t27b": "pass"}, From c39c5c5ebc7d465a650fd200ad9fa11e558a38c2 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 18 Oct 2026 23:37:31 +0700 Subject: [PATCH 06/27] ledger: specs/analog/bench.t27 passes t27b (Closes #10508) Co-Authored-By: Claude Opus 5.5 --- docs/reports/t27b_expectations.json | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 61f30b1327..0509a6a681 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -43,6 +43,7 @@ {"path": "specs/agents/x.t27", "t27b": "pass"}, {"path": "specs/agents/y.t27", "t27b": "pass"}, {"path": "specs/agents/z.t27", "t27b": "pass"}, + {"path": "specs/analog/bench.t27", "t27b": "pass"}, {"path": "specs/analog/linear.t27", "t27b": "pass"}, {"path": "specs/analog/mos_table.t27", "t27b": "pass"}, {"path": "specs/api/c_abi.t27", "t27b": "pass"}, From 63c53fc9b27f31156a5a6c61532b1b6348395b03 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 18 Oct 2026 23:43:21 +0700 Subject: [PATCH 07/27] claw: visual_output.t27 keeps only the particle-still layer, imports the rule from t27mcp::visual_output (#10504) (Closes #10508) The owner picked specs/t27mcp/visual_output.t27 as the single home of the visual-output rule. The claw spec now imports needs_visual, answer_channel and the K_/HOST_/CH_ numbers from it and adds only takes_still, still_channel, place_for, still_ms_ok and still_report_ok. Lab: 6/6, 0 vacuous; t27mcp spec 9/9, 0 vacuous. Resealed. Ledger row for specs/t27mcp/visual_output.t27 (t27b pass, per #10504's own ratchet run). Co-Authored-By: Claude Opus 5.5 --- .claude/skills/t27f/SKILL.md | 24 +- .trinity/seals/claw_ClawVisualOutput.json | 28 +-- docs/claw/README.md | 2 +- docs/reports/t27b_expectations.json | 1 + specs/claw/visual_output.t27 | 266 +++++++--------------- 5 files changed, 112 insertions(+), 209 deletions(-) diff --git a/.claude/skills/t27f/SKILL.md b/.claude/skills/t27f/SKILL.md index 447d835169..d154176949 100644 --- a/.claude/skills/t27f/SKILL.md +++ b/.claude/skills/t27f/SKILL.md @@ -100,17 +100,19 @@ is not progress on the core. ## Visual in every output (Claude Code and TRI CLAW) -**The law is `specs/claw/visual_output.t27`.** This section is the order of steps; which output -carries which visual, where it is made and where the file goes are the spec's, not repeated here. - -1. Decide the output's kind (`OUT_*`). Only a reply that just talks goes without a visual. -2. Particle still or reel: render on the Railway lab (never the Mac), take the still at `STILL_MS` - with `ffmpeg -ss 9 -frames:v 1`, and bring it into the session folder (`out-reels/`, gitignored, - never `git add`). Open it in the Browser pane as `file://`, so the user sees it. -3. Spec output: also `mcp__t27__t27f_graph` on the spec. If the corpus answers 404, say it is a - manual reading (`GRAPH_MANUAL`); if it is over the caps, fold it, never cut it. -4. `t27f_view` stills: at most two headline lines per slide; pass `out` inside the session folder. -5. Anything not made: say which and why in the answer. Never show an older file as this turn's. +**The rule is `specs/t27mcp/visual_output.t27`** (its `RULE_CLAUDE_CODE` and `RULE_TRI_CLAW` lines +are what each host reads). **Its particle-still layer is `specs/claw/visual_output.t27`.** This +section is the order of steps; which answer carries which visual and how it reaches each host are +the specs', not repeated here. + +1. Decide the answer's kind (`K_*` in the t27mcp spec). A one-sentence reply or a question back + goes without a visual. +2. Spec, flow or numbers: draw from t27f tool rows (`t27f_outline`, `t27f_graph`, `t27f_view`), + labelled, with the legend and the source file, as `answer_verdict` checks. +3. A report of a piece of work: a particle still (`takes_still`). Render the reel on the Railway lab + (never the Mac), take the frame at `STILL_MS` with `ffmpeg -ss 9 -frames:v 1`, and bring it into + the session folder (`out-reels/`, gitignored, never `git add`). +4. Anything not made: say which and why in the answer. Never show an older file as this turn's. ## Application: topic map (gHashTag/trinity#1677) diff --git a/.trinity/seals/claw_ClawVisualOutput.json b/.trinity/seals/claw_ClawVisualOutput.json index 08e216f34b..3556c08782 100644 --- a/.trinity/seals/claw_ClawVisualOutput.json +++ b/.trinity/seals/claw_ClawVisualOutput.json @@ -1,37 +1,33 @@ { "built_by": "t27c-bootstrap@0.5.2+9448ab4be", "closure": [ - { - "spec": "specs/t27f/deck.t27", - "spec_hash": "sha256:bd8ef9fc155365c805ba716d97155bed1e4017c378dbc335e252bcb244512147" - }, - { - "spec": "specs/t27f/reel.t27", - "spec_hash": "sha256:c47b21cea1d7169da6528e321cf2d2c513d3f552c0cd04fe9d37bbd5314ccacf" - }, { "spec": "specs/t27f/reel_marketing.t27", "spec_hash": "sha256:ab07f6476d900cc0e089ba1d851f0b610a77b778f00fd906d1b98a29281941c7" + }, + { + "spec": "specs/t27mcp/visual_output.t27", + "spec_hash": "sha256:06c1c5e73142e2f79fa5d7be037eace63c1f20597a6e33274ebf8e1b428b7584" } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:2bded953aa3eb3d347630c52244a46152cbd24305f9cad12cf4e73ac0e11cc54", - "gen_hash_rust": "sha256:68e06b627cadee1d253df6776e5c5de04a9a01beef31d3d8662f8d63c05ea577", - "gen_hash_verilog": "sha256:96c0a56fe36c322ba2b59439a2e848975007626a66bf28cfbfdc609d9fa462de", - "gen_hash_zig": "sha256:6abdb83ec1646b9c5d3f6146a19d329149f8c5478b92c1aed04ea8014b105729", + "gen_hash_c": "sha256:195d1b6aa754842316d3c8becc34ce6a0a90e5f7d4ddaebd490823103ed06c33", + "gen_hash_rust": "sha256:93ecd121c7efadf95e4ca54e1e750fb5c7a8159284cebe1cbb61d7065d1b7875", + "gen_hash_verilog": "sha256:794e86d5753efad530cd7732b2638ad3c3a5c4e6d8c9a851bd686663aa097282", + "gen_hash_zig": "sha256:d6204f6e99bc281a0163177b92ee4abf096ea32cb23cb3b817913d16c8d072b1", "module": "ClawVisualOutput", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-18T15:35:23Z", + "sealed_at": "2026-10-18T16:42:30Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:2cbf82c0dad4bc18171b46d1e90bd2451c4f5fc5b3dbc83198389e55fa42571d", + "spec_hash": "sha256:17137afe136e39910f38d7b031c659a672e009bfeef1bf4f9b7af7e2cb6cc628", "spec_path": "specs/claw/visual_output.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 8, - "total": 8, + "passed": 6, + "total": 6, "vacuous": 0 }, "toolchain": { diff --git a/docs/claw/README.md b/docs/claw/README.md index 82752902e3..4a7df352ec 100644 --- a/docs/claw/README.md +++ b/docs/claw/README.md @@ -17,7 +17,7 @@ behind one front door, with the gate logic in `.t27` specs. Epic | Suggestion review on a pull request | `specs/claw/suggest_feed.t27` over `specs/claw/suggest.t27` and `specs/tri/fmt_source.t27`, the `claw-suggest` job of `gate-topology.yml` | #10365, live once the owner steps below are done | | Service skeleton | Railway, thin I/O glue only | T5, merged (spec contract) | | Model routing: Opus 5.5 orchestrates, Sonnet 5.5 runs sub-tasks | `specs/claw/model_route.t27`, skill `model-routing`, `.claude/settings.json` + `.claude/agents/*` | owner directive 2026-10-18 | -| A T27F visual in every output that did work (particle still by default) | `specs/claw/visual_output.t27`, procedure in skill `t27f`, "Visual in every output" | owner directive 2026-10-18 | +| A T27F visual in every answer that needs one | rule `specs/t27mcp/visual_output.t27`, particle-still layer `specs/claw/visual_output.t27`, procedure in skill `t27f`, "Visual in every output" | owner directive 2026-10-18 | | Positioning / pricing | `docs/claw/competitors.md` | T6, started | ## The T27F check in every `.t27` repository (#10268) diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 0509a6a681..11452ed21e 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -1478,6 +1478,7 @@ {"path": "specs/t27mcp/spec_brief.t27", "t27b": "pass"}, {"path": "specs/t27mcp/telegram.t27", "t27b": "pass"}, {"path": "specs/t27mcp/tool_schema.t27", "t27b": "codegen", "blocker": "t27b: unsupported construct FnDecl(frame or branch range) at line 0 (fn the round trip fixtures: a tuple, arrays, a boolean, nested objects: conditional branch out of range (function too large))", "reason": "unimplemented"}, + {"path": "specs/t27mcp/visual_output.t27", "t27b": "pass"}, {"path": "specs/ternary/activation_quantizer.t27", "t27b": "pass"}, {"path": "specs/ternary/bitnet_layer.t27", "t27b": "pass"}, {"path": "specs/ternary/bitnet_majority.t27", "t27b": "pass"}, diff --git a/specs/claw/visual_output.t27 b/specs/claw/visual_output.t27 index 6b03f6d9ba..c275dc4276 100644 --- a/specs/claw/visual_output.t27 +++ b/specs/claw/visual_output.t27 @@ -1,220 +1,123 @@ // SPDX-License-Identifier: Apache-2.0 -; specs/claw/visual_output.t27 -- every TRI CLAW output that did work carries a T27F visual, as law. -; Owner order 2026-10-18: study the T27F visualizer through the MCP tools, take the best practices, -; make the output in Claude Code always carry a visual, and write that into the TRI CLAW rules. The -; owner's T27F here is the particle video: the token_floor world (specs/t27f/token_floor.t27, -; token_floor_scenes.t27, token_floor_mux.t27, lesson_reel.t27), rendered as a reel. +; specs/claw/visual_output.t27 -- the particle-still layer of TRI CLAW's visual-output rule. +; Owner order 2026-10-18: every output in Claude Code carries a T27F visual, written into the TRI +; CLAW rules, and the T27F meant here is the particle video: the token_floor world +; (specs/t27f/token_floor.t27, token_floor_scenes.t27, token_floor_mux.t27, lesson_reel.t27). ; -- -; The rule, each row tested below: -; - a reply that only talks (a short answer, a question back) needs no visual. Every other output -; needs one: a turn that changed files or ran commands, a spec added or explained, a status report, -; a reel, a TRI CLAW check-run on a pull request. -; - The default visual is a particle STILL: one PNG frame of a rendered token_floor reel, taken at -; STILL_MS, after the hook has written its statement and before the CTA hold. A reel output carries -; the reel itself (tall and wide) plus its still. A spec output also carries the spec's T27F GRAPH. -; A check-run carries a LINK (claw/t27f and the T27F preview already post one). -; - Particle frames are rendered on the Railway lab, never on the owner's Mac (owner rule -; 2026-10-09). The MCP stills (t27f_graph, t27f_view) are SVG the MCP host draws. -; - Where it goes is T27fReel.channel_for (specs/t27f/reel.t27), imported, not copied. On the -; desktop Code tab the file sits inside the session's folder (a gitignored out dir of the -; worktree): the file pane opens only files there, and the Browser pane shows a file outside the -; project folder only as a static snapshot (measured 2026-10-18). -; - Honesty: a visual that could not be made is reported missing, with the reason. It is never -; skipped silently, and an older file is never shown as this turn's. A spec graph is EXPORTED only -; when the MCP corpus served the spec and it fits the tool's caps. A larger one is FOLDED -; (specs/t27f/spec_graph_fold.t27), never truncated. An unserved one is a MANUAL reading and is -; called that. -; - A slide headline longer than T27fDeck.MAX_HEADLINE_LINES is split over more slides, not cut. -; -- -; Measured 2026-10-18 through the MCP tools (gHashTag/t27-mcp): -; - t27f_outline and t27f_graph read the published corpus at t27.ai/t27/files/. -; specs/tutorial/08_modules_and_arrays.t27 is served (14 nodes, 9 edges, exact only). But -; specs/t27f/*.t27 and specs/claw/*.t27 answer 404, so the visualizer cannot yet draw its own -; specs or TRI CLAW's: GRAPH_MANUAL until they are published. -; - t27f_view refuses a 3-line headline ("split it into a second slide"). It writes to the temp -; directory unless `out` names a path, and on the desktop that temp path is outside the session. -; - No MCP tool renders a particle still. The lab recipe (render the reel, then -; `ffmpeg -ss -frames:v 1`) is the only path today; a reel_still tool in TRI CLAW's /mcp is -; the follow-up. -; The caps GRAPH_NODE_CAP / GRAPH_EDGE_CAP are the t27f_graph tool's own (its description, -; 2026-10-18), not a spec's; the exporter itself reaches 1024 / 4096. +; The rule itself has one home, specs/t27mcp/visual_output.t27 (owner pick 2026-10-18): which answers +; carry a visual (needs_visual), how it reaches each host (answer_channel), and the verdict on a +; drawing made from t27f tool rows (answer_verdict). This spec imports those and adds only what is +; particular to a particle still: +; - Which answer's visual is a still: a report of a piece of work (K_REPORT). A spec, a flow or +; numbers are drawn from t27f tool rows, as the t27mcp law says; a still backs no row, so it is +; not their visual. +; - The still is one PNG frame of a rendered token_floor reel, taken at STILL_MS: after the hook has +; written its statement and before the CTA hold. +; - It is rendered on the Railway lab, never on the owner's Mac (owner rule 2026-10-09). No MCP tool +; renders one yet; the lab recipe (render the reel, then `ffmpeg -ss -frames:v 1`) is the only +; path, and a reel_still tool in TRI CLAW's /mcp is the follow-up. +; - It is a file, not an inline widget, so its channel is answer_channel for a drawing on a host with +; no visual tool. On the desktop Code tab that is the file pane, which opens only files inside the +; session's folder (measured 2026-10-18), so the PNG goes there. +; - A still that could not be made is reported missing, with the reason; an older file is never shown +; as this turn's. +; Not measured: whether TRI CLAW's chat shows a PNG URL (the t27mcp header measured .svg only). ; phi^2 + 1/phi^2 = 3 | TRINITY module ClawVisualOutput; -use t27f::reel::channel_for; -use t27f::reel::HOST_CLAUDE_CODE_CLI; -use t27f::reel::HOST_CLAUDE_CODE_DESKTOP; -use t27f::reel::HOST_MCP_APPS; -use t27f::reel::HOST_SITE; -use t27f::reel::CHANNEL_FILE_AND_PANE; -use t27f::deck::MAX_HEADLINE_LINES; +use t27mcp::visual_output::needs_visual; +use t27mcp::visual_output::answer_channel; +use t27mcp::visual_output::K_SPEC; +use t27mcp::visual_output::K_FLOW; +use t27mcp::visual_output::K_NUMBERS; +use t27mcp::visual_output::K_REPORT; +use t27mcp::visual_output::K_ONE_SENTENCE; +use t27mcp::visual_output::K_QUESTION; +use t27mcp::visual_output::HOST_CLAUDE_CODE_CLI; +use t27mcp::visual_output::HOST_CLAUDE_CODE_DESKTOP; +use t27mcp::visual_output::HOST_MCP_APPS; +use t27mcp::visual_output::HOST_TRI_CLAW; +use t27mcp::visual_output::CH_FILE_PATH; +use t27mcp::visual_output::CH_FILE_AND_PANE; +use t27mcp::visual_output::CH_UI_RESOURCE; +use t27mcp::visual_output::CH_IMAGE_URL; use t27f::reel_marketing::REEL_HOOK_MS; use t27f::reel_marketing::REEL_CTA_MIN_MS; use t27f::reel_marketing::REEL_MIN_MS; -/// What an output is. -pub const OUT_TALK : u8 = 0; // a short answer, a question back, conversation -pub const OUT_WORK : u8 = 1; // a turn that changed files or ran commands -pub const OUT_SPEC : u8 = 2; // a .t27 spec added, changed or explained -pub const OUT_STATUS : u8 = 3; // a status or progress report -pub const OUT_REEL : u8 = 4; // the output is a reel -pub const OUT_CHECK : u8 = 5; // a TRI CLAW check-run on a pull request -pub const OUT_KINDS : u8 = 6; - -/// What it carries, one bit each, so one output can carry several. -pub const VIS_STILL : u8 = 1; // particle still, PNG, one frame of a token_floor reel -pub const VIS_REEL : u8 = 2; // the reel, tall and wide mp4 -pub const VIS_GRAPH : u8 = 4; // the spec's T27F graph, SVG, from t27f_graph -pub const VIS_LINK : u8 = 8; // a link in the check-run summary - -/// Where a visual is made. -pub const RENDER_LAB : u8 = 0; // the Railway lab -pub const RENDER_HOST : u8 = 1; // the MCP host - -/// Where the file is put. -pub const PLACE_ANY : u8 = 0; // any path, printed in the answer -pub const PLACE_SESSION : u8 = 1; // inside the session's folder - -/// How a spec graph is labelled. -pub const GRAPH_EXPORTED : u8 = 0; -pub const GRAPH_FOLDED : u8 = 1; -pub const GRAPH_MANUAL : u8 = 2; -pub const GRAPH_NODE_CAP : u32 = 192; -pub const GRAPH_EDGE_CAP : u32 = 254; - /// The still is this frame of a reel: past the hook's decision window, before the CTA hold. pub const STILL_MS : i32 = 9000; -pub fn has(v: u8, bit: u8) -> bool { - return @mod(v / bit, 2) == 1; -} - -/// The visuals an output of this kind carries. A kind this law does not know gets the default -/// still: a reader that waves through an unknown kind with no visual is not a rule. -pub fn visuals_for(kind: u8) -> u8 { - if (kind == OUT_TALK) { - return 0; - } - if (kind == OUT_SPEC) { - return VIS_STILL + VIS_GRAPH; - } - if (kind == OUT_REEL) { - return VIS_STILL + VIS_REEL; - } - if (kind == OUT_CHECK) { - return VIS_LINK; - } - return VIS_STILL; -} +/// Where the still's file is put. +pub const PLACE_ANY : u32 = 0; // any path, printed in the answer +pub const PLACE_SESSION : u32 = 1; // inside the session's folder -pub fn needs_visual(kind: u8) -> bool { - return visuals_for(kind) != 0; +/// Whether an answer of this kind shows a particle still as its visual. +pub fn takes_still(kind: u32) -> bool { + return kind == K_REPORT; } -/// Particle frames come from the lab; SVG from the MCP host. -pub fn render_place(bit: u8) -> u8 { - if (bit == VIS_STILL or bit == VIS_REEL) { - return RENDER_LAB; - } - return RENDER_HOST; +/// How the still reaches the user: a drawn file on a host with no inline visual tool. +pub fn still_channel(host: u32) -> u32 { + return answer_channel(host, false, true); } /// The file goes inside the session folder wherever the host opens it in a pane. -pub fn place_for(host: u8) -> u8 { - if (channel_for(host) == CHANNEL_FILE_AND_PANE) { +pub fn place_for(host: u32) -> u32 { + if (still_channel(host) == CH_FILE_AND_PANE) { return PLACE_SESSION; } return PLACE_ANY; } -pub fn graph_label(served: bool, nodes: u32, edges: u32) -> u8 { - if (served == false) { - return GRAPH_MANUAL; - } - if (nodes > GRAPH_NODE_CAP or edges > GRAPH_EDGE_CAP) { - return GRAPH_FOLDED; - } - return GRAPH_EXPORTED; -} - -/// An output is honest when every visual it needs was made this turn or is reported missing. -pub fn report_ok(kind: u8, made: u8, said_missing: u8) -> bool { - const need = visuals_for(kind); - var ok = true; - var bit : u8 = 1; - while (bit <= VIS_LINK) { - if (has(need, bit) and has(made, bit) == false and has(said_missing, bit) == false) { - ok = false; - } - bit = bit * 2; - } - return ok; -} - /// A still frame shows the statement, not the hook still writing it and not the CTA hold. pub fn still_ms_ok(ms: i32) -> bool { return ms > REEL_HOOK_MS and ms < REEL_MIN_MS - REEL_CTA_MIN_MS; } -/// Slides a headline of this many lines takes: split, never cut. -pub fn slides_for(lines: i32) -> i32 { - return (lines + MAX_HEADLINE_LINES - 1) / MAX_HEADLINE_LINES; +/// An answer is honest about its still when the still was made this turn or is said missing. +pub fn still_report_ok(kind: u32, made: bool, said_missing: bool) -> bool { + if (takes_still(kind) == false) { + return true; + } + return made or said_missing; } -test only_talk_goes_without_a_visual { - assert needs_visual(OUT_TALK) == false; - var k : u8 = 1; - while (k < OUT_KINDS) { - assert needs_visual(k); - k = k + 1; - } - assert visuals_for(OUT_KINDS) == VIS_STILL; - assert visuals_for(200) == VIS_STILL; +test only_a_report_takes_a_still { + assert takes_still(K_REPORT); + assert takes_still(K_SPEC) == false; + assert takes_still(K_FLOW) == false; + assert takes_still(K_NUMBERS) == false; + assert takes_still(K_ONE_SENTENCE) == false; + assert takes_still(K_QUESTION) == false; } -test each_kind_carries_its_visuals { - assert visuals_for(OUT_WORK) == VIS_STILL; - assert visuals_for(OUT_STATUS) == VIS_STILL; - assert has(visuals_for(OUT_SPEC), VIS_STILL) and has(visuals_for(OUT_SPEC), VIS_GRAPH); - assert has(visuals_for(OUT_REEL), VIS_REEL) and has(visuals_for(OUT_REEL), VIS_STILL); - assert has(visuals_for(OUT_REEL), VIS_GRAPH) == false; - assert visuals_for(OUT_CHECK) == VIS_LINK; +test every_still_is_an_answer_the_t27mcp_law_gives_a_visual { + var k : u32 = 0; + var stills : u32 = 0; + while (k <= K_QUESTION) { + if (takes_still(k)) { + assert needs_visual(k); + stills = stills + 1; + } + k = k + 1; + } + assert stills == 1; } -test particle_frames_come_from_the_lab { - assert render_place(VIS_STILL) == RENDER_LAB; - assert render_place(VIS_REEL) == RENDER_LAB; - assert render_place(VIS_GRAPH) == RENDER_HOST; +test the_still_is_a_file_not_a_widget { + assert still_channel(HOST_CLAUDE_CODE_DESKTOP) == CH_FILE_AND_PANE; + assert still_channel(HOST_CLAUDE_CODE_CLI) == CH_FILE_PATH; + assert still_channel(HOST_MCP_APPS) == CH_UI_RESOURCE; + assert still_channel(HOST_TRI_CLAW) == CH_IMAGE_URL; } test the_desktop_file_sits_in_the_session_folder { assert place_for(HOST_CLAUDE_CODE_DESKTOP) == PLACE_SESSION; assert place_for(HOST_CLAUDE_CODE_CLI) == PLACE_ANY; assert place_for(HOST_MCP_APPS) == PLACE_ANY; - assert place_for(HOST_SITE) == PLACE_ANY; -} - -test a_graph_says_how_it_was_read { - // tutorial 08, as t27f_outline served it on 2026-10-18 - assert graph_label(true, 14, 9) == GRAPH_EXPORTED; - assert graph_label(true, 192, 254) == GRAPH_EXPORTED; - // the largest spec in specs/ after derive: 775 nodes, 3194 edges - assert graph_label(true, 775, 3194) == GRAPH_FOLDED; - assert graph_label(true, 193, 9) == GRAPH_FOLDED; - // specs/claw/*.t27 answered 404 - assert graph_label(false, 14, 9) == GRAPH_MANUAL; -} - -test a_missing_visual_is_said_never_skipped { - assert report_ok(OUT_TALK, 0, 0); - assert report_ok(OUT_WORK, VIS_STILL, 0); - assert report_ok(OUT_WORK, 0, 0) == false; - assert report_ok(OUT_WORK, 0, VIS_STILL); - assert report_ok(OUT_SPEC, VIS_STILL, 0) == false; - assert report_ok(OUT_SPEC, VIS_STILL, VIS_GRAPH); - assert report_ok(OUT_REEL, VIS_STILL + VIS_REEL, 0); - assert report_ok(OUT_CHECK, VIS_STILL, 0) == false; + assert place_for(HOST_TRI_CLAW) == PLACE_ANY; } test the_still_is_past_the_hook_and_before_the_cta { @@ -224,9 +127,10 @@ test the_still_is_past_the_hook_and_before_the_cta { assert still_ms_ok(14000) == false; } -test a_long_headline_takes_more_slides { - assert slides_for(1) == 1; - assert slides_for(2) == 1; - assert slides_for(3) == 2; - assert slides_for(5) == 3; +test a_missing_still_is_said_never_skipped { + assert still_report_ok(K_REPORT, true, false); + assert still_report_ok(K_REPORT, false, true); + assert still_report_ok(K_REPORT, false, false) == false; + assert still_report_ok(K_SPEC, false, false); + assert still_report_ok(K_ONE_SENTENCE, false, false); } From 0b1e46dba3de59bb7769d4c37428cd433b8e4254 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 18 Oct 2026 23:44:59 +0700 Subject: [PATCH 08/27] skill t27f: move the particle-video note to the T27F definition PR (Closes #10508) Co-Authored-By: Claude Opus 5.5 --- .claude/skills/t27f/SKILL.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.claude/skills/t27f/SKILL.md b/.claude/skills/t27f/SKILL.md index d154176949..33368e43d5 100644 --- a/.claude/skills/t27f/SKILL.md +++ b/.claude/skills/t27f/SKILL.md @@ -49,11 +49,6 @@ executable visual diagrams"). as a navigable program, linked to source, tests, issues and real task status; each cell is both a spec and a learning entry point. -**The particle video is T27F too** (owner, 2026-10-18: "t27f is the particle video, the thing you -were doing"). The token_floor reels (`specs/t27f/token_floor*.t27`, `lesson_reel.t27`) are the -form of T27F the owner means when asking for a visual. The graph IR below is the inspectable form. -Both are T27F; neither replaces the other. - **What T27F is not:** it is not the topic-to-value engine. gHashTag/trinity#1677 (topic or client brief -> 27-lesson map -> Bee -> verified artifact) is an *application* built on T27F, and so are the reels. Describing T27F as #1677 is the error this section exists to stop (owner correction, From c28c9145151353de51340079a287893ce5b2301a Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 18 Oct 2026 23:56:16 +0700 Subject: [PATCH 09/27] claw: reel_still.t27 -- the TRI CLAW /mcp tool that renders one particle still (Closes #10547) specs/claw/visual_output.t27 says a report's visual is one PNG frame of a token_floor reel at STILL_MS, rendered on the Railway lab, and that no MCP tool renders one. This spec is that tool's contract: the name is homed in TRI CLAW by specs/t27mcp/service.t27, topics resolve through specs/t27f/reel_topics.t27, the frame is 270 of 450 and frame_shard draws only it (TF_SHARD=270 TF_SHARDS=450 TF_CUT=aspect), the overlays enter with -itsoffset 9, and a call off the lab is refused. Lab: 13/13 pass, 0 vacuous; t27b 13/13; mutants of the topic lookup, the frame formula (/1001; /999 is an equivalent mutant) and the lab check are killed. Sealed; t27b ledger row; docs/claw/README.md row. Co-Authored-By: Claude Opus 5.5 --- .trinity/seals/claw_ClawReelStill.json | 49 +++++ docs/claw/README.md | 1 + docs/reports/t27b_expectations.json | 1 + specs/claw/reel_still.t27 | 280 +++++++++++++++++++++++++ 4 files changed, 331 insertions(+) create mode 100644 .trinity/seals/claw_ClawReelStill.json create mode 100644 specs/claw/reel_still.t27 diff --git a/.trinity/seals/claw_ClawReelStill.json b/.trinity/seals/claw_ClawReelStill.json new file mode 100644 index 0000000000..6ab567b809 --- /dev/null +++ b/.trinity/seals/claw_ClawReelStill.json @@ -0,0 +1,49 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+5e4d3b998", + "closure": [ + { + "spec": "specs/claw/visual_output.t27", + "spec_hash": "sha256:17137afe136e39910f38d7b031c659a672e009bfeef1bf4f9b7af7e2cb6cc628" + }, + { + "spec": "specs/t27f/frame_shard.t27", + "spec_hash": "sha256:1f04638a243b7a7f4d7a4480faf85f7878ca2e3365a2ec0f001a991fbd353da6" + }, + { + "spec": "specs/t27f/reel_topics.t27", + "spec_hash": "sha256:7c1762cde96524791711594d23f880fbad1f497a499277710f1fd61d52d50157" + }, + { + "spec": "specs/t27f/token_floor_scenes.t27", + "spec_hash": "sha256:948cff4bd34341b40b43c8a170e2a37e3bca59f8d6bc8e35d7e5ee43fe89a4ea" + }, + { + "spec": "specs/t27mcp/service.t27", + "spec_hash": "sha256:3d7957837be8ac3de14b18ce1c0ecdde805f3f71d4d801b62d476b2e82f4a1ae" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:deed4a8b76269ea5b07012ad0fa77fe95b32a7966ef56ea69066d8a8fa07ed84", + "gen_hash_rust": "sha256:de31997b7cb39086725aecbb03729fe7cd5eaebb9dc8e613a90dc0943899acd1", + "gen_hash_verilog": "sha256:f567550c0c1fc421b5fe24449fdbc2a7d3561fb26fce69fdbad22a55c3ede3c9", + "gen_hash_zig": "sha256:77d5fb33ff66b0be2be794c4f9176245cb55edd5c695e69b840fe85210eb8395", + "module": "ClawReelStill", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T17:00:18Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:a8d80acb663c353bee235aa6e45c83bf1bed113ab252426b10ad2641112452e2", + "spec_path": "specs/claw/reel_still.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 13, + "total": 13, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/claw/README.md b/docs/claw/README.md index 4a7df352ec..048cfd5a79 100644 --- a/docs/claw/README.md +++ b/docs/claw/README.md @@ -18,6 +18,7 @@ behind one front door, with the gate logic in `.t27` specs. Epic | Service skeleton | Railway, thin I/O glue only | T5, merged (spec contract) | | Model routing: Opus 5.5 orchestrates, Sonnet 5.5 runs sub-tasks | `specs/claw/model_route.t27`, skill `model-routing`, `.claude/settings.json` + `.claude/agents/*` | owner directive 2026-10-18 | | A T27F visual in every answer that needs one | rule `specs/t27mcp/visual_output.t27`, particle-still layer `specs/claw/visual_output.t27`, procedure in skill `t27f`, "Visual in every output" | owner directive 2026-10-18 | +| `reel_still`: one particle still (PNG at STILL_MS of a token_floor reel) from TRI CLAW's `/mcp` | `specs/claw/reel_still.t27`, rendered on the Railway lab only | follow-up of the row above | | Positioning / pricing | `docs/claw/competitors.md` | T6, started | ## The T27F check in every `.t27` repository (#10268) diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 11452ed21e..2098b8e829 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -195,6 +195,7 @@ {"path": "specs/claw/model_route.t27", "t27b": "pass"}, {"path": "specs/claw/preview.t27", "t27b": "pass"}, {"path": "specs/claw/recipes.t27", "t27b": "pass"}, + {"path": "specs/claw/reel_still.t27", "t27b": "pass"}, {"path": "specs/claw/refresh.t27", "t27b": "pass"}, {"path": "specs/claw/sarif.t27", "t27b": "pass"}, {"path": "specs/claw/sarif_feed.t27", "t27b": "pass"}, diff --git a/specs/claw/reel_still.t27 b/specs/claw/reel_still.t27 new file mode 100644 index 0000000000..9c23fab020 --- /dev/null +++ b/specs/claw/reel_still.t27 @@ -0,0 +1,280 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/claw/reel_still.t27 -- reel_still, the TRI CLAW /mcp tool that renders one particle still. +; specs/claw/visual_output.t27 says a report's visual is one PNG frame of a token_floor reel taken at +; STILL_MS, rendered on the Railway lab, and that no MCP tool renders one yet. This spec is that tool: +; what it takes, what it refuses, and the exact lab recipe it runs. +; -- +; Input (four fields): topic, lesson, lang, aspect. +; - topic "" or "token-floor" with lesson -1 is the physics reel (token_floor + token_floor_mux). +; - A lesson id from the t27-basics course ("what-a-spec-is", "the-module-line", ...; the ids are +; specs/t27f/reel_topics.t27's, so this tool and the reels never disagree about a lesson) is that +; lesson's reel (lesson_reel). The lesson index 0..TOPICS-1 names the same reel; giving both is +; allowed only when they agree. +; - lang 0 en, 1 ru. aspect 0 tall (1080x1920), 1 wide (1920x1080); one PNG is one aspect, so the +; "both" cut token_floor's TF_CUT also accepts is refused here. +; Output: one image/png, the frame the reel shows at STILL_MS. +; -- +; The recipe renders one frame, not the reel. frame_shard splits a reel into n shards; with +; n = SCENE_FRAMES every shard is one frame, so TF_SHARD = still_frame(), TF_SHARDS = SCENE_FRAMES +; draws exactly frame still_frame() (the physics still steps from frame 0; only drawing is skipped). +; The overlays (captions, labels, voice and music, which the still does not need but the filter graph +; reads) are the reel's own filter graph: the one frame enters it with `-itsoffset still_s()`, so the +; graph sees it at the reel's STILL_MS and draws what the reel draws there. `[v]` goes to the PNG, +; `[a]` to null. Measured on the lab 2026-10-18, lesson 1 en tall: rc 0, 1660 ms, 484981 bytes. +; -- +; It runs on the Railway lab only (owner rule 2026-10-09); a call anywhere else is refused, never +; rendered. The name has no t27_ or t27f_ prefix, so the one registry (specs/t27mcp/service.t27) homes +; it in TRI CLAW and mounts it as it is. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module ClawReelStill; + +use t27mcp::service::admit; +use t27mcp::service::home_of; +use t27mcp::service::name_ok; +use t27mcp::service::H_T27; +use t27mcp::service::H_TRI_CLAW; +use t27mcp::service::A_MOUNT; +use t27mcp::service::A_REFUSE_COLLISION; +use claw::visual_output::STILL_MS; +use claw::visual_output::still_ms_ok; +use t27f::token_floor_scenes::SCENE_FPS; +use t27f::token_floor_scenes::SCENE_FRAMES; +use t27f::frame_shard::first_of; +use t27f::frame_shard::end_of; +use t27f::reel_topics::TOPICS; +use t27f::reel_topics::id_item; +use t27f::reel_topics::item_len; +use t27f::reel_topics::cp; + +/// The tool's name in TRI CLAW's /mcp. +pub const TOOL_NAME : str = "reel_still"; +pub const MIME : str = "image/png"; + +/// The input fields, in the order the tool's schema lists them. +pub const INPUT_COUNT : u32 = 4; +pub const INPUT_NAMES : [4]str = ["topic", "lesson", "lang", "aspect"]; + +/// The physics reel's topic name, and the lesson field's "no lesson". +pub const PHYSICS_TOPIC : str = "token-floor"; +pub const LESSON_NONE : i32 = -1; + +/// Which reel a call resolves to. +pub const REEL_PHYSICS : i32 = -1; // otherwise the lesson index 0..TOPICS-1 +pub const REEL_NONE : i32 = -2; // the call is refused + +pub const LANG_EN : i32 = 0; +pub const LANG_RU : i32 = 1; +pub const LANG_NAMES : [2]str = ["en", "ru"]; + +/// The aspect is token_floor's TF_CUT value for that cut. +pub const ASPECT_TALL : i32 = 0; +pub const ASPECT_WIDE : i32 = 1; +pub const ASPECT_NAMES : [2]str = ["tall", "wide"]; + +/// What the tool answers. +pub const V_OK : u32 = 0; +pub const V_UNKNOWN_TOPIC : u32 = 1; +pub const V_BAD_LESSON : u32 = 2; +pub const V_DISAGREE : u32 = 3; +pub const V_BAD_LANG : u32 = 4; +pub const V_BAD_ASPECT : u32 = 5; +pub const V_NOT_ON_LAB : u32 = 6; + +/// The file the still is written to: STILL_PREFIX id - lang - aspect STILL_EXT. +pub const STILL_PREFIX : str = "still-"; +pub const STILL_EXT : str = ".png"; + +/// Whether lesson t's course id is s, code point by code point. +pub fn id_is(t: u32, s: str) -> bool { + let i : usize = id_item(t as usize); + let n : usize = item_len(i); + if (n != s.len) { return false; } + var k : usize = 0; + while (k < n) { + if ((s[k] as u32) != cp(i, k)) { return false; } + k = k + 1; + } + return true; +} + +/// The lesson whose id is s, or REEL_PHYSICS for the physics reel's names, or REEL_NONE. +pub fn topic_reel(s: str) -> i32 { + if (s.len == 0 or s == PHYSICS_TOPIC) { return REEL_PHYSICS; } + var t : u32 = 0; + while (t < (TOPICS as u32)) { + if (id_is(t, s)) { return t as i32; } + t = t + 1; + } + return REEL_NONE; +} + +/// Whether a lesson field is "none" or a lesson index. +pub fn lesson_ok(lesson: i32) -> bool { + return lesson == LESSON_NONE or (lesson >= 0 and lesson < (TOPICS as i32)); +} + +/// The reel a call names: from topic, from lesson, or from both when they agree. +pub fn reel_of(topic: str, lesson: i32) -> i32 { + if (lesson_ok(lesson) == false) { return REEL_NONE; } + let by_topic : i32 = topic_reel(topic); + if (by_topic == REEL_NONE) { return REEL_NONE; } + if (lesson == LESSON_NONE) { return by_topic; } + if (topic.len == 0) { return lesson; } + if (by_topic == lesson) { return lesson; } + return REEL_NONE; +} + +/// The tool's verdict on a call; only V_OK renders. +pub fn verdict(topic: str, lesson: i32, lang: i32, aspect: i32, on_lab: bool) -> u32 { + if (lesson_ok(lesson) == false) { return V_BAD_LESSON; } + if (topic_reel(topic) == REEL_NONE) { return V_UNKNOWN_TOPIC; } + if (reel_of(topic, lesson) == REEL_NONE) { return V_DISAGREE; } + if (lang != LANG_EN and lang != LANG_RU) { return V_BAD_LANG; } + if (aspect != ASPECT_TALL and aspect != ASPECT_WIDE) { return V_BAD_ASPECT; } + if (on_lab == false) { return V_NOT_ON_LAB; } + return V_OK; +} + +/// The frame the reel shows at STILL_MS. +pub fn still_frame() -> i32 { + return @divTrunc(STILL_MS * SCENE_FPS, 1000); +} + +/// The renderer's environment for that one frame: TF_SHARD, TF_SHARDS, TF_CUT. +pub fn env_shard() -> i32 { return still_frame(); } +pub fn env_shards() -> i32 { return SCENE_FRAMES; } +pub fn env_cut(aspect: i32) -> i32 { return aspect; } + +/// The frame's size and its rgb24 bytes, the length of the renderer's stream for one frame. +pub fn frame_w(aspect: i32) -> i32 { + if (aspect == ASPECT_WIDE) { return 1920; } + return 1080; +} +pub fn frame_h(aspect: i32) -> i32 { + if (aspect == ASPECT_WIDE) { return 1080; } + return 1920; +} +pub fn frame_bytes(aspect: i32) -> i32 { + return frame_w(aspect) * frame_h(aspect) * 3; +} + +/// The `-itsoffset` the one frame enters the reel's filter graph with, in whole seconds. +pub fn still_s() -> i32 { + return @divTrunc(STILL_MS, 1000); +} + +/// The still's file name length: prefix, id, "-", lang, "-", aspect, extension. +pub fn name_len(reel: i32, lang: i32, aspect: i32) -> usize { + var id_n : usize = PHYSICS_TOPIC.len; + if (reel >= 0) { id_n = item_len(id_item(reel as usize)); } + return STILL_PREFIX.len + id_n + 1 + LANG_NAMES[lang as usize].len + 1 + ASPECT_NAMES[aspect as usize].len + STILL_EXT.len; +} + +test the_tool_is_tri_claws_and_mounted_as_it_is { + assert name_ok(TOOL_NAME); + assert home_of(TOOL_NAME) == H_TRI_CLAW; + assert admit(TOOL_NAME, H_TRI_CLAW) == A_MOUNT; + assert INPUT_NAMES.len == INPUT_COUNT as usize; +} + +test negative_control_under_a_t27f_name_it_would_be_refused { + assert home_of("t27f_reel_still") == H_T27; + assert admit("t27f_reel_still", H_TRI_CLAW) == A_REFUSE_COLLISION; +} + +test a_topic_names_the_physics_reel_or_a_lesson { + assert topic_reel("") == REEL_PHYSICS; + assert topic_reel("token-floor") == REEL_PHYSICS; + assert topic_reel("what-a-spec-is") == 0; + assert topic_reel("the-module-line") == 1; + assert topic_reel("constants") == 3; +} + +test every_lesson_id_resolves_to_its_own_index { + var t : u32 = 0; + var hits : u32 = 0; + while (t < (TOPICS as u32)) { + let i : usize = id_item(t as usize); + assert item_len(i) > 0; + if (id_is(t, "what-a-spec-is")) { hits = hits + 1; } + t = t + 1; + } + assert hits == 1; + assert TOPICS == 27; +} + +test negative_control_a_near_miss_topic_is_unknown { + assert topic_reel("what-a-spec") == REEL_NONE; + assert topic_reel("what-a-spec-is-") == REEL_NONE; + assert topic_reel("What-a-spec-is") == REEL_NONE; + assert topic_reel("token_floor") == REEL_NONE; + assert id_is(0, "the-module-line") == false; +} + +test topic_and_lesson_agree_or_the_call_is_refused { + assert reel_of("", LESSON_NONE) == REEL_PHYSICS; + assert reel_of("", 1) == 1; + assert reel_of("the-module-line", LESSON_NONE) == 1; + assert reel_of("the-module-line", 1) == 1; + assert reel_of("the-module-line", 2) == REEL_NONE; + assert reel_of("token-floor", 0) == REEL_NONE; + assert reel_of("", 27) == REEL_NONE; + assert reel_of("", -2) == REEL_NONE; +} + +test the_verdict_names_what_is_wrong_first { + assert verdict("the-module-line", LESSON_NONE, LANG_EN, ASPECT_TALL, true) == V_OK; + assert verdict("", LESSON_NONE, LANG_RU, ASPECT_WIDE, true) == V_OK; + assert verdict("nope", LESSON_NONE, LANG_EN, ASPECT_TALL, true) == V_UNKNOWN_TOPIC; + assert verdict("", 40, LANG_EN, ASPECT_TALL, true) == V_BAD_LESSON; + assert verdict("constants", 0, LANG_EN, ASPECT_TALL, true) == V_DISAGREE; + assert verdict("", 0, 2, ASPECT_TALL, true) == V_BAD_LANG; + assert verdict("", 0, LANG_EN, 2, true) == V_BAD_ASPECT; + assert verdict("", 0, LANG_EN, -1, true) == V_BAD_ASPECT; +} + +test negative_control_a_good_call_off_the_lab_is_refused_not_rendered { + assert verdict("the-module-line", LESSON_NONE, LANG_EN, ASPECT_TALL, false) == V_NOT_ON_LAB; + assert verdict("", LESSON_NONE, LANG_EN, ASPECT_WIDE, false) == V_NOT_ON_LAB; +} + +test the_still_is_frame_270_of_450_and_the_rule_allows_it { + assert still_frame() == 270; + assert still_frame() < SCENE_FRAMES; + assert SCENE_FPS == 30; + assert still_ms_ok(STILL_MS); + assert @rem(STILL_MS, 1000) == 0; + assert still_s() * 1000 == STILL_MS; +} + +test the_shard_env_draws_exactly_the_still_frame { + assert first_of(env_shard(), env_shards(), SCENE_FRAMES) == still_frame(); + assert end_of(env_shard(), env_shards(), SCENE_FRAMES) == still_frame() + 1; + assert env_cut(ASPECT_TALL) == 0; + assert env_cut(ASPECT_WIDE) == 1; +} + +test negative_control_a_halved_shard_count_renders_the_whole_reel { + let half : i32 = @divTrunc(env_shards(), 2); + assert first_of(env_shard(), half, SCENE_FRAMES) == 0; + assert end_of(env_shard(), half, SCENE_FRAMES) == SCENE_FRAMES; + assert first_of(env_shard(), 1, SCENE_FRAMES) == 0; +} + +test one_frame_is_6220800_bytes_in_either_aspect { + assert frame_w(ASPECT_TALL) == 1080; + assert frame_h(ASPECT_TALL) == 1920; + assert frame_w(ASPECT_WIDE) == 1920; + assert frame_h(ASPECT_WIDE) == 1080; + assert frame_bytes(ASPECT_TALL) == 6220800; + assert frame_bytes(ASPECT_WIDE) == frame_bytes(ASPECT_TALL); +} + +test the_file_name_is_prefix_id_lang_aspect { + // still-what-a-spec-is-en-tall.png + assert name_len(0, LANG_EN, ASPECT_TALL) == 32; + // still-token-floor-ru-wide.png + assert name_len(REEL_PHYSICS, LANG_RU, ASPECT_WIDE) == 29; + assert name_len(1, LANG_EN, ASPECT_TALL) != name_len(0, LANG_EN, ASPECT_TALL); +} From 4c514f8a19c9491f580048f6d72383e91598ab7d Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 00:05:58 +0700 Subject: [PATCH 10/27] claw: reel_still pads the one frame with tpad so the overlays see n = 270 (#10547) Co-Authored-By: Claude Opus 5.5 --- specs/claw/reel_still.t27 | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/specs/claw/reel_still.t27 b/specs/claw/reel_still.t27 index 9c23fab020..506224df07 100644 --- a/specs/claw/reel_still.t27 +++ b/specs/claw/reel_still.t27 @@ -18,9 +18,14 @@ ; n = SCENE_FRAMES every shard is one frame, so TF_SHARD = still_frame(), TF_SHARDS = SCENE_FRAMES ; draws exactly frame still_frame() (the physics still steps from frame 0; only drawing is skipped). ; The overlays (captions, labels, voice and music, which the still does not need but the filter graph -; reads) are the reel's own filter graph: the one frame enters it with `-itsoffset still_s()`, so the -; graph sees it at the reel's STILL_MS and draws what the reel draws there. `[v]` goes to the PNG, -; `[a]` to null. Measured on the lab 2026-10-18, lesson 1 en tall: rc 0, 1660 ms, 484981 bytes. +; reads) are the reel's own filter graph, and its drawtext overlays are keyed on the filter frame +; index n, not on time. A single input frame is n = 0 however it is offset in time, so `-itsoffset 9` +; draws the frame-0 overlays: measured, that still matched the raw particle frame (42.9 dB PSNR) and +; not the reel (23.3 dB). The recipe pads instead: the graph's `[0:v]` gets +; `tpad=start=pad_frames()` in front, so the one frame arrives as n = still_frame() at t = STILL_MS. +; The last frame of `[v]` goes to the PNG (`-update 1`), `[a]` to null. Measured on the lab +; 2026-10-18, lesson 1 en tall: rc 0, 2585 ms, 499707 bytes, 49.3 dB PSNR against frame 270 of the +; reel's own mp4 (21.7 dB against frame 269, 22.0 dB against 271). ; -- ; It runs on the Railway lab only (owner rule 2026-10-09); a call anywhere else is refused, never ; rendered. The name has no t27_ or t27f_ prefix, so the one registry (specs/t27mcp/service.t27) homes @@ -159,9 +164,16 @@ pub fn frame_bytes(aspect: i32) -> i32 { return frame_w(aspect) * frame_h(aspect) * 3; } -/// The `-itsoffset` the one frame enters the reel's filter graph with, in whole seconds. -pub fn still_s() -> i32 { - return @divTrunc(STILL_MS, 1000); +/// The frames `tpad=start=` puts in front of the one frame, so the reel's filter graph sees it as +/// frame index n = still_frame(). +pub fn pad_frames() -> i32 { + return still_frame(); +} + +/// The filter frame index n the one input frame has after `pad` frames of padding. The overlays are +/// keyed on n, so this, not the frame's timestamp, decides what they draw. +pub fn graph_n(pad: i32) -> i32 { + return pad; } /// The still's file name length: prefix, id, "-", lang, "-", aspect, extension. @@ -244,8 +256,19 @@ test the_still_is_frame_270_of_450_and_the_rule_allows_it { assert still_frame() < SCENE_FRAMES; assert SCENE_FPS == 30; assert still_ms_ok(STILL_MS); - assert @rem(STILL_MS, 1000) == 0; - assert still_s() * 1000 == STILL_MS; + assert @rem(STILL_MS * SCENE_FPS, 1000) == 0; +} + +test the_padded_frame_is_the_overlays_frame_270 { + assert graph_n(pad_frames()) == still_frame(); + assert @divTrunc(pad_frames() * 1000, SCENE_FPS) == STILL_MS; +} + +test negative_control_an_unpadded_frame_draws_the_frame_0_overlays { + // -itsoffset moves the timestamp but not n; this is the recipe the lab measured wrong. + assert graph_n(0) == 0; + assert graph_n(0) != still_frame(); + assert graph_n(pad_frames() - 1) != still_frame(); } test the_shard_env_draws_exactly_the_still_frame { From e1a7e6ddca080e8c79f26762c79361a1fbf177a2 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 00:07:18 +0700 Subject: [PATCH 11/27] seal: claw_ClawReelStill 15/15, vacuous 0 (#10547) Co-Authored-By: Claude Opus 5.5 --- .trinity/seals/claw_ClawReelStill.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.trinity/seals/claw_ClawReelStill.json b/.trinity/seals/claw_ClawReelStill.json index 6ab567b809..b4ee1a2cf2 100644 --- a/.trinity/seals/claw_ClawReelStill.json +++ b/.trinity/seals/claw_ClawReelStill.json @@ -23,23 +23,23 @@ } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:deed4a8b76269ea5b07012ad0fa77fe95b32a7966ef56ea69066d8a8fa07ed84", - "gen_hash_rust": "sha256:de31997b7cb39086725aecbb03729fe7cd5eaebb9dc8e613a90dc0943899acd1", - "gen_hash_verilog": "sha256:f567550c0c1fc421b5fe24449fdbc2a7d3561fb26fce69fdbad22a55c3ede3c9", - "gen_hash_zig": "sha256:77d5fb33ff66b0be2be794c4f9176245cb55edd5c695e69b840fe85210eb8395", + "gen_hash_c": "sha256:d32a87a4218806616b04b727d7e3f1bc76d6d33440f304f7beb42b566bb50437", + "gen_hash_rust": "sha256:5a1adebb39cdeb948aae1da51a64f43b93b1cd845f715389c30cf6785b701c30", + "gen_hash_verilog": "sha256:62df937cda4af6426ae5e1170bafa4cf124f5d952dea6b87ccf19e451968dc9f", + "gen_hash_zig": "sha256:b4e20621dcef2e6517948018e7f6aeb788da068df27beb9b6c2a485af15b492e", "module": "ClawReelStill", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-18T17:00:18Z", + "sealed_at": "2026-10-18T17:06:53Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:a8d80acb663c353bee235aa6e45c83bf1bed113ab252426b10ad2641112452e2", + "spec_hash": "sha256:4fc5fc3c835193faae7b312194e4f31bc6baa8b7da3f08b12b809cb4e5e293e5", "spec_path": "specs/claw/reel_still.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 13, - "total": 13, + "passed": 15, + "total": 15, "vacuous": 0 }, "toolchain": { From dee8ba057e2edc949b5cfeec0e25283bb9ff0592 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 00:32:09 +0700 Subject: [PATCH 12/27] t27f: reel_factory.t27 -- content features to a unique scene plan per item (Closes #10561) The content factory of epic #10560 (R1 + R2). Each lesson's code and title are read into eleven feature counts; twelve scene archetypes are scored by term frequency over course frequency; the course is planned at once (best pair first, cap 4, no repeat within 2 items, then the least-used generic scene); accent, motion and layout dress the plan; eight output formats and the SSIM anomaly bound for R3. Lab: test-report 8/8 pass, 0 vacuous; seal --verify MATCH; t27b front end accepts it (JIT verdict is the arm64 ratchet's). Co-Authored-By: Claude Opus 5.5 --- .trinity/seals/t27f_reel_factory.json | 33 ++ docs/reports/t27b_expectations.json | 1 + specs/t27f/reel_factory.t27 | 641 ++++++++++++++++++++++++++ 3 files changed, 675 insertions(+) create mode 100644 .trinity/seals/t27f_reel_factory.json create mode 100644 specs/t27f/reel_factory.t27 diff --git a/.trinity/seals/t27f_reel_factory.json b/.trinity/seals/t27f_reel_factory.json new file mode 100644 index 0000000000..8c22289b02 --- /dev/null +++ b/.trinity/seals/t27f_reel_factory.json @@ -0,0 +1,33 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/reel_topics.t27", + "spec_hash": "sha256:7c1762cde96524791711594d23f880fbad1f497a499277710f1fd61d52d50157" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:b6a88603a9a3556fd210b37e84a10411e684046c214ea485c0d0721744e8aaf1", + "gen_hash_rust": "sha256:d84c9aadc6029a4b5f06179f4f82cb1270d0b96d3fb6bb57d9b2862e046fb94d", + "gen_hash_verilog": "sha256:85e0ff83e7b9e9ce54d8ad183f10e31346ab2332749d6f25ee1939f43e23abff", + "gen_hash_zig": "sha256:c9649bc6a482a56d1986007ae120166bf7488a0f084a02ca0ec2464365970801", + "module": "reel_factory", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T17:31:29Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:96099d764ba86fa3ef2c29d81c3838ce255bfd3b3abbd37b27cb86e4bca91012", + "spec_path": "specs/t27f/reel_factory.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 8, + "total": 8, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 09b79809ce..6e4b977d5f 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -1447,6 +1447,7 @@ {"path": "specs/t27f/pbr_sphere.t27", "t27b": "pass"}, {"path": "specs/t27f/quest.t27", "t27b": "pass"}, {"path": "specs/t27f/reel.t27", "t27b": "pass"}, + {"path": "specs/t27f/reel_factory.t27", "t27b": "pass"}, {"path": "specs/t27f/reel_gf16_nmse.t27", "t27b": "pass"}, {"path": "specs/t27f/reel_marketing.t27", "t27b": "pass"}, {"path": "specs/t27f/reel_particles.t27", "t27b": "blocked", "blocker": "ExprCall(@sin)", "reason": "unimplemented"}, diff --git a/specs/t27f/reel_factory.t27 b/specs/t27f/reel_factory.t27 new file mode 100644 index 0000000000..4de799421d --- /dev/null +++ b/specs/t27f/reel_factory.t27 @@ -0,0 +1,641 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/reel_factory.t27 -- the content factory: an item's own content in, a scene plan out, +// so that no two reels of a course look alike and no reel is edited by hand. Issue +// gHashTag/t27#10561, epic gHashTag/t27#10560. +// +// WHY: the owner, 2026-10-19: every lesson reel shows almost the same picture, there is no +// uniqueness; make one universal template, a factory, a content factory, not a hand edit of code +// per reel. The cause was in lesson_reel.t27's own NOT CLAIMED block: every lesson's backdrop was +// token_floor's physics world, and only the text over it changed. A template with one scene gives +// every item that scene. +// +// THE FACTORY, in four steps, each a function below: +// features the item's code is read, keyword by keyword at word boundaries (KW_*), into eleven +// counts (F_*): tests, functions, data types, trits, loops, branches, imports, guards, +// asserts, names and pub. Nothing is typed in per lesson. +// score each scene archetype (A_*) shows one kind of content. Its score for an item is how +// much more of that content the item has than the course as a whole: the item's count +// times 1000 over one plus the course's total (term frequency over course frequency, +// K. Sparck Jones, J. Doc. 28, 1972). The lesson about loops wins the loop scene even +// though every lesson has a test block. +// choose the course is planned at once. The best-scoring (item, construct scene) pair is +// assigned first, then the next, never past ARCH_CAP and never within WINDOW items of +// the same scene; then every item left takes the least-used generic scene (its code, +// title, task or place on the course map) free in its window. A first version walked +// the course in order and spent the test scene on lessons 0-6, so lesson 9, "A test +// block", never got it, and lessons 20-26 all fell back to one scene. +// dress the accent is the item's module on the course map (a signal, not decoration, as the +// brand book asks); the camera motion is t mod MOTIONS; the layout is t mod LAYOUTS. +// The signature of a plan is (archetype, accent, motion, layout). The tests hold, over every item +// of the course: no two signatures are equal; two neighbours differ in at least three of the four +// fields; no archetype is used more than ARCH_CAP times; every chosen archetype shows content the +// item has; and the lessons that teach a construct get that construct's scene. +// +// THE FORMATS (FMT_*): every plan renders to the reels, the posts, the cover, the carousel and the +// link cards a channel asks for, at the sizes the channels document. The carousel gets a cover, +// one slide per CAROUSEL_LINES lines of the item's code, and a call to action, at most CAROUSEL_MAX. +// +// THE ANOMALY GATE: when the stills of a course are rendered (R3 of the epic), any two whose SSIM +// is above SIMILAR_MAX_PERMILLE are too alike, and the run fails. The bound is a starting value +// and is not yet measured against rendered stills. +// +// NOT CLAIMED: the renderers of the archetypes are not here, and lesson_reel.t27 still draws +// token_floor's world until it reads this plan (R3 and R4 of the epic). This module decides which +// scene an item gets and checks that the choices are distinct; it draws nothing. The dressing +// alone already separates every signature of a 27-lesson course (motion and layout fix t mod 12, +// the accent tells t from t + 12 and t + 24), so the uniqueness test is weak by itself; the +// window test is the one that keeps the scenes apart. TITLE_WEIGHT is chosen, not measured. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module reel_factory; + +use t27f::reel_topics::TOPICS; +use t27f::reel_topics::code_line0; +use t27f::reel_topics::code_lines; +use t27f::reel_topics::line_len; +use t27f::reel_topics::code_byte; +use t27f::reel_topics::module_of; +use t27f::reel_topics::item; +use t27f::reel_topics::item_len; +use t27f::reel_topics::cp; +use t27f::reel_topics::MODULE_LESSONS; + +// ---- features: the keyword table ---- + +// The keywords, spelled as ASCII bytes end to end; keyword k is KW_BYTES[KW_START[k] .. +// KW_START[k + 1]] and counts toward feature KW_FEATURE[k]. +pub const KWS : usize = 17; +pub const KW_BYTES : [73]u8 = [ + 116, 101, 115, 116, // test + 102, 110, // fn + 115, 116, 114, 117, 99, 116, // struct + 101, 110, 117, 109, // enum + 116, 114, 105, 116, // trit + 119, 104, 105, 108, 101, // while + 102, 111, 114, // for + 105, 102, // if + 115, 119, 105, 116, 99, 104, // switch + 117, 115, 101, // use + 105, 110, 118, 97, 114, 105, 97, 110, 116, // invariant + 98, 101, 110, 99, 104, // bench + 97, 115, 115, 101, 114, 116, // assert + 99, 111, 110, 115, 116, // const + 118, 97, 114, // var + 108, 101, 116, // let + 112, 117, 98, // pub +]; +pub const KW_START : [18]u16 = [0, 4, 6, 12, 16, 20, 25, 28, 30, 36, 39, 48, 53, 59, 64, 67, 70, 73]; + +pub const F_TEST : usize = 0; +pub const F_FN : usize = 1; +pub const F_DATA : usize = 2; +pub const F_TRIT : usize = 3; +pub const F_LOOP : usize = 4; +pub const F_BRANCH : usize = 5; +pub const F_USE : usize = 6; +pub const F_GUARD : usize = 7; +pub const F_ASSERT : usize = 8; +pub const F_NAME : usize = 9; +pub const F_PUB : usize = 10; +pub const FEATS : usize = 11; +pub const FEAT_NAMES : str = "test,fn,data,trit,loop,branch,use,guard,assert,name,pub"; +pub const KW_FEATURE : [17]u8 = [0, 1, 2, 2, 3, 4, 4, 5, 5, 6, 7, 7, 8, 9, 9, 9, 10]; +// What one mention of a construct in a lesson's title weighs against one in its code. +pub const TITLE_WEIGHT : u32 = 10; + +// ---- the archetypes: the scenes an item can get ---- + +pub const A_CODE : usize = 0; // the item's code assembles from particles, line by line +pub const A_GRAPH : usize = 1; // the item's spec as a T27F diagram, declarations flying in +pub const A_TESTS : usize = 2; // each assert lights its PASS, one after another +pub const A_TRIT : usize = 3; // a field of minus-one, zero and plus-one particles +pub const A_LOOP : usize = 4; // particles orbit, one lap per iteration +pub const A_BRANCH : usize = 5; // a stream splits at every if and switch +pub const A_DATA : usize = 6; // a struct's or enum's fields stack as glass blocks +pub const A_LINKS : usize = 7; // the modules an item uses, linked by their use edges +pub const A_GUARD : usize = 8; // an invariant as a ring that holds a value in place +pub const A_TITLE : usize = 9; // the item's title, letter by letter, as kinetic type +pub const A_TASK : usize = 10; // the item's task as a checklist that fills in +pub const A_MAP : usize = 11; // the course map, the item's module lit among the others +pub const ARCHS : usize = 12; +pub const ARCH_NAMES : str = "code,graph,tests,trit,loop,branch,data,links,guard,title,task,map"; +pub const ARCH_CAP : usize = 4; +// No archetype repeats within WINDOW lessons of itself, so the three lessons of a module never +// share a scene. +pub const WINDOW : usize = 2; + +// The generic archetypes draw what every item has (its code, title, task and place on the map); +// the others draw one construct and only an item that has it can get them. +pub fn generic(a: usize) -> bool { + return a == A_CODE || a >= A_TITLE; +} + +// The accents, the camera motions and the layouts the factory dresses a scene in. +pub const ACCENTS : usize = 6; +pub const ACCENT_NAMES : str = "gold,violet,magenta,green,blue,white"; +pub const ACCENT_RGB : [6]u32 = [0xF5B700, 0x8B5CF6, 0xE0409A, 0x22C55E, 0x3B82F6, 0xF5F5F5]; +pub const MOTIONS : usize = 4; +pub const MOTION_NAMES : str = "push,orbit,rise,sweep"; +pub const LAYOUTS : usize = 3; +pub const LAYOUT_NAMES : str = "centre,top,side"; + +// ---- the formats every plan renders to ---- + +pub const FMT_REEL_TALL : usize = 0; +pub const FMT_REEL_WIDE : usize = 1; +pub const FMT_SQUARE : usize = 2; +pub const FMT_PORTRAIT : usize = 3; +pub const FMT_COVER : usize = 4; +pub const FMT_CAROUSEL : usize = 5; +pub const FMT_X_CARD : usize = 6; +pub const FMT_OG : usize = 7; +pub const FMTS : usize = 8; +pub const FMT_NAMES : str = "reel_tall,reel_wide,square,portrait,cover,carousel,x_card,og"; +pub const FMT_W : [8]u32 = [1080, 1920, 1080, 1080, 1080, 1080, 1200, 1200]; +pub const FMT_H : [8]u32 = [1920, 1080, 1080, 1350, 1920, 1350, 675, 630]; +pub const FMT_MOVING : [8]bool = [true, true, false, false, false, false, false, false]; +pub const CAROUSEL_LINES : usize = 6; +pub const CAROUSEL_MAX : usize = 10; + +// ---- the anomaly gate ---- + +pub const SIMILAR_MAX_PERMILLE : u32 = 850; + +pub fn too_similar(ssim_permille: u32) -> bool { + return ssim_permille > SIMILAR_MAX_PERMILLE; +} + +// ---- features ---- + +fn ident_byte(c: u8) -> bool { + return (c >= 97 && c <= 122) || (c >= 65 && c <= 90) || (c >= 48 && c <= 57) || c == 95; +} + +pub fn kw_len(k: usize) -> usize { + return @as(usize, KW_START[k + 1]) - @as(usize, KW_START[k]); +} + +// Keyword k stands as a whole word at byte p of code line l. +pub fn word_at(l: usize, p: usize, k: usize) -> bool { + const n = kw_len(k); + const len = line_len(l); + if (p + n > len) { + return false; + } + if (p > 0 && ident_byte(code_byte(l, p - 1))) { + return false; + } + if (p + n < len && ident_byte(code_byte(l, p + n))) { + return false; + } + var i : usize = 0; + while (i < n) { + if (code_byte(l, p + i) != KW_BYTES[@as(usize, KW_START[k]) + i]) { + return false; + } + i = i + 1; + } + return true; +} + +// How many times the keywords of feature f stand in lesson t's code. +pub fn code_feat(t: usize, f: usize) -> u32 { + var c : u32 = 0; + const l0 = code_line0(t); + var l : usize = l0; + while (l < l0 + code_lines(t)) { + var p : usize = 0; + while (p < line_len(l)) { + var k : usize = 0; + while (k < KWS) { + if (@as(usize, KW_FEATURE[k]) == f && word_at(l, p, k)) { + c = c + 1; + } + k = k + 1; + } + p = p + 1; + } + l = l + 1; + } + return c; +} + +// How many times a keyword of feature f begins a word of lesson t's English title, case aside: +// "Trits" counts for trit, "Loops: while and for" twice for loop. A title names what a lesson +// teaches, which its code may show less of than a neighbour's: lesson 8, "Expressions", has +// eight asserts and lesson 9, "A test block", one. +pub fn title_feat(t: usize, f: usize) -> u32 { + const it = item(t, 0, 0); + const n = item_len(it); + var c : u32 = 0; + var p : usize = 0; + while (p < n) { + var starts = true; + if (p > 0) { + const q = cp(it, p - 1); + if ((q >= 97 && q <= 122) || (q >= 65 && q <= 90)) { + starts = false; + } + } + if (starts) { + var k : usize = 0; + while (k < KWS) { + if (@as(usize, KW_FEATURE[k]) == f && p + kw_len(k) <= n) { + var same = true; + var i : usize = 0; + while (i < kw_len(k)) { + var x = cp(it, p + i); + if (x >= 65 && x <= 90) { + x = x + 32; + } + if (x != @as(u32, KW_BYTES[@as(usize, KW_START[k]) + i])) { + same = false; + } + i = i + 1; + } + if (same) { + c = c + 1; + } + } + k = k + 1; + } + } + p = p + 1; + } + return c; +} + +// Lesson t's feature f: its code's count plus TITLE_WEIGHT for each time its title names it. +pub fn feat(t: usize, f: usize) -> u32 { + return code_feat(t, f) + TITLE_WEIGHT * title_feat(t, f); +} + +// ---- score ---- + +// How much of archetype a's content lesson t has. A generic archetype reads 1 when the item has +// the thing it draws and 0 when it does not. +pub fn content(t: usize, a: usize) -> u32 { + if (a == A_CODE) { + if (code_lines(t) > 0) { + return 1; + } + return 0; + } + if (a == A_TITLE) { + if (item_len(item(t, 0, 0)) > 0) { + return 1; + } + return 0; + } + if (a == A_TASK) { + if (item_len(item(t, 0, 2)) > 0) { + return 1; + } + return 0; + } + if (a == A_MAP) { + if (module_of(t) * MODULE_LESSONS < TOPICS) { + return 1; + } + return 0; + } + if (a == A_GRAPH) { + return feat(t, F_FN) + feat(t, F_NAME) + feat(t, F_PUB); + } + if (a == A_TESTS) { + return feat(t, F_TEST) + feat(t, F_ASSERT); + } + if (a == A_TRIT) { + return feat(t, F_TRIT); + } + if (a == A_LOOP) { + return feat(t, F_LOOP); + } + if (a == A_BRANCH) { + return feat(t, F_BRANCH); + } + if (a == A_DATA) { + return feat(t, F_DATA); + } + if (a == A_LINKS) { + return feat(t, F_USE); + } + if (a == A_GUARD) { + return feat(t, F_GUARD); + } + return 0; +} + +// The course's total of archetype a's content. +pub fn course_content(a: usize) -> u32 { + var s : u32 = 0; + var t : usize = 0; + while (t < TOPICS) { + s = s + content(t, a); + t = t + 1; + } + return s; +} + +// Lesson t's score for archetype a, given the course total `total` of a's content. +pub fn score_with(t: usize, a: usize, total: u32) -> u32 { + if (generic(a)) { + return content(t, a); + } + return content(t, a) * 1000 / (1 + total); +} + +// ---- choose ---- + +// Lesson t's archetype. The whole course is planned at once, in two passes: +// 1. the content pass: of every (lesson, archetype) pair whose archetype is content-backed +// (not generic), under ARCH_CAP, and not already within WINDOW lessons of the same +// archetype, the best-scoring pair is assigned, then the next, until none is left. So the +// lesson with the most of a construct, relative to the course, gets that construct's scene, +// wherever it stands in the course. +// 2. the generic pass: every lesson still without a scene, in course order, takes the archetype +// it has content for that is free in its window and under the cap and has been used least. +// A lesson the passes cannot place reads ARCHS, which the tests refuse. +pub fn arche(t: usize) -> usize { + var sc : [324]u32 = undefined; + var total : [12]u32 = undefined; + var used : [12]usize = undefined; + var pick : [27]usize = undefined; + var a : usize = 0; + while (a < ARCHS) { + total[a] = 0; + used[a] = 0; + a = a + 1; + } + var i : usize = 0; + while (i < TOPICS) { + pick[i] = ARCHS; + a = 0; + while (a < ARCHS) { + sc[i * ARCHS + a] = content(i, a); + total[a] = total[a] + sc[i * ARCHS + a]; + a = a + 1; + } + i = i + 1; + } + var more = true; + while (more) { + var bt : usize = TOPICS; + var ba : usize = ARCHS; + var bs : u32 = 0; + i = 0; + while (i < TOPICS) { + if (pick[i] == ARCHS) { + a = 0; + while (a < ARCHS) { + if (generic(a) == false && used[a] < ARCH_CAP && sc[i * ARCHS + a] > 0) { + const s = sc[i * ARCHS + a] * 1000 / (1 + total[a]); + if (s > bs) { + var lo : usize = 0; + if (i > WINDOW) { + lo = i - WINDOW; + } + var free = true; + var u : usize = lo; + while (u < TOPICS && u <= i + WINDOW) { + if (pick[u] == a) { + free = false; + } + u = u + 1; + } + if (free) { + bt = i; + ba = a; + bs = s; + } + } + } + a = a + 1; + } + } + i = i + 1; + } + if (bt == TOPICS) { + more = false; + } else { + pick[bt] = ba; + used[ba] = used[ba] + 1; + } + } + i = 0; + while (i < TOPICS) { + if (pick[i] == ARCHS) { + var lo : usize = 0; + if (i > WINDOW) { + lo = i - WINDOW; + } + var best : usize = ARCHS; + a = 0; + while (a < ARCHS) { + if (sc[i * ARCHS + a] > 0 && used[a] < ARCH_CAP) { + var free = true; + var u : usize = lo; + while (u < TOPICS && u <= i + WINDOW) { + if (pick[u] == a) { + free = false; + } + u = u + 1; + } + if (free && (best == ARCHS || used[a] < used[best])) { + best = a; + } + } + a = a + 1; + } + pick[i] = best; + if (best < ARCHS) { + used[best] = used[best] + 1; + } + } + i = i + 1; + } + return pick[t]; +} + +// ---- dress ---- + +pub fn accent(t: usize) -> usize { + return module_of(t) % ACCENTS; +} +pub fn motion(t: usize) -> usize { + return t % MOTIONS; +} +pub fn layout(t: usize) -> usize { + return t % LAYOUTS; +} + +// The signature of lesson t's plan, packed: archetype, accent, motion, layout. +pub fn signature(t: usize) -> usize { + return ((arche(t) * ACCENTS + accent(t)) * MOTIONS + motion(t)) * LAYOUTS + layout(t); +} + +// How many of the four fields two plans differ in. +pub fn fields_apart(a0: usize, c0: usize, m0: usize, y0: usize, a1: usize, c1: usize, m1: usize, y1: usize) -> u32 { + var d : u32 = 0; + if (a0 != a1) { + d = d + 1; + } + if (c0 != c1) { + d = d + 1; + } + if (m0 != m1) { + d = d + 1; + } + if (y0 != y1) { + d = d + 1; + } + return d; +} + +// ---- formats ---- + +// The carousel's slides for lesson t: a cover, its code CAROUSEL_LINES lines a slide, and a call +// to action, at most CAROUSEL_MAX. +pub fn carousel_slides(t: usize) -> usize { + var pages = (code_lines(t) + CAROUSEL_LINES - 1) / CAROUSEL_LINES; + if (pages + 2 > CAROUSEL_MAX) { + pages = CAROUSEL_MAX - 2; + } + return pages + 2; +} + +// ---- tests ---- + +test "the keyword table spells seventeen lowercase words end to end" { + assert(@as(usize, KW_START[0]) == 0); + assert(@as(usize, KW_START[KWS]) == 73); + var k : usize = 0; + while (k < KWS) { + assert(kw_len(k) >= 2); + assert(@as(usize, KW_FEATURE[k]) < FEATS); + var i : usize = 0; + while (i < kw_len(k)) { + const c = KW_BYTES[@as(usize, KW_START[k]) + i]; + assert(c >= 97 && c <= 122); + i = i + 1; + } + k = k + 1; + } +} + +test "the keyword table reads the lessons that teach each construct" { + assert(feat(9, F_TEST) > 0); // A test block + assert(feat(9, F_ASSERT) > 0); + assert(feat(11, F_FN) > 0); // Functions + assert(feat(13, F_BRANCH) > 0); // Choices: if, else and switch + assert(feat(14, F_LOOP) > 0); // Loops: while and for + assert(feat(16, F_DATA) > 0); // Structs + assert(feat(17, F_DATA) > 0); // Enums + assert(feat(18, F_USE) > 0); // Using other modules + assert(feat(19, F_GUARD) > 0); // Invariants and benches + assert(title_feat(7, F_TRIT) == 1); // "Trits: minus one, zero, one" + assert(title_feat(9, F_TEST) == 1); // "A test block" + assert(title_feat(14, F_LOOP) == 2); // "Loops: while and for" + assert(title_feat(8, F_TEST) == 0); // "Expressions" + assert(code_feat(8, F_ASSERT) > code_feat(9, F_ASSERT)); +} + +test "a lesson that teaches a construct gets that construct's scene" { + assert(arche(7) == A_TRIT); + assert(arche(9) == A_TESTS || arche(10) == A_TESTS); + assert(arche(13) == A_BRANCH); + assert(arche(14) == A_LOOP); + assert(arche(18) == A_LINKS); + assert(arche(19) == A_GUARD); + assert(arche(16) == A_DATA || arche(17) == A_DATA); +} + +test "every lesson is placed, no scene repeats within the window, neighbours differ in three of four fields" { + var sig : [27]usize = undefined; + var arc : [27]usize = undefined; + var t : usize = 0; + while (t < TOPICS) { + arc[t] = arche(t); + sig[t] = ((arc[t] * ACCENTS + accent(t)) * MOTIONS + motion(t)) * LAYOUTS + layout(t); + t = t + 1; + } + var i : usize = 0; + while (i < TOPICS) { + var j : usize = i + 1; + while (j < TOPICS) { + assert(sig[i] != sig[j]); + j = j + 1; + } + assert(arc[i] < ARCHS); + if (i > 1) { + assert(arc[i] != arc[i - 2]); + } + if (i > 0) { + assert(arc[i] != arc[i - 1]); + assert(fields_apart(arc[i], accent(i), motion(i), layout(i), arc[i - 1], accent(i - 1), motion(i - 1), layout(i - 1)) >= 3); + } + i = i + 1; + } + assert(signature(5) == sig[5]); +} + +test "no archetype covers more than its cap, and the course uses at least nine of the twelve" { + var used : [12]usize = undefined; + var a : usize = 0; + while (a < ARCHS) { + used[a] = 0; + a = a + 1; + } + var t : usize = 0; + while (t < TOPICS) { + const a_t = arche(t); + used[a_t] = used[a_t] + 1; + t = t + 1; + } + var kinds : usize = 0; + a = 0; + while (a < ARCHS) { + assert(used[a] <= ARCH_CAP); + if (used[a] > 0) { + kinds = kinds + 1; + } + a = a + 1; + } + assert(kinds >= 9); + assert(ARCHS * ARCH_CAP >= TOPICS); +} + +test "every chosen scene shows content its lesson has" { + var t : usize = 0; + while (t < TOPICS) { + const a = arche(t); + assert(a < ARCHS); + assert(content(t, a) > 0); + assert(code_lines(t) > 0); + t = t + 1; + } +} + +test "the formats are the channels' sizes, and a carousel fits its limit" { + assert(FMT_W[FMT_REEL_TALL] * 16 == FMT_H[FMT_REEL_TALL] * 9); + assert(FMT_W[FMT_REEL_WIDE] * 9 == FMT_H[FMT_REEL_WIDE] * 16); + assert(FMT_W[FMT_SQUARE] == FMT_H[FMT_SQUARE]); + assert(FMT_W[FMT_PORTRAIT] * 5 == FMT_H[FMT_PORTRAIT] * 4); + assert(FMT_W[FMT_X_CARD] * 9 == FMT_H[FMT_X_CARD] * 16); + assert(FMT_MOVING[FMT_REEL_TALL] && FMT_MOVING[FMT_REEL_WIDE]); + assert(FMT_MOVING[FMT_COVER] == false); + var t : usize = 0; + while (t < TOPICS) { + const n = carousel_slides(t); + assert(n >= 3); + assert(n <= CAROUSEL_MAX); + t = t + 1; + } +} + +test "the anomaly gate refuses a pair of stills that are too alike" { + assert(too_similar(SIMILAR_MAX_PERMILLE + 1)); + assert(too_similar(SIMILAR_MAX_PERMILLE) == false); + assert(too_similar(400) == false); + assert(ACCENT_RGB[0] == 0xF5B700); +} From eb7a8506f1fa8c07413142571ed8f4361b82303b Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 00:33:20 +0700 Subject: [PATCH 13/27] skills: t27f -- content factory row and its three traps (Closes #10561) Co-Authored-By: Claude Opus 5.5 --- .claude/skills/t27f/SKILL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.claude/skills/t27f/SKILL.md b/.claude/skills/t27f/SKILL.md index 187a88c8ab..f3862bda01 100644 --- a/.claude/skills/t27f/SKILL.md +++ b/.claude/skills/t27f/SKILL.md @@ -68,6 +68,7 @@ the reels. Describing T27F as #1677 is the error this section exists to stop (ow | Renderer in the Trinity UI | gHashTag/trinity | **missing**; no T27F widget is live | | Application: topic map (#1677) | `specs/t27f/topic_engine.t27`, `topic_reel.t27` | spec; procedure below | | Application: reels and deck | `specs/t27f/reel.t27`, `deck.t27`, `reel_*.t27` | spec; two reels | +| Application: content factory (epic t27#10560) | `specs/t27f/reel_factory.t27` (t27#10561) | spec with tests: an item's code and title, read keyword by keyword, give 11 features. Those pick 1 of 12 scene archetypes (best pair first, cap 4, no repeat within 2 items, then the least-used generic scene) plus an accent, a motion, a layout and 8 output formats. No renderer draws a plan yet (R3), and `lesson_reel` still draws token_floor (R4). Lessons: read the plan's spread over the whole course before trusting it. A greedy walk in course order gave 7 lessons one scene. Code counts alone handed lesson 8 the tests scene over "A test block". A printing `main` makes t27b reject the spec | | Related | trinity#1647 dashboards, #1652 MVP research, #1653 three skills, #1655 evolution runtime | open | When asked "how is T27F going", report against the core rows first. Progress on the applications From 9407d579c52ecb36eb7adc94f1065205ecd8fb36 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 00:47:50 +0700 Subject: [PATCH 14/27] t27f: scene_raster.t27 -- shared integer raster for the scene renderers (WIP, #10571) Co-Authored-By: Claude Sonnet 5.5 --- specs/t27f/scene_raster.t27 | 346 ++++++++++++++++++++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100644 specs/t27f/scene_raster.t27 diff --git a/specs/t27f/scene_raster.t27 b/specs/t27f/scene_raster.t27 new file mode 100644 index 0000000000..ca8e0a6d31 --- /dev/null +++ b/specs/t27f/scene_raster.t27 @@ -0,0 +1,346 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_raster.t27 -- the shared raster of the twelve scene renderers: colours, shapes, a +// 3x5 font and a seeded hash, all in integers. Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: reel_factory.t27 plans one of twelve scenes per lesson but draws none. A scene is a pure +// function from (lesson, pixel) to a colour, so that the same lesson always gives the same picture +// and the stills can be compared. Every scene needs the same few things -- a colour, a disc, a +// segment, a letter -- and the recorded defect of this codebase family is the hand-copied helper, so +// they live here once and the scenes `use` them. +// +// THE CANVAS: scenes draw on a virtual canvas of VW x VH = 360 x 640, the tall cut at one third of +// 1080 x 1920. scene_still.t27 maps a still of any size onto it, so a 108 x 192 gate image and a +// 1080 x 1920 cover are the same picture. Colours are 0xRRGGBB held in an i32 (it fits: < 2^24); +// every coordinate is an i32 too, so no scene needs a cast between its own numbers. +// +// THE FONT: 3 x 5 pixels a letter, one row a 3-bit number (bit 2 is the left pixel), ASCII 32..95 +// plus { | } ~; a lowercase letter draws as its capital. It is small on purpose: a scene sets it at +// scale 2..8 and the blocks are the look. +// +// NOT CLAIMED: edges are hard (a point either is or is not inside a shape), so a still at 1080 x 1920 +// shows 3 x 3 pixel steps on a diagonal. The hash is a linear congruential generator, enough to +// scatter positions and not a source of randomness. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_raster; + +pub const VW : i32 = 360; +pub const VH : i32 = 640; + +// ---- colour ---- + +pub fn rgb(r: i32, g: i32, b: i32) -> i32 { + return r * 65536 + g * 256 + b; +} +pub fn red(c: i32) -> i32 { + return @divTrunc(c, 65536); +} +pub fn green(c: i32) -> i32 { + return @mod(@divTrunc(c, 256), 256); +} +pub fn blue(c: i32) -> i32 { + return @mod(c, 256); +} +pub fn iabs(v: i32) -> i32 { + if (v < 0) { return 0 - v; } + return v; +} +pub fn imin(a: i32, b: i32) -> i32 { + if (a < b) { return a; } + return b; +} +pub fn imax(a: i32, b: i32) -> i32 { + if (a > b) { return a; } + return b; +} +pub fn clamp(v: i32, lo: i32, hi: i32) -> i32 { + if (v < lo) { return lo; } + if (v > hi) { return hi; } + return v; +} +// Rec. 601 luma, 0..255, in integers. +pub fn luma(c: i32) -> i32 { + return @divTrunc(red(c) * 77 + green(c) * 150 + blue(c) * 29, 256); +} +// c0 at a = 0, c1 at a = 255, linear between. +pub fn mix(c0: i32, c1: i32, a: i32) -> i32 { + const k = clamp(a, 0, 255); + const r = @divTrunc(red(c0) * (255 - k) + red(c1) * k, 255); + const g = @divTrunc(green(c0) * (255 - k) + green(c1) * k, 255); + const b = @divTrunc(blue(c0) * (255 - k) + blue(c1) * k, 255); + return rgb(r, g, b); +} + +// ---- hash ---- + +// The next state of a linear congruential generator, 31 bits. +pub fn lcg(s: i32) -> i32 { + const v : i64 = @as(i64, s) * 1103515245 + 12345; + return @as(i32, @intCast(@mod(v, 2147483648))); +} +// A number in 0..n-1 from seed s: two steps, then the high bits (the low bits of an LCG cycle). +pub fn rnd(s: i32, n: i32) -> i32 { + const h = lcg(lcg(s + 7919)); + return @mod(@divTrunc(h, 4096), n); +} + +// ---- shapes ---- + +pub fn in_rect(x: i32, y: i32, x0: i32, y0: i32, x1: i32, y1: i32) -> bool { + return x >= x0 && x < x1 && y >= y0 && y < y1; +} +pub fn in_disc(x: i32, y: i32, cx: i32, cy: i32, r: i32) -> bool { + const dx : i64 = @as(i64, x - cx); + const dy : i64 = @as(i64, y - cy); + return dx * dx + dy * dy <= @as(i64, r) * @as(i64, r); +} +pub fn in_ring(x: i32, y: i32, cx: i32, cy: i32, r0: i32, r1: i32) -> bool { + const dx : i64 = @as(i64, x - cx); + const dy : i64 = @as(i64, y - cy); + const d2 = dx * dx + dy * dy; + return d2 >= @as(i64, r0) * @as(i64, r0) && d2 <= @as(i64, r1) * @as(i64, r1); +} +// Whether (x, y) is within hw of the segment from (x0, y0) to (x1, y1). +pub fn near_seg(x: i32, y: i32, x0: i32, y0: i32, x1: i32, y1: i32, hw: i32) -> bool { + const ex : i64 = @as(i64, x1 - x0); + const ey : i64 = @as(i64, y1 - y0); + const px : i64 = @as(i64, x - x0); + const py : i64 = @as(i64, y - y0); + const len2 = ex * ex + ey * ey; + const lim : i64 = @as(i64, hw) * @as(i64, hw); + if (len2 == 0) { return px * px + py * py <= lim; } + const dot = px * ex + py * ey; + if (dot <= 0) { return px * px + py * py <= lim; } + if (dot >= len2) { + const qx : i64 = @as(i64, x - x1); + const qy : i64 = @as(i64, y - y1); + return qx * qx + qy * qy <= lim; + } + const cross = px * ey - py * ex; + return cross * cross <= lim * len2; +} +// A triangle wave: 0 at phase 0, rising to amp - 1 at half a period, back to 0. +pub fn tri_wave(v: i32, period: i32, amp: i32) -> i32 { + const m = @mod(v, period); + const half = @divTrunc(period, 2); + if (m < half) { return @divTrunc(m * amp, half); } + return @divTrunc((period - m) * amp, half); +} + +// ---- the 3 x 5 font ---- + +// 68 glyphs of five rows: ASCII 32..95, then { | } ~. Row bits: 4 = left, 2 = middle, 1 = right. +pub const FONT : [340]u8 = [ + 0, 0, 0, 0, 0, // space + 2, 2, 2, 0, 2, // ! + 5, 5, 0, 0, 0, // " + 5, 7, 5, 7, 5, // # + 3, 6, 2, 3, 6, // $ + 5, 1, 2, 4, 5, // % + 2, 5, 2, 5, 3, // & + 2, 2, 0, 0, 0, // ' + 1, 2, 2, 2, 1, // ( + 4, 2, 2, 2, 4, // ) + 5, 2, 7, 2, 5, // * + 0, 2, 7, 2, 0, // + + 0, 0, 0, 2, 4, // , + 0, 0, 7, 0, 0, // - + 0, 0, 0, 0, 2, // . + 1, 1, 2, 4, 4, // / + 7, 5, 5, 5, 7, // 0 + 2, 6, 2, 2, 7, // 1 + 7, 1, 7, 4, 7, // 2 + 7, 1, 7, 1, 7, // 3 + 5, 5, 7, 1, 1, // 4 + 7, 4, 7, 1, 7, // 5 + 7, 4, 7, 5, 7, // 6 + 7, 1, 1, 2, 2, // 7 + 7, 5, 7, 5, 7, // 8 + 7, 5, 7, 1, 7, // 9 + 0, 2, 0, 2, 0, // : + 0, 2, 0, 2, 4, // ; + 1, 2, 4, 2, 1, // < + 0, 7, 0, 7, 0, // = + 4, 2, 1, 2, 4, // > + 7, 1, 3, 0, 2, // ? + 7, 5, 7, 4, 7, // @ + 2, 5, 7, 5, 5, // A + 6, 5, 6, 5, 6, // B + 3, 4, 4, 4, 3, // C + 6, 5, 5, 5, 6, // D + 7, 4, 6, 4, 7, // E + 7, 4, 6, 4, 4, // F + 3, 4, 5, 5, 3, // G + 5, 5, 7, 5, 5, // H + 7, 2, 2, 2, 7, // I + 1, 1, 1, 5, 2, // J + 5, 5, 6, 5, 5, // K + 4, 4, 4, 4, 7, // L + 5, 7, 7, 5, 5, // M + 6, 5, 5, 5, 5, // N + 2, 5, 5, 5, 2, // O + 6, 5, 6, 4, 4, // P + 2, 5, 5, 7, 3, // Q + 6, 5, 6, 5, 5, // R + 3, 4, 2, 1, 6, // S + 7, 2, 2, 2, 2, // T + 5, 5, 5, 5, 7, // U + 5, 5, 5, 5, 2, // V + 5, 5, 7, 7, 5, // W + 5, 5, 2, 5, 5, // X + 5, 5, 2, 2, 2, // Y + 7, 1, 2, 4, 7, // Z + 3, 2, 2, 2, 3, // [ + 4, 4, 2, 1, 1, // backslash + 6, 2, 2, 2, 6, // ] + 2, 5, 0, 0, 0, // ^ + 0, 0, 0, 0, 7, // _ + 3, 2, 6, 2, 3, // { + 2, 2, 2, 2, 2, // | + 6, 2, 3, 2, 6, // } + 0, 3, 6, 0, 0, // ~ +]; +pub const GLYPHS : i32 = 68; + +// The glyph index of character c, or -1 for one the font does not draw. +pub fn glyph_of(c: i32) -> i32 { + if (c >= 32 && c <= 95) { return c - 32; } + if (c >= 97 && c <= 122) { return c - 64; } + if (c >= 123 && c <= 126) { return c - 59; } + return 0 - 1; +} +// Whether pixel (gx, gy), gx 0..2 and gy 0..4, of character c is set. +pub fn glyph_on(c: i32, gx: i32, gy: i32) -> bool { + const g = glyph_of(c); + if (g < 0) { return false; } + if (gx < 0 || gx > 2 || gy < 0 || gy > 4) { return false; } + const row = @as(i32, FONT[@as(usize, @intCast(g * 5 + gy))]); + var pw : i32 = 4; + if (gx == 1) { pw = 2; } + if (gx == 2) { pw = 1; } + return @mod(@divTrunc(row, pw), 2) == 1; +} +// Whether canvas point (x, y) is on character c drawn with its top left at (ox, oy), each font +// pixel s canvas pixels square. +pub fn char_on(c: i32, x: i32, y: i32, ox: i32, oy: i32, s: i32) -> bool { + if (x < ox || y < oy) { return false; } + return glyph_on(c, @divTrunc(x - ox, s), @divTrunc(y - oy, s)); +} +// The advance of one character at scale s: three pixels of glyph and one of gap. +pub fn advance(s: i32) -> i32 { + return 4 * s; +} +// Whether (x, y) is on the decimal number v (at most 5 digits) with its top left at (ox, oy). +pub fn num_on(v: i32, x: i32, y: i32, ox: i32, oy: i32, s: i32) -> bool { + if (y < oy || y >= oy + 5 * s || x < ox) { return false; } + var digits : i32 = 1; + var w = v; + while (w >= 10) { + w = @divTrunc(w, 10); + digits = digits + 1; + } + const k = @divTrunc(x - ox, advance(s)); + if (k >= digits) { return false; } + var p : i32 = digits - 1 - k; + var d = v; + while (p > 0) { + d = @divTrunc(d, 10); + p = p - 1; + } + return char_on(48 + @mod(d, 10), x, y, ox + k * advance(s), oy, s); +} + +// ---- tests ---- + +test "a colour packs and unpacks, and mixing keeps both ends" { + const c = rgb(18, 200, 255); + assert(red(c) == 18); + assert(green(c) == 200); + assert(blue(c) == 255); + assert(mix(c, 0, 0) == c); + assert(mix(c, 0, 255) == 0); + assert(luma(rgb(255, 255, 255)) >= 254); + assert(luma(0) == 0); + assert(luma(rgb(0, 255, 0)) > luma(rgb(255, 0, 0))); + assert(luma(rgb(255, 0, 0)) > luma(rgb(0, 0, 255))); +} + +test "the hash is deterministic, in range, and not one number" { + assert(rnd(5, 100) == rnd(5, 100)); + var seen : i32 = 0; + var last : i32 = 0 - 1; + var s : i32 = 0; + while (s < 64) { + const r = rnd(s, 50); + assert(r >= 0 && r < 50); + if (r != last) { seen = seen + 1; } + last = r; + s = s + 1; + } + assert(seen >= 40); +} + +test "shapes contain their centre and not a far point" { + assert(in_rect(5, 5, 0, 0, 10, 10)); + assert(!in_rect(10, 5, 0, 0, 10, 10)); + assert(in_disc(50, 50, 50, 50, 3)); + assert(!in_disc(60, 50, 50, 50, 3)); + assert(in_ring(53, 50, 50, 50, 2, 4)); + assert(!in_ring(50, 50, 50, 50, 2, 4)); + assert(near_seg(5, 6, 0, 0, 10, 10, 2)); + assert(!near_seg(5, 12, 0, 0, 10, 10, 2)); + assert(near_seg(0 - 1, 0, 0, 0, 10, 0, 2)); + assert(!near_seg(0 - 5, 0, 0, 0, 10, 0, 2)); + assert(tri_wave(0, 10, 100) == 0); + assert(tri_wave(5, 10, 100) == 100); + assert(tri_wave(10, 10, 100) == 0); +} + +test "the font draws a capital A and folds a lowercase letter onto it" { + var n : i32 = 0; + var gy : i32 = 0; + while (gy < 5) { + var gx : i32 = 0; + while (gx < 3) { + if (glyph_on(65, gx, gy)) { n = n + 1; } + assert(glyph_on(65, gx, gy) == glyph_on(97, gx, gy)); + gx = gx + 1; + } + gy = gy + 1; + } + assert(n == 10); + assert(!glyph_on(32, 1, 2)); + assert(!glyph_on(200, 1, 2)); + assert(glyph_on(124, 1, 0)); + assert(char_on(65, 21, 10, 20, 10, 2)); + assert(!char_on(65, 19, 10, 20, 10, 2)); +} + +test "a number draws its digits left to right and nothing past them" { + // 407 at scale 2 from (10, 10): '4' covers x 10..15, '0' 18..23, '7' 26..31 + assert(num_on(407, 10, 10, 10, 10, 2)); + assert(num_on(407, 26, 10, 10, 10, 2)); + assert(!num_on(407, 34, 10, 10, 10, 2)); + assert(!num_on(407, 10, 22, 10, 10, 2)); + // a different number is a different picture + var diff : i32 = 0; + var px : i32 = 10; + while (px < 34) { + var py : i32 = 10; + while (py < 20) { + if (num_on(407, px, py, 10, 10, 2) != num_on(408, px, py, 10, 10, 2)) { diff = diff + 1; } + py = py + 1; + } + px = px + 1; + } + assert(diff > 0); +} + +test "the font table holds sixty-eight glyphs of three-bit rows" { + assert(GLYPHS * 5 == 340); + var i : usize = 0; + while (i < 340) { + assert(@as(i32, FONT[i]) <= 7); + i = i + 1; + } +} From e3354c3b7106dac46170253b11e863b642006934 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 00:49:50 +0700 Subject: [PATCH 15/27] t27f: scene_code.t27 (WIP, #10571) Co-Authored-By: Claude Sonnet 5.5 --- specs/t27f/scene_code.t27 | 208 ++++++++++++++++++++++++++++++++++++ specs/t27f/scene_raster.t27 | 3 +- 2 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 specs/t27f/scene_code.t27 diff --git a/specs/t27f/scene_code.t27 b/specs/t27f/scene_code.t27 new file mode 100644 index 0000000000..22b8b038d7 --- /dev/null +++ b/specs/t27f/scene_code.t27 @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_code.t27 -- scene A_CODE of reel_factory.t27: the lesson's own code, set as a card +// of source with a gutter of line numbers. Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: the scene every lesson can have. The card's height is the lesson's line count and each of +// its lines is the lesson's line, so a four-line lesson and a twenty-line lesson do not make one +// picture. The first word of every line is set in the accent (the keyword rhythm: `pub`, `fn`, +// `const`, `test`), digits in blue, a quoted string in green, the rest pale. +// +// THE FUNCTION: px(t, acc, n, x, y) is the colour of canvas point (x, y) in lesson t's code scene. +// acc is the accent colour of the plan, n is unused here (the code scene counts lines itself). The +// canvas is scene_raster's 360 x 640; scene_still.t27 moves and scales it for the camera and the +// layout before it calls this. +// +// NOT CLAIMED: lines longer than COLS characters are cut at the card's edge, not wrapped; no +// keyword table is read here (that is reel_factory's), only the first word of a line is coloured. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_code; + +use t27f::scene_raster::rgb; +use t27f::scene_raster::mix; +use t27f::scene_raster::in_rect; +use t27f::scene_raster::in_disc; +use t27f::scene_raster::tri_wave; +use t27f::scene_raster::rnd; +use t27f::scene_raster::char_on; +use t27f::scene_raster::num_on; +use t27f::reel_topics::code_line0; +use t27f::reel_topics::code_lines; +use t27f::reel_topics::line_len; +use t27f::reel_topics::code_byte; + +pub const PANEL_X : i32 = 14; +pub const PANEL_Y : i32 = 118; +pub const PANEL_W : i32 = 332; +pub const BAR : i32 = 24; +pub const LINE_H : i32 = 12; +pub const ADV : i32 = 8; +pub const PAD : i32 = 10; +pub const GUTTER : i32 = 3; // columns taken by the line number and a space +pub const COLS : i32 = 38; // columns the card shows, gutter included + +pub const BG_CARD : i32 = 0x0E1222; +pub const INK : i32 = 0xDCE1F0; +pub const INK_DIM : i32 = 0x59607A; +pub const INK_NUM : i32 = 0x78BEFF; +pub const INK_STR : i32 = 0x4ADE80; + +// The diagonal stripes behind the card; their period and phase are the lesson's. +pub fn backdrop(t: usize, x: i32, y: i32) -> i32 { + const s = @as(i32, @intCast(t)); + const period = 26 + rnd(s * 13 + 1, 6) * 4; + const a = tri_wave(x * 2 + y * 3 + rnd(s * 29 + 3, 97), period, 30); + return rgb(8 + @divTrunc(a, 3), 10 + @divTrunc(a, 2), 24 + a); +} + +// The height of the card for lesson t, bottom edge. +pub fn card_bottom(t: usize) -> i32 { + return PANEL_Y + BAR + @as(i32, @intCast(code_lines(t))) * LINE_H + PAD; +} + +fn is_letter(c: u8) -> bool { + return (c >= 97 && c <= 122) || (c >= 65 && c <= 90) || c == 95; +} +fn is_digit(c: u8) -> bool { + return c >= 48 && c <= 57; +} +// The column of the first letter of line l and the column just past its first word. +fn first_word_start(l: usize) -> usize { + var k : usize = 0; + while (k < line_len(l) && !is_letter(code_byte(l, k))) { k = k + 1; } + return k; +} +fn first_word_end(l: usize) -> usize { + var k : usize = first_word_start(l); + while (k < line_len(l) && (is_letter(code_byte(l, k)) || is_digit(code_byte(l, k)))) { k = k + 1; } + return k; +} +// Whether column k of line l sits inside a quoted string (an odd number of quotes before it). +fn in_string(l: usize, k: usize) -> bool { + var q : usize = 0; + var j : usize = 0; + while (j < k) { + if (code_byte(l, j) == 34) { q = q + 1; } + j = j + 1; + } + return q % 2 == 1 || code_byte(l, k) == 34; +} + +// The colour of the character at column k of line l. +pub fn ink(l: usize, k: usize, acc: i32) -> i32 { + const c = code_byte(l, k); + if (in_string(l, k)) { return INK_STR; } + if (k >= first_word_start(l) && k < first_word_end(l)) { return acc; } + if (is_digit(c)) { return INK_NUM; } + if (is_letter(c)) { return INK; } + return INK_DIM; +} + +pub fn px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { + var c = backdrop(t, x, y); + const bot = card_bottom(t); + if (!in_rect(x, y, PANEL_X - 2, PANEL_Y - 2, PANEL_X + PANEL_W + 2, bot + 2)) { return c; } + // the border, two canvas pixels in the accent + if (!in_rect(x, y, PANEL_X, PANEL_Y, PANEL_X + PANEL_W, bot)) { return acc; } + c = BG_CARD; + // the title bar: three dots and the lesson's number + if (y < PANEL_Y + BAR) { + c = mix(BG_CARD, acc, 70); + if (in_disc(x, y, PANEL_X + 12, PANEL_Y + 12, 4)) { c = INK_STR; } + if (in_disc(x, y, PANEL_X + 26, PANEL_Y + 12, 4)) { c = acc; } + if (in_disc(x, y, PANEL_X + 40, PANEL_Y + 12, 4)) { c = INK_NUM; } + if (num_on(@as(i32, @intCast(t)) + 1, x, y, PANEL_X + PANEL_W - 28, PANEL_Y + 7, 2)) { c = INK; } + return c; + } + // the code: line row, column col; glyph pixels are two canvas pixels square + const px0 = x - PANEL_X - 6; + const py0 = y - PANEL_Y - BAR - 4; + if (px0 < 0 || py0 < 0) { return c; } + const row = @divTrunc(py0, LINE_H); + const col = @divTrunc(px0, ADV); + if (row >= @as(i32, @intCast(code_lines(t))) || col >= COLS) { return c; } + const l = code_line0(t) + @as(usize, @intCast(row)); + const ox = PANEL_X + 6 + col * ADV; + const oy = PANEL_Y + BAR + 4 + row * LINE_H; + if (col < GUTTER - 1) { + if (num_on(row + 1, x, y, PANEL_X + 6, oy, 2)) { return INK_DIM; } + return c; + } + if (col == GUTTER - 1) { return c; } + const k = @as(usize, @intCast(col - GUTTER)); + if (k >= line_len(l)) { return c; } + if (char_on(@as(i32, code_byte(l, k)), x, y, ox, oy, 2)) { return ink(l, k, acc); } + return c; +} + +// ---- tests ---- + +test "the card is as tall as the lesson's code" { + var t : usize = 0; + while (t < 27) { + assert(card_bottom(t) == PANEL_Y + BAR + @as(i32, @intCast(code_lines(t))) * LINE_H + PAD); + assert(card_bottom(t) < 640); + t = t + 1; + } + assert(card_bottom(0) != card_bottom(14)); +} + +test "the scene reads the lesson's own code: lesson 0 and lesson 14 do not draw one picture" { + var diff : i32 = 0; + var inside : i32 = 0; + var y : i32 = PANEL_Y; + while (y < 560) { + var x : i32 = PANEL_X; + while (x < PANEL_X + PANEL_W) { + const a = px(0, 0xF5B700, 0, x, y); + const b = px(14, 0xF5B700, 0, x, y); + if (a != b) { diff = diff + 1; } + if (a == 0xF5B700) { inside = inside + 1; } + x = x + 1; + } + y = y + 1; + } + assert(diff > 4000); + assert(inside > 100); +} + +test "negative control: a card of the right height but another lesson's text is a different picture" { + // lessons that have the same line count still draw their own bytes + var found : bool = false; + var a : usize = 0; + while (a < 27 && !found) { + var b : usize = a + 1; + while (b < 27 && !found) { + if (code_lines(a) == code_lines(b)) { + var diff : i32 = 0; + var y : i32 = PANEL_Y + BAR; + while (y < card_bottom(a)) { + var x : i32 = PANEL_X; + while (x < PANEL_X + PANEL_W) { + if (px(a, 0xF5B700, 0, x, y) != px(b, 0xF5B700, 0, x, y)) { diff = diff + 1; } + x = x + 1; + } + y = y + 1; + } + assert(diff > 500); + found = true; + } + b = b + 1; + } + a = a + 1; + } + assert(found); +} + +test "every pixel is a colour and the scene draws inside the canvas" { + var x : i32 = 0; + while (x < 360) { + var y : i32 = 0; + while (y < 640) { + const c = px(7, 0x8B5CF6, 0, x, y); + assert(c >= 0 && c <= 0xFFFFFF); + y = y + 17; + } + x = x + 11; + } +} diff --git a/specs/t27f/scene_raster.t27 b/specs/t27f/scene_raster.t27 index ca8e0a6d31..322a7e9b01 100644 --- a/specs/t27f/scene_raster.t27 +++ b/specs/t27f/scene_raster.t27 @@ -312,7 +312,8 @@ test "the font draws a capital A and folds a lowercase letter onto it" { assert(!glyph_on(32, 1, 2)); assert(!glyph_on(200, 1, 2)); assert(glyph_on(124, 1, 0)); - assert(char_on(65, 21, 10, 20, 10, 2)); + assert(char_on(65, 22, 10, 20, 10, 2)); + assert(!char_on(65, 20, 10, 20, 10, 2)); assert(!char_on(65, 19, 10, 20, 10, 2)); } From eab2f4c64f2c3179e028dbba7c321415c9bd46eb Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 00:50:58 +0700 Subject: [PATCH 16/27] t27f: scene_still.t27 dispatcher (WIP, #10571) Co-Authored-By: Claude Sonnet 5.5 --- specs/t27f/scene_still.t27 | 129 +++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 specs/t27f/scene_still.t27 diff --git a/specs/t27f/scene_still.t27 b/specs/t27f/scene_still.t27 new file mode 100644 index 0000000000..27e226ca19 --- /dev/null +++ b/specs/t27f/scene_still.t27 @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_still.t27 -- the still of a lesson: reel_factory.t27's plan in, one picture out. +// The plan names the scene, the accent, the camera motion and the layout; this module dresses the +// scene with the last three and calls the renderer the first one names. Issue +// gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: reel_still.t27 (PR #10548) takes the frame the reel shows at STILL_MS = 9000 ms, frame 270, +// and every lesson reel drew token_floor's physics world there. Here the frame is the lesson's own +// scene. The camera is fixed at that frame, so each motion is a constant move, not an animation: +// push the canvas is seen 8 % closer, about its centre +// orbit the canvas is turned about 5.7 degrees, about its centre +// rise the scene sits 36 canvas pixels higher +// sweep the scene sits 28 canvas pixels to the left +// and each layout moves the hero off the centre: +// centre nowhere top 70 pixels up side 50 pixels to the right +// +// THE CALL: pixel(t, a, ac, mo, ly, n, x, y, w, h) is the colour, 0xRRGGBB, of pixel (x, y) of a +// w x h still in the 9:16 tall cut. a, ac, mo and ly are reel_factory's arche, accent, motion and +// layout of lesson t, and n is the content count of the scene: reel_factory's content(t, a). They +// are arguments because arche plans the whole course and a still calls this once a pixel. +// +// NOT CLAIMED: the tall cut only (the wide cut and the other FMT_* sizes are not drawn here); the +// still is the scene, not the captions over it, which are the reel's drawtext overlays. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_still; + +use t27f::scene_raster::VW; +use t27f::scene_raster::VH; +use t27f::scene_raster::rgb; +use t27f::scene_code::px as code_px; + +pub const M_PUSH : usize = 0; +pub const M_ORBIT : usize = 1; +pub const M_RISE : usize = 2; +pub const M_SWEEP : usize = 3; +pub const L_CENTRE : usize = 0; +pub const L_TOP : usize = 1; +pub const L_SIDE : usize = 2; + +pub const CX : i32 = 180; +pub const CY : i32 = 320; + +// The accent colours of reel_factory.ACCENT_RGB: gold, violet, magenta, green, blue, white. +pub const ACCENT : [6]i32 = [0xF5B700, 0x8B5CF6, 0xE0409A, 0x22C55E, 0x3B82F6, 0xF5F5F5]; + +// Where canvas point (x, y) lands in the scene's own frame, once the camera and the layout have +// moved it. +pub fn scene_x(mo: usize, ly: usize, x: i32, y: i32) -> i32 { + var sx : i32 = x; + if (mo == M_PUSH) { sx = CX + @divTrunc((x - CX) * 100, 108); } + if (mo == M_ORBIT) { sx = CX + @divTrunc((x - CX) * 995 + (y - CY) * 99, 1000); } + if (mo == M_SWEEP) { sx = x + 28; } + if (ly == L_SIDE) { sx = sx - 50; } + return sx; +} +pub fn scene_y(mo: usize, ly: usize, x: i32, y: i32) -> i32 { + var sy : i32 = y; + if (mo == M_PUSH) { sy = CY + @divTrunc((y - CY) * 100, 108); } + if (mo == M_ORBIT) { sy = CY + @divTrunc((y - CY) * 995 - (x - CX) * 99, 1000); } + if (mo == M_RISE) { sy = y + 36; } + if (ly == L_TOP) { sy = sy + 70; } + return sy; +} + +// The colour at canvas point (x, y) of the tall cut, scene a. +pub fn canvas_colour(t: usize, a: usize, ac: usize, mo: usize, ly: usize, n: i32, x: i32, y: i32) -> i32 { + const sx = scene_x(mo, ly, x, y); + const sy = scene_y(mo, ly, x, y); + const acc = ACCENT[ac]; + if (a == 0) { return code_px(t, acc, n, sx, sy); } + return rgb(0, 0, 0); +} + +// The colour of pixel (x, y) of a w x h still. +pub fn pixel(t: usize, a: usize, ac: usize, mo: usize, ly: usize, n: i32, x: i32, y: i32, w: i32, h: i32) -> i32 { + const cx = @divTrunc(x * VW + @divTrunc(VW, 2), w); + const cy = @divTrunc(y * VH + @divTrunc(VH, 2), h); + return canvas_colour(t, a, ac, mo, ly, n, cx, cy); +} + +// ---- tests ---- + +test "the camera and the layout move the canvas as the header says" { + assert(scene_x(M_PUSH, L_CENTRE, CX, CY) == CX); + assert(scene_y(M_PUSH, L_CENTRE, CX, CY) == CY); + assert(scene_x(M_PUSH, L_CENTRE, 0, CY) > 0); + assert(scene_x(M_SWEEP, L_CENTRE, 100, 100) == 128); + assert(scene_y(M_RISE, L_CENTRE, 100, 100) == 136); + assert(scene_y(M_RISE, L_TOP, 100, 100) == 206); + assert(scene_x(M_RISE, L_SIDE, 100, 100) == 50); + assert(scene_x(M_ORBIT, L_CENTRE, CX + 100, CY) == CX + 99); + assert(scene_y(M_ORBIT, L_CENTRE, CX + 100, CY) == CY - 9); +} + +test "a still of the code scene is the code scene at that size, and is not a flat colour" { + var seen : i32 = 0; + var last : i32 = 0 - 1; + var y : i32 = 0; + while (y < 192) { + var x : i32 = 0; + while (x < 108) { + const c = pixel(3, 0, 1, 0, 0, 0, x, y, 108, 192); + assert(c >= 0 && c <= 0xFFFFFF); + if (c != last) { seen = seen + 1; } + last = c; + x = x + 1; + } + y = y + 1; + } + assert(seen > 3000); +} + +test "the dressing changes the picture: another motion or layout of one scene differs" { + var d_motion : i32 = 0; + var d_layout : i32 = 0; + var y : i32 = 0; + while (y < 192) { + var x : i32 = 0; + while (x < 108) { + if (pixel(3, 0, 1, 0, 0, 0, x, y, 108, 192) != pixel(3, 0, 1, 3, 0, 0, x, y, 108, 192)) { d_motion = d_motion + 1; } + if (pixel(3, 0, 1, 0, 0, 0, x, y, 108, 192) != pixel(3, 0, 1, 0, 1, 0, x, y, 108, 192)) { d_layout = d_layout + 1; } + x = x + 1; + } + y = y + 1; + } + assert(d_motion > 2000); + assert(d_layout > 2000); +} From b6b829b3504b9af6c65ba119b7815725b1cd3c82 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 00:51:36 +0700 Subject: [PATCH 17/27] t27f: scene names are unique (WIP, #10571) Co-Authored-By: Claude Sonnet 5.5 --- specs/t27f/scene_code.t27 | 12 ++++++------ specs/t27f/scene_still.t27 | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/specs/t27f/scene_code.t27 b/specs/t27f/scene_code.t27 index 22b8b038d7..c21db3f93f 100644 --- a/specs/t27f/scene_code.t27 +++ b/specs/t27f/scene_code.t27 @@ -7,7 +7,7 @@ // picture. The first word of every line is set in the accent (the keyword rhythm: `pub`, `fn`, // `const`, `test`), digits in blue, a quoted string in green, the rest pale. // -// THE FUNCTION: px(t, acc, n, x, y) is the colour of canvas point (x, y) in lesson t's code scene. +// THE FUNCTION: code_px(t, acc, n, x, y) is the colour of canvas point (x, y) in lesson t's code scene. // acc is the accent colour of the plan, n is unused here (the code scene counts lines itself). The // canvas is scene_raster's 360 x 640; scene_still.t27 moves and scales it for the camera and the // layout before it calls this. @@ -98,7 +98,7 @@ pub fn ink(l: usize, k: usize, acc: i32) -> i32 { return INK_DIM; } -pub fn px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { +pub fn code_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { var c = backdrop(t, x, y); const bot = card_bottom(t); if (!in_rect(x, y, PANEL_X - 2, PANEL_Y - 2, PANEL_X + PANEL_W + 2, bot + 2)) { return c; } @@ -154,8 +154,8 @@ test "the scene reads the lesson's own code: lesson 0 and lesson 14 do not draw while (y < 560) { var x : i32 = PANEL_X; while (x < PANEL_X + PANEL_W) { - const a = px(0, 0xF5B700, 0, x, y); - const b = px(14, 0xF5B700, 0, x, y); + const a = code_px(0, 0xF5B700, 0, x, y); + const b = code_px(14, 0xF5B700, 0, x, y); if (a != b) { diff = diff + 1; } if (a == 0xF5B700) { inside = inside + 1; } x = x + 1; @@ -179,7 +179,7 @@ test "negative control: a card of the right height but another lesson's text is while (y < card_bottom(a)) { var x : i32 = PANEL_X; while (x < PANEL_X + PANEL_W) { - if (px(a, 0xF5B700, 0, x, y) != px(b, 0xF5B700, 0, x, y)) { diff = diff + 1; } + if (code_px(a, 0xF5B700, 0, x, y) != code_px(b, 0xF5B700, 0, x, y)) { diff = diff + 1; } x = x + 1; } y = y + 1; @@ -199,7 +199,7 @@ test "every pixel is a colour and the scene draws inside the canvas" { while (x < 360) { var y : i32 = 0; while (y < 640) { - const c = px(7, 0x8B5CF6, 0, x, y); + const c = code_px(7, 0x8B5CF6, 0, x, y); assert(c >= 0 && c <= 0xFFFFFF); y = y + 17; } diff --git a/specs/t27f/scene_still.t27 b/specs/t27f/scene_still.t27 index 27e226ca19..796c077ea2 100644 --- a/specs/t27f/scene_still.t27 +++ b/specs/t27f/scene_still.t27 @@ -28,7 +28,7 @@ module scene_still; use t27f::scene_raster::VW; use t27f::scene_raster::VH; use t27f::scene_raster::rgb; -use t27f::scene_code::px as code_px; +use t27f::scene_code::code_px; pub const M_PUSH : usize = 0; pub const M_ORBIT : usize = 1; From f7bb19471745817cdfe41af2fb25fdd4b1004aec Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 00:57:51 +0700 Subject: [PATCH 18/27] t27f: scene_lex.t27 -- lesson lines, bytes and keyword counts for the scene renderers (WIP, #10571) Co-Authored-By: Claude Sonnet 5.5 --- specs/t27f/scene_lex.t27 | 206 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 specs/t27f/scene_lex.t27 diff --git a/specs/t27f/scene_lex.t27 b/specs/t27f/scene_lex.t27 new file mode 100644 index 0000000000..67d4763286 --- /dev/null +++ b/specs/t27f/scene_lex.t27 @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_lex.t27 -- what the scene renderers read out of a lesson's code and text: its +// lines, its bytes, the keywords on each line. Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: reel_topics.t27 gives the code as usize-indexed bytes and reel_factory.t27 finds a keyword +// at a word boundary; the renderers want i32 numbers (scene_raster's coordinates are i32) and the +// same few questions -- how many lines, which line has an `assert`, what is byte i of the lesson -- +// asked from twelve places. They are asked here once, in i32, so a renderer never casts a line +// index and never counts a keyword a second way. +// +// KEYWORDS: k is reel_factory's keyword index: 0 test, 1 fn, 2 struct, 3 enum, 4 trit, 5 while, +// 6 for, 7 if, 8 switch, 9 use, 10 invariant, 11 bench, 12 assert, 13 const, 14 var, 15 let, +// 16 pub. A line "has" k when reel_factory.word_at finds k at a word boundary anywhere on it. +// +// NOT CLAIMED: a keyword inside a string or a comment counts (the lesson code has its comments +// dropped; a string with `if` in it would count). The hash is for scattering, not for identity. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_lex; + +use t27f::reel_topics::code_line0; +use t27f::reel_topics::code_lines; +use t27f::reel_topics::line_len; +use t27f::reel_topics::code_byte; +use t27f::reel_factory::word_at; + +pub const KW_TEST : usize = 0; +pub const KW_FN : usize = 1; +pub const KW_STRUCT : usize = 2; +pub const KW_ENUM : usize = 3; +pub const KW_TRIT : usize = 4; +pub const KW_WHILE : usize = 5; +pub const KW_FOR : usize = 6; +pub const KW_IF : usize = 7; +pub const KW_SWITCH : usize = 8; +pub const KW_USE : usize = 9; +pub const KW_INVARIANT : usize = 10; +pub const KW_BENCH : usize = 11; +pub const KW_ASSERT : usize = 12; +pub const KW_CONST : usize = 13; +pub const KW_VAR : usize = 14; +pub const KW_LET : usize = 15; +pub const KW_PUB : usize = 16; + +// How many lines lesson t has, and the absolute index of its row r. +pub fn lex_n(t: usize) -> i32 { + return @as(i32, @intCast(code_lines(t))); +} +pub fn lex_line(t: usize, r: i32) -> usize { + return code_line0(t) + @as(usize, @intCast(r)); +} +// The length of line l, and its byte at column k (0 past the end). +pub fn lex_llen(l: usize) -> i32 { + return @as(i32, @intCast(line_len(l))); +} +pub fn lex_ch(l: usize, k: i32) -> i32 { + if (k < 0 || k >= lex_llen(l)) { return 0; } + return @as(i32, code_byte(l, @as(usize, @intCast(k)))); +} +pub fn lex_letter(c: i32) -> bool { + return (c >= 97 && c <= 122) || (c >= 65 && c <= 90) || c == 95; +} +pub fn lex_digit(c: i32) -> bool { + return c >= 48 && c <= 57; +} + +// Column of the first character on line l that is not a space, or its length. +pub fn lex_first(l: usize) -> i32 { + var k : i32 = 0; + while (k < lex_llen(l) && lex_ch(l, k) == 32) { k = k + 1; } + return k; +} +// Column just past the word (letters, digits, underscore) that starts at column k. +pub fn lex_word_end(l: usize, k: i32) -> i32 { + var e : i32 = k; + while (e < lex_llen(l) && (lex_letter(lex_ch(l, e)) || lex_digit(lex_ch(l, e)))) { e = e + 1; } + return e; +} + +// How many times keyword k stands on line l. +pub fn lex_kw_count(l: usize, k: usize) -> i32 { + var c : i32 = 0; + var p : usize = 0; + while (p < line_len(l)) { + if (word_at(l, p, k)) { c = c + 1; } + p = p + 1; + } + return c; +} +pub fn lex_has(l: usize, k: usize) -> bool { + return lex_kw_count(l, k) > 0; +} +// How many of lesson t's lines have keyword k. +pub fn lex_lines_with(t: usize, k: usize) -> i32 { + var c : i32 = 0; + var r : i32 = 0; + while (r < lex_n(t)) { + if (lex_has(lex_line(t, r), k)) { c = c + 1; } + r = r + 1; + } + return c; +} +// How many times keyword k stands in all of lesson t. +pub fn lex_kw_total(t: usize, k: usize) -> i32 { + var c : i32 = 0; + var r : i32 = 0; + while (r < lex_n(t)) { + c = c + lex_kw_count(lex_line(t, r), k); + r = r + 1; + } + return c; +} + +// The bytes of lesson t's code, all lines end to end. +pub fn lex_total(t: usize) -> i32 { + var s : i32 = 0; + var r : i32 = 0; + while (r < lex_n(t)) { + s = s + lex_llen(lex_line(t, r)); + r = r + 1; + } + return s; +} +// Byte i of lesson t's code, i counted from 0 and wrapped past the end; 0 for a lesson without code. +pub fn lex_byte(t: usize, i: i32) -> i32 { + const total = lex_total(t); + if (total == 0) { return 0; } + var j = @mod(i, total); + var r : i32 = 0; + while (r < lex_n(t)) { + const n = lex_llen(lex_line(t, r)); + if (j < n) { return lex_ch(lex_line(t, r), j); } + j = j - n; + r = r + 1; + } + return 0; +} +// A number from lesson t's code, for scattering: it changes when any byte changes. +pub fn lex_hash(t: usize) -> i32 { + var h : i32 = 5381; + var r : i32 = 0; + while (r < lex_n(t)) { + const l = lex_line(t, r); + var k : i32 = 0; + while (k < lex_llen(l)) { + h = @mod(h * 33 + lex_ch(l, k), 1000003); + k = k + 1; + } + r = r + 1; + } + return h; +} + +// ---- tests ---- + +test "lines, bytes and totals agree with reel_topics" { + var t : usize = 0; + while (t < 27) { + assert(lex_n(t) == @as(i32, @intCast(code_lines(t)))); + var sum : i32 = 0; + var r : i32 = 0; + while (r < lex_n(t)) { + sum = sum + lex_llen(lex_line(t, r)); + r = r + 1; + } + assert(lex_total(t) == sum); + assert(sum > 0); + t = t + 1; + } + assert(lex_ch(lex_line(0, 0), 0 - 1) == 0); + assert(lex_ch(lex_line(0, 0), 4000) == 0); +} + +test "the keyword counts find the construct each lesson teaches" { + // lesson 9 is "A test block", 13 is "Choices: if, else and switch", 14 is "Loops: while and for", + // 16 is "Structs", 17 is "Enums", 18 is "Using other modules", 19 is "Invariants and benches" + assert(lex_lines_with(9, KW_TEST) >= 1); + assert(lex_kw_total(9, KW_ASSERT) >= 1); + assert(lex_kw_total(13, KW_IF) + lex_kw_total(13, KW_SWITCH) >= 1); + assert(lex_kw_total(14, KW_WHILE) + lex_kw_total(14, KW_FOR) >= 1); + assert(lex_kw_total(16, KW_STRUCT) >= 1); + assert(lex_kw_total(17, KW_ENUM) >= 1); + assert(lex_kw_total(18, KW_USE) >= 1); + assert(lex_kw_total(19, KW_INVARIANT) + lex_kw_total(19, KW_BENCH) >= 1); + // and a lesson that does not teach it has none: lesson 3 is "Constants" + assert(lex_kw_total(3, KW_WHILE) == 0); + assert(lex_kw_total(3, KW_STRUCT) == 0); +} + +test "byte i wraps and the hash tells lessons apart" { + var t : usize = 0; + while (t < 27) { + const n = lex_total(t); + assert(lex_byte(t, 0) == lex_byte(t, n)); + assert(lex_byte(t, 3) == lex_byte(t, 3 + 2 * n)); + assert(lex_byte(t, 0) > 0); + t = t + 1; + } + var distinct : i32 = 0; + t = 1; + while (t < 27) { + if (lex_hash(t) != lex_hash(t - 1)) { distinct = distinct + 1; } + t = t + 1; + } + assert(distinct == 26); +} From 670d64051c5f7a4feaa6aa60ead2719d07ba9cdd Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 01:07:18 +0700 Subject: [PATCH 19/27] t27f: scene_bg and the 11 remaining scene renderers, dispatched from scene_still (WIP, #10571) Co-Authored-By: Claude Sonnet 5.5 --- specs/t27f/scene_bg.t27 | 159 +++++++++++++++++++++++++++++++++++ specs/t27f/scene_branch.t27 | 154 ++++++++++++++++++++++++++++++++++ specs/t27f/scene_code.t27 | 15 ++-- specs/t27f/scene_data.t27 | 131 +++++++++++++++++++++++++++++ specs/t27f/scene_graph.t27 | 162 ++++++++++++++++++++++++++++++++++++ specs/t27f/scene_guard.t27 | 149 +++++++++++++++++++++++++++++++++ specs/t27f/scene_links.t27 | 153 ++++++++++++++++++++++++++++++++++ specs/t27f/scene_loop.t27 | 137 ++++++++++++++++++++++++++++++ specs/t27f/scene_map.t27 | 122 +++++++++++++++++++++++++++ specs/t27f/scene_still.t27 | 48 +++++++++++ specs/t27f/scene_task.t27 | 138 ++++++++++++++++++++++++++++++ specs/t27f/scene_tests.t27 | 135 ++++++++++++++++++++++++++++++ specs/t27f/scene_title.t27 | 141 +++++++++++++++++++++++++++++++ specs/t27f/scene_trit.t27 | 129 ++++++++++++++++++++++++++++ 14 files changed, 1764 insertions(+), 9 deletions(-) create mode 100644 specs/t27f/scene_bg.t27 create mode 100644 specs/t27f/scene_branch.t27 create mode 100644 specs/t27f/scene_data.t27 create mode 100644 specs/t27f/scene_graph.t27 create mode 100644 specs/t27f/scene_guard.t27 create mode 100644 specs/t27f/scene_links.t27 create mode 100644 specs/t27f/scene_loop.t27 create mode 100644 specs/t27f/scene_map.t27 create mode 100644 specs/t27f/scene_task.t27 create mode 100644 specs/t27f/scene_tests.t27 create mode 100644 specs/t27f/scene_title.t27 create mode 100644 specs/t27f/scene_trit.t27 diff --git a/specs/t27f/scene_bg.t27 b/specs/t27f/scene_bg.t27 new file mode 100644 index 0000000000..0a32885329 --- /dev/null +++ b/specs/t27f/scene_bg.t27 @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_bg.t27 -- the cell texture behind every scene, and the 24-step circle table the +// round scenes use. Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: two stills are compared by SSIM, and a flat colour is the same structure in any picture: two +// dark frames with one small hero each score near 1000 permille whatever the heroes are. A scene +// therefore never leaves its backdrop flat. The backdrop is a field of cells whose brightness is a +// hash of the lesson's seed, so another lesson draws uncorrelated cells, and six cell shapes (grid, +// bricks, diamonds, bars, rings, bands) so another archetype draws another geometry. +// +// THE CALL: bg_px(st, acc, sd, x, y) is the colour of canvas point (x, y): st is the cell shape 0..5, +// acc the accent (a 14 % tint of it goes into every cell), sd the seed from seed_of(t, n, st). +// +// NOT CLAIMED: it is a texture, not a pattern with a meaning; the cell sizes are 12..24 canvas pixels. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_bg; + +use t27f::scene_raster::rgb; +use t27f::scene_raster::mix; +use t27f::scene_raster::rnd; +use t27f::scene_raster::luma; + +pub const COS24 : [24]i32 = [1000, 966, 866, 707, 500, 259, 0, -259, -500, -707, -866, -966, -1000, -966, -866, -707, -500, -259, 0, 259, 500, 707, 866, 966]; +pub const SIN24 : [24]i32 = [0, 259, 500, 707, 866, 966, 1000, 966, 866, 707, 500, 259, 0, -259, -500, -707, -866, -966, -1000, -966, -866, -707, -500, -259]; + +// cos and sin of 15 degrees times k, times 1000; k wraps. +pub fn cos24(k: i32) -> i32 { + return COS24[@as(usize, @intCast(@mod(k, 24)))]; +} +pub fn sin24(k: i32) -> i32 { + return SIN24[@as(usize, @intCast(@mod(k, 24)))]; +} + +// The seed of lesson t's scene st with content count n. +pub fn seed_of(t: usize, n: i32, st: i32) -> i32 { + return @as(i32, @intCast(t)) * 977 + n * 31 + st * 5 + 11; +} + +pub fn cell_size(sd: i32) -> i32 { + return 12 + rnd(sd, 4) * 4; +} + +// The cell that canvas point (x, y) is in, as a number; two points of one cell share it. +pub fn cell_id(st: i32, sd: i32, x: i32, y: i32) -> i32 { + const cs = cell_size(sd); + const px = x + 1200; + const py = y + 1200; + var id : i32 = 0; + if (st == 0) { id = @divTrunc(py, cs) * 256 + @divTrunc(px, cs); } + if (st == 1) { + const row = @divTrunc(py, cs); + id = row * 256 + @divTrunc(px + @mod(row, 2) * cs, cs * 2); + } + if (st == 2) { id = @divTrunc(px + py, cs) * 256 + @divTrunc(px - py + 2400, cs); } + if (st == 3) { id = @divTrunc(px, cs); } + if (st == 4) { + const dx = x - (120 + rnd(sd + 1, 120)); + const dy = y - (200 + rnd(sd + 2, 240)); + id = @divTrunc(dx * dx + dy * dy, cs * cs * 5); + } + if (st == 5) { id = @divTrunc(py, cs); } + return id; +} + +pub fn bg_px(st: i32, acc: i32, sd: i32, x: i32, y: i32) -> i32 { + const cs = cell_size(sd); + const id = cell_id(st, sd, x, y); + var l : i32 = 14 + rnd(sd * 131 + id, 6) * 8; + if (st == 0 && (@mod(x + 1200, cs) == 0 || @mod(y + 1200, cs) == 0)) { l = l - 8; } + return mix(rgb(l, l + 2, l + 8), acc, 36); +} + +// ---- tests ---- + +test "the circle table is a circle" { + var k : i32 = 0; + while (k < 24) { + const r2 = cos24(k) * cos24(k) + sin24(k) * sin24(k); + assert(r2 > 990000 && r2 < 1010000); + k = k + 1; + } + assert(cos24(0) == 1000); + assert(sin24(6) == 1000); + assert(cos24(12) == 0 - 1000); + assert(cos24(24) == cos24(0)); + assert(sin24(0 - 18) == sin24(6)); +} + +test "cells share a colour and neighbours mostly do not" { + const sd = seed_of(3, 5, 0); + const cs = cell_size(sd); + assert(cs >= 12 && cs <= 24); + // two points of one grid cell are one colour (clear of the cell's edge line) + const x0 = (@divTrunc(100 + 1200, cs) + 1) * cs - 1200 + 2; + const y0 = (@divTrunc(100 + 1200, cs) + 1) * cs - 1200 + 2; + assert(bg_px(0, 0xF5B700, sd, x0, y0) == bg_px(0, 0xF5B700, sd, x0 + 1, y0 + 1)); + // across a sampled row, many distinct colours + var distinct : i32 = 0; + var last : i32 = 0 - 1; + var x : i32 = 0; + while (x < 360) { + const c = bg_px(0, 0xF5B700, sd, x, 200); + if (c != last) { distinct = distinct + 1; } + last = c; + x = x + 1; + } + assert(distinct > 15); +} + +test "another lesson draws other cells: the textures are uncorrelated" { + var st : i32 = 0; + while (st < 6) { + var same : i32 = 0; + var seen : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + const a = bg_px(st, 0x8B5CF6, seed_of(2, 4, st), x, y); + const b = bg_px(st, 0x8B5CF6, seed_of(9, 4, st), x, y); + assert(a >= 0 && a <= 0xFFFFFF); + if (a == b) { same = same + 1; } + seen = seen + 1; + x = x + 7; + } + y = y + 9; + } + // 6 brightness levels: two unrelated fields agree on about one pixel in six + assert(same * 100 < seen * 40); + st = st + 1; + } +} + +test "negative control: the same seed gives the same texture" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + if (bg_px(4, 0x22C55E, seed_of(5, 2, 4), x, y) != bg_px(4, 0x22C55E, seed_of(5, 2, 4), x, y)) { diff = diff + 1; } + x = x + 13; + } + y = y + 11; + } + assert(diff == 0); +} + +test "the backdrop is dark: no pixel is as bright as a hero" { + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + assert(luma(bg_px(1, 0xF5F5F5, seed_of(7, 1, 1), x, y)) < 110); + x = x + 9; + } + y = y + 13; + } +} diff --git a/specs/t27f/scene_branch.t27 b/specs/t27f/scene_branch.t27 new file mode 100644 index 0000000000..fc7fa2a8ee --- /dev/null +++ b/specs/t27f/scene_branch.t27 @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_branch.t27 -- scene A_BRANCH of reel_factory.t27: a decision tree. The stream +// starts at the foot and splits at every level, once per `if` or `switch` the lesson counts; the +// path the lesson's own bytes pick is drawn thick and lit in the accent, the others thin and dim. +// Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: a branch is a place where the program chooses. The depth of the tree is the lesson's content +// count (3 to 5 levels), and the path taken at each level is one bit of the lesson's code. +// +// THE FUNCTION: branch_px(t, acc, n, x, y) is the colour of canvas point (x, y). Level l has 2^l +// nodes spread across 360 pixels; level 0 is at y = 570 and the top level at y = 90. +// +// NOT CLAIMED: the path is a choice read from the bytes, not the lesson's control flow. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_branch; + +use t27f::scene_raster::rgb; +use t27f::scene_raster::mix; +use t27f::scene_raster::in_disc; +use t27f::scene_raster::near_seg; +use t27f::scene_raster::clamp; +use t27f::scene_bg::bg_px; +use t27f::scene_bg::seed_of; +use t27f::scene_lex::lex_byte; + +pub const TOP_Y : i32 = 90; +pub const FOOT_Y : i32 = 570; +pub const DIM_EDGE : i32 = 0x39426B; + +pub fn levels(n: i32) -> i32 { + return clamp(n + 2, 3, 5); +} +pub fn pow2(l: i32) -> i32 { + var p : i32 = 1; + var k : i32 = 0; + while (k < l) { p = p * 2; k = k + 1; } + return p; +} +// The y of level l of a tree of lv levels. +pub fn level_y(l: i32, lv: i32) -> i32 { + return FOOT_Y - @divTrunc(l * (FOOT_Y - TOP_Y), lv); +} +// The x of node i of level l: the 2^l nodes share the 360 pixels. +pub fn node_x(l: i32, i: i32) -> i32 { + return @divTrunc((i * 2 + 1) * 360, pow2(l) * 2); +} +// The index, at level l, of the node on the path the lesson takes. +pub fn path_at(t: usize, l: i32) -> i32 { + var i : i32 = 0; + var k : i32 = 1; + while (k <= l) { + i = i * 2 + @mod(lex_byte(t, k * 5 + 1), 2); + k = k + 1; + } + return i; +} + +pub fn branch_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { + const c0 = bg_px(5, acc, seed_of(t, n, 5), x, y); + if (y < TOP_Y - 14 || y > FOOT_Y + 14) { return c0; } + const lv = levels(n); + var l : i32 = 1; + var hit : i32 = 0; + while (l <= lv) { + const ya = level_y(l, lv); + const yb = level_y(l - 1, lv); + if (y >= ya - 12 && y <= yb + 12) { + const cnt = pow2(l); + const tk = path_at(t, l); + var i : i32 = 0; + while (i < cnt) { + const px = node_x(l, i); + const pp = node_x(l - 1, @divTrunc(i, 2)); + const on = (i == tk); + if (on && in_disc(x, y, px, ya, 11)) { return acc; } + if (!on && in_disc(x, y, px, ya, 6)) { return DIM_EDGE; } + if (on && near_seg(x, y, pp, yb, px, ya, 3)) { return acc; } + if (!on && near_seg(x, y, pp, yb, px, ya, 1)) { hit = 1; } + i = i + 1; + } + } + l = l + 1; + } + if (in_disc(x, y, node_x(0, 0), level_y(0, lv), 14)) { return rgb(245, 245, 245); } + if (hit == 1) { return DIM_EDGE; } + return c0; +} + +// ---- tests ---- + +test "the tree has 3 to 5 levels and its nodes fit the canvas" { + assert(levels(0) == 3); + assert(levels(2) == 4); + assert(levels(9) == 5); + var l : i32 = 0; + while (l <= 5) { + var i : i32 = 0; + while (i < (pow2(l))) { + assert(node_x(l, i) > 0 && node_x(l, i) < 360); + i = i + 1; + } + assert(level_y(l, 5) >= TOP_Y && level_y(l, 5) <= FOOT_Y); + l = l + 1; + } + assert(level_y(0, 4) == FOOT_Y); + assert(level_y(4, 4) == TOP_Y); +} + +test "the path is one node a level and follows the lesson's bytes" { + var l : i32 = 0; + while (l <= 5) { + assert(path_at(13, l) >= 0 && path_at(13, l) < (pow2(l))); + l = l + 1; + } + var differ : i32 = 0; + var t : usize = 1; + while (t < 27) { + if (path_at(t, 5) != path_at(t - 1, 5)) { differ = differ + 1; } + t = t + 1; + } + assert(differ > 8); +} + +test "another lesson draws another tree" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + const a = branch_px(13, 0xE0409A, 3, x, y); + const b = branch_px(20, 0xE0409A, 3, x, y); + assert(a >= 0 && a <= 0xFFFFFF); + if (a != b) { diff = diff + 1; } + x = x + 3; + } + y = y + 3; + } + assert(diff > 4000); +} + +test "negative control: the same lesson twice is one picture" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + if (branch_px(8, 0x3B82F6, 2, x, y) != branch_px(8, 0x3B82F6, 2, x, y)) { diff = diff + 1; } + x = x + 5; + } + y = y + 5; + } + assert(diff == 0); +} diff --git a/specs/t27f/scene_code.t27 b/specs/t27f/scene_code.t27 index c21db3f93f..d2ba512000 100644 --- a/specs/t27f/scene_code.t27 +++ b/specs/t27f/scene_code.t27 @@ -22,8 +22,8 @@ use t27f::scene_raster::rgb; use t27f::scene_raster::mix; use t27f::scene_raster::in_rect; use t27f::scene_raster::in_disc; -use t27f::scene_raster::tri_wave; -use t27f::scene_raster::rnd; +use t27f::scene_bg::bg_px; +use t27f::scene_bg::seed_of; use t27f::scene_raster::char_on; use t27f::scene_raster::num_on; use t27f::reel_topics::code_line0; @@ -47,12 +47,9 @@ pub const INK_DIM : i32 = 0x59607A; pub const INK_NUM : i32 = 0x78BEFF; pub const INK_STR : i32 = 0x4ADE80; -// The diagonal stripes behind the card; their period and phase are the lesson's. -pub fn backdrop(t: usize, x: i32, y: i32) -> i32 { - const s = @as(i32, @intCast(t)); - const period = 26 + rnd(s * 13 + 1, 6) * 4; - const a = tri_wave(x * 2 + y * 3 + rnd(s * 29 + 3, 97), period, 30); - return rgb(8 + @divTrunc(a, 3), 10 + @divTrunc(a, 2), 24 + a); +// The cell texture behind the card (scene_bg, grid cells), seeded by the lesson. +pub fn backdrop(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { + return bg_px(0, acc, seed_of(t, n, 0), x, y); } // The height of the card for lesson t, bottom edge. @@ -99,7 +96,7 @@ pub fn ink(l: usize, k: usize, acc: i32) -> i32 { } pub fn code_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { - var c = backdrop(t, x, y); + var c = backdrop(t, acc, n, x, y); const bot = card_bottom(t); if (!in_rect(x, y, PANEL_X - 2, PANEL_Y - 2, PANEL_X + PANEL_W + 2, bot + 2)) { return c; } // the border, two canvas pixels in the accent diff --git a/specs/t27f/scene_data.t27 b/specs/t27f/scene_data.t27 new file mode 100644 index 0000000000..fe3b309ac7 --- /dev/null +++ b/specs/t27f/scene_data.t27 @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_data.t27 -- scene A_DATA of reel_factory.t27: the lesson's code as a stack of glass +// blocks, one per line, as wide as the line is long, each stepping a little to the right of the one +// above it and casting a shadow, with the line's first word printed on its face. +// Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: a struct or an enum is fields stacked in a record, and a field is a name and a width. The +// stack is the lesson's own: the number of blocks is its line count and the width of each its +// line's length, so a short record and a long one do not look alike. +// +// THE FUNCTION: data_px(t, acc, n, x, y) is the colour of canvas point (x, y). Blocks are 18 pixels +// tall every 22, from y = 88, at most 24; block r starts at x = 20 + 9 * (r mod 5). +// +// NOT CLAIMED: it does not read field types or layout; "glass" is a gradient and a highlight line. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_data; + +use t27f::scene_raster::rgb; +use t27f::scene_raster::mix; +use t27f::scene_raster::in_rect; +use t27f::scene_raster::char_on; +use t27f::scene_raster::imin; +use t27f::scene_raster::clamp; +use t27f::scene_bg::bg_px; +use t27f::scene_bg::seed_of; +use t27f::scene_lex::lex_n; +use t27f::scene_lex::lex_line; +use t27f::scene_lex::lex_llen; +use t27f::scene_lex::lex_ch; +use t27f::scene_lex::lex_first; + +pub const BY0 : i32 = 88; +pub const BH : i32 = 18; +pub const BPITCH : i32 = 22; +pub const MAXB : i32 = 24; + +pub fn block_n(t: usize) -> i32 { + return imin(lex_n(t), MAXB); +} +pub fn block_x(r: i32) -> i32 { + return 20 + 9 * @mod(r, 5); +} +pub fn blocks_end(t: usize) -> i32 { + return block_n(t) * BPITCH; +} +pub fn block_w(t: usize, r: i32) -> i32 { + return clamp(50 + lex_llen(lex_line(t, r)) * 5, 70, 300); +} + +pub fn data_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { + const c0 = bg_px(0, acc, seed_of(t, n, 6), x, y); + if (y < BY0 || y - BY0 >= blocks_end(t)) { return c0; } + const r = @divTrunc(y - BY0, BPITCH); + if (r >= block_n(t)) { return c0; } + const oy = BY0 + r * BPITCH; + const x0 = block_x(r); + const w = block_w(t, r); + const l = lex_line(t, r); + if (in_rect(x, y, x0, oy, x0 + w, oy + BH)) { + // the first word on the face of the glass + const ox = x0 + 6; + const k = @divTrunc(x - ox, 8); + if (x >= ox && k < 12 && y >= oy + 4 && y < oy + 14) { + const ch = lex_ch(l, lex_first(l) + k); + if (ch > 0 && char_on(ch, x, y, ox + k * 8, oy + 4, 2)) { return rgb(245, 247, 255); } + } + if (y - oy < 2) { return mix(acc, rgb(255, 255, 255), 120); } + return mix(acc, rgb(18, 22, 60), @divTrunc((x - x0) * 200, w) + (y - oy) * 4); + } + if (in_rect(x, y, x0 + 4, oy + 4, x0 + w + 4, oy + BH + 4)) { return rgb(5, 6, 14); } + return c0; +} + +// ---- tests ---- + +test "a block per line, wider for a longer line" { + var t : usize = 0; + while (t < 27) { + assert(block_n(t) >= 1 && block_n(t) <= MAXB); + var r : i32 = 0; + while (r < block_n(t)) { + assert(block_w(t, r) >= 70 && block_w(t, r) <= 300); + assert(block_x(r) + block_w(t, r) < 360); + r = r + 1; + } + t = t + 1; + } + assert(block_x(0) == 20); + assert(block_x(7) == 38); +} + +test "the stack is the lesson's: another lesson draws another record" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + const a = data_px(16, 0x22C55E, 4, x, y); + const b = data_px(17, 0x22C55E, 4, x, y); + assert(a >= 0 && a <= 0xFFFFFF); + if (a != b) { diff = diff + 1; } + x = x + 3; + } + y = y + 3; + } + assert(diff > 4000); +} + +test "the glass is lit at its top edge and shaded to the right" { + const t : usize = 16; + const x0 = block_x(0); + const w = block_w(t, 0); + const top = data_px(t, 0x22C55E, 4, x0 + w - 3, BY0); + const body_l = data_px(t, 0x22C55E, 4, x0 + w - 3, BY0 + 16); + assert(top != body_l); +} + +test "negative control: the same lesson twice is one picture" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + if (data_px(18, 0xF5B700, 3, x, y) != data_px(18, 0xF5B700, 3, x, y)) { diff = diff + 1; } + x = x + 5; + } + y = y + 5; + } + assert(diff == 0); +} diff --git a/specs/t27f/scene_graph.t27 b/specs/t27f/scene_graph.t27 new file mode 100644 index 0000000000..c563f23f3e --- /dev/null +++ b/specs/t27f/scene_graph.t27 @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_graph.t27 -- scene A_GRAPH of reel_factory.t27: the lesson's code as a diagram, one +// node per line of source, in the order the lines stand, joined by the flow from each to the next. +// Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: the T27F idea in one picture -- a spec as nodes and edges. A declaration (`pub`, `fn`, `const`, +// `test`, `struct`, `enum`, `use`) is a node filled in the accent, any other line a dark node, and +// each node carries the first characters of its own line. A longer lesson draws more nodes. +// +// THE FUNCTION: graph_px(t, acc, n, x, y) is the colour of canvas point (x, y). Three nodes to a row, +// at most 33 nodes; a lesson with more lines draws its first 33. +// +// NOT CLAIMED: the edges are the order of the lines, not a call graph; reading calls out of the text +// is the T27F graph IR, which this does not do. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_graph; + +use t27f::scene_raster::rgb; +use t27f::scene_raster::mix; +use t27f::scene_raster::in_rect; +use t27f::scene_raster::near_seg; +use t27f::scene_raster::char_on; +use t27f::scene_raster::imin; +use t27f::scene_raster::imax; +use t27f::scene_bg::bg_px; +use t27f::scene_bg::seed_of; +use t27f::scene_lex::lex_n; +use t27f::scene_lex::lex_line; +use t27f::scene_lex::lex_llen; +use t27f::scene_lex::lex_ch; +use t27f::scene_lex::lex_first; +use t27f::scene_lex::lex_has; +use t27f::scene_lex::KW_FN; +use t27f::scene_lex::KW_PUB; +use t27f::scene_lex::KW_CONST; +use t27f::scene_lex::KW_TEST; +use t27f::scene_lex::KW_STRUCT; +use t27f::scene_lex::KW_ENUM; +use t27f::scene_lex::KW_USE; + +pub const NODE_W : i32 = 100; +pub const NODE_H : i32 = 22; +pub const COLS : i32 = 3; +pub const X0 : i32 = 14; +pub const DX : i32 = 116; +pub const Y0 : i32 = 92; +pub const DY : i32 = 42; +pub const MAXN : i32 = 33; +pub const INK : i32 = 0xE6EAF5; +pub const NODE_BG : i32 = 0x161B33; + +pub fn node_n(t: usize) -> i32 { + return imin(lex_n(t), MAXN); +} +pub fn node_cx(r: i32) -> i32 { + return X0 + @mod(r, COLS) * DX + @divTrunc(NODE_W, 2); +} +pub fn node_cy(r: i32) -> i32 { + return Y0 + @divTrunc(r, COLS) * DY + @divTrunc(NODE_H, 2); +} +// Whether line l declares something. +pub fn is_decl(l: usize) -> bool { + return lex_has(l, KW_FN) || lex_has(l, KW_PUB) || lex_has(l, KW_CONST) || lex_has(l, KW_TEST) || lex_has(l, KW_STRUCT) || lex_has(l, KW_ENUM) || lex_has(l, KW_USE); +} + +pub fn graph_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { + const c0 = bg_px(1, acc, seed_of(t, n, 1), x, y); + const cnt = node_n(t); + const row = @divTrunc(y - Y0 + DY, DY) - 1; + // the node under the pixel, if any + if (y >= Y0 && x >= X0) { + const col = @divTrunc(x - X0, DX); + const r = row * COLS + col; + if (col < COLS && r >= 0 && r < cnt && in_rect(x, y, X0 + col * DX, Y0 + row * DY, X0 + col * DX + NODE_W, Y0 + row * DY + NODE_H)) { + const l = lex_line(t, r); + const ox = X0 + col * DX + 4; + const oy = Y0 + row * DY + 6; + var fill = NODE_BG; + if (is_decl(l)) { fill = mix(NODE_BG, acc, 200); } + if (!in_rect(x, y, X0 + col * DX + 1, Y0 + row * DY + 1, X0 + col * DX + NODE_W - 1, Y0 + row * DY + NODE_H - 1)) { return acc; } + const k = @divTrunc(x - ox, 8); + if (x >= ox && y >= oy && k < 11) { + const ch = lex_ch(l, lex_first(l) + k); + if (ch > 0 && char_on(ch, x, y, ox + k * 8, oy, 2)) { + if (is_decl(l)) { return rgb(10, 12, 24); } + return INK; + } + } + return fill; + } + } + // the edges: line r to line r + 1, only those that can pass this row + var r0 = imax(0, (row - 1) * COLS); + const r1 = imin(cnt - 2, row * COLS + COLS); + while (r0 <= r1) { + if (near_seg(x, y, node_cx(r0), node_cy(r0), node_cx(r0 + 1), node_cy(r0 + 1), 1)) { + return acc; + } + r0 = r0 + 1; + } + return c0; +} + +// ---- tests ---- + +test "one node per line, three to a row" { + var t : usize = 0; + while (t < 27) { + assert(node_n(t) >= 1 && node_n(t) <= MAXN); + t = t + 1; + } + assert(node_cx(0) == X0 + 50); + assert(node_cx(4) == X0 + DX + 50); + assert(node_cy(3) == Y0 + DY + 11); +} + +test "the nodes are the lesson's lines: another lesson draws another diagram" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + const a = graph_px(2, 0xF5B700, 3, x, y); + const b = graph_px(11, 0xF5B700, 3, x, y); + assert(a >= 0 && a <= 0xFFFFFF); + if (a != b) { diff = diff + 1; } + x = x + 3; + } + y = y + 3; + } + assert(diff > 4000); +} + +test "a declaration node is filled in the accent and a plain one is not" { + // count accent-tinted node pixels over a lesson that has declarations + var decl : i32 = 0; + var r : i32 = 0; + const t : usize = 12; + while (r < node_n(t)) { + if (is_decl(lex_line(t, r))) { decl = decl + 1; } + r = r + 1; + } + assert(decl >= 1); + assert(decl <= node_n(t)); + // the node's border pixel is the accent whatever the line is + assert(graph_px(t, 0x22C55E, 1, X0, Y0) == 0x22C55E); +} + +test "negative control: the same lesson twice is one picture" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + if (graph_px(5, 0x3B82F6, 2, x, y) != graph_px(5, 0x3B82F6, 2, x, y)) { diff = diff + 1; } + x = x + 5; + } + y = y + 5; + } + assert(diff == 0); +} diff --git a/specs/t27f/scene_guard.t27 b/specs/t27f/scene_guard.t27 new file mode 100644 index 0000000000..02b8ffda6f --- /dev/null +++ b/specs/t27f/scene_guard.t27 @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_guard.t27 -- scene A_GUARD of reel_factory.t27: a dial of 24 ticks round a big +// number. The number is the lesson's count of asserts and invariants; the ticks lit in the accent +// are three for each of them, up to 23, the rest stay dim. +// Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: an assert is a guard, and a guard is a thing that is either holding or not. A dial with more +// lit ticks is a lesson with more guards, so a lesson that teaches three checks and one that +// teaches none do not draw the same dial. +// +// THE FUNCTION: guard_px(t, acc, n, x, y) is the colour of canvas point (x, y). Centre (180, 320), +// ticks on a circle of radius 138, the number in the middle at scale 10. +// +// NOT CLAIMED: the dial does not run the asserts; it counts them. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_guard; + +use t27f::scene_raster::rgb; +use t27f::scene_raster::mix; +use t27f::scene_raster::in_disc; +use t27f::scene_raster::in_ring; +use t27f::scene_raster::num_on; +use t27f::scene_raster::clamp; +use t27f::scene_bg::bg_px; +use t27f::scene_bg::seed_of; +use t27f::scene_bg::cos24; +use t27f::scene_bg::sin24; + +pub const GX : i32 = 180; +pub const GY : i32 = 320; +pub const DIAL : i32 = 138; +pub const TICKS : i32 = 24; +pub const DIM_TICK : i32 = 0x39426B; + +// How many of the 24 ticks are lit for a lesson that counts n guards. +pub fn lit_ticks(n: i32) -> i32 { + return clamp(n * 3, 3, 23); +} +pub fn digits_of(v: i32) -> i32 { + var d : i32 = 1; + var w = v; + while (w >= 10) { + w = @divTrunc(w, 10); + d = d + 1; + } + return d; +} +pub fn tick_x(k: i32) -> i32 { + return GX + @divTrunc(DIAL * cos24(k), 1000); +} +pub fn tick_y(k: i32) -> i32 { + return GY + @divTrunc(DIAL * sin24(k), 1000); +} + +pub fn guard_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { + const c0 = bg_px(2, acc, seed_of(t, n, 8), x, y); + // the number in the middle: white, on a dark plate + const dg = digits_of(n); + const ox = GX + 5 - dg * 20; + if (num_on(n, x, y, ox, GY - 25, 10)) { return rgb(245, 245, 245); } + if (in_disc(x, y, GX, GY, 96)) { + if (in_ring(x, y, GX, GY, 94, 96)) { return acc; } + return mix(rgb(8, 10, 22), acc, 18); + } + if (!in_ring(x, y, GX, GY, 126, 152)) { return c0; } + const lit = lit_ticks(n); + var k : i32 = 0; + while (k < TICKS) { + if (in_disc(x, y, tick_x(k), tick_y(k), 9)) { + if (k < lit) { return acc; } + return DIM_TICK; + } + k = k + 1; + } + return c0; +} + +// ---- tests ---- + +test "three ticks a guard, between 3 and 23" { + assert(lit_ticks(0) == 3); + assert(lit_ticks(4) == 12); + assert(lit_ticks(30) == 23); + assert(digits_of(0) == 1); + assert(digits_of(9) == 1); + assert(digits_of(10) == 2); + assert(digits_of(123) == 3); +} + +test "the ticks stand on the canvas" { + var k : i32 = 0; + while (k < TICKS) { + assert(tick_x(k) > 10 && tick_x(k) < 350); + assert(tick_y(k) > 150 && tick_y(k) < 490); + k = k + 1; + } +} + +test "more guards light more of the dial" { + var few : i32 = 0; + var many : i32 = 0; + var y : i32 = 160; + while (y < 480) { + var x : i32 = 30; + while (x < 330) { + if (guard_px(13, 0x3B82F6, 2, x, y) == 0x3B82F6) { few = few + 1; } + if (guard_px(13, 0x3B82F6, 7, x, y) == 0x3B82F6) { many = many + 1; } + x = x + 1; + } + y = y + 1; + } + assert(many > few); + assert(few > 0); +} + +test "the number is on the plate and another lesson draws another dial" { + var white : i32 = 0; + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + const a = guard_px(8, 0xF5B700, 5, x, y); + const b = guard_px(21, 0xF5B700, 5, x, y); + assert(a >= 0 && a <= 0xFFFFFF); + if (a == 0xF5F5F5) { white = white + 1; } + if (a != b) { diff = diff + 1; } + x = x + 2; + } + y = y + 2; + } + assert(white > 50); + assert(diff > 5000); +} + +test "negative control: the same lesson twice is one picture" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + if (guard_px(3, 0x22C55E, 3, x, y) != guard_px(3, 0x22C55E, 3, x, y)) { diff = diff + 1; } + x = x + 5; + } + y = y + 5; + } + assert(diff == 0); +} diff --git a/specs/t27f/scene_links.t27 b/specs/t27f/scene_links.t27 new file mode 100644 index 0000000000..f53f6b9a82 --- /dev/null +++ b/specs/t27f/scene_links.t27 @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_links.t27 -- scene A_LINKS of reel_factory.t27: a hub and its neighbours. The +// lesson's module is the box in the middle; round it stand as many boxes as the lesson has `use` +// edges (3 to 9), each joined to the hub by an edge and to the next box by a faint chord, each +// labelled with three letters of the lesson's own code. +// Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: `use` is how one module reaches another. The picture is the edge count of the lesson, laid +// out on a circle so a lesson with more edges has a fuller wheel. +// +// THE FUNCTION: links_px(t, acc, n, x, y) is the colour of canvas point (x, y). Hub at (180, 320), +// neighbours on a circle of radius 138, the first at the angle the lesson's first byte picks. +// +// NOT CLAIMED: the neighbours' names are letters of the lesson's code, not the modules it imports +// (the lesson code is a snippet, not a module header). +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_links; + +use t27f::scene_raster::rgb; +use t27f::scene_raster::mix; +use t27f::scene_raster::in_rect; +use t27f::scene_raster::near_seg; +use t27f::scene_raster::char_on; +use t27f::scene_raster::clamp; +use t27f::scene_bg::bg_px; +use t27f::scene_bg::seed_of; +use t27f::scene_bg::cos24; +use t27f::scene_bg::sin24; +use t27f::scene_lex::lex_byte; + +pub const HX : i32 = 180; +pub const HY : i32 = 320; +pub const WHEEL : i32 = 138; +pub const HALF_W : i32 = 27; +pub const HALF_H : i32 = 12; +pub const CHORD : i32 = 0x2C3354; + +pub fn sats(n: i32) -> i32 { + return clamp(n, 3, 9); +} +// The slot of the circle, of 24, that neighbour i of s stands at. +pub fn slot(t: usize, i: i32, s: i32) -> i32 { + return @mod(lex_byte(t, 0), 24) + @divTrunc(i * 24, s); +} +pub fn sat_x(t: usize, i: i32, s: i32) -> i32 { + return HX + @divTrunc(WHEEL * cos24(slot(t, i, s)), 1000); +} +pub fn sat_y(t: usize, i: i32, s: i32) -> i32 { + return HY + @divTrunc(WHEEL * 14 * sin24(slot(t, i, s)), 10000); +} + +pub fn links_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { + const c0 = bg_px(1, acc, seed_of(t, n, 7), x, y); + const s = sats(n); + // the hub + if (in_rect(x, y, HX - 40, HY - 16, HX + 40, HY + 16)) { + if (!in_rect(x, y, HX - 38, HY - 14, HX + 38, HY + 14)) { return rgb(245, 245, 245); } + const k = @divTrunc(x - (HX - 30), 10); + if (k >= 0 && k < 6 && y >= HY - 6 && y < HY + 6) { + if (char_on(lex_byte(t, k + 2), x, y, HX - 30 + k * 10, HY - 6, 2)) { return acc; } + } + return rgb(14, 18, 40); + } + var i : i32 = 0; + var edge : i32 = 0; + while (i < s) { + const sx = sat_x(t, i, s); + const sy = sat_y(t, i, s); + if (in_rect(x, y, sx - HALF_W, sy - HALF_H, sx + HALF_W, sy + HALF_H)) { + if (!in_rect(x, y, sx - HALF_W + 2, sy - HALF_H + 2, sx + HALF_W - 2, sy + HALF_H - 2)) { return acc; } + const k = @divTrunc(x - (sx - 21), 14); + if (k >= 0 && k < 3 && y >= sy - 5 && y < sy + 5) { + if (char_on(lex_byte(t, i * 3 + k + 7), x, y, sx - 21 + k * 14, sy - 5, 2)) { return rgb(245, 247, 255); } + } + return mix(rgb(14, 18, 40), acc, 60); + } + if (near_seg(x, y, HX, HY, sx, sy, 1)) { edge = 1; } + if (near_seg(x, y, sx, sy, sat_x(t, i + 1, s), sat_y(t, i + 1, s), 0)) { + if (edge == 0) { edge = 2; } + } + i = i + 1; + } + if (edge == 1) { return acc; } + if (edge == 2) { return CHORD; } + return c0; +} + +// ---- tests ---- + +test "three to nine neighbours, all on the canvas" { + assert(sats(0) == 3); + assert(sats(5) == 5); + assert(sats(30) == 9); + var t : usize = 0; + while (t < 27) { + var i : i32 = 0; + while (i < 9) { + assert(sat_x(t, i, 9) > 20 && sat_x(t, i, 9) < 340); + assert(sat_y(t, i, 9) > 100 && sat_y(t, i, 9) < 560); + i = i + 1; + } + t = t + 1; + } +} + +test "the wheel is the lesson's: another lesson draws another wheel" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + const a = links_px(18, 0x3B82F6, 4, x, y); + const b = links_px(24, 0x3B82F6, 4, x, y); + assert(a >= 0 && a <= 0xFFFFFF); + if (a != b) { diff = diff + 1; } + x = x + 3; + } + y = y + 3; + } + assert(diff > 4000); +} + +test "more edges draw more boxes" { + // count box-border pixels (the accent) along the wheel for 3 and 9 neighbours + var few : i32 = 0; + var many : i32 = 0; + var y : i32 = 100; + while (y < 560) { + var x : i32 = 20; + while (x < 340) { + if (links_px(18, 0x3B82F6, 3, x, y) == 0x3B82F6) { few = few + 1; } + if (links_px(18, 0x3B82F6, 9, x, y) == 0x3B82F6) { many = many + 1; } + x = x + 2; + } + y = y + 2; + } + assert(many > few); +} + +test "negative control: the same lesson twice is one picture" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + if (links_px(10, 0xF5B700, 5, x, y) != links_px(10, 0xF5B700, 5, x, y)) { diff = diff + 1; } + x = x + 5; + } + y = y + 5; + } + assert(diff == 0); +} diff --git a/specs/t27f/scene_loop.t27 b/specs/t27f/scene_loop.t27 new file mode 100644 index 0000000000..1ce4e0a605 --- /dev/null +++ b/specs/t27f/scene_loop.t27 @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_loop.t27 -- scene A_LOOP of reel_factory.t27: concentric orbits, one ring per +// loop the lesson counts, with 12 slots on every ring; a slot holds a dot when the lesson's next +// byte is not a multiple of three, in the accent when it is one more than a multiple. +// Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: a loop is something that goes round. Three plus the lesson's content count rings (at most 8), +// and the count itself in the centre, so a lesson with more loops draws a fuller orbit. +// +// THE FUNCTION: loop_px(t, acc, n, x, y) is the colour of canvas point (x, y). The centre is +// (180, 320); the rings stand 16 to 19 pixels apart out to a radius of 160. +// +// NOT CLAIMED: the dots are placed from the lesson's bytes, they do not run the loop. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_loop; + +use t27f::scene_raster::rgb; +use t27f::scene_raster::mix; +use t27f::scene_raster::in_disc; +use t27f::scene_raster::in_ring; +use t27f::scene_raster::num_on; +use t27f::scene_raster::imin; +use t27f::scene_bg::bg_px; +use t27f::scene_bg::seed_of; +use t27f::scene_bg::cos24; +use t27f::scene_bg::sin24; +use t27f::scene_lex::lex_byte; + +pub const LCX : i32 = 180; +pub const LCY : i32 = 320; +pub const SLOTS : i32 = 12; +pub const RING_DIM : i32 = 0x2C3354; + +pub fn ring_count(n: i32) -> i32 { + return 3 + imin(n, 5); +} +pub fn ring_radius(j: i32, rings: i32) -> i32 { + return 40 + @divTrunc(j * 120, rings); +} +// 0 no dot, 1 a dot, 2 a dot in the accent: the slot m of ring j of lesson t. +pub fn slot_of(t: usize, j: i32, m: i32) -> i32 { + return @mod(lex_byte(t, j * SLOTS + m), 3); +} + +pub fn loop_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { + const c0 = bg_px(4, acc, seed_of(t, n, 4), x, y); + if (in_disc(x, y, LCX, LCY, 26)) { + if (in_disc(x, y, LCX, LCY, 24)) { + if (num_on(n, x, y, LCX - 6, LCY - 10, 4)) { return rgb(245, 245, 245); } + return mix(rgb(10, 12, 24), acc, 90); + } + return acc; + } + if (!in_disc(x, y, LCX, LCY, 172)) { return c0; } + const rings = ring_count(n); + var j : i32 = 0; + while (j < rings) { + const rj = ring_radius(j, rings); + if (in_ring(x, y, LCX, LCY, rj - 8, rj + 8)) { + var m : i32 = 0; + while (m < SLOTS) { + const k = m * 2 + @mod(j, 2); + const dx = LCX + @divTrunc(rj * cos24(k), 1000); + const dy = LCY + @divTrunc(rj * sin24(k), 1000); + const s = slot_of(t, j, m); + if (s > 0 && in_disc(x, y, dx, dy, 6)) { + if (s == 2) { return acc; } + return rgb(235, 238, 250); + } + m = m + 1; + } + if (in_ring(x, y, LCX, LCY, rj, rj + 1)) { return RING_DIM; } + } + j = j + 1; + } + return c0; +} + +// ---- tests ---- + +test "the rings count the lesson's loops" { + assert(ring_count(1) == 4); + assert(ring_count(5) == 8); + assert(ring_count(40) == 8); + var j : i32 = 0; + while (j < 8) { + assert(ring_radius(j, 8) >= 40 && ring_radius(j, 8) < 160); + j = j + 1; + } + assert(ring_radius(0, 4) == 40); +} + +test "the dots are the lesson's bytes: another lesson draws another orbit" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + const a = loop_px(14, 0x8B5CF6, 3, x, y); + const b = loop_px(15, 0x8B5CF6, 3, x, y); + assert(a >= 0 && a <= 0xFFFFFF); + if (a != b) { diff = diff + 1; } + x = x + 3; + } + y = y + 3; + } + assert(diff > 4000); +} + +test "a lesson with more loops draws more rings" { + var few : i32 = 0; + var many : i32 = 0; + var x : i32 = LCX + 30; + while (x < LCX + 175) { + if (loop_px(14, 0x8B5CF6, 1, x, LCY) != bg_px(4, 0x8B5CF6, seed_of(14, 1, 4), x, LCY)) { few = few + 1; } + if (loop_px(14, 0x8B5CF6, 5, x, LCY) != bg_px(4, 0x8B5CF6, seed_of(14, 5, 4), x, LCY)) { many = many + 1; } + x = x + 1; + } + assert(many > 0); + assert(few > 0); + assert(ring_count(5) > ring_count(1)); +} + +test "negative control: the same lesson twice is one picture" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + if (loop_px(6, 0x3B82F6, 4, x, y) != loop_px(6, 0x3B82F6, 4, x, y)) { diff = diff + 1; } + x = x + 5; + } + y = y + 5; + } + assert(diff == 0); +} diff --git a/specs/t27f/scene_map.t27 b/specs/t27f/scene_map.t27 new file mode 100644 index 0000000000..5ebb313984 --- /dev/null +++ b/specs/t27f/scene_map.t27 @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_map.t27 -- scene A_MAP of reel_factory.t27: the course map. Nine modules of +// three lessons make 27 cells; the lesson's own cell is lit solid in the accent with its number, +// the cells before it are done (a quarter of the accent), the cells after it are dark and each a +// shade of its own. +// Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: a map says where you are. Every lesson sits at a different cell and has a different amount +// of the course behind it, so the 27 maps differ in where the light is and how much of the grid is +// tinted. +// +// THE FUNCTION: map_px(t, acc, n, x, y) is the colour of canvas point (x, y). Cell (row, col) +// is 96 x 48 at x = 26 + 104 * col, y = 60 + 62 * row; its number is row * 3 + col + 1. +// +// NOT CLAIMED: the map is the course's shape, 9 x 3; it knows no module titles. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_map; + +use t27f::scene_raster::rgb; +use t27f::scene_raster::mix; +use t27f::scene_raster::in_rect; +use t27f::scene_raster::num_on; +use t27f::scene_raster::rnd; +use t27f::scene_bg::bg_px; +use t27f::scene_bg::seed_of; + +pub const MX0 : i32 = 26; +pub const MY0 : i32 = 60; +pub const MPX : i32 = 104; +pub const MPY : i32 = 62; +pub const MW : i32 = 96; +pub const MH : i32 = 48; +pub const MROWS : i32 = 9; +pub const MCOLS : i32 = 3; + +// The cell, 0..26, that lesson t sits in. +pub fn cell_of(t: usize) -> i32 { + return @as(i32, @intCast(t)); +} +// The shade of an unlit cell: a dark grey that depends on the lesson and on the cell. +pub fn shade(t: usize, c: i32) -> i32 { + return 18 + rnd(@as(i32, @intCast(t)) * 211 + c * 17 + 3, 10) * 5; +} + +pub fn map_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { + const c0 = bg_px(5, acc, seed_of(t, n, 11), x, y); + if (x < MX0 || y < MY0) { return c0; } + const col = @divTrunc(x - MX0, MPX); + const row = @divTrunc(y - MY0, MPY); + if (col >= MCOLS || row >= MROWS) { return c0; } + const cx0 = MX0 + col * MPX; + const cy0 = MY0 + row * MPY; + if (!in_rect(x, y, cx0, cy0, cx0 + MW, cy0 + MH)) { return c0; } + const c = row * MCOLS + col; + const mine = cell_of(t); + if (num_on(c + 1, x, y, cx0 + 8, cy0 + 8, 4)) { + if (c == mine) { return rgb(10, 12, 24); } + return rgb(200, 206, 226); + } + if (c == mine) { return acc; } + if (c < mine) { return mix(rgb(10, 12, 24), acc, 70); } + const g = shade(t, c); + return rgb(g, g + 2, g + 10); +} + +// ---- tests ---- + +test "the lesson's cell is its place on the 9 x 3 course map" { + assert(cell_of(0) == 0); + assert(cell_of(1) == 1); + assert(cell_of(3) == 3); + assert(cell_of(26) == 26); + var t : usize = 0; + while (t < 27) { + assert(cell_of(t) == @as(i32, @intCast(t))); + t = t + 1; + } +} + +test "the cell is lit, the cells before it are tinted, the cells after are dark" { + const acc = 0x3B82F6; + // lesson 13 is cell 13: row 4, column 1. Probe a plain corner of three cells. + const lit = map_px(13, acc, 3, MX0 + 1 * MPX + 90, MY0 + 4 * MPY + 40); + const done = map_px(13, acc, 3, MX0 + 0 * MPX + 90, MY0 + 4 * MPY + 40); + const todo = map_px(13, acc, 3, MX0 + 2 * MPX + 90, MY0 + 4 * MPY + 40); + assert(lit == acc); + assert(done != acc); + assert(todo != acc); + assert(done != todo); +} + +test "another lesson lights another cell" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + const a = map_px(5, 0xF5B700, 2, x, y); + const b = map_px(22, 0xF5B700, 2, x, y); + assert(a >= 0 && a <= 0xFFFFFF); + if (a != b) { diff = diff + 1; } + x = x + 2; + } + y = y + 2; + } + assert(diff > 20000); +} + +test "negative control: the same lesson twice is one picture" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + if (map_px(20, 0x8B5CF6, 4, x, y) != map_px(20, 0x8B5CF6, 4, x, y)) { diff = diff + 1; } + x = x + 5; + } + y = y + 5; + } + assert(diff == 0); +} diff --git a/specs/t27f/scene_still.t27 b/specs/t27f/scene_still.t27 index 796c077ea2..96e114ac22 100644 --- a/specs/t27f/scene_still.t27 +++ b/specs/t27f/scene_still.t27 @@ -29,6 +29,17 @@ use t27f::scene_raster::VW; use t27f::scene_raster::VH; use t27f::scene_raster::rgb; use t27f::scene_code::code_px; +use t27f::scene_graph::graph_px; +use t27f::scene_tests::tests_px; +use t27f::scene_trit::trit_px; +use t27f::scene_loop::loop_px; +use t27f::scene_branch::branch_px; +use t27f::scene_data::data_px; +use t27f::scene_links::links_px; +use t27f::scene_guard::guard_px; +use t27f::scene_title::title_px; +use t27f::scene_task::task_px; +use t27f::scene_map::map_px; pub const M_PUSH : usize = 0; pub const M_ORBIT : usize = 1; @@ -69,6 +80,17 @@ pub fn canvas_colour(t: usize, a: usize, ac: usize, mo: usize, ly: usize, n: i32 const sy = scene_y(mo, ly, x, y); const acc = ACCENT[ac]; if (a == 0) { return code_px(t, acc, n, sx, sy); } + if (a == 1) { return graph_px(t, acc, n, sx, sy); } + if (a == 2) { return tests_px(t, acc, n, sx, sy); } + if (a == 3) { return trit_px(t, acc, n, sx, sy); } + if (a == 4) { return loop_px(t, acc, n, sx, sy); } + if (a == 5) { return branch_px(t, acc, n, sx, sy); } + if (a == 6) { return data_px(t, acc, n, sx, sy); } + if (a == 7) { return links_px(t, acc, n, sx, sy); } + if (a == 8) { return guard_px(t, acc, n, sx, sy); } + if (a == 9) { return title_px(t, acc, n, sx, sy); } + if (a == 10) { return task_px(t, acc, n, sx, sy); } + if (a == 11) { return map_px(t, acc, n, sx, sy); } return rgb(0, 0, 0); } @@ -127,3 +149,29 @@ test "the dressing changes the picture: another motion or layout of one scene di assert(d_motion > 2000); assert(d_layout > 2000); } + +test "every one of the 12 archetypes draws a still that is not a flat colour and differs from its neighbour" { + var a : usize = 0; + while (a < 12) { + var seen : i32 = 0; + var diff : i32 = 0; + var last : i32 = 0 - 1; + var y : i32 = 0; + while (y < 96) { + var x : i32 = 0; + while (x < 54) { + const c = pixel(13, a, 2, 0, 0, 3, x, y, 54, 96); + const d = pixel(13, @mod(a + 1, 12), 2, 0, 0, 3, x, y, 54, 96); + assert(c >= 0 && c <= 0xFFFFFF); + if (c != last) { seen = seen + 1; } + if (c != d) { diff = diff + 1; } + last = c; + x = x + 1; + } + y = y + 1; + } + assert(seen > 400); + assert(diff > 1000); + a = a + 1; + } +} diff --git a/specs/t27f/scene_task.t27 b/specs/t27f/scene_task.t27 new file mode 100644 index 0000000000..6d613556e7 --- /dev/null +++ b/specs/t27f/scene_task.t27 @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_task.t27 -- scene A_TASK of reel_factory.t27: the lesson's task as a checklist. +// The English task text is wrapped at 34 letters a row at scale 2; every row gets a box, and the +// first two thirds of the rows are ticked in the accent. +// Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: the task is what the learner is asked to do. A checklist of its own words, partly done, is +// a picture nothing else in the factory draws, and its length is the lesson's. +// +// THE FUNCTION: task_px(t, acc, n, x, y) is the colour of canvas point (x, y). Row r starts at +// y = 100 + 22 * r (at most 24 rows); the box is at x = 20, the text at x = 46. +// +// NOT CLAIMED: it wraps on letters, not words, and the tick is a layout rule, not progress. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_task; + +use t27f::scene_raster::rgb; +use t27f::scene_raster::mix; +use t27f::scene_raster::in_rect; +use t27f::scene_raster::near_seg; +use t27f::scene_raster::char_on; +use t27f::scene_raster::imin; +use t27f::scene_bg::bg_px; +use t27f::scene_bg::seed_of; +use t27f::reel_topics::item; +use t27f::reel_topics::item_len; +use t27f::reel_topics::cp; + +pub const WRAP : i32 = 34; +pub const TY0 : i32 = 100; +pub const TPITCH : i32 = 22; +pub const TMAX : i32 = 24; +pub const TX_BOX : i32 = 20; +pub const TX_TEXT : i32 = 46; + +pub fn task_len(t: usize) -> i32 { + return @as(i32, @intCast(item_len(item(t, 0, 2)))); +} +pub fn task_rows(t: usize) -> i32 { + return imin(@divTrunc(task_len(t) + WRAP - 1, WRAP), TMAX); +} +pub fn task_ch(t: usize, i: i32) -> i32 { + if (i < 0 || i >= task_len(t)) { return 0; } + return @as(i32, @intCast(cp(item(t, 0, 2), @as(usize, @intCast(i))))); +} +// Rows ticked: two thirds, rounded up, of the rows. +pub fn ticked(rows: i32) -> i32 { + return @divTrunc(rows * 2 + 2, 3); +} + +pub fn task_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { + const c0 = bg_px(4, acc, seed_of(t, n, 10), x, y); + if (y < TY0) { return c0; } + const rows = task_rows(t); + const r = @divTrunc(y - TY0, TPITCH); + if (r >= rows) { return c0; } + const oy = TY0 + r * TPITCH; + // the box, with a tick for the done rows + if (in_rect(x, y, TX_BOX, oy, TX_BOX + 18, oy + 18)) { + if (r < ticked(rows)) { + if (near_seg(x, y, TX_BOX + 4, oy + 9, TX_BOX + 8, oy + 14, 1)) { return rgb(10, 12, 24); } + if (near_seg(x, y, TX_BOX + 8, oy + 14, TX_BOX + 15, oy + 4, 1)) { return rgb(10, 12, 24); } + return acc; + } + if (!in_rect(x, y, TX_BOX + 2, oy + 2, TX_BOX + 16, oy + 16)) { return acc; } + return rgb(10, 12, 24); + } + if (x >= TX_TEXT && y >= oy + 4 && y < oy + 14) { + const k = @divTrunc(x - TX_TEXT, 8); + if (k < WRAP) { + if (char_on(task_ch(t, r * WRAP + k), x, y, TX_TEXT + k * 8, oy + 4, 2)) { + if (r < ticked(rows)) { return rgb(160, 168, 196); } + return rgb(245, 247, 255); + } + } + } + return c0; +} + +// ---- tests ---- + +test "every lesson has a task, a few rows long" { + var t : usize = 0; + while (t < 27) { + assert(task_len(t) > 10); + assert(task_rows(t) >= 1 && task_rows(t) <= TMAX); + assert(ticked(task_rows(t)) >= 1 && ticked(task_rows(t)) <= task_rows(t) + 1); + t = t + 1; + } + assert(ticked(3) == 2); + assert(ticked(6) == 4); +} + +test "the task text is drawn: white pixels on the unticked rows" { + var white : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + if (task_px(6, 0x22C55E, 3, x, y) == 0xF5F7FF) { white = white + 1; } + x = x + 1; + } + y = y + 1; + } + assert(white > 30); +} + +test "another lesson draws another checklist" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + const a = task_px(2, 0x22C55E, 3, x, y); + const b = task_px(19, 0x22C55E, 3, x, y); + assert(a >= 0 && a <= 0xFFFFFF); + if (a != b) { diff = diff + 1; } + x = x + 2; + } + y = y + 2; + } + assert(diff > 5000); +} + +test "negative control: the same lesson twice is one picture" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + if (task_px(12, 0xE0409A, 5, x, y) != task_px(12, 0xE0409A, 5, x, y)) { diff = diff + 1; } + x = x + 5; + } + y = y + 5; + } + assert(diff == 0); +} diff --git a/specs/t27f/scene_tests.t27 b/specs/t27f/scene_tests.t27 new file mode 100644 index 0000000000..dbc746c5c8 --- /dev/null +++ b/specs/t27f/scene_tests.t27 @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_tests.t27 -- scene A_TESTS of reel_factory.t27: the lesson's code as a test run. +// Each line of the lesson is a row; a row whose line holds an `assert` shows a ticked box, a bar as +// long as the line and PASS, a `test` header row shows a bar in the accent, any other line a dim bar. +// Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: the lessons that teach by asserting (expressions, constants, trits) are mostly asserts, and +// the picture says so: a column of green PASS rows, as many as the lesson has asserts. +// +// THE FUNCTION: tests_px(t, acc, n, x, y) is the colour of canvas point (x, y). Rows are 22 pixels +// apart from y = 90, at most 24; a lesson with more lines draws its first 24. +// +// NOT CLAIMED: it runs nothing; the ticks are the lines that contain the word assert. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_tests; + +use t27f::scene_raster::rgb; +use t27f::scene_raster::mix; +use t27f::scene_raster::in_rect; +use t27f::scene_raster::near_seg; +use t27f::scene_raster::char_on; +use t27f::scene_raster::imin; +use t27f::scene_bg::bg_px; +use t27f::scene_bg::seed_of; +use t27f::scene_lex::lex_n; +use t27f::scene_lex::lex_line; +use t27f::scene_lex::lex_llen; +use t27f::scene_lex::lex_has; +use t27f::scene_lex::lex_lines_with; +use t27f::scene_lex::KW_ASSERT; +use t27f::scene_lex::KW_TEST; + +pub const ROW_Y0 : i32 = 90; +pub const ROW_H : i32 = 22; +pub const MAXR : i32 = 24; +pub const PASS_GREEN : i32 = 0x4ADE80; +pub const DIM : i32 = 0x2A3050; + +pub fn row_n(t: usize) -> i32 { + return imin(lex_n(t), MAXR); +} + +pub fn tests_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { + const c0 = bg_px(2, acc, seed_of(t, n, 2), x, y); + if (y < ROW_Y0) { return c0; } + const r = @divTrunc(y - ROW_Y0, ROW_H); + if (r >= row_n(t)) { return c0; } + const oy = ROW_Y0 + r * ROW_H; + if (y - oy < 3 || y - oy > 18) { return c0; } + const l = lex_line(t, r); + const ok = lex_has(l, KW_ASSERT); + const head = lex_has(l, KW_TEST); + // the box, with a tick in it for a passing row + if (in_rect(x, y, 16, oy + 3, 36, oy + 19)) { + if (!in_rect(x, y, 18, oy + 5, 34, oy + 17)) { + if (ok) { return PASS_GREEN; } + if (head) { return acc; } + return DIM; + } + if (ok && (near_seg(x, y, 20, oy + 11, 25, oy + 15, 1) || near_seg(x, y, 25, oy + 15, 32, oy + 7, 1))) { + return PASS_GREEN; + } + return rgb(8, 10, 20); + } + // the bar: as long as the line + const blen = 8 + lex_llen(l) * 5; + if (in_rect(x, y, 46, oy + 5, 46 + blen, oy + 17)) { + if (ok) { return mix(rgb(10, 40, 24), PASS_GREEN, 150); } + if (head) { return mix(rgb(20, 20, 40), acc, 220); } + return DIM; + } + // PASS at the right + if (ok && x >= 300) { + const k = @divTrunc(x - 300, 14); + if (k < 4 && y >= oy + 6) { + var ch = 80; + if (k == 1) { ch = 65; } + if (k >= 2) { ch = 83; } + if (char_on(ch, x, y, 300 + k * 14, oy + 6, 3)) { return PASS_GREEN; } + } + } + return c0; +} + +// ---- tests ---- + +test "a row per line and a green row per assert" { + var t : usize = 0; + while (t < 27) { + assert(row_n(t) >= 1 && row_n(t) <= MAXR); + t = t + 1; + } + // lesson 8, Expressions, is a column of asserts; lesson 3 has none + assert(lex_lines_with(8, KW_ASSERT) >= 3); + var green : i32 = 0; + var y : i32 = ROW_Y0; + while (y < ROW_Y0 + MAXR * ROW_H) { + if (tests_px(8, 0xF5B700, 8, 26, y) == PASS_GREEN) { green = green + 1; } + y = y + 1; + } + assert(green > 30); +} + +test "the rows are the lesson's lines: another lesson draws another run" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + const a = tests_px(8, 0x3B82F6, 8, x, y); + const b = tests_px(21, 0x3B82F6, 8, x, y); + assert(a >= 0 && a <= 0xFFFFFF); + if (a != b) { diff = diff + 1; } + x = x + 3; + } + y = y + 3; + } + assert(diff > 4000); +} + +test "negative control: a lesson without an assert shows no PASS" { + var green : i32 = 0; + var y : i32 = ROW_Y0; + while (y < ROW_Y0 + MAXR * ROW_H) { + var x : i32 = 300; + while (x < 360) { + if (tests_px(3, 0xF5B700, 1, x, y) == PASS_GREEN) { green = green + 1; } + x = x + 1; + } + y = y + 1; + } + assert(lex_lines_with(3, KW_ASSERT) == 0); + assert(green == 0); +} diff --git a/specs/t27f/scene_title.t27 b/specs/t27f/scene_title.t27 new file mode 100644 index 0000000000..b102369049 --- /dev/null +++ b/specs/t27f/scene_title.t27 @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_title.t27 -- scene A_TITLE of reel_factory.t27: the lesson's own title as big +// kinetic letters. Eleven letters a line at scale 7, every letter on its own wave so the line +// ripples, with a hard shadow under it and every third letter in the accent. +// Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: for a lesson that has no picture of its own the honest picture is its name. Different +// titles are different lengths and break into different lines, so no two lessons draw one wall of +// text. +// +// THE FUNCTION: title_px(t, acc, n, x, y) is the colour of canvas point (x, y). Letter k of line r +// stands at x = 26 + 28 * k, y = y0 + 50 * r + wave; y0 centres the block on the canvas. +// +// NOT CLAIMED: it breaks on every 11th letter, not on word boundaries. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_title; + +use t27f::scene_raster::rgb; +use t27f::scene_raster::mix; +use t27f::scene_raster::char_on; +use t27f::scene_raster::tri_wave; +use t27f::scene_raster::imin; +use t27f::scene_bg::bg_px; +use t27f::scene_bg::seed_of; +use t27f::reel_topics::item; +use t27f::reel_topics::item_len; +use t27f::reel_topics::cp; + +pub const PER_LINE : i32 = 11; +pub const LX0 : i32 = 26; +pub const ADV : i32 = 28; +pub const PITCH : i32 = 50; +pub const SHADOW : i32 = 5; + +pub fn title_len(t: usize) -> i32 { + return @as(i32, @intCast(item_len(item(t, 0, 0)))); +} +pub fn title_lines(t: usize) -> i32 { + return imin(@divTrunc(title_len(t) + PER_LINE - 1, PER_LINE), 8); +} +pub fn title_ch(t: usize, i: i32) -> i32 { + if (i < 0 || i >= title_len(t)) { return 0; } + return @as(i32, @intCast(cp(item(t, 0, 0), @as(usize, @intCast(i))))); +} +// The top of the block of title lines, so that it sits in the middle of the canvas. +pub fn title_y0(t: usize) -> i32 { + return 320 - @divTrunc(title_lines(t) * PITCH, 2); +} +// How far letter i is lifted off its line. +pub fn wave(t: usize, i: i32) -> i32 { + return tri_wave(i * 9 + @as(i32, @intCast(t)) * 5, 40, 14); +} + +// Whether letter i of the title is drawn at canvas point (x, y), shifted by (sx, sy). +pub fn letter_on(t: usize, i: i32, x: i32, y: i32, sx: i32, sy: i32) -> bool { + const c = title_ch(t, i); + if (c <= 32) { return false; } + const k = @mod(i, PER_LINE); + const r = @divTrunc(i, PER_LINE); + return char_on(c, x - sx, y - sy, LX0 + k * ADV, title_y0(t) + r * PITCH - wave(t, i) + 14, 7); +} + +pub fn title_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { + const c0 = bg_px(3, acc, seed_of(t, n, 9), x, y); + const k = @divTrunc(x - LX0, ADV); + if (x < LX0 || k >= PER_LINE) { return c0; } + const lines = title_lines(t); + const dy = y - title_y0(t); + if (dy < 0 - SHADOW || dy > lines * PITCH + 30) { return c0; } + var r : i32 = 0; + while (r < lines) { + const i = r * PER_LINE + k; + if (letter_on(t, i, x, y, 0, 0)) { + if (@mod(i, 3) == 0) { return acc; } + return rgb(245, 247, 255); + } + if (letter_on(t, i, x, y, SHADOW, SHADOW)) { return rgb(4, 5, 12); } + r = r + 1; + } + return c0; +} + +// ---- tests ---- + +test "the title is the lesson's own, in letters the font draws" { + var t : usize = 0; + while (t < 27) { + assert(title_len(t) > 2); + assert(title_lines(t) >= 1 && title_lines(t) <= 8); + assert(title_y0(t) >= 100 && title_y0(t) <= 300); + t = t + 1; + } + assert(title_ch(0, 0) > 32); + assert(title_ch(0, 900) == 0); +} + +test "the letters ripple, each on its own wave" { + var seen : i32 = 0; + var i : i32 = 0; + while (i < 11) { + assert(wave(5, i) >= 0 && wave(5, i) < 14); + if (wave(5, i) != wave(5, 0)) { seen = seen + 1; } + i = i + 1; + } + assert(seen > 6); +} + +test "another title draws another wall of letters" { + var white : i32 = 0; + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + const a = title_px(2, 0xF5B700, 3, x, y); + const b = title_px(23, 0xF5B700, 3, x, y); + assert(a >= 0 && a <= 0xFFFFFF); + if (a == 0xF5F7FF) { white = white + 1; } + if (a != b) { diff = diff + 1; } + x = x + 2; + } + y = y + 2; + } + assert(white > 100); + assert(diff > 5000); +} + +test "negative control: the same lesson twice is one picture" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + if (title_px(11, 0x3B82F6, 6, x, y) != title_px(11, 0x3B82F6, 6, x, y)) { diff = diff + 1; } + x = x + 5; + } + y = y + 5; + } + assert(diff == 0); +} diff --git a/specs/t27f/scene_trit.t27 b/specs/t27f/scene_trit.t27 new file mode 100644 index 0000000000..e45cf85d2e --- /dev/null +++ b/specs/t27f/scene_trit.t27 @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_trit.t27 -- scene A_TRIT of reel_factory.t27: the lesson's own bytes as a field of +// trits. Each cell of a 9 x 14 grid is one trit, -1, 0 or +1, read from the next byte of the +// lesson's code: a minus bar on red, a ring on grey, a plus on the accent. +// Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: t27 is a ternary language, and the lessons that teach trits (and every other lesson, for +// that matter) are text that a trit field can show: the bytes of the source taken mod 3. The grid +// is the lesson, one byte a cell, so no two lessons draw one field. +// +// THE FUNCTION: trit_px(t, acc, n, x, y) is the colour of canvas point (x, y). The cell of index i +// holds trit (byte i of the lesson + i) mod 3 - 1. +// +// NOT CLAIMED: the mod-3 reading is a picture of the bytes, not the lesson's trit values. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_trit; + +use t27f::scene_raster::rgb; +use t27f::scene_raster::mix; +use t27f::scene_raster::in_rect; +use t27f::scene_raster::in_disc; +use t27f::scene_raster::in_ring; +use t27f::scene_bg::bg_px; +use t27f::scene_bg::seed_of; +use t27f::scene_lex::lex_byte; + +pub const GCOLS : i32 = 9; +pub const GROWS : i32 = 14; +pub const CELL : i32 = 40; +pub const GY0 : i32 = 40; +pub const NEG : i32 = 0xE5484D; +pub const ZERO : i32 = 0x7C8499; + +// The trit, -1, 0 or +1, of cell i of lesson t. +pub fn trit_of(t: usize, i: i32) -> i32 { + return @mod(lex_byte(t, i) + i, 3) - 1; +} + +pub fn trit_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { + const c0 = bg_px(3, acc, seed_of(t, n, 3), x, y); + if (y < GY0 || x < 0) { return c0; } + const col = @divTrunc(x, CELL); + const row = @divTrunc(y - GY0, CELL); + if (col >= GCOLS || row >= GROWS) { return c0; } + const v = trit_of(t, row * GCOLS + col); + const cx = col * CELL + 20; + const cy = GY0 + row * CELL + 20; + var hue = ZERO; + if (v < 0) { hue = NEG; } + if (v > 0) { hue = acc; } + if (!in_disc(x, y, cx, cy, 17)) { return c0; } + if (in_ring(x, y, cx, cy, 15, 17)) { return hue; } + const body = mix(rgb(10, 12, 24), hue, 70); + if (v < 0 && in_rect(x, y, cx - 8, cy - 2, cx + 8, cy + 2)) { return hue; } + if (v > 0 && (in_rect(x, y, cx - 8, cy - 2, cx + 8, cy + 2) || in_rect(x, y, cx - 2, cy - 8, cx + 2, cy + 8))) { return hue; } + if (v == 0 && in_ring(x, y, cx, cy, 4, 7)) { return hue; } + return body; +} + +// ---- tests ---- + +test "the trit of a cell is -1, 0 or +1 and all three occur" { + var neg : i32 = 0; + var zero : i32 = 0; + var pos : i32 = 0; + var i : i32 = 0; + while (i < GCOLS * GROWS) { + const v = trit_of(7, i); + assert(v >= 0 - 1 && v <= 1); + if (v < 0) { neg = neg + 1; } + if (v == 0) { zero = zero + 1; } + if (v > 0) { pos = pos + 1; } + i = i + 1; + } + assert(neg > 10 && zero > 10 && pos > 10); +} + +test "the field is the lesson's bytes: another lesson draws another field" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + const a = trit_px(4, 0x22C55E, 6, x, y); + const b = trit_px(19, 0x22C55E, 6, x, y); + assert(a >= 0 && a <= 0xFFFFFF); + if (a != b) { diff = diff + 1; } + x = x + 3; + } + y = y + 3; + } + assert(diff > 5000); +} + +test "each of the three trits has its own look" { + // find one cell of each sign in lesson 4 and compare the middle pixel of each + var cn : i32 = 0 - 1; + var cz : i32 = 0 - 1; + var cp : i32 = 0 - 1; + var i : i32 = 0; + while (i < GCOLS * GROWS) { + const v = trit_of(4, i); + if (v < 0 && cn < 0) { cn = i; } + if (v == 0 && cz < 0) { cz = i; } + if (v > 0 && cp < 0) { cp = i; } + i = i + 1; + } + assert(cn >= 0 && cz >= 0 && cp >= 0); + const rn = trit_px(4, 0x22C55E, 6, @mod(cn, GCOLS) * CELL + 20 + 5, GY0 + @divTrunc(cn, GCOLS) * CELL + 20); + const rp = trit_px(4, 0x22C55E, 6, @mod(cp, GCOLS) * CELL + 20 + 5, GY0 + @divTrunc(cp, GCOLS) * CELL + 20); + assert(rn == NEG); + assert(rp == 0x22C55E); + assert(rn != rp); +} + +test "negative control: the same lesson twice is one picture" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + if (trit_px(9, 0xE0409A, 2, x, y) != trit_px(9, 0xE0409A, 2, x, y)) { diff = diff + 1; } + x = x + 5; + } + y = y + 5; + } + assert(diff == 0); +} From 820bcff66f9b0e90c81aa244db9fccedcd582ad9 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 01:08:27 +0700 Subject: [PATCH 20/27] t27f: scene_tests ch is i32 (WIP, #10571) Co-Authored-By: Claude Sonnet 5.5 --- specs/t27f/scene_tests.t27 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/t27f/scene_tests.t27 b/specs/t27f/scene_tests.t27 index dbc746c5c8..1922fdb138 100644 --- a/specs/t27f/scene_tests.t27 +++ b/specs/t27f/scene_tests.t27 @@ -74,7 +74,7 @@ pub fn tests_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { if (ok && x >= 300) { const k = @divTrunc(x - 300, 14); if (k < 4 && y >= oy + 6) { - var ch = 80; + var ch : i32 = 80; if (k == 1) { ch = 65; } if (k >= 2) { ch = 83; } if (char_on(ch, x, y, 300 + k * 14, oy + 6, 3)) { return PASS_GREEN; } From 3d50dc2a8aa96f49d049f37b3b617499a8990d18 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 01:09:30 +0700 Subject: [PATCH 21/27] t27f: unique scene constants, scene_tests negative control finds its bare lesson (WIP, #10571) Co-Authored-By: Claude Sonnet 5.5 --- specs/t27f/scene_graph.t27 | 18 +++++++++--------- specs/t27f/scene_tests.t27 | 10 ++++++++-- specs/t27f/scene_title.t27 | 6 +++--- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/specs/t27f/scene_graph.t27 b/specs/t27f/scene_graph.t27 index c563f23f3e..703f979afe 100644 --- a/specs/t27f/scene_graph.t27 +++ b/specs/t27f/scene_graph.t27 @@ -41,23 +41,23 @@ use t27f::scene_lex::KW_USE; pub const NODE_W : i32 = 100; pub const NODE_H : i32 = 22; -pub const COLS : i32 = 3; +pub const GRAPH_COLS : i32 = 3; pub const X0 : i32 = 14; pub const DX : i32 = 116; pub const Y0 : i32 = 92; pub const DY : i32 = 42; pub const MAXN : i32 = 33; -pub const INK : i32 = 0xE6EAF5; +pub const GRAPH_INK : i32 = 0xE6EAF5; pub const NODE_BG : i32 = 0x161B33; pub fn node_n(t: usize) -> i32 { return imin(lex_n(t), MAXN); } pub fn node_cx(r: i32) -> i32 { - return X0 + @mod(r, COLS) * DX + @divTrunc(NODE_W, 2); + return X0 + @mod(r, GRAPH_COLS) * DX + @divTrunc(NODE_W, 2); } pub fn node_cy(r: i32) -> i32 { - return Y0 + @divTrunc(r, COLS) * DY + @divTrunc(NODE_H, 2); + return Y0 + @divTrunc(r, GRAPH_COLS) * DY + @divTrunc(NODE_H, 2); } // Whether line l declares something. pub fn is_decl(l: usize) -> bool { @@ -71,8 +71,8 @@ pub fn graph_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { // the node under the pixel, if any if (y >= Y0 && x >= X0) { const col = @divTrunc(x - X0, DX); - const r = row * COLS + col; - if (col < COLS && r >= 0 && r < cnt && in_rect(x, y, X0 + col * DX, Y0 + row * DY, X0 + col * DX + NODE_W, Y0 + row * DY + NODE_H)) { + const r = row * GRAPH_COLS + col; + if (col < GRAPH_COLS && r >= 0 && r < cnt && in_rect(x, y, X0 + col * DX, Y0 + row * DY, X0 + col * DX + NODE_W, Y0 + row * DY + NODE_H)) { const l = lex_line(t, r); const ox = X0 + col * DX + 4; const oy = Y0 + row * DY + 6; @@ -84,15 +84,15 @@ pub fn graph_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { const ch = lex_ch(l, lex_first(l) + k); if (ch > 0 && char_on(ch, x, y, ox + k * 8, oy, 2)) { if (is_decl(l)) { return rgb(10, 12, 24); } - return INK; + return GRAPH_INK; } } return fill; } } // the edges: line r to line r + 1, only those that can pass this row - var r0 = imax(0, (row - 1) * COLS); - const r1 = imin(cnt - 2, row * COLS + COLS); + var r0 = imax(0, (row - 1) * GRAPH_COLS); + const r1 = imin(cnt - 2, row * GRAPH_COLS + GRAPH_COLS); while (r0 <= r1) { if (near_seg(x, y, node_cx(r0), node_cy(r0), node_cx(r0 + 1), node_cy(r0 + 1), 1)) { return acc; diff --git a/specs/t27f/scene_tests.t27 b/specs/t27f/scene_tests.t27 index 1922fdb138..a5f46f7221 100644 --- a/specs/t27f/scene_tests.t27 +++ b/specs/t27f/scene_tests.t27 @@ -120,16 +120,22 @@ test "the rows are the lesson's lines: another lesson draws another run" { } test "negative control: a lesson without an assert shows no PASS" { + var bare : usize = 27; + var s : usize = 0; + while (s < 27) { + if (bare == 27 && lex_lines_with(s, KW_ASSERT) == 0) { bare = s; } + s = s + 1; + } + assert(bare < 27); var green : i32 = 0; var y : i32 = ROW_Y0; while (y < ROW_Y0 + MAXR * ROW_H) { var x : i32 = 300; while (x < 360) { - if (tests_px(3, 0xF5B700, 1, x, y) == PASS_GREEN) { green = green + 1; } + if (tests_px(bare, 0xF5B700, 1, x, y) == PASS_GREEN) { green = green + 1; } x = x + 1; } y = y + 1; } - assert(lex_lines_with(3, KW_ASSERT) == 0); assert(green == 0); } diff --git a/specs/t27f/scene_title.t27 b/specs/t27f/scene_title.t27 index b102369049..27bb408e63 100644 --- a/specs/t27f/scene_title.t27 +++ b/specs/t27f/scene_title.t27 @@ -29,7 +29,7 @@ use t27f::reel_topics::cp; pub const PER_LINE : i32 = 11; pub const LX0 : i32 = 26; -pub const ADV : i32 = 28; +pub const TITLE_ADV : i32 = 28; pub const PITCH : i32 = 50; pub const SHADOW : i32 = 5; @@ -58,12 +58,12 @@ pub fn letter_on(t: usize, i: i32, x: i32, y: i32, sx: i32, sy: i32) -> bool { if (c <= 32) { return false; } const k = @mod(i, PER_LINE); const r = @divTrunc(i, PER_LINE); - return char_on(c, x - sx, y - sy, LX0 + k * ADV, title_y0(t) + r * PITCH - wave(t, i) + 14, 7); + return char_on(c, x - sx, y - sy, LX0 + k * TITLE_ADV, title_y0(t) + r * PITCH - wave(t, i) + 14, 7); } pub fn title_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { const c0 = bg_px(3, acc, seed_of(t, n, 9), x, y); - const k = @divTrunc(x - LX0, ADV); + const k = @divTrunc(x - LX0, TITLE_ADV); if (x < LX0 || k >= PER_LINE) { return c0; } const lines = title_lines(t); const dy = y - title_y0(t); From f303bb6317d755de6883de0341bff455b9cc8fdc Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 01:10:19 +0700 Subject: [PATCH 22/27] t27f: scene_tests negative control checks a row without an assert (WIP, #10571) Co-Authored-By: Claude Sonnet 5.5 --- specs/t27f/scene_tests.t27 | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/specs/t27f/scene_tests.t27 b/specs/t27f/scene_tests.t27 index a5f46f7221..e37cb691ae 100644 --- a/specs/t27f/scene_tests.t27 +++ b/specs/t27f/scene_tests.t27 @@ -119,23 +119,25 @@ test "the rows are the lesson's lines: another lesson draws another run" { assert(diff > 4000); } -test "negative control: a lesson without an assert shows no PASS" { - var bare : usize = 27; - var s : usize = 0; - while (s < 27) { - if (bare == 27 && lex_lines_with(s, KW_ASSERT) == 0) { bare = s; } - s = s + 1; - } - assert(bare < 27); +test "negative control: a row without an assert shows no PASS" { + var checked : i32 = 0; var green : i32 = 0; - var y : i32 = ROW_Y0; - while (y < ROW_Y0 + MAXR * ROW_H) { - var x : i32 = 300; - while (x < 360) { - if (tests_px(bare, 0xF5B700, 1, x, y) == PASS_GREEN) { green = green + 1; } - x = x + 1; + var r : i32 = 0; + while (r < row_n(8)) { + if (!lex_has(lex_line(8, r), KW_ASSERT)) { + checked = checked + 1; + var y : i32 = ROW_Y0 + r * ROW_H; + while (y < ROW_Y0 + r * ROW_H + ROW_H) { + var x : i32 = 300; + while (x < 360) { + if (tests_px(8, 0xF5B700, 1, x, y) == PASS_GREEN) { green = green + 1; } + x = x + 1; + } + y = y + 1; + } } - y = y + 1; + r = r + 1; } + assert(checked > 0); assert(green == 0); } From 0d8fc01972ecc8d40960eacbe9481f69606f2de2 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 01:13:19 +0700 Subject: [PATCH 23/27] t27f: still_gate -- block SSIM over the 27 stills, worst pair and refusal (WIP, #10571) Co-Authored-By: Claude Sonnet 5.5 --- specs/t27f/still_gate.t27 | 250 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 specs/t27f/still_gate.t27 diff --git a/specs/t27f/still_gate.t27 b/specs/t27f/still_gate.t27 new file mode 100644 index 0000000000..6f0ea23a3f --- /dev/null +++ b/specs/t27f/still_gate.t27 @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/still_gate.t27 -- the anomaly gate of the T27F content factory. It renders the 27 +// stills of course 0 from the factory's own plan (reel_factory.t27 picks the archetype, accent, +// motion and layout of each lesson; scene_still.t27 draws it), compares every pair with a block +// SSIM, and refuses the course when the most similar pair is above SIMILAR_MAX_PERMILLE. +// Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: the owner's complaint was that every reel looks the same. reel_factory.t27 plans 27 +// different scenes, but a plan is not a picture: two renderers can still draw near-identical +// frames. This is the gate that looks at the pictures, so "they are all different" is measured +// instead of believed. +// +// THE METRIC: structural similarity on the luma of a 108 x 192 still, in 12 x 12 blocks (9 x 16 of +// them), the mean over the blocks, in permille. For one block with n = 144 pixels, sums Sa, Sb, +// Saa, Sbb, Sab of the two stills' luma, and C1 = 7, C2 = 59 (the usual (0.01 * 255)^2 and +// (0.03 * 255)^2, rounded up to integers): +// SSIM = (2 Sa Sb + C1 n^2) (2 (n Sab - Sa Sb) + C2 n^2) +// / ((Sa^2 + Sb^2 + C1 n^2) (n Saa - Sa^2 + n Sbb - Sb^2 + C2 n^2)) +// which is the textbook formula multiplied through by n^2 so that it needs no division until the +// end. Integers only, in i64, so the gate gives the same number on every machine. +// +// THE GATE: gate() renders the 27 stills into a caller-owned buffer, scores all 351 pairs and +// reports the worst pair in out[0..3] = (lesson a, lesson b, permille). It returns 1 when that +// score is above SIMILAR_MAX_PERMILLE (the course is refused) and 0 when it is not. +// +// NOT CLAIMED: SSIM on luma sees layout and contrast, not meaning; two stills with different +// words in the same boxes would pass. The 850 bound is reel_factory's, now measured (see the +// test that pins the worst pair of course 0). No file is written and nothing is printed: a +// scratch driver can print the table, the module itself holds no main. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module still_gate; + +use t27f::reel_factory::arche; +use t27f::reel_factory::accent; +use t27f::reel_factory::motion; +use t27f::reel_factory::layout; +use t27f::reel_factory::content; +use t27f::reel_factory::too_similar; +use t27f::reel_factory::SIMILAR_MAX_PERMILLE; +use t27f::scene_still::pixel; +use t27f::scene_raster::luma; + +pub const GATE_W : i32 = 108; +pub const GATE_H : i32 = 192; +pub const GATE_PIX : usize = 20736; +pub const GATE_BLOCK : i32 = 12; +pub const GATE_BX : i32 = 9; +pub const GATE_BY : i32 = 16; +pub const GATE_BLOCKS : usize = 144; +pub const GATE_STILLS : usize = 27; +pub const GATE_PAIRS : usize = 351; + +// The scale of one block: n = 144 pixels, n^2 = 20736. +pub const SSIM_N : i64 = 144; +pub const SSIM_NN : i64 = 20736; +pub const SSIM_C1 : i64 = 7; +pub const SSIM_C2 : i64 = 59; + +// Draw lesson t's still from the plan into img[base .. base + GATE_PIX], as luma. +pub fn render_luma(t: usize, img: []u8, base: usize) -> void { + const a = arche(t); + const ac = accent(t); + const mo = motion(t); + const ly = layout(t); + const n = @as(i32, @intCast(content(t, a))); + var y : i32 = 0; + while (y < GATE_H) { + var x : i32 = 0; + while (x < GATE_W) { + const c = pixel(t, a, ac, mo, ly, n, x, y, GATE_W, GATE_H); + img[base + @as(usize, @intCast(y * GATE_W + x))] = @as(u8, @intCast(luma(c))); + x = x + 1; + } + y = y + 1; + } +} + +// The index in img of pixel (i, j) of block (bx, by) of the still that starts at base. +pub fn at(base: usize, bx: i32, by: i32, i: i32, j: i32) -> usize { + return base + @as(usize, @intCast((by * GATE_BLOCK + j) * GATE_W + bx * GATE_BLOCK + i)); +} + +// Fill sum[s * 144 + b] and sq[s * 144 + b] for still s, whose pixels start at base. +pub fn block_stats(img: []u8, s: usize, base: usize, sum: []i64, sq: []i64) -> void { + var by : i32 = 0; + while (by < GATE_BY) { + var bx : i32 = 0; + while (bx < GATE_BX) { + var a : i64 = 0; + var q : i64 = 0; + var j : i32 = 0; + while (j < GATE_BLOCK) { + var i : i32 = 0; + while (i < GATE_BLOCK) { + const v = @as(i64, img[at(base, bx, by, i, j)]); + a = a + v; + q = q + v * v; + i = i + 1; + } + j = j + 1; + } + const b = @as(usize, @intCast(by * GATE_BX + bx)); + sum[s * GATE_BLOCKS + b] = a; + sq[s * GATE_BLOCKS + b] = q; + bx = bx + 1; + } + by = by + 1; + } +} + +// The SSIM, in permille, of two blocks from their sums, sums of squares and sum of products. +pub fn block_ssim(sa: i64, sb: i64, qa: i64, qb: i64, sab: i64) -> i64 { + const l1 = 2 * sa * sb + SSIM_C1 * SSIM_NN; + const l2 = 2 * (SSIM_N * sab - sa * sb) + SSIM_C2 * SSIM_NN; + const d1 = sa * sa + sb * sb + SSIM_C1 * SSIM_NN; + const d2 = (SSIM_N * qa - sa * sa) + (SSIM_N * qb - sb * sb) + SSIM_C2 * SSIM_NN; + const q1 = @divTrunc(l1 * 1000, d1); + return @divTrunc(q1 * l2, d2); +} + +// The mean block SSIM, in permille, of stills a and b (their images are at a * GATE_PIX and b * +// GATE_PIX in img, their block statistics already in sum and sq). +pub fn pair_ssim(img: []u8, a: usize, b: usize, sum: []i64, sq: []i64) -> i32 { + const ba = a * GATE_PIX; + const bb = b * GATE_PIX; + var total : i64 = 0; + var by : i32 = 0; + while (by < GATE_BY) { + var bx : i32 = 0; + while (bx < GATE_BX) { + var sab : i64 = 0; + var j : i32 = 0; + while (j < GATE_BLOCK) { + var i : i32 = 0; + while (i < GATE_BLOCK) { + sab = sab + @as(i64, img[at(ba, bx, by, i, j)]) * @as(i64, img[at(bb, bx, by, i, j)]); + i = i + 1; + } + j = j + 1; + } + const k = @as(usize, @intCast(by * GATE_BX + bx)); + total = total + block_ssim(sum[a * GATE_BLOCKS + k], sum[b * GATE_BLOCKS + k], sq[a * GATE_BLOCKS + k], sq[b * GATE_BLOCKS + k], sab); + bx = bx + 1; + } + by = by + 1; + } + return @as(i32, @intCast(@divTrunc(total, @as(i64, @intCast(GATE_BLOCKS))))); +} + +// Render the 27 stills of the course into img (27 * 20736 bytes) and their block statistics into +// sum and sq (27 * 144 each). +pub fn render_course(img: []u8, sum: []i64, sq: []i64) -> void { + var s : usize = 0; + while (s < GATE_STILLS) { + render_luma(s, img, s * GATE_PIX); + block_stats(img, s, s * GATE_PIX, sum, sq); + s = s + 1; + } +} + +// Score every pair; out[0] = lesson a, out[1] = lesson b, out[2] = permille of the most similar. +pub fn worst_pair(img: []u8, sum: []i64, sq: []i64, out: []i32) -> void { + var best : i32 = 0 - 2000; + var ba : usize = 0; + var bb : usize = 1; + var a : usize = 0; + while (a < GATE_STILLS) { + var b : usize = a + 1; + while (b < GATE_STILLS) { + const v = pair_ssim(img, a, b, sum, sq); + if (v > best) { + best = v; + ba = a; + bb = b; + } + b = b + 1; + } + a = a + 1; + } + out[0] = @as(i32, @intCast(ba)); + out[1] = @as(i32, @intCast(bb)); + out[2] = best; +} + +// The whole gate. 1 when the course is refused, 0 when every pair is told apart. +pub fn gate(img: []u8, sum: []i64, sq: []i64, out: []i32) -> i32 { + render_course(img, sum, sq); + worst_pair(img, sum, sq, out); + if (too_similar(@as(u32, @intCast(out[2] + 0)))) { return 1; } + return 0; +} + +// ---- tests ---- + +test "one block against itself is 1000 and against its negative is far below" { + // a block of mixed luma: half 40, half 200 + const sa = 72 * 40 + 72 * 200; + const qa = 72 * 40 * 40 + 72 * 200 * 200; + assert(block_ssim(sa, sa, qa, qa, qa) == 1000); + // the same block against its inverse (40 <-> 200): same mean, covariance negative + const sab = 144 * 40 * 200; + assert(block_ssim(sa, sa, qa, qa, sab) < 0); + // a flat block against another flat block of the same luma is 1000 (nothing to tell apart) + assert(block_ssim(144 * 30, 144 * 30, 144 * 900, 144 * 900, 144 * 900) == 1000); + // flat dark against flat bright: the luminance term separates them + assert(block_ssim(144 * 30, 144 * 200, 144 * 900, 144 * 40000, 144 * 6000) < 500); +} + +test "the gate scores the 27 stills of course 0 and reports the most similar pair" { + var img : [559872]u8 = undefined; + var sum : [3888]i64 = undefined; + var sq : [3888]i64 = undefined; + var out : [3]i32 = undefined; + const refused = gate(&img, &sum, &sq, &out); + assert(out[0] >= 0 && out[0] < 26); + assert(out[1] > out[0] && out[1] < 27); + assert(out[2] > 0 - 1000 && out[2] <= 1000); + // the pair really is the maximum: it is not beaten by the pair (0, 1) or the pair (25, 26) + assert(out[2] >= pair_ssim(&img, 0, 1, &sum, &sq)); + assert(out[2] >= pair_ssim(&img, 25, 26, &sum, &sq)); + // the verdict follows the bound + assert((refused == 1) == too_similar(@as(u32, @intCast(out[2] + 0)))); + assert(refused == 0); + assert(out[2] <= @as(i32, @intCast(SIMILAR_MAX_PERMILLE))); +} + +test "negative control: a still against itself is 1000 and the gate would refuse a repeat" { + var img : [41472]u8 = undefined; + var sum : [288]i64 = undefined; + var sq : [288]i64 = undefined; + render_luma(5, &img, 0); + render_luma(5, &img, GATE_PIX); + block_stats(&img, 0, 0, &sum, &sq); + block_stats(&img, 1, GATE_PIX, &sum, &sq); + const same = pair_ssim(&img, 0, 1, &sum, &sq); + assert(same == 1000); + assert(too_similar(@as(u32, @intCast(same)))); +} + +test "negative control: two different lessons are scored below a repeat" { + var img : [41472]u8 = undefined; + var sum : [288]i64 = undefined; + var sq : [288]i64 = undefined; + render_luma(5, &img, 0); + render_luma(6, &img, GATE_PIX); + block_stats(&img, 0, 0, &sum, &sq); + block_stats(&img, 1, GATE_PIX, &sum, &sq); + assert(pair_ssim(&img, 0, 1, &sum, &sq) < 1000); +} From 5ea334fc16d7e8dbbe3d8747cf04a573515d018e Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 01:15:04 +0700 Subject: [PATCH 24/27] t27f: still_gate pins the measured worst pair, lessons 8 and 20 at 607 permille (WIP, #10571) Co-Authored-By: Claude Sonnet 5.5 --- specs/t27f/still_gate.t27 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/specs/t27f/still_gate.t27 b/specs/t27f/still_gate.t27 index 6f0ea23a3f..a7f854d11c 100644 --- a/specs/t27f/still_gate.t27 +++ b/specs/t27f/still_gate.t27 @@ -223,6 +223,11 @@ test "the gate scores the 27 stills of course 0 and reports the most similar pai assert((refused == 1) == too_similar(@as(u32, @intCast(out[2] + 0)))); assert(refused == 0); assert(out[2] <= @as(i32, @intCast(SIMILAR_MAX_PERMILLE))); + // measured on the Railway lab (R3, #10571): the most similar pair of course 0 is lessons 8 and + // 20 at 607 permille, 243 under the bound. A renderer change that moves it must move this line. + assert(out[0] == 8); + assert(out[1] == 20); + assert(out[2] == 607); } test "negative control: a still against itself is 1000 and the gate would refuse a repeat" { From b8e60e1c9f36dc4c4e90f17b107b9e526c541950 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 01:17:49 +0700 Subject: [PATCH 25/27] t27f: scene specs spell the floor modulo as fmod so t27b can compile them (WIP, #10571) Co-Authored-By: Claude Sonnet 5.5 --- specs/t27f/scene_bg.t27 | 9 +++++---- specs/t27f/scene_branch.t27 | 3 ++- specs/t27f/scene_data.t27 | 3 ++- specs/t27f/scene_graph.t27 | 3 ++- specs/t27f/scene_lex.t27 | 5 +++-- specs/t27f/scene_links.t27 | 3 ++- specs/t27f/scene_loop.t27 | 5 +++-- specs/t27f/scene_raster.t27 | 25 ++++++++++++++++++------- specs/t27f/scene_still.t27 | 3 ++- specs/t27f/scene_title.t27 | 5 +++-- specs/t27f/scene_trit.t27 | 7 ++++--- 11 files changed, 46 insertions(+), 25 deletions(-) diff --git a/specs/t27f/scene_bg.t27 b/specs/t27f/scene_bg.t27 index 0a32885329..c27b5624c4 100644 --- a/specs/t27f/scene_bg.t27 +++ b/specs/t27f/scene_bg.t27 @@ -16,6 +16,7 @@ module scene_bg; +use t27f::scene_raster::fmod; use t27f::scene_raster::rgb; use t27f::scene_raster::mix; use t27f::scene_raster::rnd; @@ -26,10 +27,10 @@ pub const SIN24 : [24]i32 = [0, 259, 500, 707, 866, 966, 1000, 966, 866, 707, 50 // cos and sin of 15 degrees times k, times 1000; k wraps. pub fn cos24(k: i32) -> i32 { - return COS24[@as(usize, @intCast(@mod(k, 24)))]; + return COS24[@as(usize, @intCast(fmod(k, 24)))]; } pub fn sin24(k: i32) -> i32 { - return SIN24[@as(usize, @intCast(@mod(k, 24)))]; + return SIN24[@as(usize, @intCast(fmod(k, 24)))]; } // The seed of lesson t's scene st with content count n. @@ -50,7 +51,7 @@ pub fn cell_id(st: i32, sd: i32, x: i32, y: i32) -> i32 { if (st == 0) { id = @divTrunc(py, cs) * 256 + @divTrunc(px, cs); } if (st == 1) { const row = @divTrunc(py, cs); - id = row * 256 + @divTrunc(px + @mod(row, 2) * cs, cs * 2); + id = row * 256 + @divTrunc(px + fmod(row, 2) * cs, cs * 2); } if (st == 2) { id = @divTrunc(px + py, cs) * 256 + @divTrunc(px - py + 2400, cs); } if (st == 3) { id = @divTrunc(px, cs); } @@ -67,7 +68,7 @@ pub fn bg_px(st: i32, acc: i32, sd: i32, x: i32, y: i32) -> i32 { const cs = cell_size(sd); const id = cell_id(st, sd, x, y); var l : i32 = 14 + rnd(sd * 131 + id, 6) * 8; - if (st == 0 && (@mod(x + 1200, cs) == 0 || @mod(y + 1200, cs) == 0)) { l = l - 8; } + if (st == 0 && (fmod(x + 1200, cs) == 0 || fmod(y + 1200, cs) == 0)) { l = l - 8; } return mix(rgb(l, l + 2, l + 8), acc, 36); } diff --git a/specs/t27f/scene_branch.t27 b/specs/t27f/scene_branch.t27 index fc7fa2a8ee..7c75ef9aec 100644 --- a/specs/t27f/scene_branch.t27 +++ b/specs/t27f/scene_branch.t27 @@ -15,6 +15,7 @@ module scene_branch; +use t27f::scene_raster::fmod; use t27f::scene_raster::rgb; use t27f::scene_raster::mix; use t27f::scene_raster::in_disc; @@ -50,7 +51,7 @@ pub fn path_at(t: usize, l: i32) -> i32 { var i : i32 = 0; var k : i32 = 1; while (k <= l) { - i = i * 2 + @mod(lex_byte(t, k * 5 + 1), 2); + i = i * 2 + fmod(lex_byte(t, k * 5 + 1), 2); k = k + 1; } return i; diff --git a/specs/t27f/scene_data.t27 b/specs/t27f/scene_data.t27 index fe3b309ac7..8d1a315cd8 100644 --- a/specs/t27f/scene_data.t27 +++ b/specs/t27f/scene_data.t27 @@ -16,6 +16,7 @@ module scene_data; +use t27f::scene_raster::fmod; use t27f::scene_raster::rgb; use t27f::scene_raster::mix; use t27f::scene_raster::in_rect; @@ -39,7 +40,7 @@ pub fn block_n(t: usize) -> i32 { return imin(lex_n(t), MAXB); } pub fn block_x(r: i32) -> i32 { - return 20 + 9 * @mod(r, 5); + return 20 + 9 * fmod(r, 5); } pub fn blocks_end(t: usize) -> i32 { return block_n(t) * BPITCH; diff --git a/specs/t27f/scene_graph.t27 b/specs/t27f/scene_graph.t27 index 703f979afe..40ca23837c 100644 --- a/specs/t27f/scene_graph.t27 +++ b/specs/t27f/scene_graph.t27 @@ -16,6 +16,7 @@ module scene_graph; +use t27f::scene_raster::fmod; use t27f::scene_raster::rgb; use t27f::scene_raster::mix; use t27f::scene_raster::in_rect; @@ -54,7 +55,7 @@ pub fn node_n(t: usize) -> i32 { return imin(lex_n(t), MAXN); } pub fn node_cx(r: i32) -> i32 { - return X0 + @mod(r, GRAPH_COLS) * DX + @divTrunc(NODE_W, 2); + return X0 + fmod(r, GRAPH_COLS) * DX + @divTrunc(NODE_W, 2); } pub fn node_cy(r: i32) -> i32 { return Y0 + @divTrunc(r, GRAPH_COLS) * DY + @divTrunc(NODE_H, 2); diff --git a/specs/t27f/scene_lex.t27 b/specs/t27f/scene_lex.t27 index 67d4763286..ddc25526c5 100644 --- a/specs/t27f/scene_lex.t27 +++ b/specs/t27f/scene_lex.t27 @@ -18,6 +18,7 @@ module scene_lex; +use t27f::scene_raster::fmod; use t27f::reel_topics::code_line0; use t27f::reel_topics::code_lines; use t27f::reel_topics::line_len; @@ -125,7 +126,7 @@ pub fn lex_total(t: usize) -> i32 { pub fn lex_byte(t: usize, i: i32) -> i32 { const total = lex_total(t); if (total == 0) { return 0; } - var j = @mod(i, total); + var j = fmod(i, total); var r : i32 = 0; while (r < lex_n(t)) { const n = lex_llen(lex_line(t, r)); @@ -143,7 +144,7 @@ pub fn lex_hash(t: usize) -> i32 { const l = lex_line(t, r); var k : i32 = 0; while (k < lex_llen(l)) { - h = @mod(h * 33 + lex_ch(l, k), 1000003); + h = fmod(h * 33 + lex_ch(l, k), 1000003); k = k + 1; } r = r + 1; diff --git a/specs/t27f/scene_links.t27 b/specs/t27f/scene_links.t27 index f53f6b9a82..79890e02d3 100644 --- a/specs/t27f/scene_links.t27 +++ b/specs/t27f/scene_links.t27 @@ -17,6 +17,7 @@ module scene_links; +use t27f::scene_raster::fmod; use t27f::scene_raster::rgb; use t27f::scene_raster::mix; use t27f::scene_raster::in_rect; @@ -41,7 +42,7 @@ pub fn sats(n: i32) -> i32 { } // The slot of the circle, of 24, that neighbour i of s stands at. pub fn slot(t: usize, i: i32, s: i32) -> i32 { - return @mod(lex_byte(t, 0), 24) + @divTrunc(i * 24, s); + return fmod(lex_byte(t, 0), 24) + @divTrunc(i * 24, s); } pub fn sat_x(t: usize, i: i32, s: i32) -> i32 { return HX + @divTrunc(WHEEL * cos24(slot(t, i, s)), 1000); diff --git a/specs/t27f/scene_loop.t27 b/specs/t27f/scene_loop.t27 index 1ce4e0a605..6b4ff2607d 100644 --- a/specs/t27f/scene_loop.t27 +++ b/specs/t27f/scene_loop.t27 @@ -15,6 +15,7 @@ module scene_loop; +use t27f::scene_raster::fmod; use t27f::scene_raster::rgb; use t27f::scene_raster::mix; use t27f::scene_raster::in_disc; @@ -40,7 +41,7 @@ pub fn ring_radius(j: i32, rings: i32) -> i32 { } // 0 no dot, 1 a dot, 2 a dot in the accent: the slot m of ring j of lesson t. pub fn slot_of(t: usize, j: i32, m: i32) -> i32 { - return @mod(lex_byte(t, j * SLOTS + m), 3); + return fmod(lex_byte(t, j * SLOTS + m), 3); } pub fn loop_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { @@ -60,7 +61,7 @@ pub fn loop_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { if (in_ring(x, y, LCX, LCY, rj - 8, rj + 8)) { var m : i32 = 0; while (m < SLOTS) { - const k = m * 2 + @mod(j, 2); + const k = m * 2 + fmod(j, 2); const dx = LCX + @divTrunc(rj * cos24(k), 1000); const dy = LCY + @divTrunc(rj * sin24(k), 1000); const s = slot_of(t, j, m); diff --git a/specs/t27f/scene_raster.t27 b/specs/t27f/scene_raster.t27 index 322a7e9b01..21cb2c2e63 100644 --- a/specs/t27f/scene_raster.t27 +++ b/specs/t27f/scene_raster.t27 @@ -27,6 +27,15 @@ module scene_raster; pub const VW : i32 = 360; pub const VH : i32 = 640; +// ---- integer helpers ---- + +// a modulo m for m > 0, always in 0..m-1 (the floor modulo; spelled out so t27b can compile it). +pub fn fmod(a: i32, m: i32) -> i32 { + const r = a - @divTrunc(a, m) * m; + if (r < 0) { return r + m; } + return r; +} + // ---- colour ---- pub fn rgb(r: i32, g: i32, b: i32) -> i32 { @@ -36,10 +45,10 @@ pub fn red(c: i32) -> i32 { return @divTrunc(c, 65536); } pub fn green(c: i32) -> i32 { - return @mod(@divTrunc(c, 256), 256); + return fmod(@divTrunc(c, 256), 256); } pub fn blue(c: i32) -> i32 { - return @mod(c, 256); + return fmod(c, 256); } pub fn iabs(v: i32) -> i32 { if (v < 0) { return 0 - v; } @@ -76,12 +85,14 @@ pub fn mix(c0: i32, c1: i32, a: i32) -> i32 { // The next state of a linear congruential generator, 31 bits. pub fn lcg(s: i32) -> i32 { const v : i64 = @as(i64, s) * 1103515245 + 12345; - return @as(i32, @intCast(@mod(v, 2147483648))); + var m : i64 = v - @divTrunc(v, 2147483648) * 2147483648; + if (m < 0) { m = m + 2147483648; } + return @as(i32, @intCast(m)); } // A number in 0..n-1 from seed s: two steps, then the high bits (the low bits of an LCG cycle). pub fn rnd(s: i32, n: i32) -> i32 { const h = lcg(lcg(s + 7919)); - return @mod(@divTrunc(h, 4096), n); + return fmod(@divTrunc(h, 4096), n); } // ---- shapes ---- @@ -121,7 +132,7 @@ pub fn near_seg(x: i32, y: i32, x0: i32, y0: i32, x1: i32, y1: i32, hw: i32) -> } // A triangle wave: 0 at phase 0, rising to amp - 1 at half a period, back to 0. pub fn tri_wave(v: i32, period: i32, amp: i32) -> i32 { - const m = @mod(v, period); + const m = fmod(v, period); const half = @divTrunc(period, 2); if (m < half) { return @divTrunc(m * amp, half); } return @divTrunc((period - m) * amp, half); @@ -218,7 +229,7 @@ pub fn glyph_on(c: i32, gx: i32, gy: i32) -> bool { var pw : i32 = 4; if (gx == 1) { pw = 2; } if (gx == 2) { pw = 1; } - return @mod(@divTrunc(row, pw), 2) == 1; + return fmod(@divTrunc(row, pw), 2) == 1; } // Whether canvas point (x, y) is on character c drawn with its top left at (ox, oy), each font // pixel s canvas pixels square. @@ -247,7 +258,7 @@ pub fn num_on(v: i32, x: i32, y: i32, ox: i32, oy: i32, s: i32) -> bool { d = @divTrunc(d, 10); p = p - 1; } - return char_on(48 + @mod(d, 10), x, y, ox + k * advance(s), oy, s); + return char_on(48 + fmod(d, 10), x, y, ox + k * advance(s), oy, s); } // ---- tests ---- diff --git a/specs/t27f/scene_still.t27 b/specs/t27f/scene_still.t27 index 96e114ac22..5f4f1a6f68 100644 --- a/specs/t27f/scene_still.t27 +++ b/specs/t27f/scene_still.t27 @@ -25,6 +25,7 @@ module scene_still; +use t27f::scene_raster::fmod; use t27f::scene_raster::VW; use t27f::scene_raster::VH; use t27f::scene_raster::rgb; @@ -161,7 +162,7 @@ test "every one of the 12 archetypes draws a still that is not a flat colour and var x : i32 = 0; while (x < 54) { const c = pixel(13, a, 2, 0, 0, 3, x, y, 54, 96); - const d = pixel(13, @mod(a + 1, 12), 2, 0, 0, 3, x, y, 54, 96); + const d = pixel(13, fmod(a + 1, 12), 2, 0, 0, 3, x, y, 54, 96); assert(c >= 0 && c <= 0xFFFFFF); if (c != last) { seen = seen + 1; } if (c != d) { diff = diff + 1; } diff --git a/specs/t27f/scene_title.t27 b/specs/t27f/scene_title.t27 index 27bb408e63..9fd30ad9d0 100644 --- a/specs/t27f/scene_title.t27 +++ b/specs/t27f/scene_title.t27 @@ -16,6 +16,7 @@ module scene_title; +use t27f::scene_raster::fmod; use t27f::scene_raster::rgb; use t27f::scene_raster::mix; use t27f::scene_raster::char_on; @@ -56,7 +57,7 @@ pub fn wave(t: usize, i: i32) -> i32 { pub fn letter_on(t: usize, i: i32, x: i32, y: i32, sx: i32, sy: i32) -> bool { const c = title_ch(t, i); if (c <= 32) { return false; } - const k = @mod(i, PER_LINE); + const k = fmod(i, PER_LINE); const r = @divTrunc(i, PER_LINE); return char_on(c, x - sx, y - sy, LX0 + k * TITLE_ADV, title_y0(t) + r * PITCH - wave(t, i) + 14, 7); } @@ -72,7 +73,7 @@ pub fn title_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { while (r < lines) { const i = r * PER_LINE + k; if (letter_on(t, i, x, y, 0, 0)) { - if (@mod(i, 3) == 0) { return acc; } + if (fmod(i, 3) == 0) { return acc; } return rgb(245, 247, 255); } if (letter_on(t, i, x, y, SHADOW, SHADOW)) { return rgb(4, 5, 12); } diff --git a/specs/t27f/scene_trit.t27 b/specs/t27f/scene_trit.t27 index e45cf85d2e..7e53dcacda 100644 --- a/specs/t27f/scene_trit.t27 +++ b/specs/t27f/scene_trit.t27 @@ -16,6 +16,7 @@ module scene_trit; +use t27f::scene_raster::fmod; use t27f::scene_raster::rgb; use t27f::scene_raster::mix; use t27f::scene_raster::in_rect; @@ -34,7 +35,7 @@ pub const ZERO : i32 = 0x7C8499; // The trit, -1, 0 or +1, of cell i of lesson t. pub fn trit_of(t: usize, i: i32) -> i32 { - return @mod(lex_byte(t, i) + i, 3) - 1; + return fmod(lex_byte(t, i) + i, 3) - 1; } pub fn trit_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { @@ -107,8 +108,8 @@ test "each of the three trits has its own look" { i = i + 1; } assert(cn >= 0 && cz >= 0 && cp >= 0); - const rn = trit_px(4, 0x22C55E, 6, @mod(cn, GCOLS) * CELL + 20 + 5, GY0 + @divTrunc(cn, GCOLS) * CELL + 20); - const rp = trit_px(4, 0x22C55E, 6, @mod(cp, GCOLS) * CELL + 20 + 5, GY0 + @divTrunc(cp, GCOLS) * CELL + 20); + const rn = trit_px(4, 0x22C55E, 6, fmod(cn, GCOLS) * CELL + 20 + 5, GY0 + @divTrunc(cn, GCOLS) * CELL + 20); + const rp = trit_px(4, 0x22C55E, 6, fmod(cp, GCOLS) * CELL + 20 + 5, GY0 + @divTrunc(cp, GCOLS) * CELL + 20); assert(rn == NEG); assert(rp == 0x22C55E); assert(rn != rp); From 0257e74f1c6c5d01d60bdaee19c4235bd5ee1ec5 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 01:18:38 +0700 Subject: [PATCH 26/27] t27f: scene_still test steps to the next archetype without a modulo (WIP, #10571) Co-Authored-By: Claude Sonnet 5.5 --- specs/t27f/scene_still.t27 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/specs/t27f/scene_still.t27 b/specs/t27f/scene_still.t27 index 5f4f1a6f68..932d74f2d9 100644 --- a/specs/t27f/scene_still.t27 +++ b/specs/t27f/scene_still.t27 @@ -25,7 +25,6 @@ module scene_still; -use t27f::scene_raster::fmod; use t27f::scene_raster::VW; use t27f::scene_raster::VH; use t27f::scene_raster::rgb; @@ -154,6 +153,8 @@ test "the dressing changes the picture: another motion or layout of one scene di test "every one of the 12 archetypes draws a still that is not a flat colour and differs from its neighbour" { var a : usize = 0; while (a < 12) { + var next_a : usize = a + 1; + if (next_a == 12) { next_a = 0; } var seen : i32 = 0; var diff : i32 = 0; var last : i32 = 0 - 1; @@ -162,7 +163,7 @@ test "every one of the 12 archetypes draws a still that is not a flat colour and var x : i32 = 0; while (x < 54) { const c = pixel(13, a, 2, 0, 0, 3, x, y, 54, 96); - const d = pixel(13, fmod(a + 1, 12), 2, 0, 0, 3, x, y, 54, 96); + const d = pixel(13, next_a, 2, 0, 0, 3, x, y, 54, 96); assert(c >= 0 && c <= 0xFFFFFF); if (c != last) { seen = seen + 1; } if (c != d) { diff = diff + 1; } From 0444be2c79f8fa469b852b727f7e2063df1c13b3 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 01:21:28 +0700 Subject: [PATCH 27/27] t27f: seals, t27b ledger rows and skill row for the scene renderers and the still gate (Closes #10571) Co-Authored-By: Claude Sonnet 5.5 --- .claude/skills/t27f/SKILL.md | 2 +- .trinity/seals/t27f_scene_bg.json | 33 +++++++++++ .trinity/seals/t27f_scene_branch.json | 41 ++++++++++++++ .trinity/seals/t27f_scene_code.json | 41 ++++++++++++++ .trinity/seals/t27f_scene_data.json | 41 ++++++++++++++ .trinity/seals/t27f_scene_graph.json | 41 ++++++++++++++ .trinity/seals/t27f_scene_guard.json | 37 ++++++++++++ .trinity/seals/t27f_scene_lex.json | 41 ++++++++++++++ .trinity/seals/t27f_scene_links.json | 41 ++++++++++++++ .trinity/seals/t27f_scene_loop.json | 41 ++++++++++++++ .trinity/seals/t27f_scene_map.json | 37 ++++++++++++ .trinity/seals/t27f_scene_raster.json | 28 +++++++++ .trinity/seals/t27f_scene_still.json | 81 +++++++++++++++++++++++++++ .trinity/seals/t27f_scene_task.json | 41 ++++++++++++++ .trinity/seals/t27f_scene_tests.json | 41 ++++++++++++++ .trinity/seals/t27f_scene_title.json | 41 ++++++++++++++ .trinity/seals/t27f_scene_trit.json | 41 ++++++++++++++ .trinity/seals/t27f_still_gate.json | 41 ++++++++++++++ docs/reports/t27b_expectations.json | 17 ++++++ 19 files changed, 726 insertions(+), 1 deletion(-) create mode 100644 .trinity/seals/t27f_scene_bg.json create mode 100644 .trinity/seals/t27f_scene_branch.json create mode 100644 .trinity/seals/t27f_scene_code.json create mode 100644 .trinity/seals/t27f_scene_data.json create mode 100644 .trinity/seals/t27f_scene_graph.json create mode 100644 .trinity/seals/t27f_scene_guard.json create mode 100644 .trinity/seals/t27f_scene_lex.json create mode 100644 .trinity/seals/t27f_scene_links.json create mode 100644 .trinity/seals/t27f_scene_loop.json create mode 100644 .trinity/seals/t27f_scene_map.json create mode 100644 .trinity/seals/t27f_scene_raster.json create mode 100644 .trinity/seals/t27f_scene_still.json create mode 100644 .trinity/seals/t27f_scene_task.json create mode 100644 .trinity/seals/t27f_scene_tests.json create mode 100644 .trinity/seals/t27f_scene_title.json create mode 100644 .trinity/seals/t27f_scene_trit.json create mode 100644 .trinity/seals/t27f_still_gate.json diff --git a/.claude/skills/t27f/SKILL.md b/.claude/skills/t27f/SKILL.md index f4e35ec8f6..bec509af20 100644 --- a/.claude/skills/t27f/SKILL.md +++ b/.claude/skills/t27f/SKILL.md @@ -68,7 +68,7 @@ the reels. Describing T27F as #1677 is the error this section exists to stop (ow | Renderer in the Trinity UI | gHashTag/trinity | **missing**; no T27F widget is live | | Application: topic map (#1677) | `specs/t27f/topic_engine.t27`, `topic_reel.t27` | spec; procedure below | | Application: reels and deck | `specs/t27f/reel.t27`, `deck.t27`, `reel_*.t27` | spec; two reels | -| Application: content factory (epic t27#10560) | `specs/t27f/reel_factory.t27` (t27#10561) | spec with tests: an item's code and title, read keyword by keyword, give 11 features. Those pick 1 of 12 scene archetypes (best pair first, cap 4, no repeat within 2 items, then the least-used generic scene) plus an accent, a motion, a layout and 8 output formats. No renderer draws a plan yet (R3), and `lesson_reel` still draws token_floor (R4). Lessons: read the plan's spread over the whole course before trusting it. A greedy walk in course order gave 7 lessons one scene. Code counts alone handed lesson 8 the tests scene over "A test block". A printing `main` makes t27b reject the spec | +| Application: content factory (epic t27#10560) | `specs/t27f/reel_factory.t27` (t27#10561) | spec with tests: an item's code and title, read keyword by keyword, give 11 features. Those pick 1 of 12 scene archetypes (best pair first, cap 4, no repeat within 2 items, then the least-used generic scene) plus an accent, a motion, a layout and 8 output formats. R3 (t27#10571) draws it: `scene_raster`, `scene_lex`, `scene_bg` plus one `scene_.t27` per archetype (code, graph, tests, trit, loop, branch, data, links, guard, title, task, map), each `_px(t, acc, n, x, y)` over the lesson's own content on a 360 x 640 canvas, dispatched by `scene_still.pixel`; `still_gate.t27` renders the 27 course-0 stills (108 x 192 luma), scores all 351 pairs with a block SSIM and refuses above 850 permille, measured worst pair lessons 8 and 20 at 607. `lesson_reel` still draws token_floor (R4). SSIM on luma sees layout and contrast, not meaning. The floor modulo is `fmod` (t27b rejects `@mod`). Lessons: read the plan's spread over the whole course before trusting it. A greedy walk in course order gave 7 lessons one scene. Code counts alone handed lesson 8 the tests scene over "A test block". A printing `main` makes t27b reject the spec | | Related | trinity#1647 dashboards, #1652 MVP research, #1653 three skills, #1655 evolution runtime | open | When asked "how is T27F going", report against the core rows first. Progress on the applications diff --git a/.trinity/seals/t27f_scene_bg.json b/.trinity/seals/t27f_scene_bg.json new file mode 100644 index 0000000000..5454fa6399 --- /dev/null +++ b/.trinity/seals/t27f_scene_bg.json @@ -0,0 +1,33 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:af5c89e6bd6c81265d9d86c4f93eeb738ea91bfae40f294928bd586d5e98e7d0", + "gen_hash_rust": "sha256:b919e5036e999c0a6f329d928c954691083b2b139fe82ff160dabee993a6d1e8", + "gen_hash_verilog": "sha256:4470ad8b1d224134c143e03ce299f04c18dcfe61a47dfa036f9b7410f3568a6d", + "gen_hash_zig": "sha256:6b42744f7cacf6a9dd8efefee85de697a6a40137a0b4964285589c10e42f14c0", + "module": "scene_bg", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:09Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:be66b1113d05d634b2bfb26a1b3d455e2a24b1fd5a4f98da28c9e821978be1cf", + "spec_path": "specs/t27f/scene_bg.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 5, + "total": 5, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_branch.json b/.trinity/seals/t27f_scene_branch.json new file mode 100644 index 0000000000..7b7a79a261 --- /dev/null +++ b/.trinity/seals/t27f_scene_branch.json @@ -0,0 +1,41 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/scene_bg.t27", + "spec_hash": "sha256:be66b1113d05d634b2bfb26a1b3d455e2a24b1fd5a4f98da28c9e821978be1cf" + }, + { + "spec": "specs/t27f/scene_lex.t27", + "spec_hash": "sha256:a531db018ba0776b1d044fe39b323ba9f8c739fc60b06b57f6bab538022585c9" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:07f5247c4f8578519a9d66081d2622a8350a7fbd467c4bc7c81d8e21c3c0100e", + "gen_hash_rust": "sha256:812a8d57ed2554ba33bb71e8f0194f5c65c7db268713b10823c61cbc6eae7261", + "gen_hash_verilog": "sha256:f4f5ef5ca03bb0f14c3e6279f8134fdd5c83f4c5c9632fc2c55382415eee8855", + "gen_hash_zig": "sha256:928740ee46a8f715527f4e4f4476abc9f95d323ab016878d8f7054f0961850a3", + "module": "scene_branch", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:18Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:c4b584b8b5a4f5cc849dc9c1a4323609702adcdb710cb97b6441059210eccc15", + "spec_path": "specs/t27f/scene_branch.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_code.json b/.trinity/seals/t27f_scene_code.json new file mode 100644 index 0000000000..b4763bd79d --- /dev/null +++ b/.trinity/seals/t27f_scene_code.json @@ -0,0 +1,41 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/reel_topics.t27", + "spec_hash": "sha256:7c1762cde96524791711594d23f880fbad1f497a499277710f1fd61d52d50157" + }, + { + "spec": "specs/t27f/scene_bg.t27", + "spec_hash": "sha256:be66b1113d05d634b2bfb26a1b3d455e2a24b1fd5a4f98da28c9e821978be1cf" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:24b083661da23623773e140dd892388a51a52bbea5c932ca1e511b14390f416a", + "gen_hash_rust": "sha256:0d695db4f92342098d31e91855036ce21ad328328c6092afec89db64ce52fb5a", + "gen_hash_verilog": "sha256:524b3fafa43e57c7e6473c002498409f9bd60707acb3de9ef44488ae5072227f", + "gen_hash_zig": "sha256:7aac819ff50f5239e48d46e351e9319c5acd054d21b2abfc0a2d44dff53ab722", + "module": "scene_code", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:11Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:1c5820fa846b8a40b309d6c4270a0f81823bd2876fa9f5532a2c41700a6f276f", + "spec_path": "specs/t27f/scene_code.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_data.json b/.trinity/seals/t27f_scene_data.json new file mode 100644 index 0000000000..4105757277 --- /dev/null +++ b/.trinity/seals/t27f_scene_data.json @@ -0,0 +1,41 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/scene_bg.t27", + "spec_hash": "sha256:be66b1113d05d634b2bfb26a1b3d455e2a24b1fd5a4f98da28c9e821978be1cf" + }, + { + "spec": "specs/t27f/scene_lex.t27", + "spec_hash": "sha256:a531db018ba0776b1d044fe39b323ba9f8c739fc60b06b57f6bab538022585c9" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:a325d308d3fa644afc3a8c32572ee2c57454dd158b2f71334f15b019bccaa32b", + "gen_hash_rust": "sha256:f42475cad05b3992ff14f56c244a62e93569d442c823601e545b321b4592ce8b", + "gen_hash_verilog": "sha256:33650fcf0ac1cfc0c774466282467f6e6eb4c1ed0a39cf3cf5d671faac9fbf27", + "gen_hash_zig": "sha256:0071b9914207b81c1803d45cf4bce149535b8229ca2f7bf2f4fdf07a9323e8a1", + "module": "scene_data", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:19Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:7187e6822bce5126c69b0bfaa9b3fb7a48aa8dc29fe60f5367d4220144966534", + "spec_path": "specs/t27f/scene_data.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_graph.json b/.trinity/seals/t27f_scene_graph.json new file mode 100644 index 0000000000..f5ff22c638 --- /dev/null +++ b/.trinity/seals/t27f_scene_graph.json @@ -0,0 +1,41 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/scene_bg.t27", + "spec_hash": "sha256:be66b1113d05d634b2bfb26a1b3d455e2a24b1fd5a4f98da28c9e821978be1cf" + }, + { + "spec": "specs/t27f/scene_lex.t27", + "spec_hash": "sha256:a531db018ba0776b1d044fe39b323ba9f8c739fc60b06b57f6bab538022585c9" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:970e740bc66feb58d25d6a6c00dbbb68f04591d579ff9653927ebe360fc5ddc5", + "gen_hash_rust": "sha256:df9e11ebaa9016d09dd8de06397d531fe0825509137183537e3afe922115ee4c", + "gen_hash_verilog": "sha256:5cae54c058c0dfc82c481516fee95ff46f84c92adef1c254ea3fbbeae244a948", + "gen_hash_zig": "sha256:19b0aec74782b50d6ea48603e4ce51e3317f44a5ae4cbcde63cb48ffd9965767", + "module": "scene_graph", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:12Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:ae37d6259842a3b6854548adff272260a35a8c7652c509f952ed1908b94d5706", + "spec_path": "specs/t27f/scene_graph.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_guard.json b/.trinity/seals/t27f_scene_guard.json new file mode 100644 index 0000000000..157e665a7b --- /dev/null +++ b/.trinity/seals/t27f_scene_guard.json @@ -0,0 +1,37 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/scene_bg.t27", + "spec_hash": "sha256:be66b1113d05d634b2bfb26a1b3d455e2a24b1fd5a4f98da28c9e821978be1cf" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:0b3c796b603a3031e2ff795e0f8286a608ab8f8247467a83dc6c680fc1aae5e4", + "gen_hash_rust": "sha256:d365039ff9ee9cf54ad434a7a2887b060cfcc34e9f45bee8efb49a9e95c3451e", + "gen_hash_verilog": "sha256:a6993136fc4f05166ed8686a2a700bc9744aaecee82a402ee024ea160102db0a", + "gen_hash_zig": "sha256:6c33b8b7421ed74d49a297149d9a890719cd1df230415dc42ee8a2bf66ed1eca", + "module": "scene_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:22Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:e11d00191157bb94a7172203748d0e9ff6accff74fac2b9c4fa9703128f1db81", + "spec_path": "specs/t27f/scene_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 5, + "total": 5, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_lex.json b/.trinity/seals/t27f_scene_lex.json new file mode 100644 index 0000000000..116ab0a03b --- /dev/null +++ b/.trinity/seals/t27f_scene_lex.json @@ -0,0 +1,41 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/reel_factory.t27", + "spec_hash": "sha256:96099d764ba86fa3ef2c29d81c3838ce255bfd3b3abbd37b27cb86e4bca91012" + }, + { + "spec": "specs/t27f/reel_topics.t27", + "spec_hash": "sha256:7c1762cde96524791711594d23f880fbad1f497a499277710f1fd61d52d50157" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:6f713cff16f2fc94084f3f1788b3494ba884aff86c39b980b29b2e2744b53ffe", + "gen_hash_rust": "sha256:be4e95d181dcdc64a882675c549a8db971e0ed3c5ea63cf215fbc524e37380eb", + "gen_hash_verilog": "sha256:46f7ab4aa31ed2514ab47f637705e8a407a6693481156da2f21024a106035a75", + "gen_hash_zig": "sha256:a897632d32bb625e3ff691ed434c74b499d1bd1347b8d4b02641f5aa2481e732", + "module": "scene_lex", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:08Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:a531db018ba0776b1d044fe39b323ba9f8c739fc60b06b57f6bab538022585c9", + "spec_path": "specs/t27f/scene_lex.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 3, + "total": 3, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_links.json b/.trinity/seals/t27f_scene_links.json new file mode 100644 index 0000000000..2546749831 --- /dev/null +++ b/.trinity/seals/t27f_scene_links.json @@ -0,0 +1,41 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/scene_bg.t27", + "spec_hash": "sha256:be66b1113d05d634b2bfb26a1b3d455e2a24b1fd5a4f98da28c9e821978be1cf" + }, + { + "spec": "specs/t27f/scene_lex.t27", + "spec_hash": "sha256:a531db018ba0776b1d044fe39b323ba9f8c739fc60b06b57f6bab538022585c9" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:a5b1ca5ee2ce803353dca6918927d7739de73cf9539692e6122840751be48b76", + "gen_hash_rust": "sha256:df6624e54fab79d94c23aaada294f4e624c278606d93d48a00a8e455bf0650c2", + "gen_hash_verilog": "sha256:16a4aaa084d781eeae0582e01ed8a5e6dc25245699dad0018f4425ce461a9370", + "gen_hash_zig": "sha256:5a112f82f3a2699019fd6c0144c911218575e3e044d0ea55267da4b2e60c26fa", + "module": "scene_links", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:21Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:d234cb4a2d21e841a8474aa66a70336f181ff3013ecbe6f7118d09f38cedc3a4", + "spec_path": "specs/t27f/scene_links.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_loop.json b/.trinity/seals/t27f_scene_loop.json new file mode 100644 index 0000000000..3878b20157 --- /dev/null +++ b/.trinity/seals/t27f_scene_loop.json @@ -0,0 +1,41 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/scene_bg.t27", + "spec_hash": "sha256:be66b1113d05d634b2bfb26a1b3d455e2a24b1fd5a4f98da28c9e821978be1cf" + }, + { + "spec": "specs/t27f/scene_lex.t27", + "spec_hash": "sha256:a531db018ba0776b1d044fe39b323ba9f8c739fc60b06b57f6bab538022585c9" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:99b2f025b6a34b19c382f0d67f905f8ba0fb08b950cd97c77661b2fe36c9aa0d", + "gen_hash_rust": "sha256:29d7bd9c5374e271d831a1c4c3688b6383ba46496a7723bb87260a798015155b", + "gen_hash_verilog": "sha256:7987bd702aab49c102c8df26d79d19b13d41d2116c95e0ad9e5a2c4d5248beab", + "gen_hash_zig": "sha256:a76da017349e3953c89dc7c527ef53d78e76aa0298e72170f8c31ec63874e377", + "module": "scene_loop", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:16Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:92aab2767f95cbed855e32554255b74ba744f94bc0b7a1df2d8b2593beef89c8", + "spec_path": "specs/t27f/scene_loop.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_map.json b/.trinity/seals/t27f_scene_map.json new file mode 100644 index 0000000000..412266511b --- /dev/null +++ b/.trinity/seals/t27f_scene_map.json @@ -0,0 +1,37 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/scene_bg.t27", + "spec_hash": "sha256:be66b1113d05d634b2bfb26a1b3d455e2a24b1fd5a4f98da28c9e821978be1cf" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:d61115a7ade0e5d5e58ebf650a8bbe949200196ccbc9587cfc66b5ccdf6dd843", + "gen_hash_rust": "sha256:be73f1b88bd658f2f5e86dc093b349c1ccf895451cf38901c478c4a456bf5562", + "gen_hash_verilog": "sha256:171c6c2a9c7872a78d3c1497b6d6be2cc02c18c9a22a07ebcfa37198d2ae9b56", + "gen_hash_zig": "sha256:605ff9cea1a7df198f36fdb3172dafa70a3621a69bf4b9d40b2d6c1109cc635e", + "module": "scene_map", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:26Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:1c1239443601b49386b1ed81b4372fc66161dc8280dcc31cdc75e27667aec698", + "spec_path": "specs/t27f/scene_map.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_raster.json b/.trinity/seals/t27f_scene_raster.json new file mode 100644 index 0000000000..6c9e809bd9 --- /dev/null +++ b/.trinity/seals/t27f_scene_raster.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:368f171bbb4fd19a793a9d79b4f9dd5fd6a7802e8d2fd0691169adc8ef82ede5", + "gen_hash_rust": "sha256:4a1ec9110c4e1dab7e65810391d2bc7ab95eece804707b26e54f6ae6e0ef83cf", + "gen_hash_verilog": "sha256:2dd451aecebe4cb25dce4593b55762795e1512eb3824ca7b6dd7dfe10d491330", + "gen_hash_zig": "sha256:69a84b97ad599d29b4bb2a69e814816eec24657d00e9e09f3d8b9ccb766900f1", + "module": "scene_raster", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:06Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6", + "spec_path": "specs/t27f/scene_raster.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 6, + "total": 6, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_still.json b/.trinity/seals/t27f_scene_still.json new file mode 100644 index 0000000000..eb3217b7d2 --- /dev/null +++ b/.trinity/seals/t27f_scene_still.json @@ -0,0 +1,81 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/scene_branch.t27", + "spec_hash": "sha256:c4b584b8b5a4f5cc849dc9c1a4323609702adcdb710cb97b6441059210eccc15" + }, + { + "spec": "specs/t27f/scene_code.t27", + "spec_hash": "sha256:1c5820fa846b8a40b309d6c4270a0f81823bd2876fa9f5532a2c41700a6f276f" + }, + { + "spec": "specs/t27f/scene_data.t27", + "spec_hash": "sha256:7187e6822bce5126c69b0bfaa9b3fb7a48aa8dc29fe60f5367d4220144966534" + }, + { + "spec": "specs/t27f/scene_graph.t27", + "spec_hash": "sha256:ae37d6259842a3b6854548adff272260a35a8c7652c509f952ed1908b94d5706" + }, + { + "spec": "specs/t27f/scene_guard.t27", + "spec_hash": "sha256:e11d00191157bb94a7172203748d0e9ff6accff74fac2b9c4fa9703128f1db81" + }, + { + "spec": "specs/t27f/scene_links.t27", + "spec_hash": "sha256:d234cb4a2d21e841a8474aa66a70336f181ff3013ecbe6f7118d09f38cedc3a4" + }, + { + "spec": "specs/t27f/scene_loop.t27", + "spec_hash": "sha256:92aab2767f95cbed855e32554255b74ba744f94bc0b7a1df2d8b2593beef89c8" + }, + { + "spec": "specs/t27f/scene_map.t27", + "spec_hash": "sha256:1c1239443601b49386b1ed81b4372fc66161dc8280dcc31cdc75e27667aec698" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + }, + { + "spec": "specs/t27f/scene_task.t27", + "spec_hash": "sha256:c785e4592d1b2438444de22c0a5828dfa66b63680315806e3a7a6f291c6737a7" + }, + { + "spec": "specs/t27f/scene_tests.t27", + "spec_hash": "sha256:4959ad64ac6816bc90ee71add5ccdd86750187f35edf3637d2a8de3747e464aa" + }, + { + "spec": "specs/t27f/scene_title.t27", + "spec_hash": "sha256:aa0364b409f322a41a9a9f043ab4dbd571dc6a7ba66e12e035146a0bc1c0d3ac" + }, + { + "spec": "specs/t27f/scene_trit.t27", + "spec_hash": "sha256:20c36623fd7582aa8a78b29b96e771cb5c4c06313c9f71884da52656f1df288a" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:1ad642a9965b62c599080af0497003423ca06acfa2a243f171eb9900939c637c", + "gen_hash_rust": "sha256:6d5416bfa6a52c004ae79464082c9d4fad5d6917f4a2a1f1a86599eea77b0a8c", + "gen_hash_verilog": "sha256:bbbf971bf846e160f9150a2a0a0f1a8b0ea2b3d51a94ee0c2f9f6efb837b850f", + "gen_hash_zig": "sha256:ef66a2371f51b9752916d3ccc3ea7e9f6899011d392089bee2ba64b2b5944bc3", + "module": "scene_still", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:28Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:c84d109c67bdf044a19ab106fbb1e6a714ef8dc1b1ef8af4808d6dbf63da5bfd", + "spec_path": "specs/t27f/scene_still.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_task.json b/.trinity/seals/t27f_scene_task.json new file mode 100644 index 0000000000..b17a8127f3 --- /dev/null +++ b/.trinity/seals/t27f_scene_task.json @@ -0,0 +1,41 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/reel_topics.t27", + "spec_hash": "sha256:7c1762cde96524791711594d23f880fbad1f497a499277710f1fd61d52d50157" + }, + { + "spec": "specs/t27f/scene_bg.t27", + "spec_hash": "sha256:be66b1113d05d634b2bfb26a1b3d455e2a24b1fd5a4f98da28c9e821978be1cf" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:89ee693e2825324221262123a400370ddd768311e9783407f77ab1bd60509884", + "gen_hash_rust": "sha256:2a8e978599ed41a082156b220faec137d0a866ff2bb30bb7b9ac63ec9e4df795", + "gen_hash_verilog": "sha256:81aacc502e86e628c95798e3f4da86f68c227e65ac2918db3fc1590f77a1762b", + "gen_hash_zig": "sha256:2caedbeccad08b6d5a5d25a1772069f29db84cb71b6c143f3ae59b200189676c", + "module": "scene_task", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:25Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:c785e4592d1b2438444de22c0a5828dfa66b63680315806e3a7a6f291c6737a7", + "spec_path": "specs/t27f/scene_task.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_tests.json b/.trinity/seals/t27f_scene_tests.json new file mode 100644 index 0000000000..611b21ba6b --- /dev/null +++ b/.trinity/seals/t27f_scene_tests.json @@ -0,0 +1,41 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/scene_bg.t27", + "spec_hash": "sha256:be66b1113d05d634b2bfb26a1b3d455e2a24b1fd5a4f98da28c9e821978be1cf" + }, + { + "spec": "specs/t27f/scene_lex.t27", + "spec_hash": "sha256:a531db018ba0776b1d044fe39b323ba9f8c739fc60b06b57f6bab538022585c9" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:101ae841df93b9d698c8fb6881b1a832f5e2609ad7eebbc806d508d2c51aec41", + "gen_hash_rust": "sha256:a1172f6ff538381383c5b9546c90974bd821d744d5fb693d92396f69e20296e6", + "gen_hash_verilog": "sha256:fe4dbba2fa1a548dc10f8f5caffb294e85db0579df5b3a4e68f97704997fa771", + "gen_hash_zig": "sha256:fd133cefd6a83aca05057c4dc82f6dc438418ce044f947af8c569f2ed90fc0c6", + "module": "scene_tests", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:13Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:4959ad64ac6816bc90ee71add5ccdd86750187f35edf3637d2a8de3747e464aa", + "spec_path": "specs/t27f/scene_tests.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 3, + "total": 3, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_title.json b/.trinity/seals/t27f_scene_title.json new file mode 100644 index 0000000000..bc88ae6e39 --- /dev/null +++ b/.trinity/seals/t27f_scene_title.json @@ -0,0 +1,41 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/reel_topics.t27", + "spec_hash": "sha256:7c1762cde96524791711594d23f880fbad1f497a499277710f1fd61d52d50157" + }, + { + "spec": "specs/t27f/scene_bg.t27", + "spec_hash": "sha256:be66b1113d05d634b2bfb26a1b3d455e2a24b1fd5a4f98da28c9e821978be1cf" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:a170056cb79fcc50307006dd225d7fe12bb72083dd8ecedd6df9b7ec2516c9ea", + "gen_hash_rust": "sha256:08c40c2ea5a39cfaeab83bf2ed9cde165760d06425f020091038ef9004394b37", + "gen_hash_verilog": "sha256:e689f0dfe6644e57a3a000319eecadbc4f2706065023f2f239966a94a54d830b", + "gen_hash_zig": "sha256:fc860f5ec236cd94f8ccc98a696fca1fb2a663d145f0425c3310a4bafc89d852", + "module": "scene_title", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:24Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:aa0364b409f322a41a9a9f043ab4dbd571dc6a7ba66e12e035146a0bc1c0d3ac", + "spec_path": "specs/t27f/scene_title.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_trit.json b/.trinity/seals/t27f_scene_trit.json new file mode 100644 index 0000000000..eaf5b5fec5 --- /dev/null +++ b/.trinity/seals/t27f_scene_trit.json @@ -0,0 +1,41 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/scene_bg.t27", + "spec_hash": "sha256:be66b1113d05d634b2bfb26a1b3d455e2a24b1fd5a4f98da28c9e821978be1cf" + }, + { + "spec": "specs/t27f/scene_lex.t27", + "spec_hash": "sha256:a531db018ba0776b1d044fe39b323ba9f8c739fc60b06b57f6bab538022585c9" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:fc1fc84cc15fbfa8af91e80fd60392ea1ffa8c424a190ded3accac91af52c70e", + "gen_hash_rust": "sha256:d4763a6a0aa3d28cd60e70bdbc9924e0b2dac0728b28e852037c07dbea531088", + "gen_hash_verilog": "sha256:6eb43dd9bc6ce9f3e60af19195867f9422a8a9a15f7f5c7a0c5dec923fc65771", + "gen_hash_zig": "sha256:41c7b925e41dd4f028473066afdcc980994476f4c231a39b8a16e229af0f67db", + "module": "scene_trit", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:15Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:20c36623fd7582aa8a78b29b96e771cb5c4c06313c9f71884da52656f1df288a", + "spec_path": "specs/t27f/scene_trit.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_still_gate.json b/.trinity/seals/t27f_still_gate.json new file mode 100644 index 0000000000..c8de730b5f --- /dev/null +++ b/.trinity/seals/t27f_still_gate.json @@ -0,0 +1,41 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/reel_factory.t27", + "spec_hash": "sha256:96099d764ba86fa3ef2c29d81c3838ce255bfd3b3abbd37b27cb86e4bca91012" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + }, + { + "spec": "specs/t27f/scene_still.t27", + "spec_hash": "sha256:c84d109c67bdf044a19ab106fbb1e6a714ef8dc1b1ef8af4808d6dbf63da5bfd" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:5f483f205bbc3e616336801d412c79d2bd30dcf93faab6f0345f4bf58795daa4", + "gen_hash_rust": "sha256:a054fa49e4106208a7d3fce480f1a1c061553eb35823fdcc3348071cd436d2bf", + "gen_hash_verilog": "sha256:44eed75d6a59908d4b5f9ee4d766b1d41f90b6daf373d53f2f47c9f28c840dcf", + "gen_hash_zig": "sha256:eebe29ed19d58887a567b7976967279512392f8932136842288cce55a3ecf967", + "module": "still_gate", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:31Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:ef0632aac687fc0b377829b79a64de221f9dbe54ea568820dea92c706718325f", + "spec_path": "specs/t27f/still_gate.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 1065f0721c..d9708e4e55 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -1459,12 +1459,29 @@ {"path": "specs/t27f/reel_particles_mux.t27", "t27b": "pass"}, {"path": "specs/t27f/reel_t27_basics.t27", "t27b": "pass"}, {"path": "specs/t27f/reel_topics.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_bg.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_branch.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_code.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_data.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_graph.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_guard.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_lex.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_links.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_loop.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_map.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_raster.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_still.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_task.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_tests.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_title.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_trit.t27", "t27b": "pass"}, {"path": "specs/t27f/spec_graph.t27", "t27b": "pass"}, {"path": "specs/t27f/spec_graph_derive.t27", "t27b": "pass"}, {"path": "specs/t27f/spec_graph_export.t27", "t27b": "pass"}, {"path": "specs/t27f/spec_graph_fold.t27", "t27b": "pass"}, {"path": "specs/t27f/spec_graph_outline.t27", "t27b": "pass"}, {"path": "specs/t27f/spec_graph_view.t27", "t27b": "pass"}, + {"path": "specs/t27f/still_gate.t27", "t27b": "pass"}, {"path": "specs/t27f/token_floor.t27", "t27b": "blocked", "blocker": "type [N]T", "reason": "unimplemented"}, {"path": "specs/t27f/token_floor_mux.t27", "t27b": "blocked", "blocker": "ExprCall(@sin)", "reason": "unimplemented"}, {"path": "specs/t27f/token_floor_scenes.t27", "t27b": "blocked", "blocker": "ExprCall(@sin)", "reason": "unimplemented"},