From 6d060fdfb23bdf3a7674d264f82362de725e76a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 14:25:20 +0700 Subject: [PATCH 01/33] widgets: catalogue all 82 widget folders and the factory law -- 28 tools carry 22833 hand lines, 2775 of them pure logic (Closes #10938) specs/widgets/catalog.t27 pins the census measured at trinity origin/main. specs/t27f/widget_factory.t27 states six stages, the t27c-seal build key and the port gate. specs/t27f/frontend.t27 registers SF_WIDGET_FACTORY. Epic #10937. Co-Authored-By: Claude Sonnet 5.5 --- specs/t27f/frontend.t27 | 25 +++- specs/t27f/widget_factory.t27 | 223 +++++++++++++++++++++++++++++++++ specs/widgets/catalog.t27 | 227 ++++++++++++++++++++++++++++++++++ 3 files changed, 470 insertions(+), 5 deletions(-) create mode 100644 specs/t27f/widget_factory.t27 create mode 100644 specs/widgets/catalog.t27 diff --git a/specs/t27f/frontend.t27 b/specs/t27f/frontend.t27 index 40fb2aa79c..d744a1ab65 100644 --- a/specs/t27f/frontend.t27 +++ b/specs/t27f/frontend.t27 @@ -55,10 +55,11 @@ pub const SF_PARTICLE_LIVE : u32 = 10; // T27F Live, particle_field.t27 + partic pub const SF_UI_PLAN : u32 = 11; // trusted-widget UI plan, ui_plan.t27 pub const SF_DASHBOARD : u32 = 12; // corpus and session health, t27mcp/dashboard.t27, tool t27_dashboard pub const SF_PETAL_MENU : u32 = 13; // T27 OS home: the 27-petal triangle menu, petal_menu.t27 -pub const SF_COUNT : u32 = 14; +pub const SF_WIDGET_FACTORY : u32 = 14; // widgets as specs: logic, plan, page, data, card -- widget_factory.t27, catalogue widgets/catalog.t27 +pub const SF_COUNT : u32 = 15; pub const SF_NONE : u32 = 255; -pub const SURFACE_NAMES : str = "graph,outline,map_still,graph_page,hot,check,preview,particle_reel,still,map_video,particle_live,ui_plan,dashboard,petal_menu"; +pub const SURFACE_NAMES : str = "graph,outline,map_still,graph_page,hot,check,preview,particle_reel,still,map_video,particle_live,ui_plan,dashboard,petal_menu,widget_factory"; /// What draws a surface today. pub const ST_LIVE : u8 = 0; @@ -67,7 +68,7 @@ pub const ST_PARTIAL : u8 = 2; pub const ST_BLOCKED : u8 = 3; pub const ST_SPEC : u8 = 4; -pub const SURFACE_STATUS : [14]u8 = [0, 0, 0, 0, 2, 3, 4, 1, 1, 4, 4, 4, 0, 4]; +pub const SURFACE_STATUS : [15]u8 = [0, 0, 0, 0, 2, 3, 4, 1, 1, 4, 4, 4, 0, 4, 4]; /// Where a surface is made: an MCP tool, a t27.ai page, the Railway lab, the TRI CLAW App, nowhere. pub const MK_MCP : u8 = 0; @@ -76,7 +77,7 @@ pub const MK_LAB : u8 = 2; pub const MK_APP : u8 = 3; pub const MK_NONE : u8 = 4; -pub const SURFACE_MAKER : [14]u8 = [0, 0, 0, 1, 0, 3, 3, 2, 2, 4, 4, 4, 0, 4]; +pub const SURFACE_MAKER : [15]u8 = [0, 0, 0, 1, 0, 3, 3, 2, 2, 4, 4, 4, 0, 4, 4]; /// What a caller wants shown. pub const I_SPEC_SHAPE : u32 = 0; // the structure of one spec @@ -90,7 +91,8 @@ pub const I_HEALTH : u32 = 7; // the state of the corpus and the session pub const I_LIVE_AGENT : u32 = 8; // an agent's work as a live particle field pub const I_APP : u32 = 9; // an application screen built from trusted widgets pub const I_OS_HOME : u32 = 10; // the T27 OS home screen: 27 petals, a petal opens its menu -pub const I_COUNT : u32 = 11; +pub const I_WIDGET : u32 = 11; // a shareable widget made from a spec, and the census of the ones that exist +pub const I_COUNT : u32 = 12; /// The drawing tools TRI CLAW's chat agent carries. service.t27 gives its coder persona CODER_TOOLS /// for writing a spec; these are for showing one. @@ -179,6 +181,9 @@ pub fn route(intent: u32, host: u8) -> u32 { if (intent == I_OS_HOME) { return SF_PETAL_MENU; } + if (intent == I_WIDGET) { + return SF_WIDGET_FACTORY; + } return SF_NONE; } @@ -322,3 +327,13 @@ test tri_claw_carries_every_drawing_tool { assert claw_visual_tool(CLAW_VISUAL_COUNT) == TOOL_COUNT; assert TOOLS[claw_visual_tool(0)].name == "t27f_graph"; } + + +test the_widget_factory_is_a_spec_and_nothing_makes_it_yet { + assert route(I_WIDGET, HOST_CLAUDE_CODE_DESKTOP) == SF_WIDGET_FACTORY; + assert route(I_WIDGET, HOST_SITE) == SF_WIDGET_FACTORY; + assert SURFACE_STATUS[SF_WIDGET_FACTORY] == ST_SPEC; + assert pick(I_WIDGET, HOST_CLAUDE_CODE_DESKTOP) == SF_NONE; + assert claim_ok(SF_WIDGET_FACTORY, true) == false; + assert surface_tool(SF_WIDGET_FACTORY) == TOOL_COUNT; +} diff --git a/specs/t27f/widget_factory.t27 b/specs/t27f/widget_factory.t27 new file mode 100644 index 0000000000..f757c21c6f --- /dev/null +++ b/specs/t27f/widget_factory.t27 @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/t27f/widget_factory.t27 -- the widget factory: how a widget goes from a spec to a page, its +; numbers and its card with no hand-written line in between, and the gate that lets a hand-written +; widget be retired. Owner, 2026-10-19: catalogue every widget, rewrite the hand-written ones in +; .t27, make working with them easy, put the experience into an epic for tri CLI and TRI CLAW, and +; build the factory natively in t27f, t27b and t27c seal. The census is specs/widgets/catalog.t27: +; 28 tools, 22833 hand-written lines, of which 2775 are pure logic that ports as it stands. +; -- +; The factory in six stages. Each is a function of the stage before and nothing else: +; spec the widget's .t27 (words, rules, tests, data sources). The hand column of the catalog is +; everything that is NOT this stage. +; logic the pure functions of the tool (parse, grade, layout), written in .t27 and generated to +; JavaScript by gen-ts. The spec's own test blocks are the fixed answers; a data builder no +; longer re-imports a hand file "so the page cannot drift from the spec". +; plan what is drawn, as a ui_plan.t27 plan: widget ids from an allowlist, no markup, no script. +; The primitives below extend ui_plan's catalog; they are not a second catalog. +; page one trusted renderer draws a plan. It replaces 28 tool.js files with one program. +; data the tool's data builder, a spec compiled by t27b to a native binary (t27b is the +; native path; it is the reason a 696-line test-touch.mjs does not need Node). +; card the share picture, drawn by t27f as a still (SF_STILL) from the same spec, not by a +; Python script beside it. +; -- +; THE SEAL IS THE ACCELERATOR. t27c seal gives every spec a hash of its parse. A built artefact +; records the seal of the spec it was built from. needs_build() is false when the seals match, so a +; one-spec edit rebuilds one widget of 82 and skips the other 81. The renderer's own version is part +; of the key: a renderer change rebuilds every page, never every card. +; -- +; THE PORT GATE. A widget moves up one stage at a time, and only when (a) the new stage's output is +; byte-identical to the hand-written output it replaces, (b) the spec's tests pass, and (c) the +; hand-written lines do not grow. (c) is the same rule as t27b's "net <= 0" and FROZEN_HASH ports. +; -- +; Not claimed: no stage past `spec` exists as a program today. gen-ts has not been run on a tool's +; logic; no plan is drawn by a renderer; t27b has not compiled a data builder. The primitives are +; PROPOSED from reading the 28 tools by name, not measured; wave 1 (catalog.t27, 11 tools of at +; most 700 hand lines) measures them and may rename or merge them. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module t27f_widget_factory; + +use widgets::catalog::wave_of; +use widgets::catalog::PORT_HAND; +use widgets::catalog::PORT_WORDS; +use widgets::catalog::PORT_LOGIC; +use widgets::catalog::PORT_DRAW; +use widgets::catalog::PORT_SEALED; +use widgets::catalog::PORT_STAGES; +use t27f::ui_plan::widget_id; +use t27f::ui_plan::WIDGET_UNKNOWN; +use t27mcp::service::home_of; +use t27mcp::service::has_prefix; +use t27mcp::service::H_T27; + +/// The stages, in order. A stage reads only the one before it. +pub const ST_SPEC : u8 = 0; +pub const ST_LOGIC : u8 = 1; +pub const ST_PLAN : u8 = 2; +pub const ST_PAGE : u8 = 3; +pub const ST_DATA : u8 = 4; +pub const ST_CARD : u8 = 5; +pub const ST_COUNT : u8 = 6; +pub const STAGE_NAMES : [6]str = ["spec", "logic", "plan", "page", "data", "card"]; + +/// The tool of the toolchain that makes each stage. t27c is the compiler and the seal; gen-ts +/// writes the logic; t27b compiles the data builder native; t27f draws plan, page and card. +pub const MAKER_T27C : u8 = 0; +pub const MAKER_GEN_TS : u8 = 1; +pub const MAKER_T27F : u8 = 2; +pub const MAKER_T27B : u8 = 3; +pub const STAGE_MAKER : [6]u8 = [0, 1, 2, 2, 3, 2]; + +/// Draw primitives the factory adds to ui_plan's catalog (which already has metrics_table and +/// text_block). PROPOSED, see the header. None of them may be a forbidden name. +pub const PRIMITIVE_COUNT : u32 = 8; +pub const PRIMITIVES : [8]str = ["bars", "treemap", "timeline", "grid_map", "graph", "waves", "calc", "tabs"]; +pub const FORBIDDEN : [6]str = ["script", "eval", "html", "iframe", "raw_js", "function"]; + +/// The verbs tri CLI and the tools TRI CLAW carries. `tri widget` exists today (trinity, the +/// widget-forge skill); these are what it gains. Every tool is a t27f_ name so that service.t27 +/// mounts it into TRI CLAW's /mcp with the other t27 tools. +pub const TRI_VERB_COUNT : u32 = 5; +pub const TRI_VERBS : [5]str = ["widget list", "widget port", "widget make", "widget seal", "widget gate"]; +pub const CLAW_TOOL_COUNT : u32 = 3; +pub const CLAW_TOOLS : [3]str = ["t27f_widgets", "t27f_widget_make", "t27f_widget_port_plan"]; + +/// A stage is the next of a port stage only by one step. +pub fn next_port(stage: u8) -> u8 { + if (stage + 1 >= PORT_STAGES) { + return PORT_STAGES; + } + return stage + 1; +} + +/// The port gate. before and after are the widget's hand-written lines at the two ends of the move. +pub fn port_gate(from: u8, to: u8, byte_identical: bool, tests_pass: bool, hand_before: u32, hand_after: u32) -> bool { + if (to >= PORT_STAGES) { + return false; + } + if (to != next_port(from)) { + return false; + } + if (byte_identical == false) { + return false; + } + if (tests_pass == false) { + return false; + } + return hand_after <= hand_before; +} + +/// The key a built artefact is filed under: the spec's seal and the renderer version it was drawn by. +pub fn build_key(spec_seal: u32, renderer: u32, uses_renderer: bool) -> u32 { + if (uses_renderer) { + return spec_seal * 31 + renderer; + } + return spec_seal * 31; +} + +/// Build only when the key of what exists is not the key of what is wanted. +pub fn needs_build(want: u32, have: u32) -> bool { + return want != have; +} + +/// Of n widgets, how many are skipped when `changed` of them have a new key. +pub fn skipped(n: u32, changed: u32) -> u32 { + if (changed >= n) { + return 0; + } + return n - changed; +} + +/// A primitive name is allowed when it is not a forbidden word. +pub fn primitive_ok(name: str) -> bool { + var f : u32 = 0; + while (f < 6) { + if (FORBIDDEN[f as usize] == name) { return false; } + f = f + 1; + } + return true; +} + +/// How many hand-written lines a wave-1 tool of `hand` lines may keep after its logic is ported: +/// none of the logic file, since gen-ts writes it. A pure bookkeeping rule the epic's issues quote. +pub fn after_logic(hand: u32, logic: u32) -> u32 { + if (logic >= hand) { + return 0; + } + return hand - logic; +} + +test the_stages_are_six_in_order_with_a_maker_each { + assert ST_COUNT == 6; + assert STAGE_NAMES[ST_SPEC] == "spec"; + assert STAGE_NAMES[ST_CARD] == "card"; + assert STAGE_MAKER[ST_LOGIC] == MAKER_GEN_TS; + assert STAGE_MAKER[ST_DATA] == MAKER_T27B; + assert STAGE_MAKER[ST_SPEC] == MAKER_T27C; + assert STAGE_MAKER[ST_PAGE] == MAKER_T27F; + assert STAGE_MAKER[ST_CARD] == MAKER_T27F; +} + +test a_port_moves_one_stage_at_a_time { + assert next_port(PORT_WORDS) == PORT_LOGIC; + assert next_port(PORT_LOGIC) == PORT_DRAW; + assert next_port(PORT_SEALED) == PORT_STAGES; + assert port_gate(PORT_WORDS, PORT_LOGIC, true, true, 1000, 800); + assert port_gate(PORT_WORDS, PORT_DRAW, true, true, 1000, 800) == false; + assert port_gate(PORT_DRAW, PORT_LOGIC, true, true, 1000, 800) == false; + assert port_gate(PORT_SEALED, PORT_STAGES, true, true, 1000, 0) == false; +} + +test a_port_needs_identical_output_green_tests_and_no_growth { + assert port_gate(PORT_HAND, PORT_WORDS, true, true, 500, 500); + assert port_gate(PORT_HAND, PORT_WORDS, false, true, 500, 400) == false; + assert port_gate(PORT_HAND, PORT_WORDS, true, false, 500, 400) == false; + assert port_gate(PORT_HAND, PORT_WORDS, true, true, 500, 501) == false; + assert port_gate(PORT_LOGIC, PORT_DRAW, true, true, 800, 0); +} + +test the_seal_skips_what_did_not_change { + const k1 : u32 = build_key(1000, 7, true); + assert needs_build(k1, k1) == false; + assert needs_build(build_key(1001, 7, true), k1); + assert needs_build(build_key(1000, 8, true), k1); + assert needs_build(build_key(1000, 8, false), build_key(1000, 7, false)) == false; + assert skipped(82, 1) == 81; + assert skipped(82, 0) == 82; + assert skipped(82, 82) == 0; + assert skipped(5, 9) == 0; +} + +test the_primitives_extend_ui_plan_and_hold_no_code { + var i : u32 = 0; + while (i < PRIMITIVE_COUNT) { + assert primitive_ok(PRIMITIVES[i as usize]); + assert widget_id(PRIMITIVES[i as usize]) == WIDGET_UNKNOWN; + i = i + 1; + } + assert widget_id("metrics_table") != WIDGET_UNKNOWN; + assert widget_id("text_block") != WIDGET_UNKNOWN; + assert widget_id("script") == WIDGET_UNKNOWN; + assert primitive_ok("script") == false; + assert primitive_ok("bars"); +} + +test every_claw_tool_is_a_t27_name_the_service_mounts { + var i : u32 = 0; + while (i < CLAW_TOOL_COUNT) { + assert has_prefix(CLAW_TOOLS[i as usize], "t27f_"); + assert home_of(CLAW_TOOLS[i as usize]) == H_T27; + i = i + 1; + } + assert TRI_VERB_COUNT == 5; + assert TRI_VERBS[0] == "widget list"; +} + +test the_logic_stage_removes_exactly_the_logic_file { + assert after_logic(1000, 51) == 949; + assert after_logic(686, 0) == 686; + assert after_logic(10, 20) == 0; + assert wave_of(after_logic(1432, 370)) == 3; + assert wave_of(after_logic(1000, 51)) == 2; + assert port_gate(PORT_WORDS, PORT_LOGIC, true, true, 1432, after_logic(1432, 370)); +} diff --git a/specs/widgets/catalog.t27 b/specs/widgets/catalog.t27 new file mode 100644 index 0000000000..35f526116f --- /dev/null +++ b/specs/widgets/catalog.t27 @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/catalog.t27 -- the census of every widget in every t27 project, and how much of each +; is still written by hand. Owner, 2026-10-19: find all widgets in all projects, catalogue them, +; rewrite the hand-written ones in .t27, make them easy to work with, move the lesson into an epic +; for tri CLI and TRI CLAW, and build a widget factory natively in t27f, t27b and t27c seal. +; This file is the catalogue. The factory that retires the hand-written column is +; specs/t27f/widget_factory.t27; the shipped gallery (what a reader may share) is +; specs/widgets/gallery.t27 in gHashTag/trinity and is NOT copied here -- this spec counts, it does +; not list titles. +; -- +; MEASURED 2026-10-19 at gHashTag/trinity origin/main, wc -l of every file, nothing estimated: +; apps/website/public/widgets// 82 widget folders, app.t27.ai/game/widgets previews them +; apps/website/specs/widgets/.t27 the words and numbers of each (SAY_ and K_ constants) +; apps/website/scripts/widget-data/ the data builders (.mjs) and the card renderers (.py) +; apps/queen/QueenUI/Widgets/ 105 SwiftUI views, 42052 lines +; apps/website/src/components/ 11 React *Widget*.tsx, 4902 lines +; Of the 82 folders, 54 are LESSONS (27 basics-*, 27 golden-*): a 3-line tool.js and a 47-48 line +; page, both generated from the spec. They are already native. The other 28 are TOOLS and carry +; hand-written JavaScript, CSS, a Node data builder and a Python card renderer. +; -- +; What the census found, which no one had written down: +; 1. A tool is TWO layers. 12 of the 28 have a pure logic file beside tool.js (rule.js, bitparse.js, +; fsmparse.js, lut.js, treemap.js, xdc.js, statparse.js, ...): 2775 lines of functions of +; numbers and text, no DOM. They port to .t27 and gen-ts as they stand, and the spec's test +; blocks replace the fixed answers the data builder re-checks today (gatle.mjs imports rule.js +; "so the page cannot drift from the spec" -- one source would make that true by construction). +; 2. The other 8920 lines are DOM drawing. Every tool.js opens with the same helpers (fill, el, +; link, a stylesheet link) and then builds tables, bars, maps and timelines. That is a renderer, +; not 28 programs: ui_plan.t27 already decides that a plan is data and a trusted renderer draws +; it. The factory extends that catalog instead of adding a second one. +; 3. 23 tools have a Node data builder (5572 lines) and 24 a Python card renderer (3415 lines). +; The card is a picture of the page; t27f draws pictures from specs already (SF_STILL). +; -- +; Not claimed: that any hand column has been rewritten. Every tool is at PORT_WORDS today: its +; words live in its spec, its logic and drawing still live in JavaScript. A tool moves up only when +; its output is byte-identical to the hand version, and the factory spec states that gate. +; The Swift and React widgets are counted, not classified: they are another renderer's targets. +; ASCII only (L3), English only (LANG-EN). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widgets_catalog; + +pub const KIND : str = "widgets-catalog"; +pub const ID : str = "widgets/catalog"; +pub const MEASURED : str = "2026-10-19"; +pub const MEASURED_AT : str = "gHashTag/trinity origin/main"; + +; --- What the gallery serves, by kind (gallery.t27: WIDGET_COUNT) ------------------------------- +pub const GALLERY_CASTS : u32 = 104; +pub const GALLERY_PLAYERS : u32 = 8; +pub const GALLERY_TABS : u32 = 1; +pub const GALLERY_TOOLS : u32 = 82; +pub const GALLERY_COUNT : u32 = 195; + +; --- The 82 tool folders ---------------------------------------------------------------------- +pub const LESSON_BASICS : u32 = 27; +pub const LESSON_GOLDEN : u32 = 27; +pub const TOOL_COUNT : u32 = 28; + +; --- Other renderers' widgets, counted not classified ------------------------------------------ +pub const SWIFT_FILES : u32 = 105; +pub const SWIFT_LINES : u32 = 42052; +pub const REACT_FILES : u32 = 11; +pub const REACT_LINES : u32 = 4902; + +; --- The 28 tools, in alphabetical order; every array below is TOOL_COUNT long ----------------- +pub const TOOL_IDS : [28]str = ["bit-autopsy", "bit-diff", "build-receipt", "cost-curves", "fasm-skyline", "fits-on", "fsm-sketch", "gatle", "gf16-calc", "idcode", "jtag-verdict", "lut-depth", "lut-explain", "lut-treemap", "pin-map", "placement-map", "race-the-bee", "resource-bars", "slack-waterfall", "stat-card", "synth-smells", "t27-cell", "t27-vs-nvfp4", "t27c-stages", "test-touch", "test-waves", "uart-bucket", "vcd-wrapped"]; + +; Hand-written lines per tool. LOGIC is the pure parser/rule file beside tool.js (0 when the tool +; has none), DRAW is tool.js. CSS is tool.css. DATA is scripts/widget-data/.mjs, CARD is +; scripts/widget-data/-card.py (0 when the tool has neither). SPEC is specs/widgets/.t27. +pub const LOGIC_LINES : [28]u32 = [289, 371, 0, 0, 122, 0, 379, 51, 0, 0, 0, 97, 370, 289, 207, 0, 0, 0, 0, 127, 192, 0, 0, 0, 0, 0, 0, 281]; +pub const DRAW_LINES : [28]u32 = [376, 274, 221, 273, 533, 232, 361, 309, 511, 316, 171, 241, 588, 322, 340, 379, 105, 172, 369, 318, 304, 187, 192, 364, 146, 726, 228, 362]; +pub const CSS_LINES : [28]u32 = [57, 50, 127, 65, 43, 51, 56, 98, 114, 96, 61, 89, 128, 85, 151, 32, 51, 33, 42, 48, 76, 78, 78, 169, 89, 34, 72, 78]; +pub const DATA_LINES : [28]u32 = [184, 124, 329, 395, 266, 209, 294, 411, 0, 153, 0, 187, 242, 252, 187, 0, 208, 0, 0, 163, 203, 144, 221, 109, 696, 144, 177, 274]; +pub const CARD_LINES : [28]u32 = [103, 72, 201, 131, 153, 126, 194, 131, 0, 121, 117, 150, 104, 252, 206, 0, 89, 0, 0, 147, 148, 85, 101, 163, 133, 189, 129, 170]; +pub const SPEC_LINES : [28]u32 = [183, 148, 139, 246, 196, 187, 197, 188, 193, 180, 159, 197, 304, 233, 189, 125, 110, 121, 135, 201, 201, 134, 168, 153, 183, 124, 129, 198]; + +; Port stage of one tool. A tool moves up only through widget_factory.t27's port_gate. +pub const PORT_HAND : u8 = 0; // words and rules in JavaScript +pub const PORT_WORDS : u8 = 1; // words and numbers in the spec (every tool today) +pub const PORT_LOGIC : u8 = 2; // pure logic is .t27, generated to JS by gen-ts +pub const PORT_DRAW : u8 = 3; // the page is drawn by the trusted renderer from a plan +pub const PORT_SEALED : u8 = 4; // the spec seal keys the page, the data and the card +pub const PORT_STAGES : u8 = 5; +pub const TOOL_PORT : [28]u8 = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]; + +; Port waves, by the hand lines a tool still carries (logic + draw + css + data + card). +; Wave 1 is the small tools, wave 3 the largest; a wave is a batch of port issues. +pub const WAVE_1_MAX : u32 = 700; +pub const WAVE_2_MAX : u32 = 1000; + +pub fn gallery_sum() -> u32 { + return GALLERY_CASTS + GALLERY_PLAYERS + GALLERY_TABS + GALLERY_TOOLS; +} + +pub fn folder_sum() -> u32 { + return LESSON_BASICS + LESSON_GOLDEN + TOOL_COUNT; +} + +pub fn hand_lines(i: u32) -> u32 { + return LOGIC_LINES[i] + DRAW_LINES[i] + CSS_LINES[i] + DATA_LINES[i] + CARD_LINES[i]; +} + +pub fn wave_of(lines: u32) -> u32 { + if (lines <= WAVE_1_MAX) { + return 1; + } + if (lines <= WAVE_2_MAX) { + return 2; + } + return 3; +} + +pub fn has_logic_file(i: u32) -> bool { + return LOGIC_LINES[i] > 0; +} + +pub fn sum_lines(a: [28]u32) -> u32 { + var s : u32 = 0; + var i : u32 = 0; + while (i < TOOL_COUNT) { + s = s + a[i]; + i = i + 1; + } + return s; +} + +pub fn total_hand() -> u32 { + return sum_lines(LOGIC_LINES) + sum_lines(DRAW_LINES) + sum_lines(CSS_LINES) + sum_lines(DATA_LINES) + sum_lines(CARD_LINES); +} + +pub fn count_with(a: [28]u32) -> u32 { + var n : u32 = 0; + var i : u32 = 0; + while (i < TOOL_COUNT) { + if (a[i] > 0) { + n = n + 1; + } + i = i + 1; + } + return n; +} + +pub fn count_wave(w: u32) -> u32 { + var n : u32 = 0; + var i : u32 = 0; + while (i < TOOL_COUNT) { + if (wave_of(hand_lines(i)) == w) { + n = n + 1; + } + i = i + 1; + } + return n; +} + +pub fn count_port(stage: u8) -> u32 { + var n : u32 = 0; + var i : u32 = 0; + while (i < TOOL_COUNT) { + if (TOOL_PORT[i] == stage) { + n = n + 1; + } + i = i + 1; + } + return n; +} + +test the_gallery_kinds_add_up_to_its_count { + assert gallery_sum() == GALLERY_COUNT; + assert GALLERY_COUNT == 195; +} + +test the_tool_folders_are_lessons_plus_tools { + assert folder_sum() == GALLERY_TOOLS; +} + +test every_tool_has_an_id_and_a_spec { + var i : u32 = 0; + while (i < TOOL_COUNT) { + assert SPEC_LINES[i] > 100; + assert DRAW_LINES[i] > 0; + assert CSS_LINES[i] > 0; + i = i + 1; + } + assert TOOL_IDS[0] == "bit-autopsy"; + assert TOOL_IDS[7] == "gatle"; + assert TOOL_IDS[27] == "vcd-wrapped"; +} + +test the_hand_written_column_is_the_measured_total { + assert sum_lines(LOGIC_LINES) == 2775; + assert sum_lines(DRAW_LINES) == 8920; + assert sum_lines(CSS_LINES) == 2151; + assert sum_lines(DATA_LINES) == 5572; + assert sum_lines(CARD_LINES) == 3415; + assert sum_lines(SPEC_LINES) == 4921; + assert total_hand() == 22833; +} + +test twelve_tools_split_logic_from_drawing { + assert count_with(LOGIC_LINES) == 12; + assert has_logic_file(7); + assert has_logic_file(2) == false; + assert LOGIC_LINES[7] == 51; + assert count_with(DATA_LINES) == 23; + assert count_with(CARD_LINES) == 24; +} + +test the_waves_cover_every_tool_once { + assert count_wave(1) == 11; + assert count_wave(2) == 8; + assert count_wave(3) == 9; + assert count_wave(1) + count_wave(2) + count_wave(3) == TOOL_COUNT; + assert hand_lines(17) == 205; + assert hand_lines(7) == 1000; + assert wave_of(hand_lines(7)) == 2; + assert hand_lines(26) == 606; + assert hand_lines(12) == 1432; +} + +test no_tool_has_left_the_words_stage_yet { + assert count_port(PORT_WORDS) == TOOL_COUNT; + assert count_port(PORT_LOGIC) == 0; + assert count_port(PORT_DRAW) == 0; + assert count_port(PORT_SEALED) == 0; +} From 9d714ffd965e31dfd526206d77f6d55cb1a35734 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 14:35:31 +0700 Subject: [PATCH 02/33] widgets: gatle rules ported to .t27 -- 3 tests, 19 asserts; gen-js lowers no fn bodies, so the logic stage waits (Closes #10938) specs/widgets/gatle_rule.t27 replaces rule.js as the single source. widget_factory.t27 records that gen-js/gen-ts emit declarations only. Epic #10937. Co-Authored-By: Claude Sonnet 5.5 --- specs/t27f/widget_factory.t27 | 19 +++++- specs/widgets/gatle_rule.t27 | 113 ++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 specs/widgets/gatle_rule.t27 diff --git a/specs/t27f/widget_factory.t27 b/specs/t27f/widget_factory.t27 index f757c21c6f..934d3a159f 100644 --- a/specs/t27f/widget_factory.t27 +++ b/specs/t27f/widget_factory.t27 @@ -29,8 +29,8 @@ ; byte-identical to the hand-written output it replaces, (b) the spec's tests pass, and (c) the ; hand-written lines do not grow. (c) is the same rule as t27b's "net <= 0" and FROZEN_HASH ports. ; -- -; Not claimed: no stage past `spec` exists as a program today. gen-ts has not been run on a tool's -; logic; no plan is drawn by a renderer; t27b has not compiled a data builder. The primitives are +; Not claimed: no stage past `spec` exists as a program today. gatle's rules are ported to +; specs/widgets/gatle_rule.t27 and pass, but gen-js/gen-ts do not lower fn bodies, so no JS is generated; no plan is drawn by a renderer; t27b has not compiled a data builder. The primitives are ; PROPOSED from reading the 28 tools by name, not measured; wave 1 (catalog.t27, 11 tools of at ; most 700 hand lines) measures them and may rename or merge them. ; phi^2 + 1/phi^2 = 3 | TRINITY @@ -82,6 +82,17 @@ pub const TRI_VERBS : [5]str = ["widget list", "widget port", "widget make", "wi pub const CLAW_TOOL_COUNT : u32 = 3; pub const CLAW_TOOLS : [3]str = ["t27f_widgets", "t27f_widget_make", "t27f_widget_port_plan"]; +/// MEASURED 2026-10-19 with t27c 0.5.2 on specs/widgets/gatle_rule.t27: gen-js and gen-ts emit +/// declarations (consts, structs) and skip every fn body -- "this backend lowers declarations, not +/// bodies". Until a body-lowering JS backend exists the logic stage writes a tested .t27 rule +/// (the single source) but cannot generate the JavaScript the page runs. Flip this when it can. +pub const GEN_JS_LOWERS_BODIES : bool = false; + +/// The logic stage can produce its artefact only when the backend lowers bodies. +pub fn logic_stage_ready() -> bool { + return GEN_JS_LOWERS_BODIES; +} + /// A stage is the next of a port stage only by one step. pub fn next_port(stage: u8) -> u8 { if (stage + 1 >= PORT_STAGES) { @@ -213,6 +224,10 @@ test every_claw_tool_is_a_t27_name_the_service_mounts { assert TRI_VERBS[0] == "widget list"; } +test the_logic_stage_waits_for_a_body_lowering_backend { + assert logic_stage_ready() == false; +} + test the_logic_stage_removes_exactly_the_logic_file { assert after_logic(1000, 51) == 949; assert after_logic(686, 0) == 686; diff --git a/specs/widgets/gatle_rule.t27 b/specs/widgets/gatle_rule.t27 new file mode 100644 index 0000000000..c082dc8ccb --- /dev/null +++ b/specs/widgets/gatle_rule.t27 @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/gatle_rule.t27 -- the rules of Gatle as pure functions: the first widget logic file +; ported from JavaScript (trinity apps/website/public/widgets/gatle/rule.js, 51 lines) to .t27. +; Epic gHashTag/t27#10937. Until now rule.js was hand-written and gatle.mjs re-imported it to check +; it against the fixed answers in gatle.t27, "so the page cannot drift from the spec". Here the +; rules and their fixed answers are one file, so there is nothing to drift. +; Ported: dayNumber, dayIndex, puzzleNumber, grade (heat and dir), emoji_code. Not yet ported: +; parseHash and formatHash (URL text handling) -- the next step, once gen-js of str ops is measured. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_gatle_rule; + +pub const DAY_MS : u64 = 86400000; + +pub const HEAT_CLOSE : u8 = 0; +pub const HEAT_WARM : u8 = 1; +pub const HEAT_COLD : u8 = 2; +pub const DIR_HIT : u8 = 0; +pub const DIR_UP : u8 = 1; +pub const DIR_DOWN : u8 = 2; + +pub const EMOJI_HEAT : [3]u32 = [129001, 129000, 128997]; +pub const ARROWS : [2]u32 = [11014, 11015]; + +/// Whole UTC days since 1970-01-01 for a time in milliseconds. +pub fn day_number(ms: u64) -> u64 { + return ms / DAY_MS; +} + +/// The puzzle a UTC day gets: days since the epoch, modulo the pool size (never negative). +pub fn day_index(day: u64, epoch_day: u64, size: u64) -> u64 { + if (day >= epoch_day) { + return (day - epoch_day) % size; + } + return (size - ((epoch_day - day) % size)) % size; +} + +/// The running number a day's puzzle is shared under: the epoch day is #1. +pub fn puzzle_number(day: u64, epoch_day: u64) -> u64 { + return day - epoch_day + 1; +} + +/// |guess - truth| * 100, integer arithmetic only. +pub fn off100(guess: u64, truth: u64) -> u64 { + if (guess > truth) { + return (guess - truth) * 100; + } + return (truth - guess) * 100; +} + +/// close within close_pct percent of the truth (boundary counts), warm within warm_pct, else cold. +pub fn heat(guess: u64, truth: u64, close_pct: u64, warm_pct: u64) -> u8 { + const off : u64 = off100(guess, truth); + if (off <= truth * close_pct) { + return HEAT_CLOSE; + } + if (off <= truth * warm_pct) { + return HEAT_WARM; + } + return HEAT_COLD; +} + +/// hit when close, else up (the truth is higher) or down. +pub fn dir(guess: u64, truth: u64, close_pct: u64, warm_pct: u64) -> u8 { + if (heat(guess, truth, close_pct, warm_pct) == HEAT_CLOSE) { + return DIR_HIT; + } + if (truth > guess) { + return DIR_UP; + } + return DIR_DOWN; +} + +/// The code point of the heat square of one graded guess. +pub fn heat_code(h: u8) -> u32 { + return EMOJI_HEAT[h as usize]; +} + +/// The code point of the arrow of one graded guess; 0 for a hit (no arrow). +pub fn arrow_code(d: u8) -> u32 { + if (d == DIR_HIT) { + return 0; + } + return ARROWS[(d - 1) as usize]; +} + +test day_index_on_two_fixed_dates { + assert day_number(1791072000000) == 20730; + assert day_index(20730, 20730, 37) == 0; + assert day_index(20819, 20730, 37) == 15; + assert puzzle_number(20730, 20730) == 1; + assert puzzle_number(20819, 20730) == 90; + assert day_index(20729, 20730, 37) == 36; +} + +test close_is_within_ten_percent_boundary_included { + assert heat(180, 200, 10, 50) == HEAT_CLOSE; + assert heat(179, 200, 10, 50) == HEAT_WARM; + assert heat(220, 200, 10, 50) == HEAT_CLOSE; + assert heat(221, 200, 10, 50) == HEAT_WARM; + assert heat(1, 200, 10, 50) == HEAT_COLD; + assert dir(180, 200, 10, 50) == DIR_HIT; + assert dir(179, 200, 10, 50) == DIR_UP; + assert dir(221, 200, 10, 50) == DIR_DOWN; +} + +test the_share_codes_follow_the_grade { + assert heat_code(HEAT_CLOSE) == 129001; + assert heat_code(HEAT_COLD) == 128997; + assert arrow_code(DIR_HIT) == 0; + assert arrow_code(DIR_UP) == 11014; + assert arrow_code(DIR_DOWN) == 11015; +} From 749e318e9404dac896e89c7f861fd59c0497308a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 14:39:00 +0700 Subject: [PATCH 03/33] widgets: gallery rules as scalar .t27 -- 195 widgets, refusal verdicts and embed sizes run native under t27b, 3 of 3 (Closes #10938) specs/widgets/gallery_rules.t27 holds the decisions of scripts/widgets-from-spec.mjs (trinity); the gallery data stays in trinity gallery.t27. Epic #10937. Co-Authored-By: Claude Sonnet 5.5 --- specs/widgets/gallery_rules.t27 | 146 ++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 specs/widgets/gallery_rules.t27 diff --git a/specs/widgets/gallery_rules.t27 b/specs/widgets/gallery_rules.t27 new file mode 100644 index 0000000000..1d31e5c1f6 --- /dev/null +++ b/specs/widgets/gallery_rules.t27 @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/gallery_rules.t27 -- the rules of the widget gallery (app.t27.ai/game/widgets) as +; scalar functions, so t27b can run them native. Epic gHashTag/t27#10937, owner 2026-10-19: the +; gallery is integrated natively into t27b and T27F. +; The gallery is 195 widgets: 104 casts, 8 players, 1 tab, 82 tools; by category 95 fpga, 92 +; compiler, 8 game. Its data is gHashTag/trinity apps/website/specs/widgets/gallery.t27 and stays +; there (this file does not copy 195 rows). Its generator, scripts/widgets-from-spec.mjs (269 +; lines), does two jobs: I/O (read each widget's page for og tags, read meta.json) and RULES (which +; widget is refused, how tall its embed is, whether its preview carries ?embed=1). The rules are +; here; the I/O stays a thin reader that hands this file booleans. verdict() is the generator's +; problems list as one number, in the generator's order. +; Not claimed: the generator still runs in node today; this file is what replaces its decisions. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widgets_gallery_rules; + +pub const K_CAST : u8 = 0; +pub const K_PLAYER : u8 = 1; +pub const K_TAB : u8 = 2; +pub const K_TOOL : u8 = 3; +pub const K_KINDS : u8 = 4; + +pub const GALLERY_COUNT : u32 = 195; +pub const KIND_COUNTS : [4]u32 = [104, 8, 1, 82]; +pub const CAT_COUNTS : [3]u32 = [95, 92, 8]; + +pub const EMBED_WIDTH : u32 = 640; +pub const PLAYER_EMBED_HEIGHT : u32 = 360; +pub const CAST_EMBED_HEIGHT : u32 = 460; + +pub const V_OK : u8 = 0; +pub const V_TAB_VIEW_MISSING : u8 = 1; +pub const V_PAGE_MISSING : u8 = 2; +pub const V_META_MISSING : u8 = 3; +pub const V_IMAGE_NOT_LOCAL : u8 = 4; +pub const V_CAST_META_MISSING : u8 = 5; +pub const V_CAST_BAD_EXIT : u8 = 6; +pub const V_TOOL_HOOK_MISSING : u8 = 7; +pub const V_PLAYER_EMBED_MISSING : u8 = 8; +pub const V_UNKNOWN_KIND : u8 = 9; + +/// Embed query a kind carries: 0 none, 1 "?embed=1", 2 "&embed=1" (a tab already has a query). +pub fn embed_query(kind: u8) -> u8 { + if (kind == K_TOOL) { + return 1; + } + if (kind == K_TAB) { + return 2; + } + return 0; +} + +pub fn embed_height(kind: u8) -> u32 { + if (kind == K_PLAYER) { + return PLAYER_EMBED_HEIGHT; + } + return CAST_EMBED_HEIGHT; +} + +/// A tab has no page of its own to preview; every other kind previews its framed page. +pub fn has_preview(kind: u8) -> bool { + return kind != K_TAB; +} + +/// Why the generator would refuse one widget, or V_OK. The booleans are what its reader found. +pub fn verdict(kind: u8, view_ok: bool, page_ok: bool, meta_ok: bool, image_local: bool, cast_meta: bool, bad_exit: bool, hook_ok: bool, player_ok: bool) -> u8 { + if (kind >= K_KINDS) { + return V_UNKNOWN_KIND; + } + if (kind == K_TAB) { + if (view_ok) { + return V_OK; + } + return V_TAB_VIEW_MISSING; + } + if (page_ok == false) { + return V_PAGE_MISSING; + } + if (meta_ok == false) { + return V_META_MISSING; + } + if (image_local == false) { + return V_IMAGE_NOT_LOCAL; + } + if (kind == K_CAST) { + if (cast_meta == false) { + return V_CAST_META_MISSING; + } + if (bad_exit) { + return V_CAST_BAD_EXIT; + } + return V_OK; + } + if (kind == K_TOOL) { + if (hook_ok == false) { + return V_TOOL_HOOK_MISSING; + } + return V_OK; + } + if (player_ok == false) { + return V_PLAYER_EMBED_MISSING; + } + return V_OK; +} + +pub fn kinds_sum() -> u32 { + return KIND_COUNTS[0] + KIND_COUNTS[1] + KIND_COUNTS[2] + KIND_COUNTS[3]; +} + +pub fn cats_sum() -> u32 { + return CAT_COUNTS[0] + CAT_COUNTS[1] + CAT_COUNTS[2]; +} + +test the_gallery_counts_agree_by_kind_and_by_category { + assert kinds_sum() == GALLERY_COUNT; + assert cats_sum() == GALLERY_COUNT; +} + +test embed_size_and_query_follow_the_kind { + assert embed_height(K_PLAYER) == 360; + assert embed_height(K_CAST) == 460; + assert embed_height(K_TOOL) == 460; + assert embed_height(K_TAB) == 460; + assert embed_query(K_TOOL) == 1; + assert embed_query(K_TAB) == 2; + assert embed_query(K_CAST) == 0; + assert embed_query(K_PLAYER) == 0; + assert has_preview(K_TAB) == false; + assert has_preview(K_TOOL); +} + +test a_widget_is_refused_for_the_first_rule_it_breaks { + assert verdict(K_CAST, true, true, true, true, true, false, false, false) == V_OK; + assert verdict(K_CAST, true, false, true, true, true, false, false, false) == V_PAGE_MISSING; + assert verdict(K_CAST, true, true, false, true, true, false, false, false) == V_META_MISSING; + assert verdict(K_CAST, true, true, true, false, true, false, false, false) == V_IMAGE_NOT_LOCAL; + assert verdict(K_CAST, true, true, true, true, false, false, false, false) == V_CAST_META_MISSING; + assert verdict(K_CAST, true, true, true, true, true, true, false, false) == V_CAST_BAD_EXIT; + assert verdict(K_TOOL, true, true, true, true, false, false, false, false) == V_TOOL_HOOK_MISSING; + assert verdict(K_TOOL, true, true, true, true, false, false, true, false) == V_OK; + assert verdict(K_PLAYER, true, true, true, true, false, false, false, false) == V_PLAYER_EMBED_MISSING; + assert verdict(K_PLAYER, true, true, true, true, false, false, false, true) == V_OK; + assert verdict(K_TAB, false, false, false, false, false, false, false, false) == V_TAB_VIEW_MISSING; + assert verdict(K_TAB, true, false, false, false, false, false, false, false) == V_OK; + assert verdict(K_KINDS, true, true, true, true, true, false, true, true) == V_UNKNOWN_KIND; +} From ddcabdf0db17afaa6f67eddd7579d1528d87804c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 14:58:14 +0700 Subject: [PATCH 04/33] widgets: the gallery reader as pure .t27 -- meta tags, entities, localOf, exit codes and hud views over bytes; 8 of 8 under t27c and t27b, byte-identical to metaOf on all 337 corpus files (Closes #10938) specs/widgets/page_reader.t27 replaces the reading half of widgets-from-spec.mjs. Epic #10937. Co-Authored-By: Claude Sonnet 5.5 --- specs/widgets/page_reader.t27 | 430 ++++++++++++++++++++++++++++++++++ 1 file changed, 430 insertions(+) create mode 100644 specs/widgets/page_reader.t27 diff --git a/specs/widgets/page_reader.t27 b/specs/widgets/page_reader.t27 new file mode 100644 index 0000000000..60098ba81d --- /dev/null +++ b/specs/widgets/page_reader.t27 @@ -0,0 +1,430 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/page_reader.t27 -- the gallery's reader as pure rules over bytes: what a widget's +; page and recording say, without touching a disk. Epic gHashTag/t27#10937; owner 2026-10-19: the +; gallery goes native into t27b and T27F. Replaces, rule for rule, the reading half of +; apps/website/scripts/widgets-from-spec.mjs in gHashTag/trinity: metaOf (the og and twitter tags +; of a share page), unescape, localOf (an absolute t27.ai address to its file), framed, the cast's +; exit_codes test, and hudViews (which Queen board views exist, for the one tab). +; -- +; The pattern is source_text.t27's (T27bSourceText): the glue passes a file's BYTES and gets back +; offsets; nothing here reads a file. A sub-text is returned as its two byte offsets packed in one +; i64 (start * 2^32 + end), NO_TEXT when there is none, so the whole module is scalar-and-slice and +; t27b can run it. A text with HTML entities is read through text_len and text_byte, which unescape +; on the fly: no buffer, no allocation. +; -- +; Exact semantics kept from the generator, so the swap changes nothing a reader can see: +; - a meta tag matches only as ` i64 { + return (lo as i64) * PACK + (hi as i64); +} + +pub fn lo_of(p: i64) -> usize { + return (p / PACK) as usize; +} + +pub fn hi_of(p: i64) -> usize { + return (p % PACK) as usize; +} + +/// JavaScript's \s for the ASCII range: space, tab, line feed, vertical tab, form feed, carriage return. +pub fn is_space(b: u8) -> bool { + return b == B_SPACE or (b >= B_TAB and b <= B_CR); +} + +/// Do the bytes of w sit at s[p ..], inside s[.. hi]? +pub fn match_in(s: []const u8, p: usize, hi: usize, w: []const u8) -> bool { + if (p + w.len > hi) { return false; } + var k : usize = 0; + while (k < w.len) { + if (s[p + k] != w[k]) { return false; } + k = k + 1; + } + return true; +} + +/// The first offset at or after `from` where w starts, or NO_TEXT. +pub fn find_from(s: []const u8, from: usize, w: []const u8) -> i64 { + var p : usize = from; + while (p + w.len <= s.len) { + if (match_in(s, p, s.len, w)) { return p as i64; } + p = p + 1; + } + return NO_TEXT; +} + +/// The offset of the next `"` at or after p, or NO_TEXT. +pub fn next_quote(s: []const u8, p0: usize) -> i64 { + var p : usize = p0; + while (p < s.len) { + if (s[p] == B_QUOTE) { return p as i64; } + p = p + 1; + } + return NO_TEXT; +} + +pub fn skip_space(s: []const u8, p0: usize) -> usize { + var p : usize = p0; + while (p < s.len and is_space(s[p])) { p = p + 1; } + return p; +} + +/// One ` i64 { + var p : usize = skip_space(html, p0); + if (p == p0) { return NO_TEXT; } + if (match_in(html, p, html.len, "property=")) { + p = p + 9; + } else { + if (match_in(html, p, html.len, "name=")) { + p = p + 5; + } else { + return NO_TEXT; + } + } + if (p >= html.len or html[p] != B_QUOTE) { return NO_TEXT; } + p = p + 1; + if (!match_in(html, p, html.len, key)) { return NO_TEXT; } + p = p + key.len; + if (key.len == 0 or p >= html.len or html[p] != B_QUOTE) { return NO_TEXT; } + p = p + 1; + const q : usize = skip_space(html, p); + if (q == p) { return NO_TEXT; } + if (!match_in(html, q, html.len, "content=")) { return NO_TEXT; } + const v : usize = q + 8; + if (v >= html.len or html[v] != B_QUOTE) { return NO_TEXT; } + const e : i64 = next_quote(html, v + 1); + if (e < 0) { return NO_TEXT; } + return pack(v + 1, e as usize); +} + +/// The packed content of the meta tag with this key; the last one wins, as in metaOf. +pub fn meta_value(html: []const u8, key: []const u8) -> i64 { + var found : i64 = NO_TEXT; + var at : i64 = find_from(html, 0, "= 0) { + const after : usize = (at as usize) + 5; + const r : i64 = meta_at(html, after, key); + if (r >= 0) { found = r; } + at = find_from(html, after, " bool { + return meta_value(html, key) >= 0; +} + +/// Which of the tags a gallery widget needs the page has. +pub fn page_flags(html: []const u8) -> u32 { + var f : u32 = 0; + if (has_meta(html, "og:title")) { f = f + F_TITLE; } + if (has_meta(html, "og:description")) { f = f + F_LINE; } + if (has_meta(html, "og:image")) { f = f + F_IMAGE; } + if (has_meta(html, "t27:hook")) { f = f + F_HOOK; } + if (has_meta(html, "twitter:player")) { f = f + F_PLAYER; } + return f; +} + +/// A share page has its title, line and image, and none is empty. +pub fn words_ok(html: []const u8) -> bool { + var k : u32 = 0; + while (k < 3) { + var key : []const u8 = "og:title"; + if (k == 1) { key = "og:description"; } + if (k == 2) { key = "og:image"; } + const v : i64 = meta_value(html, key); + if (v < 0) { return false; } + if (hi_of(v) == lo_of(v)) { return false; } + k = k + 1; + } + return true; +} + +/// The length of the HTML entity at s[p ..] inside s[.. hi], 0 when there is none of the six. +pub fn entity_len(s: []const u8, p: usize, hi: usize) -> usize { + if (p >= hi or s[p] != B_AMP) { return 0; } + if (match_in(s, p, hi, "&")) { return 5; } + if (match_in(s, p, hi, "<")) { return 4; } + if (match_in(s, p, hi, ">")) { return 4; } + if (match_in(s, p, hi, """)) { return 6; } + if (match_in(s, p, hi, "'")) { return 5; } + if (match_in(s, p, hi, "'")) { return 6; } + return 0; +} + +pub fn entity_byte(s: []const u8, p: usize, hi: usize) -> u8 { + if (match_in(s, p, hi, "&")) { return B_AMP; } + if (match_in(s, p, hi, "<")) { return B_LT; } + if (match_in(s, p, hi, ">")) { return B_GT; } + if (match_in(s, p, hi, """)) { return B_QUOTE; } + return B_APOS; +} + +/// The length of s[lo .. hi] once its entities are replaced, in one pass. +pub fn text_len(s: []const u8, lo: usize, hi: usize) -> usize { + var p : usize = lo; + var n : usize = 0; + while (p < hi) { + const e : usize = entity_len(s, p, hi); + if (e > 0) { p = p + e; } else { p = p + 1; } + n = n + 1; + } + return n; +} + +/// The k-th byte of s[lo .. hi] once its entities are replaced; 0 past the end. +pub fn text_byte(s: []const u8, lo: usize, hi: usize, k: usize) -> u8 { + var p : usize = lo; + var n : usize = 0; + while (p < hi) { + const e : usize = entity_len(s, p, hi); + if (n == k) { + if (e > 0) { return entity_byte(s, p, hi); } + return s[p]; + } + if (e > 0) { p = p + e; } else { p = p + 1; } + n = n + 1; + } + return 0; +} + +/// localOf: the packed path of url after origin, up to the first ? or #; NO_TEXT when url is not +/// under origin. +pub fn local_of(url: []const u8, origin: []const u8) -> i64 { + if (!match_in(url, 0, url.len, origin)) { return NO_TEXT; } + var e : usize = origin.len; + while (e < url.len and url[e] != B_QUERY and url[e] != B_HASH) { e = e + 1; } + return pack(origin.len, e); +} + +/// framed: a path that ends in a slash names a directory and gets index.html appended. +pub fn is_dir(path: []const u8, lo: usize, hi: usize) -> bool { + return hi > lo and path[hi - 1] == B_SLASH; +} + +/// The cast's exit codes: true when exit_codes holds anything but the number 0. A token is a run of +/// bytes between spaces, commas and quotes; "exit_codes" absent or empty is clean. +pub fn bad_exit(json: []const u8) -> bool { + const k : i64 = find_from(json, 0, "exit_codes"); + if (k < 0) { return false; } + var p : usize = (k as usize) + 10; + while (p < json.len and json[p] != B_OPEN) { p = p + 1; } + if (p >= json.len) { return false; } + p = p + 1; + var len : usize = 0; + var zero : bool = true; + while (p < json.len and json[p] != B_CLOSE) { + const b : u8 = json[p]; + if (is_space(b) or b == B_COMMA or b == B_QUOTE) { + if (len > 0 and (len != 1 or !zero)) { return true; } + len = 0; + zero = true; + } else { + if (b != B_ZERO) { zero = false; } + len = len + 1; + } + p = p + 1; + } + if (len > 0 and (len != 1 or !zero)) { return true; } + return false; +} + +/// hudViews: is `view` one of the quoted names in `export const HUD_VIEWS ... = [ ... ] as const`? +pub fn view_listed(src: []const u8, view: []const u8) -> bool { + const k : i64 = find_from(src, 0, "HUD_VIEWS"); + if (k < 0) { return false; } + const eq : i64 = find_from(src, (k as usize) + 9, "="); + if (eq < 0) { return false; } + const open : i64 = find_from(src, (eq as usize) + 1, "["); + if (open < 0) { return false; } + var close : i64 = find_from(src, (open as usize) + 1, "]"); + var ok : bool = false; + while (close >= 0 and !ok) { + const a : usize = skip_space(src, (close as usize) + 1); + if (match_in(src, a, src.len, "as const")) { ok = true; } else { close = find_from(src, (close as usize) + 1, "]"); } + } + if (!ok) { return false; } + var p : usize = (open as usize) + 1; + while (p < (close as usize)) { + if (src[p] == B_QUOTE) { + const e : i64 = next_quote(src, p + 1); + if (e < 0 or (e as usize) > (close as usize)) { return false; } + if (e as usize - p - 1 == view.len and match_in(src, p + 1, e as usize, view)) { return true; } + p = (e as usize) + 1; + } else { + p = p + 1; + } + } + return false; +} + +/// The decision about one widget: the generator's problems list as gallery_rules' verdict. The glue +/// answers what only a disk knows -- page_ok (the index.html exists), image_file_ok (the og:image is +/// a file under public/), player_file_ok (the twitter:player is a page under public/), cast_meta +/// (meta.json exists), view_ok (hudViews lists the tab's view) -- and passes the bytes. +pub fn judge(kind: u8, html: []const u8, cast_json: []const u8, origin: []const u8, page_ok: bool, image_file_ok: bool, player_file_ok: bool, cast_meta: bool, view_ok: bool) -> u8 { + const img : i64 = meta_value(html, "og:image"); + var image_local : bool = false; + if (img >= 0) { + image_local = local_of(html[lo_of(img) .. hi_of(img)], origin) >= 0 and image_file_ok; + } + const flags : u32 = page_flags(html); + const hook_ok : bool = (flags / F_HOOK) % 2 == 1; + return verdict(kind, view_ok, page_ok, words_ok(html), image_local, cast_meta, bad_exit(cast_json), hook_ok, player_file_ok); +} + +test a_meta_value_is_the_content_of_the_last_matching_tag { + const h : []const u8 = ""; + const v : i64 = meta_value(h, "og:title"); + assert v >= 0; + assert hi_of(v) - lo_of(v) == 6; + assert h[lo_of(v)] == 83; + assert meta_value(h, "og:image") == NO_TEXT; + assert hi_of(meta_value(h, "description")) - lo_of(meta_value(h, "description")) == 1; +} + +test a_meta_tag_must_be_spelled_the_way_the_generator_matches { + assert meta_value("", "og:title") >= 0; + assert meta_value("", "og:title") >= 0; + assert meta_value("", "og:title") == NO_TEXT; + assert meta_value("", "og:title") == NO_TEXT; + assert meta_value("", "og:title") == NO_TEXT; + assert meta_value("", "og:title") == NO_TEXT; + assert meta_value("", "og:title") >= 0; + assert meta_value("", "og:title") == NO_TEXT; +} + +test the_page_flags_say_which_tags_a_share_page_has { + const p : []const u8 = ""; + assert page_flags(p) == F_TITLE + F_LINE + F_IMAGE + F_HOOK; + assert words_ok(p); + assert page_flags("") == F_PLAYER; + assert words_ok("") == false; + assert words_ok("") == false; +} + +test entities_unescape_in_one_pass { + const s : []const u8 = "a & b <c> "d" it's 'e' &amp; &bogus;"; + assert text_len(s, 0, s.len) == 36; + assert text_byte(s, 0, s.len, 2) == 38; + assert text_byte(s, 0, s.len, 6) == 60; + assert text_byte(s, 0, s.len, 16) == 39; + assert text_byte(s, 0, s.len, 29) == 38; + assert text_byte(s, 0, s.len, 30) == 98; + assert text_byte(s, 0, s.len, 23) == 38; + assert text_byte(s, 0, s.len, 24) == 97; + assert text_byte(s, 0, s.len, 999) == 0; + assert text_len("plain", 0, 5) == 5; + assert text_len("&", 0, 4) == 4; + assert text_len("", 0, 0) == 0; +} + +test an_address_is_local_when_it_sits_under_the_origin { + const u : []const u8 = "https://t27.ai/widgets/gatle/card.png?v=9e7908d83118"; + const r : i64 = local_of(u, "https://t27.ai/"); + assert r >= 0; + assert lo_of(r) == 15; + assert hi_of(r) == 37; + assert local_of("https://other.example/x.png", "https://t27.ai/") == NO_TEXT; + assert hi_of(local_of("https://t27.ai/term/a/#frag", "https://t27.ai/")) == 22; + assert lo_of(local_of("https://t27.ai/", "https://t27.ai/")) == 15; + assert hi_of(local_of("https://t27.ai/", "https://t27.ai/")) == 15; + assert is_dir("term/x7-board/", 0, 14); + assert is_dir("widgets/gatle/card.png", 0, 22) == false; + assert is_dir("", 0, 0) == false; +} + +test a_cast_is_bad_when_any_exit_code_is_not_zero { + assert bad_exit("{ \"exit_codes\": [ 0, 0, 0 ], \"gif\": true }") == false; + assert bad_exit("{ \"exit_codes\": [0] }") == false; + assert bad_exit("{ \"exit_codes\": [0, 1] }"); + assert bad_exit("{ \"exit_codes\": [ 2 ] }"); + assert bad_exit("{ \"exit_codes\": [ 00 ] }"); + assert bad_exit("{ \"exit_codes\": [ 10 ] }"); + assert bad_exit("{ \"exit_codes\": [ \"0\", \"0\" ] }") == false; + assert bad_exit("{ \"exit_codes\": [] }") == false; + assert bad_exit("{ \"gif\": true }") == false; + assert bad_exit("") == false; +} + +test the_tab_view_must_be_listed_in_the_hud { + const src : []const u8 = "export const HUD_VIEWS: readonly string[] = [ \"board\", \"widgets\", \"wars\" ] as const"; + assert view_listed(src, "widgets"); + assert view_listed(src, "board"); + assert view_listed(src, "wars"); + assert view_listed(src, "widget") == false; + assert view_listed(src, "nope") == false; + assert view_listed("const X = [ \"widgets\" ]", "widgets") == false; + assert view_listed("", "widgets") == false; +} + +test judge_turns_what_was_read_into_gallery_rules_verdicts { + const page : []const u8 = ""; + const origin : []const u8 = "https://t27.ai/"; + assert judge(K_TOOL, page, "", origin, true, true, false, false, false) == V_OK; + assert judge(K_TOOL, page, "", origin, false, true, false, false, false) != V_OK; + assert judge(K_TOOL, page, "", origin, true, false, false, false, false) != V_OK; + assert judge(K_TOOL, "", "", origin, true, true, false, false, false) != V_OK; + assert judge(K_CAST, page, "{ \"exit_codes\": [0, 0] }", origin, true, true, false, true, false) == V_OK; + assert judge(K_CAST, page, "{ \"exit_codes\": [0, 1] }", origin, true, true, false, true, false) != V_OK; + assert judge(K_CAST, page, "{ \"exit_codes\": [0] }", origin, true, true, false, false, false) != V_OK; +} From def022a40db7a354a1900672a2be86934f789d9f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 15:18:48 +0700 Subject: [PATCH 05/33] widgets: the logic stage runs through wasm -- gatle_rule built with gen-c and zig, 866178 comparisons against rule.js, 0 mismatches (Closes #10938) gen-js lowers no fn bodies (#10943), but the path the Queen's app.wasm takes works: 490 bytes. Epic #10937. Co-Authored-By: Claude Sonnet 5.5 --- specs/t27f/widget_factory.t27 | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/specs/t27f/widget_factory.t27 b/specs/t27f/widget_factory.t27 index 934d3a159f..6ffefd4b06 100644 --- a/specs/t27f/widget_factory.t27 +++ b/specs/t27f/widget_factory.t27 @@ -10,7 +10,7 @@ ; spec the widget's .t27 (words, rules, tests, data sources). The hand column of the catalog is ; everything that is NOT this stage. ; logic the pure functions of the tool (parse, grade, layout), written in .t27 and generated to -; JavaScript by gen-ts. The spec's own test blocks are the fixed answers; a data builder no +; wasm (t27c gen-c, then zig cc --target=wasm32-wasi), not JavaScript: gen-js lowers no bodies. The spec's own test blocks are the fixed answers; a data builder no ; longer re-imports a hand file "so the page cannot drift from the spec". ; plan what is drawn, as a ui_plan.t27 plan: widget ids from an allowlist, no markup, no script. ; The primitives below extend ui_plan's catalog; they are not a second catalog. @@ -30,7 +30,7 @@ ; hand-written lines do not grow. (c) is the same rule as t27b's "net <= 0" and FROZEN_HASH ports. ; -- ; Not claimed: no stage past `spec` exists as a program today. gatle's rules are ported to -; specs/widgets/gatle_rule.t27 and pass, but gen-js/gen-ts do not lower fn bodies, so no JS is generated; no plan is drawn by a renderer; t27b has not compiled a data builder. The primitives are +; specs/widgets/gatle_rule.t27 and built to wasm (0 mismatches over 866178 comparisons against rule.js), but the widget page still loads the hand-written rule.js: the trinity side is not changed; no plan is drawn by a renderer; t27b has not compiled a data builder. The primitives are ; PROPOSED from reading the 28 tools by name, not measured; wave 1 (catalog.t27, 11 tools of at ; most 700 hand lines) measures them and may rename or merge them. ; phi^2 + 1/phi^2 = 3 | TRINITY @@ -83,14 +83,18 @@ pub const CLAW_TOOL_COUNT : u32 = 3; pub const CLAW_TOOLS : [3]str = ["t27f_widgets", "t27f_widget_make", "t27f_widget_port_plan"]; /// MEASURED 2026-10-19 with t27c 0.5.2 on specs/widgets/gatle_rule.t27: gen-js and gen-ts emit -/// declarations (consts, structs) and skip every fn body -- "this backend lowers declarations, not -/// bodies". Until a body-lowering JS backend exists the logic stage writes a tested .t27 rule -/// (the single source) but cannot generate the JavaScript the page runs. Flip this when it can. +/// declarations only and skip every fn body (#10943), so the logic stage does not go through them. +/// It goes through the path the Queen's app.wasm already takes: `t27c gen-c`, then +/// `zig cc --target=wasm32-wasi -Oz -mexec-model=reactor` with one -Wl,--export per function. +/// gatle_rule.t27 became a 490-byte wasm; 866178 comparisons against the hand-written rule.js +/// (random and boundary inputs, every function) gave 0 mismatches. Byte slices and out buffers pass +/// through the module's linear memory; module-level `var` arrays and `[]u8` out parameters work. +pub const LOGIC_VIA_WASM : bool = true; pub const GEN_JS_LOWERS_BODIES : bool = false; -/// The logic stage can produce its artefact only when the backend lowers bodies. +/// The logic stage can produce its artefact when some route lowers bodies. pub fn logic_stage_ready() -> bool { - return GEN_JS_LOWERS_BODIES; + return LOGIC_VIA_WASM or GEN_JS_LOWERS_BODIES; } /// A stage is the next of a port stage only by one step. @@ -224,8 +228,10 @@ test every_claw_tool_is_a_t27_name_the_service_mounts { assert TRI_VERBS[0] == "widget list"; } -test the_logic_stage_waits_for_a_body_lowering_backend { - assert logic_stage_ready() == false; +test the_logic_stage_runs_through_wasm_while_gen_js_lowers_no_bodies { + assert logic_stage_ready(); + assert LOGIC_VIA_WASM; + assert GEN_JS_LOWERS_BODIES == false; } test the_logic_stage_removes_exactly_the_logic_file { From d4d8ccd60095f2f276aa44627d23261e530681cf Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 15:21:31 +0700 Subject: [PATCH 06/33] widgets: html_out -- the one HTML writer for native widget views, bounded buffer, escaping, en-US grouped numbers; 4 of 4 under t27c and t27b (Closes #10938) Stage "page" of the widget factory: a view writes HTML into a fixed buffer and the host sets innerHTML. Epic #10937. Co-Authored-By: Claude Sonnet 5.5 --- specs/widgets/html_out.t27 | 243 +++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 specs/widgets/html_out.t27 diff --git a/specs/widgets/html_out.t27 b/specs/widgets/html_out.t27 new file mode 100644 index 0000000000..d1a05d1c5d --- /dev/null +++ b/specs/widgets/html_out.t27 @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/html_out.t27 -- the HTML writer every native widget view uses. Epic gHashTag/t27#10937, +; stage "page" of specs/t27f/widget_factory.t27. A tool.js builds its page with el()/h() helpers and +; DOM calls (28 files, 8920 lines); a native view writes HTML text into one module-level buffer and the +; host sets it as innerHTML. This file is that writer, once: reset, put a byte, put text (raw or +; HTML-escaped), put a number, open/close a tag, put an attribute. No allocation: the buffer is fixed +; at OUT_CAP and a write past the end sets OVERFLOW and drops the byte, so a view can never write +; outside it. The host reads out_len() bytes starting at out_at(0..). +; ASCII only (L3), English only (LANG-EN). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widgets_html_out; + +pub const OUT_CAP : u32 = 65536; +pub var OUT : [65536]u8 = [0; 65536]; +pub var OUT_LEN : u32 = 0; +pub var OVERFLOW : u32 = 0; + +pub const B_LT : u8 = 60; +pub const B_GT : u8 = 62; +pub const B_AMP : u8 = 38; +pub const B_QUOTE : u8 = 34; +pub const B_APOS : u8 = 39; +pub const B_SPACE : u8 = 32; +pub const B_SLASH : u8 = 47; +pub const B_EQ : u8 = 61; + +pub fn out_reset() -> u32 { + OUT_LEN = 0; + OVERFLOW = 0; + return 0; +} + +pub fn out_len() -> u32 { + return OUT_LEN; +} + +pub fn out_at(i: u32) -> u8 { + return OUT[i as usize]; +} + +pub fn out_overflowed() -> bool { + return OVERFLOW != 0; +} + +pub fn put(b: u8) -> u32 { + if (OUT_LEN >= OUT_CAP) { + OVERFLOW = 1; + return OUT_LEN; + } + OUT[OUT_LEN as usize] = b; + OUT_LEN = OUT_LEN + 1; + return OUT_LEN; +} + +/// Raw bytes, as the caller wrote them (markup the view itself owns). +pub fn raw(s: []const u8) -> u32 { + var i : usize = 0; + while (i < s.len) { + put(s[i]); + i = i + 1; + } + return OUT_LEN; +} + +/// Text from outside the view (data, words): & < > " ' are written as entities. +pub fn text(s: []const u8) -> u32 { + var i : usize = 0; + while (i < s.len) { + const b : u8 = s[i]; + if (b == B_AMP) { + raw("&"); + } else { + if (b == B_LT) { + raw("<"); + } else { + if (b == B_GT) { + raw(">"); + } else { + if (b == B_QUOTE) { + raw("""); + } else { + if (b == B_APOS) { + raw("'"); + } else { + put(b); + } + } + } + } + } + i = i + 1; + } + return OUT_LEN; +} + +/// An unsigned decimal. +pub fn num(n: u64) -> u32 { + if (n == 0) { + return put(48); + } + var digits : [20]u8 = [0; 20]; + var k : usize = 0; + var v : u64 = n; + while (v > 0) { + digits[k] = ((v % 10) as u8) + 48; + v = v / 10; + k = k + 1; + } + while (k > 0) { + k = k - 1; + put(digits[k]); + } + return OUT_LEN; +} + +/// An unsigned decimal with thousands separated by commas, as toLocaleString('en-US') writes it. +pub fn num_grouped(n: u64) -> u32 { + var digits : [20]u8 = [0; 20]; + var k : usize = 0; + var v : u64 = n; + if (v == 0) { + return put(48); + } + while (v > 0) { + digits[k] = ((v % 10) as u8) + 48; + v = v / 10; + k = k + 1; + } + while (k > 0) { + k = k - 1; + put(digits[k]); + if (k > 0 and k % 3 == 0) { + put(44); + } + } + return OUT_LEN; +} + +/// u32 { + put(B_LT); + return raw(tag); +} + +pub fn open_end() -> u32 { + return put(B_GT); +} + +/// name="value" with the value escaped, preceded by a space. +pub fn attr(name: []const u8, value: []const u8) -> u32 { + put(B_SPACE); + raw(name); + put(B_EQ); + put(B_QUOTE); + text(value); + return put(B_QUOTE); +} + +pub fn close(tag: []const u8) -> u32 { + put(B_LT); + put(B_SLASH); + raw(tag); + return put(B_GT); +} + +/// {0} {1} ... filled from up to three values, as fmt() does in the tool.js files. A placeholder with +/// no value writes nothing. The template is the view's own words, written raw; each value is escaped. +pub fn fill3(t: []const u8, a: []const u8, b: []const u8, c: []const u8) -> u32 { + var i : usize = 0; + while (i < t.len) { + if (t[i] == 123 and i + 2 < t.len and t[i + 2] == 125 and t[i + 1] >= 48 and t[i + 1] <= 50) { + if (t[i + 1] == 48) { text(a); } + if (t[i + 1] == 49) { text(b); } + if (t[i + 1] == 50) { text(c); } + i = i + 3; + } else { + put(t[i]); + i = i + 1; + } + } + return OUT_LEN; +} + +pub fn same_bytes(a: u32, b: []const u8) -> bool { + var i : usize = 0; + if (OUT_LEN != (b.len as u32) + a) { return false; } + while (i < b.len) { + if (OUT[(a as usize) + i] != b[i]) { return false; } + i = i + 1; + } + return true; +} + +test the_writer_escapes_text_and_leaves_markup_alone { + assert out_reset() == 0; + open("p"); + attr("title", "a \"b\" & c"); + open_end(); + text("x < y & 'z'"); + close("p"); + assert same_bytes(0, "

x < y & 'z'

"); + assert out_overflowed() == false; +} + +test numbers_are_decimal_and_grouped_like_en_us { + out_reset(); + num(0); + put(32); + num(1234567); + put(32); + num_grouped(0); + put(32); + num_grouped(999); + put(32); + num_grouped(1000); + put(32); + num_grouped(215360); + put(32); + num_grouped(1234567890); + assert same_bytes(0, "0 1234567 0 999 1,000 215,360 1,234,567,890"); +} + +test fill_puts_values_in_the_template_and_escapes_them { + out_reset(); + fill3("{0} of {1}, {2}{3}", "Gatle", "<6>", "end"); + assert same_bytes(0, "Gatle of <6>, end{3}"); +} + +test the_buffer_never_writes_past_its_end { + out_reset(); + var i : u32 = 0; + while (i < OUT_CAP + 10) { + put(65); + i = i + 1; + } + assert out_len() == OUT_CAP; + assert out_overflowed(); + assert out_at(OUT_CAP - 1) == 65; + out_reset(); + assert out_len() == 0; + assert out_overflowed() == false; +} From 45af0f32afe9df39b54322f9ea38dfa998f47374 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 15:48:08 +0700 Subject: [PATCH 07/33] widgets: jtag-verdict is the first widget drawn natively -- words, rules and page in one .t27, a 9269-byte wasm, 3015 DOM comparisons against tool.js, 0 differences (Closes #10938) specs/widgets/jtag_verdict.t27 holds the trinity spec's words and a native page (w_init, w_input, w_click, w_hash, w_render, w_hash_out); html_out.t27 becomes a stateless writer (a pub var does not cross a use). catalog.t27 records gatle at PORT_LOGIC and jtag-verdict at PORT_DRAW. Epic #10937. Co-Authored-By: Claude Sonnet 5.5 --- specs/widgets/catalog.t27 | 22 +- specs/widgets/html_out.t27 | 285 ++++++-------- specs/widgets/jtag_verdict.t27 | 689 +++++++++++++++++++++++++++++++++ 3 files changed, 824 insertions(+), 172 deletions(-) create mode 100644 specs/widgets/jtag_verdict.t27 diff --git a/specs/widgets/catalog.t27 b/specs/widgets/catalog.t27 index 35f526116f..f74c118276 100644 --- a/specs/widgets/catalog.t27 +++ b/specs/widgets/catalog.t27 @@ -31,9 +31,13 @@ ; 3. 23 tools have a Node data builder (5572 lines) and 24 a Python card renderer (3415 lines). ; The card is a picture of the page; t27f draws pictures from specs already (SF_STILL). ; -- -; Not claimed: that any hand column has been rewritten. Every tool is at PORT_WORDS today: its -; words live in its spec, its logic and drawing still live in JavaScript. A tool moves up only when -; its output is byte-identical to the hand version, and the factory spec states that gate. +; Stages reached, each with a differential against the hand-written version on the trinity side: +; gatle is at PORT_LOGIC (specs/widgets/gatle_rule.t27: 866178 comparisons vs rule.js, 0 mismatches) +; and jtag-verdict at PORT_DRAW (specs/widgets/jtag_verdict.t27: 3015 DOM comparisons vs tool.js, +; 0 mismatches). Every other tool is at PORT_WORDS: its words live in its spec, its logic and drawing +; still live in JavaScript. A stage is reached on the t27 side first; the page in trinity keeps +; loading the hand-written file until that repo's migration PR deletes it. A tool moves up only when +; its output is identical to the hand version, and the factory spec states that gate. ; The Swift and React widgets are counted, not classified: they are another renderer's targets. ; ASCII only (L3), English only (LANG-EN). ; phi^2 + 1/phi^2 = 3 | TRINITY @@ -83,7 +87,7 @@ pub const PORT_LOGIC : u8 = 2; // pure logic is .t27, generated to JS by gen-ts pub const PORT_DRAW : u8 = 3; // the page is drawn by the trusted renderer from a plan pub const PORT_SEALED : u8 = 4; // the spec seal keys the page, the data and the card pub const PORT_STAGES : u8 = 5; -pub const TOOL_PORT : [28]u8 = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]; +pub const TOOL_PORT : [28]u8 = [1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]; ; Port waves, by the hand lines a tool still carries (logic + draw + css + data + card). ; Wave 1 is the small tools, wave 3 the largest; a wave is a batch of port issues. @@ -219,9 +223,11 @@ test the_waves_cover_every_tool_once { assert hand_lines(12) == 1432; } -test no_tool_has_left_the_words_stage_yet { - assert count_port(PORT_WORDS) == TOOL_COUNT; - assert count_port(PORT_LOGIC) == 0; - assert count_port(PORT_DRAW) == 0; +test two_tools_have_left_the_words_stage_with_a_differential { + assert count_port(PORT_WORDS) == TOOL_COUNT - 2; + assert count_port(PORT_LOGIC) == 1; + assert count_port(PORT_DRAW) == 1; assert count_port(PORT_SEALED) == 0; + assert TOOL_PORT[7] == PORT_LOGIC; + assert TOOL_PORT[10] == PORT_DRAW; } diff --git a/specs/widgets/html_out.t27 b/specs/widgets/html_out.t27 index d1a05d1c5d..2aff837aa0 100644 --- a/specs/widgets/html_out.t27 +++ b/specs/widgets/html_out.t27 @@ -1,107 +1,82 @@ // SPDX-License-Identifier: Apache-2.0 ; specs/widgets/html_out.t27 -- the HTML writer every native widget view uses. Epic gHashTag/t27#10937, ; stage "page" of specs/t27f/widget_factory.t27. A tool.js builds its page with el()/h() helpers and -; DOM calls (28 files, 8920 lines); a native view writes HTML text into one module-level buffer and the -; host sets it as innerHTML. This file is that writer, once: reset, put a byte, put text (raw or -; HTML-escaped), put a number, open/close a tag, put an attribute. No allocation: the buffer is fixed -; at OUT_CAP and a write past the end sets OVERFLOW and drops the byte, so a view can never write -; outside it. The host reads out_len() bytes starting at out_at(0..). +; DOM calls (28 files, 8920 lines); a native view writes HTML text into the buffer the host gave it +; (a region of the wasm's linear memory) and the host sets it as innerHTML. This file is that writer, +; once: put a byte, put text (raw or HTML-escaped), put a number, put an attribute, fill a template. +; -- +; STATELESS ON PURPOSE. Every function takes the buffer o and the length written so far n and returns +; the new length. There is no module-level state because t27c's splicer does not carry a `pub var` +; across `use` (measured: a view that imported OUT and OUT_LEN got "undeclared identifier"), and +; follows only the names a module itself mentions, not the names an imported function mentions: so a +; view imports every function it calls, and these functions call only each other through the +; ANCHOR in the view (see jtag_verdict.t27, html_out_anchor). A write past the end of o returns OVER, +; and every function returns OVER at once when it is given OVER, so a view can never write outside o +; and checks once, at the end, with ok(n). ; ASCII only (L3), English only (LANG-EN). ; phi^2 + 1/phi^2 = 3 | TRINITY module widgets_html_out; -pub const OUT_CAP : u32 = 65536; -pub var OUT : [65536]u8 = [0; 65536]; -pub var OUT_LEN : u32 = 0; -pub var OVERFLOW : u32 = 0; - -pub const B_LT : u8 = 60; -pub const B_GT : u8 = 62; -pub const B_AMP : u8 = 38; -pub const B_QUOTE : u8 = 34; -pub const B_APOS : u8 = 39; -pub const B_SPACE : u8 = 32; -pub const B_SLASH : u8 = 47; -pub const B_EQ : u8 = 61; - -pub fn out_reset() -> u32 { - OUT_LEN = 0; - OVERFLOW = 0; - return 0; -} - -pub fn out_len() -> u32 { - return OUT_LEN; -} +pub const OVER : u32 = 4294967295; +pub const H_LT : u8 = 60; +pub const H_GT : u8 = 62; +pub const H_AMP : u8 = 38; +pub const H_QUOTE : u8 = 34; +pub const H_APOS : u8 = 39; +pub const H_SPACE : u8 = 32; +pub const H_SLASH : u8 = 47; +pub const H_EQ : u8 = 61; -pub fn out_at(i: u32) -> u8 { - return OUT[i as usize]; +pub fn ok(n: u32) -> bool { + return n != OVER; } -pub fn out_overflowed() -> bool { - return OVERFLOW != 0; -} - -pub fn put(b: u8) -> u32 { - if (OUT_LEN >= OUT_CAP) { - OVERFLOW = 1; - return OUT_LEN; - } - OUT[OUT_LEN as usize] = b; - OUT_LEN = OUT_LEN + 1; - return OUT_LEN; +pub fn put(o: []u8, n: u32, b: u8) -> u32 { + if (n >= (o.len as u32)) { return OVER; } + o[n as usize] = b; + return n + 1; } /// Raw bytes, as the caller wrote them (markup the view itself owns). -pub fn raw(s: []const u8) -> u32 { +pub fn raw(o: []u8, n0: u32, s: []const u8) -> u32 { + var n : u32 = n0; var i : usize = 0; while (i < s.len) { - put(s[i]); + n = put(o, n, s[i]); i = i + 1; } - return OUT_LEN; + return n; } -/// Text from outside the view (data, words): & < > " ' are written as entities. -pub fn text(s: []const u8) -> u32 { +/// One byte of text, escaped: & < > " ' become entities. +pub fn put_escaped(o: []u8, n: u32, b: u8) -> u32 { + if (b == H_AMP) { return raw(o, n, "&"); } + if (b == H_LT) { return raw(o, n, "<"); } + if (b == H_GT) { return raw(o, n, ">"); } + if (b == H_QUOTE) { return raw(o, n, """); } + if (b == H_APOS) { return raw(o, n, "'"); } + return put(o, n, b); +} + +/// Text from outside the view (data, words). +pub fn text(o: []u8, n0: u32, s: []const u8) -> u32 { + var n : u32 = n0; var i : usize = 0; while (i < s.len) { - const b : u8 = s[i]; - if (b == B_AMP) { - raw("&"); - } else { - if (b == B_LT) { - raw("<"); - } else { - if (b == B_GT) { - raw(">"); - } else { - if (b == B_QUOTE) { - raw("""); - } else { - if (b == B_APOS) { - raw("'"); - } else { - put(b); - } - } - } - } - } + n = put_escaped(o, n, s[i]); i = i + 1; } - return OUT_LEN; + return n; } /// An unsigned decimal. -pub fn num(n: u64) -> u32 { - if (n == 0) { - return put(48); - } +pub fn num(o: []u8, n0: u32, v0: u64) -> u32 { + var n : u32 = n0; + if (v0 == 0) { return put(o, n, 48); } var digits : [20]u8 = [0; 20]; var k : usize = 0; - var v : u64 = n; + var v : u64 = v0; while (v > 0) { digits[k] = ((v % 10) as u8) + 48; v = v / 10; @@ -109,19 +84,18 @@ pub fn num(n: u64) -> u32 { } while (k > 0) { k = k - 1; - put(digits[k]); + n = put(o, n, digits[k]); } - return OUT_LEN; + return n; } /// An unsigned decimal with thousands separated by commas, as toLocaleString('en-US') writes it. -pub fn num_grouped(n: u64) -> u32 { +pub fn num_grouped(o: []u8, n0: u32, v0: u64) -> u32 { + var n : u32 = n0; + if (v0 == 0) { return put(o, n, 48); } var digits : [20]u8 = [0; 20]; var k : usize = 0; - var v : u64 = n; - if (v == 0) { - return put(48); - } + var v : u64 = v0; while (v > 0) { digits[k] = ((v % 10) as u8) + 48; v = v / 10; @@ -129,115 +103,98 @@ pub fn num_grouped(n: u64) -> u32 { } while (k > 0) { k = k - 1; - put(digits[k]); - if (k > 0 and k % 3 == 0) { - put(44); - } + n = put(o, n, digits[k]); + if (k > 0 and k % 3 == 0) { n = put(o, n, 44); } } - return OUT_LEN; -} - -/// u32 { - put(B_LT); - return raw(tag); -} - -pub fn open_end() -> u32 { - return put(B_GT); + return n; } /// name="value" with the value escaped, preceded by a space. -pub fn attr(name: []const u8, value: []const u8) -> u32 { - put(B_SPACE); - raw(name); - put(B_EQ); - put(B_QUOTE); - text(value); - return put(B_QUOTE); -} - -pub fn close(tag: []const u8) -> u32 { - put(B_LT); - put(B_SLASH); - raw(tag); - return put(B_GT); -} - -/// {0} {1} ... filled from up to three values, as fmt() does in the tool.js files. A placeholder with -/// no value writes nothing. The template is the view's own words, written raw; each value is escaped. -pub fn fill3(t: []const u8, a: []const u8, b: []const u8, c: []const u8) -> u32 { +pub fn attr(o: []u8, n0: u32, name: []const u8, value: []const u8) -> u32 { + var n : u32 = put(o, n0, H_SPACE); + n = raw(o, n, name); + n = put(o, n, H_EQ); + n = put(o, n, H_QUOTE); + n = text(o, n, value); + return put(o, n, H_QUOTE); +} + +/// {0} {1} {2} filled from k values (k up to 3), as fill() does in the tool.js files: a placeholder +/// whose index is not below k stays as written. The template and each value are escaped as text. +pub fn fill(o: []u8, n0: u32, t: []const u8, a: []const u8, b: []const u8, c: []const u8, k: u32) -> u32 { + var n : u32 = n0; var i : usize = 0; while (i < t.len) { - if (t[i] == 123 and i + 2 < t.len and t[i + 2] == 125 and t[i + 1] >= 48 and t[i + 1] <= 50) { - if (t[i + 1] == 48) { text(a); } - if (t[i + 1] == 49) { text(b); } - if (t[i + 1] == 50) { text(c); } + if (t[i] == 123 and i + 2 < t.len and t[i + 2] == 125 and t[i + 1] >= 48 and t[i + 1] < 48 + (k as u8) and t[i + 1] <= 50) { + if (t[i + 1] == 48) { n = text(o, n, a); } + if (t[i + 1] == 49) { n = text(o, n, b); } + if (t[i + 1] == 50) { n = text(o, n, c); } i = i + 3; } else { - put(t[i]); + n = put_escaped(o, n, t[i]); i = i + 1; } } - return OUT_LEN; + return n; } -pub fn same_bytes(a: u32, b: []const u8) -> bool { +pub fn same_bytes(o: []const u8, n: u32, want: []const u8) -> bool { + if (n != (want.len as u32)) { return false; } var i : usize = 0; - if (OUT_LEN != (b.len as u32) + a) { return false; } - while (i < b.len) { - if (OUT[(a as usize) + i] != b[i]) { return false; } + while (i < want.len) { + if (o[i] != want[i]) { return false; } i = i + 1; } return true; } test the_writer_escapes_text_and_leaves_markup_alone { - assert out_reset() == 0; - open("p"); - attr("title", "a \"b\" & c"); - open_end(); - text("x < y & 'z'"); - close("p"); - assert same_bytes(0, "

