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..934d3a159f --- /dev/null +++ b/specs/t27f/widget_factory.t27 @@ -0,0 +1,238 @@ +// 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. 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 + +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"]; + +/// 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) { + 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_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; + 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; +} 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; +} 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; +} 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; +}