Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions specs/t27f/frontend.t27
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}
238 changes: 238 additions & 0 deletions specs/t27f/widget_factory.t27
Original file line number Diff line number Diff line change
@@ -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));
}
Loading
Loading