x < y & 'z'

"); - assert out_overflowed() == false; + var o : [128]u8 = [0; 128]; + var n : u32 = raw(o[0..], 0, ""); + n = text(o[0..], n, "x < y & 'z'"); + n = raw(o[0..], n, "

"); + assert same_bytes(o[0..], n, "

x < y & 'z'

"); + assert ok(n); } test numbers_are_decimal_and_grouped_like_en_us { - out_reset(); - num(0); - put(32); - num(1234567); - put(32); - num_grouped(0); - put(32); - num_grouped(999); - put(32); - num_grouped(1000); - put(32); - num_grouped(215360); - put(32); - num_grouped(1234567890); - assert same_bytes(0, "0 1234567 0 999 1,000 215,360 1,234,567,890"); + var o : [128]u8 = [0; 128]; + var n : u32 = num(o[0..], 0, 0); + n = put(o[0..], n, 32); + n = num(o[0..], n, 1234567); + n = put(o[0..], n, 32); + n = num_grouped(o[0..], n, 0); + n = put(o[0..], n, 32); + n = num_grouped(o[0..], n, 999); + n = put(o[0..], n, 32); + n = num_grouped(o[0..], n, 1000); + n = put(o[0..], n, 32); + n = num_grouped(o[0..], n, 215360); + n = put(o[0..], n, 32); + n = num_grouped(o[0..], n, 1234567890); + assert same_bytes(o[0..], n, "0 1234567 0 999 1,000 215,360 1,234,567,890"); } test fill_puts_values_in_the_template_and_escapes_them { - out_reset(); - fill3("{0} of {1}, {2}{3}", "Gatle", "<6>", "end"); - assert same_bytes(0, "Gatle of <6>, end{3}"); + var o : [128]u8 = [0; 128]; + var n : u32 = fill(o[0..], 0, "{0} of {1}, {2}{3}", "Gatle", "<6>", "end", 3); + assert same_bytes(o[0..], n, "Gatle of <6>, end{3}"); + n = fill(o[0..], 0, "{0}{1} it's", "x", "", "", 1); + assert same_bytes(o[0..], n, "x{1} it's"); } test the_buffer_never_writes_past_its_end { - out_reset(); - var i : u32 = 0; - while (i < OUT_CAP + 10) { - put(65); - i = i + 1; - } - assert out_len() == OUT_CAP; - assert out_overflowed(); - assert out_at(OUT_CAP - 1) == 65; - out_reset(); - assert out_len() == 0; - assert out_overflowed() == false; + var o : [8]u8 = [0; 8]; + var n : u32 = raw(o[0..], 0, "12345678"); + assert ok(n); + assert n == 8; + n = raw(o[0..], n, "9"); + assert ok(n) == false; + n = text(o[0..], n, "more"); + assert n == OVER; + assert o[7] == 56; + var p : [4]u8 = [0; 4]; + assert text(p[0..], 0, "<<") == OVER; } diff --git a/specs/widgets/jtag_verdict.t27 b/specs/widgets/jtag_verdict.t27 new file mode 100644 index 0000000000..144177d227 --- /dev/null +++ b/specs/widgets/jtag_verdict.t27 @@ -0,0 +1,689 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/jtag-verdict.t27 -- one IR capture and one DR word, and what they say about the link +; Source of truth for public/widgets/jtag-verdict/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English of its own: every +; word on screen is a SAY_ constant here, every number the tool relies on is a K_ constant. +; ASCII only (L3), English only (LANG-EN). + +; WHAT IT DOES: the reader types the raw IR capture and the raw DR word a JTAG probe shifted out +; (or picks one of the seven vectors below). The page runs the same five checks, in the same order, +; as t27 specs/port/tools/jtag/tdo_verdict.t27 and `tri fpga-jtag --decode HEX --ir HEX`: all ones, +; all zeros, the IR capture rule, bit 0 of the DR word, and only then the IDCODE itself. It shows +; which check decided, the bits it looked at, and the command that prints the same verdict. + +; THE RULE: IEEE 1149.1 Capture-IR loads binary 01 into the two least significant IR cells, so +; the first two bits out of TDO after an IR scan are 1 then 0 on every compliant TAP. Above them +; the device may put anything: Xilinx 7-series BSDL files give INSTRUCTION_CAPTURE as XXXX01, the +; upper four being status. 0x35 (110101) is one value of that shape and is used here as the +; example; it is not a bench reading. A DR word with bit 0 clear is BYPASS when the IR capture +; holds 01, and noise when it does not: the same even word gets two different verdicts. + +; DATA PROVENANCE: the seven vectors are the test vectors of t27 specs/port/tools/jtag/ +; tdo_verdict.t27 at commit 60166a56194b5730dea355e704e98bc4f540cf14 (gHashTag/t27#8032, issue +; #8030), and the tests below assert the same verdicts. 0x13636093 is the raw IDCODE read from our +; XC7A200T on 2026-10-04 (openFPGALoader 1.1.1 --detect --verbose-level 2, see the idcode widget); +; 0x03631093 is the XC7A100T entry of openFPGALoader v1.1.1 src/part.hpp. + +; WHAT IT DOES NOT CLAIM: that it talks to a cable. Nothing is read from hardware; the values are +; typed. A clean verdict says the shift looks sane, not that the board is configured or healthy. +; The device table has two entries; the idcode widget carries openFPGALoader's full list. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_jtag_verdict; + +use widgets::html_out::ok; +use widgets::html_out::OVER; +use widgets::html_out::put; +use widgets::html_out::raw; +use widgets::html_out::put_escaped; +use widgets::html_out::text; +use widgets::html_out::num; +use widgets::html_out::attr; +use widgets::html_out::fill; +use widgets::html_out::H_LT; +use widgets::html_out::H_GT; +use widgets::html_out::H_AMP; +use widgets::html_out::H_QUOTE; +use widgets::html_out::H_APOS; +use widgets::html_out::H_SPACE; +use widgets::html_out::H_SLASH; +use widgets::html_out::H_EQ; +use widgets::page_reader::match_in; + + +pub const KIND : str = "widget-tool"; +pub const ID : str = "jtag-verdict"; +pub const TITLE : str = "JTAG verdict: IR capture and DR word, BYPASS or a broken shift"; +pub const DESCRIPTION : str = "Type the IR capture and the DR word a JTAG probe shifted out. Five checks in a fixed order say stuck line, broken shift, BYPASS or a real IDCODE, and which bit decided."; +pub const IMAGE_ALT : str = "JTAG verdict card: the same even DR word 0x13636092 read twice. With IR capture 0x35, whose low two bits are 01, the verdict is BYPASS: the TAP is alive. With IR capture 0x34, low bits 00, the verdict is broken shift: the DR word means nothing yet. Below, the five checks in order, from t27 spec tdo_verdict.t27 with 8 tests."; +pub const HOOK : str = "Same DR word, two verdicts: IR 0x35 says BYPASS, IR 0x34 says your shift is broken. Type what your probe read and see which bit decides."; +pub const CATEGORY : str = "fpga"; +pub const DATA_SOURCES : [4]str = ["t27 specs/port/tools/jtag/tdo_verdict.t27 at 60166a56194b5730dea355e704e98bc4f540cf14 (gHashTag/t27#8032, issue #8030): the order of the checks and the seven test vectors", "IEEE Std 1149.1 Capture-IR: binary 01 in the two least significant instruction register cells; Xilinx 7-series BSDL INSTRUCTION_CAPTURE XXXX01", "Bench 2026-10-04 UTC: openFPGALoader 1.1.1 --detect --verbose-level 2 on our XC7A200T: raw IDCODE 0x13636093 (the idcode widget's record)", "openFPGALoader v1.1.1 src/part.hpp: 0x03636093 xc7a200, 0x03631093 xc7a100"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers the tool relies on ------------------------------------------------------------ +; Capture-IR: the low two bits must read 01. +pub const K_IR_MASK : u32 = 3; +pub const K_IR_LOW : u32 = 1; +; 0xFFFFFFFF: TDO floating high, nothing drives it. +pub const K_ALL_ONES : u32 = 4294967295; +; Verdict codes, the same numbers as tdo_verdict.t27; they index SAY_VERDICT_NAMES from 1. +pub const K_V_STUCK_HIGH : u8 = 1; +pub const K_V_STUCK_LOW : u8 = 2; +pub const K_V_BROKEN_SHIFT : u8 = 3; +pub const K_V_BYPASS : u8 = 4; +pub const K_V_IDCODE : u8 = 5; +; IDCODE lookup ignores the version nibble: 0x0FFFFFFF. +pub const K_VERSION_MASK : u32 = 268435455; +pub const K_KNOWN_IDCODE : [2]u32 = [56844435, 56823955]; +; The seven vectors: IR capture, DR word, expected verdict. +pub const K_CASE_IR : [7]u32 = [53, 53, 52, 54, 63, 0, 63]; +pub const K_CASE_DR : [7]u32 = [325279891, 325279890, 325279890, 325279891, 4294967295, 0, 325279891]; +pub const K_CASE_WANT : [7]u8 = [5, 4, 3, 3, 1, 2, 3]; +; The vector the page opens on: the even word behind a good IR capture. +pub const K_START_CASE : u8 = 1; + +; --- Words on screen ------------------------------------------------------------------------ +; Templates take {0}, {1}, ... in order. +pub const SAY_LOADING : str = "Loading"; +pub const SAY_IR_LABEL : str = "IR capture (hex)"; +pub const SAY_DR_LABEL : str = "DR word (hex)"; +pub const SAY_IR_HINT : str = "The bits TDO shifted out during the IR scan, e.g. 0x35"; +pub const SAY_DR_HINT : str = "The 32 bits shifted out of the data register, e.g. 0x13636093"; +pub const SAY_CASES_LABEL : str = "The spec's test vectors"; +pub const SAY_CASE_NAMES : [7]str = ["real IDCODE", "BYPASS", "even word, IR 0x34", "odd word, IR 0x36", "all ones", "all zeros", "real-looking word, IR 0x3F"]; +pub const SAY_VERDICT_NAMES : [5]str = ["stuck high", "stuck low", "broken shift", "BYPASS", "IDCODE"]; +pub const SAY_VERDICT_WHY : [5]str = ["Every DR bit reads 1: nothing drives TDO. Check the cable's target voltage, board power, the ribbon and its pin 1, then the TDO wire.", "DR and IR both read 0: TDO is held low. Suspect the cable's own buffer before the board.", "IR capture {0} ends in {1}, not 01. Capture-IR always loads 01, so the shift itself is broken: TCK, the TMS order, or the TDO path. The DR word means nothing yet.", "IR capture {0} ends in 01, so the TAP shifts correctly, and DR bit 0 is 0: BYPASS is selected, not IDCODE. Load the IDCODE instruction, or this was read one bit late.", "IR capture {0} ends in 01 and DR bit 0 is 1: a real IDCODE."]; +pub const SAY_LADDER_TITLE : str = "The checks, in the order they run"; +pub const SAY_LADDER_STEPS : [5]str = ["DR is all ones", "DR is 0 and IR is 0", "IR does not end in 01", "DR bit 0 is 0", "otherwise"]; +pub const SAY_LADDER_DECIDED : str = "decided here"; +pub const SAY_LADDER_PASSED : str = "passed"; +pub const SAY_LADDER_SKIPPED : str = "not reached"; +pub const SAY_BITS_TITLE : str = "The bits that decide"; +pub const SAY_BITS_IR : str = "IR, low bit first"; +pub const SAY_BITS_DR : str = "DR bit 0"; +pub const SAY_BITS_IR_RULE : str = "must read 1 then 0"; +pub const SAY_DEVICE_KNOWN : str = "Masked IDCODE {0}: {1}"; +pub const SAY_DEVICE_NAMES : [2]str = ["XC7A200T", "XC7A100T"]; +pub const SAY_DEVICE_UNKNOWN : str = "Masked IDCODE {0}: not in this page's two-entry table."; +pub const SAY_DECODER_LINK : str = "Take the word apart in the IDCODE decoder"; +pub const SAY_CLI_TITLE : str = "The same verdict on the command line"; +pub const SAY_CLI_LINE : str = "tri fpga-jtag --decode {0} --ir {1}"; +pub const SAY_CLI_EXIT : str = "exit {0}"; +pub const SAY_BAD_HEX : str = "Not a hex number that fits in 32 bits: {0}"; +pub const SAY_SELF_CHECK : str = "This page against the spec's {0} vectors: {1} agree."; +pub const SAY_NOTE : str = "Nothing is read from a cable: the values are typed. 0x35 is an example of the XXXX01 shape, not a bench reading. A clean verdict says the shift looks sane, not that the board is configured."; +pub const SAY_SPEC_LINK : str = "The spec: tdo_verdict.t27, 8 tests"; +pub const SAY_SPEC_HREF : str = "https://github.com/gHashTag/t27/pull/8032"; +pub const SAY_DECODER_HREF : str = "../idcode/#id={0}"; +; The page keeps the two words in its address: #ir=0x35&dr=0x13636092. +pub const SAY_HASH_IR : str = "ir"; +pub const SAY_HASH_DR : str = "dr"; +pub const SAY_CARD_TITLE : str = "JTAG verdict"; +pub const SAY_CARD_BRAND : str = "Trinity S3AI"; +pub const SAY_CARD_HEAD : str = "Same DR word, two verdicts"; +pub const SAY_CARD_IR_LINE : str = "IR capture {0}, low bits {1}"; +pub const SAY_CARD_SPEC : str = "t27 spec tdo_verdict.t27, 8 tests"; +pub const SAY_CARD_URL : str = "t27.ai/widgets/jtag-verdict/"; + +test the_card_is_x_shaped { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; +} + +test the_page_sends_nothing { + assert SENDS_NOTHING == true; + assert READS_LOCAL_FILES == false; + assert CATEGORY == "fpga"; +} + +test the_vectors_cover_every_verdict { + assert K_CASE_WANT[0] == K_V_IDCODE; + assert K_CASE_WANT[1] == K_V_BYPASS; + assert K_CASE_WANT[2] == K_V_BROKEN_SHIFT; + assert K_CASE_WANT[4] == K_V_STUCK_HIGH; + assert K_CASE_WANT[5] == K_V_STUCK_LOW; + assert K_CASE_DR[4] == K_ALL_ONES; +} + +test one_even_word_two_verdicts { + assert K_CASE_DR[1] == K_CASE_DR[2]; + assert K_CASE_IR[1] % 4 == K_IR_LOW; + assert K_CASE_IR[2] % 4 != K_IR_LOW; + assert K_CASE_DR[1] % 2 == 0; + assert K_CASE_WANT[1] != K_CASE_WANT[2]; +} + +test the_ir_rule_decides_the_broken_shifts { + assert K_CASE_IR[3] % 4 != K_IR_LOW; + assert K_CASE_IR[6] % 4 != K_IR_LOW; + assert K_CASE_DR[6] % 2 == 1; + assert K_CASE_IR[0] % 4 == K_IR_LOW; + assert K_CASE_DR[0] % 2 == 1; +} + +test the_bench_word_masks_to_the_200t { + assert K_CASE_DR[0] % (K_VERSION_MASK + 1) == K_KNOWN_IDCODE[0]; + assert K_IR_MASK == 3; +} + +test the_page_opens_on_bypass { + assert K_V_IDCODE == 5; + assert K_START_CASE < 7; + assert K_CASE_WANT[K_START_CASE] == K_V_BYPASS; +} + +; ================================================================================================= +; THE NATIVE PAGE (epic gHashTag/t27#10937, stage "page" of specs/t27f/widget_factory.t27) +; Everything below replaces public/widgets/jtag-verdict/tool.js (171 lines of hand-written JavaScript). +; The words and numbers above are unchanged. The page is these functions, built to wasm (t27c gen-c, +; then zig cc --target=wasm32-wasi) and driven by the one host glue every native widget shares: it +; forwards input, click and hashchange events, then sets the bytes w_render() wrote as the page's +; innerHTML, keeping the focused input. Host protocol, all exports of this module: +; w_init(hash) the page opens; hash is the address's #fragment bytes (empty when none) +; w_input(id, text) a field changed: id 0 is the IR field, 1 the DR field; its whole text +; w_click(i) preset button i was pressed +; w_hash(hash) the address changed +; w_render(dst) write the page's HTML into dst, return its length (0 on overflow) +; w_hash_out(dst) the address to show after the last event (length 0: leave it) +; The state is the two field texts; the rest is derived on every render, as tool.js's update() did. +; Not carried over from JavaScript: Unicode white space around a typed number (its \s) is not ASCII; +; field text is cut at FIELD_CAP bytes; a malformed %-escape in the address is read as no value +; (decodeURIComponent would throw). +; MEASURED 2026-10-19: t27c gen, a 6-line Zig export wrapper and `zig build-exe -target +; wasm32-freestanding -O ReleaseSmall` give a 9269-byte wasm. A browser harness loaded the original +; page (tool.js) and this one side by side and compared their DOM (tag, attributes, text, the two +; input values, the address) after 3015 actions: the initial page, the 7 preset buttons, 3000 typed +; values (valid hex with and without 0x, spaces and underscores, 9-digit overflow, empty, quotes, +; markup, random ASCII) and 7 hash changes, plus 7 page loads that carry an address: 0 differences. +; Changing one text node of the native page makes the harness report a difference (control). +; Not carried over from the page: nothing else; focus after a preset click is kept by the host. + +pub const FIELD_CAP : u32 = 255; +pub var IR_TEXT : [255]u8 = [0; 255]; +pub var IR_LEN : u32 = 0; +pub var DR_TEXT : [255]u8 = [0; 255]; +pub var DR_LEN : u32 = 0; +pub var PRESSED : [7]u8 = [0; 7]; +pub var HASH_NEW : [64]u8 = [0; 64]; +pub var HASH_NEW_LEN : u32 = 0; +pub var HEX_A : [16]u8 = [0; 16]; +pub var HEX_B : [16]u8 = [0; 16]; +pub var BIN2 : [2]u8 = [0; 2]; +pub var DECODED : [64]u8 = [0; 64]; +pub const NOT_HEX : i64 = -1; + +/// The five checks, in the order of specs/port/tools/jtag/tdo_verdict.t27 (written in an older dialect +/// that cannot be imported; the seven vectors above are its test vectors and the tests below hold this to them). +pub fn jtag_verdict(ir: u32, dr: u32) -> u8 { + if (dr == K_ALL_ONES) { return K_V_STUCK_HIGH; } + if (dr == 0 and ir == 0) { return K_V_STUCK_LOW; } + if ((ir & K_IR_MASK) != K_IR_LOW) { return K_V_BROKEN_SHIFT; } + if ((dr & 1) == 0) { return K_V_BYPASS; } + return K_V_IDCODE; +} + +fn is_ws(b: u8) -> bool { + return b == 32 or (b >= 9 and b <= 13); +} + +fn hex_val(b: u8) -> i64 { + if (b >= 48 and b <= 57) { return (b - 48) as i64; } + if (b >= 97 and b <= 102) { return (b - 97) as i64 + 10; } + if (b >= 65 and b <= 70) { return (b - 65) as i64 + 10; } + return -1; +} + +/// parseHex: trim, drop white space and underscores, optional 0x, one to eight hex digits; else -1. +pub fn parse_hex(s: []const u8) -> i64 { + var pos : u32 = 0; + var first : u8 = 0; + var digits : u32 = 0; + var val : i64 = 0; + var i : usize = 0; + while (i < s.len) { + const b : u8 = s[i]; + i = i + 1; + if (is_ws(b) or b == 95) { continue; } + if (pos == 1 and first == 48 and (b == 120 or b == 88)) { + digits = 0; + val = 0; + pos = pos + 1; + continue; + } + const h : i64 = hex_val(b); + if (h < 0) { return NOT_HEX; } + if (pos == 0) { first = b; } + digits = digits + 1; + if (digits <= 8) { val = val * 16 + h; } + pos = pos + 1; + } + if (digits < 1 or digits > 8) { return NOT_HEX; } + return val; +} + +/// hexN: "0x" and the lowercase hex digits of v, at least min_digits of them; written to dst, length returned. +pub fn hex_to(dst: []u8, v: u32, min_digits: u32) -> u32 { + dst[0] = 48; + dst[1] = 120; + var count : u32 = 1; + var t : u32 = v / 16; + while (t > 0) { + count = count + 1; + t = t / 16; + } + var width : u32 = count; + if (min_digits > width) { width = min_digits; } + var k : u32 = width; + while (k > 0) { + k = k - 1; + var d : u32 = v; + var sh : u32 = 0; + while (sh < k) { + d = d / 16; + sh = sh + 1; + } + const nib : u32 = d % 16; + if (nib < 10) { dst[(2 + width - 1 - k) as usize] = (nib as u8) + 48; } else { dst[(2 + width - 1 - k) as usize] = (nib as u8) + 87; } + } + return 2 + width; +} + +fn ir_value() -> i64 { + return parse_hex(IR_TEXT[0..(IR_LEN as usize)]); +} + +fn dr_value() -> i64 { + return parse_hex(DR_TEXT[0..(DR_LEN as usize)]); +} + +fn set_text(which: u32, s: []const u8) -> u32 { + var n : usize = s.len; + if (n > (FIELD_CAP as usize)) { n = FIELD_CAP as usize; } + var i : usize = 0; + while (i < n) { + if (which == 0) { IR_TEXT[i] = s[i]; } else { DR_TEXT[i] = s[i]; } + i = i + 1; + } + if (which == 0) { IR_LEN = n as u32; } else { DR_LEN = n as u32; } + return 0; +} + +fn set_field_hex(which: u32, v: u32, digits: u32) -> u32 { + const n : u32 = hex_to(HEX_A[0..], v, digits); + return set_text(which, HEX_A[0..(n as usize)]); +} + +/// The two fields are good when both parse; then the pressed preset and the new address follow. +fn recompute(write_hash: bool) -> u32 { + HASH_NEW_LEN = 0; + const ir : i64 = ir_value(); + const dr : i64 = dr_value(); + if (ir < 0 or dr < 0) { return 0; } + var i : u32 = 0; + while (i < 7) { + PRESSED[i as usize] = 0; + if (K_CASE_IR[i as usize] == (ir as u32) and K_CASE_DR[i as usize] == (dr as u32)) { PRESSED[i as usize] = 1; } + i = i + 1; + } + if (write_hash) { + var n : u32 = 0; + HASH_NEW[0] = 35; + n = 1; + var k : usize = 0; + while (k < SAY_HASH_IR.len) { HASH_NEW[n as usize] = SAY_HASH_IR[k]; n = n + 1; k = k + 1; } + HASH_NEW[n as usize] = 61; + n = n + 1; + n = n + hex_to(HASH_NEW[(n as usize)..], ir as u32, 2); + HASH_NEW[n as usize] = 38; + n = n + 1; + k = 0; + while (k < SAY_HASH_DR.len) { HASH_NEW[n as usize] = SAY_HASH_DR[k]; n = n + 1; k = k + 1; } + HASH_NEW[n as usize] = 61; + n = n + 1; + n = n + hex_to(HASH_NEW[(n as usize)..], dr as u32, 8); + HASH_NEW_LEN = n; + } + return 0; +} + +/// fromHash's get(): the hex value of #key=... or &key=... in the address, -1 when absent or unreadable. +fn hash_get(h: []const u8, key: []const u8) -> i64 { + var p : usize = 0; + while (p + 1 + key.len < h.len) { + if ((h[p] == 35 or h[p] == 38) and match_in(h, p + 1, h.len, key) and h[p + 1 + key.len] == 61) { + var a : usize = p + 2 + key.len; + var e : usize = a; + while (e < h.len and h[e] != 38) { e = e + 1; } + if (e == a) { return NOT_HEX; } + var n : usize = 0; + var i : usize = a; + while (i < e) { + if (n >= 64) { return NOT_HEX; } + if (h[i] == 37) { + if (i + 2 >= e) { return NOT_HEX; } + const hi : i64 = hex_val(h[i + 1]); + const lo : i64 = hex_val(h[i + 2]); + if (hi < 0 or lo < 0) { return NOT_HEX; } + DECODED[n] = ((hi * 16 + lo) as u8); + i = i + 3; + } else { + DECODED[n] = h[i]; + i = i + 1; + } + n = n + 1; + } + return parse_hex(DECODED[0..n]); + } + p = p + 1; + } + return NOT_HEX; +} + +pub fn w_init(h: []const u8) -> u32 { + const ir : i64 = hash_get(h, SAY_HASH_IR); + const dr : i64 = hash_get(h, SAY_HASH_DR); + var start : u32 = K_START_CASE as u32; + if (ir >= 0) { set_field_hex(0, ir as u32, 2); } else { set_field_hex(0, K_CASE_IR[start as usize], 2); } + if (dr >= 0) { set_field_hex(1, dr as u32, 8); } else { set_field_hex(1, K_CASE_DR[start as usize], 8); } + return recompute(false); +} + +pub fn w_input(id: u32, s: []const u8) -> u32 { + set_text(id, s); + return recompute(true); +} + +pub fn w_click(i: u32) -> u32 { + if (i >= 7) { return 0; } + set_field_hex(0, K_CASE_IR[i as usize], 2); + set_field_hex(1, K_CASE_DR[i as usize], 8); + return recompute(true); +} + +pub fn w_hash(h: []const u8) -> u32 { + const ir : i64 = hash_get(h, SAY_HASH_IR); + const dr : i64 = hash_get(h, SAY_HASH_DR); + if (ir < 0 or dr < 0) { return recompute(false); } + set_field_hex(0, ir as u32, 2); + set_field_hex(1, dr as u32, 8); + return recompute(false); +} + +pub fn w_hash_out(dst: []u8) -> u32 { + var i : u32 = 0; + while (i < HASH_NEW_LEN) { + dst[i as usize] = HASH_NEW[i as usize]; + i = i + 1; + } + return HASH_NEW_LEN; +} + +fn agree_count() -> u32 { + var n : u32 = 0; + var i : u32 = 0; + while (i < 7) { + if (jtag_verdict(K_CASE_IR[i as usize], K_CASE_DR[i as usize]) == K_CASE_WANT[i as usize]) { n = n + 1; } + i = i + 1; + } + return n; +} + +fn bit_cells(dst: []u8, n0: u32, v: u32, count: u32) -> u32 { + var n : u32 = n0; + n = raw(dst, n, ""); + var i : u32 = 0; + while (i < count) { + n = raw(dst, n, ""); + n = put(dst, n, (((v >> i) % 2) as u8) + 48); + n = raw(dst, n, ""); + i = i + 1; + } + n = raw(dst, n, ""); + return n; +} + +fn field(dst: []u8, n0: u32, id: u32, label: []const u8, hint: []const u8) -> u32 { + var n : u32 = n0; + if (id == 0) { + n = raw(dst, n, "