Skip to content

spec(t27f): renderer-neutral UI plan IR and trusted widget catalog (Closes #9962) - #9964

Merged
gHashTag merged 1 commit into
masterfrom
t27f/ui-plan-ir
Oct 17, 2026
Merged

gHashTag merged 1 commit into
masterfrom
t27f/ui-plan-ir

Conversation

@gHashTag

@gHashTag gHashTag commented Oct 17, 2026 •

Copy link
Copy Markdown
Owner

Closes #9962
Refs gHashTag/trinity#1648 (EPIC: T27F), gHashTag/trinity#1652 (MVP)

What

This PR adds specs/t27f/ui_plan.t27 (module t27f_ui_plan): the renderer-neutral UI plan IR of #1648, and the trusted widget catalog a plan may name. It is one .t27 file and nothing else. There is no TS, React or Rust in it.

A plan is data only. It holds widget ids from an allowlist, prop and action ids, data bindings, a 12-column grid layout, a theme, accessibility labels and provenance. No field holds code, markup or a script, so a renderer never evaluates anything a model wrote.

  • Catalog (allowlist): honeycomb_card, formula_view, metrics_table, run_button, text_block. Each widget lists its allowed props, allowed actions, the capabilities it needs and the data source kind it binds to. widget_id(name) gives WIDGET_UNKNOWN for any other name, and the match is case-sensitive.
  • Plan shape: UiPlan, which has ir_version, catalog_version, complete, state_rev, intent, solver, theme, title, granted capabilities, up to 4 declared sources (kind + ref), and up to 8 widget slots (widget, props, binding, action, row/col/span, label).
  • Validation: validate_plan() returns PLAN_OK or the first rule the plan breaks. first_bad_slot() names the widget that broke it. Header rules are checked first, then each slot in order:
    1. a known widget;
    2. only allowed props;
    3. a binding of the widget's own kind to a declared source;
    4. an action in the widget's list, with that action's capability granted;
    5. the widget's own capabilities granted;
    6. a label on every interactive widget;
    7. a place on the grid that no earlier slot holds.
  • Model-proposed plans (SOLVER_MODEL) go through exactly the same gate as rule-built ones (SOLVER_RULES).
  • Interrupted generation: the producer sets complete only after the last widget is written. Until then the plan is refused with ERR_INCOMPLETE, even if the part already written would pass.
  • Stale state: a plan records state_rev, the revision of the system description it was solved against. validate_fresh(p, current_rev) returns ERR_STALE before anything else, because a stale plan is solved again rather than repaired.
  • Error codes are append-only, so a code keeps its meaning across IR versions.

Two fixtures in two formats (neither is a pilot choice)

The IR fixes no number format. Choosing the T27F pilot needs a comparison of real candidates (gHashTag/trinity#1651), and this PR does not make that choice. These two plans are test fixtures only:

  • gf16_workspace(): 4 widgets.
    • Row 0: a spec card bound to specs/numeric/gf16.t27, and a formula view bound to gf16_decode_to_f32.
    • Row 1: a metrics table bound to conformance/gf16_bench_results.json, and a run button.
  • tf3_workspace(): 5 widgets, deliberately shaped differently.
    • It opens with a caption text block that has no label (not interactive).
    • It has a full-width table of conformance/tf3_vectors.json with export granted, a card for specs/numeric/tf3.t27, a formula view of tf3_to_f32 with no action (clipboard is not granted), and a run button.
    • Its sources come in another order, so every binding number differs from the GF16 plan.

Every source ref is a file in this repository.

same_rules_for_both_formats applies six defects to both fixtures through with_defect(). Each defect is located by widget kind, not by slot. Each one must earn the same verdict on both fixtures and land on that widget's own slot in each:

  1. an unknown widget;
  2. a dangling binding;
  3. an action that is not allowed;
  4. a denied permission;
  5. an unlabelled button;
  6. interrupted generation.

Malformed plans

Unless a row names TF3, each plan changes the GF16 fixture in one place and is rejected with that exact reason:

plan rejected with slot
widget id 200 ERR_UNKNOWN_WIDGET 1
widget asked for by the name eval ERR_UNKNOWN_WIDGET 3
empty slot inside widget_count ERR_UNKNOWN_WIDGET 2
binding to source 4 of 3 / source past source_count ERR_BINDING_DANGLING 2
metrics table bound to the formula source ERR_BINDING_KIND 2
card with no binding ERR_BINDING_MISSING 0
binding on a text block ERR_BINDING_KIND 4
declared source with an empty ref ERR_SOURCE -
formula view with RUN_TESTS / action id past the catalog ERR_ACTION_NOT_ALLOWED 1 / 0
run button without CAP_RUN_LAB ERR_CAPABILITY_NOT_GRANTED 3
table EXPORT_CSV without CAP_EXPORT_LOCAL (passes once granted) ERR_CAPABILITY_NOT_GRANTED 2
table without CAP_READ_METRICS ERR_CAPABILITY_NOT_GRANTED 2
run button with no label ERR_A11Y_LABEL_MISSING 3
workspace with no title ERR_A11Y_LABEL_MISSING -
columns prop on the run button ERR_PROP_NOT_ALLOWED 3
overlap, overflow past column 12, zero span, row past the grid ERR_LAYOUT 3 / 3 / 0 / 3
stream cut after 2 of 4 widgets, complete unset ERR_INCOMPLETE -
the same holes with complete set ERR_UNKNOWN_WIDGET 2
valid 2-widget TF3 prefix, complete unset (passes once set) ERR_INCOMPLETE -
plan solved at rev 3, host at rev 4 (also when otherwise malformed) ERR_STALE -
wrong IR or catalog version, empty intent, unknown solver, unknown theme, 0 or 9 widgets header errors -

Positive controls show that each rule is what decides:

  • a text block with no label passes, because it is not interactive;
  • the export plan passes once its capability is granted;
  • the TF3 prefix passes once it is marked complete;
  • validate_plan alone ignores state_rev.

Coverage of the failure cases #1648 lists:

case where it is tested
malformed output header, layout and props tests
unknown widget unknown_widget_is_rejected
unknown or disallowed action action_must_be_allowed_and_granted
missing data missing binding, dangling binding, empty source ref
denied permission missing capability for run, export and metrics
interrupted generation interrupted_generation_is_refused
stale state stale_plan_is_refused

The four invariants are checked at compile time:

  • no_script_widget_in_catalog: no name on the forbidden list (script, eval, html, iframe, raw_js, function) resolves to a catalog widget;
  • catalog_masks_are_spelled_right: each numeric mask is rebuilt from the named bits;
  • empty_slot_is_inert;
  • reserved_ids_stay_outside.

Evidence (Railway lab, t27c 90fc25b, zig 0.16.0)

The tested file has sha256 e997680ea00631eef25f0eae8b723cd6047c7c7b3639c061922d6929a8c7d4cd, the same file as head 9ae38a926.

t27c test-report --verbose specs/t27f/ui_plan.t27
  tests       13
  pass        13
  FAIL        0
  invariants  4   proved -- comptime, so compiling IS the check
  rate    100.0%
  runtime asserts executed, per test:
      14  catalog_is_an_allowlist
       9  gf16_workspace_is_valid
      11  tf3_workspace_is_valid
      25  same_rules_for_both_formats
       6  interrupted_generation_is_refused
       7  stale_plan_is_refused
       6  unknown_widget_is_rejected
      10  binding_must_name_a_declared_source
       9  action_must_be_allowed_and_granted
       9  interactive_widgets_need_a_label
       6  props_are_allowlisted
       8  layout_fits_the_grid
      11  header_rules_come_first
  vacuous passes  0 of 13   (131 runtime asserts in total)

Negative controls. Each one is a one-token mutation of a copy, with exactly one match:

mutant change result
M1 action gate if (a == ACTION_NONE) -> if (a >= ACTION_NONE) FAIL same_rules_for_both_formats, action_must_be_allowed_and_granted (11/13)
M2 interactivity WIDGET_ACTIONS[w as usize] != 0 -> == 0 FAIL in 7 tests, including both fixture tests (6/13)
M3 eval in catalog "text_block" -> "eval" in WIDGET_NAMES does not compile: the comptime assertion of invariant no_script_widget_in_catalog fails
M4 incomplete gate { return ERR_INCOMPLETE; } -> { return PLAN_OK; } FAIL same_rules_for_both_formats, interrupted_generation_is_refused (11/13)
M5 stale gate p.state_rev != current_rev -> == FAIL stale_plan_is_refused (12/13)
M6 TF3 fixture run button binding[4] = 2 -> = 1 (a spec widget bound to metrics) FAIL tf3_workspace_is_valid, same_rules_for_both_formats, stale_plan_is_refused (10/13)

CI

The only red check is duplicate-bodies, and master fails it the same way at this PR's base commit 57474a4f1. The groups it reports (bit_at, uart_tx_bit_at, led_*, flat_index, is_ascii, ...) are not in this spec.

OWNERS

specs/OWNERS.md is not edited. The t27f/ row describes topic_engine.t27, and another session may move that file out of t27f/, so that row may be rewritten elsewhere. Suggested row text for whoever edits it next:

t27f/ui_plan.t27 (module t27f_ui_plan): the T27F renderer-neutral UI plan IR of gHashTag/trinity#1648 -- the trusted widget catalog (allowlist of widgets, props, actions, capabilities), the UiPlan shape, validate_plan / validate_fresh / first_bad_slot.

Found along the way (not fixed here: Rust, needs owner-approved-foreign)

The Zig backend lowers str == str to std.mem.eql only when one side is a literal or a str parameter. A comparison of two indexed [N]str elements, such as WIDGET_NAMES[i] == FORBIDDEN_NAMES[j], is emitted as Zig == on slices and fails to compile with "operator == not allowed for type '[]const u8'". The spec avoids it by going through widget_id(name).

Not claimed

🤖 Generated with Claude Code

…loses #9962)

specs/t27f/ui_plan.t27 (module t27f_ui_plan): a widget catalog as an
allowlist (ids, allowed props, allowed actions, required capabilities,
bound source kind), the UiPlan shape (widgets, bindings, grid layout,
actions, theme, accessibility labels, provenance), and validate_plan /
first_bad_slot returning the first rule a plan breaks.

Fixture: a GF16 research workspace (spec honeycomb card, formula view,
metrics table, run button) plus ten malformed plans, each rejected with
its exact reason. Invariant: no forbidden name (script, eval, html,
iframe, raw_js, function) resolves to a catalog widget.

Lab (t27c 90fc25b, zig 0.16.0): 9/9 tests, 4 invariants proved,
0 vacuous, 82 runtime asserts. Three one-token mutants all caught.

Refs gHashTag/trinity#1648
Refs gHashTag/trinity#1652

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gHashTag
gHashTag enabled auto-merge (squash) October 17, 2026 14:46
@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-10-17 14:46:05 UTC

Summary

Status Count
Total Open PRs 50
PRs with Failing Checks 49
PRs with All Checks Green 1
READY 0
FAILING 49
PENDING 0
NO CHECKS YET 0

These columns do not partition: 0 + 49 + 0 + 0 = 49, and there are 50 open PRs. A PR is being counted twice or not at all.

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=e769ef021713 != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

@gHashTag
gHashTag merged commit cdfdc11 into master Oct 17, 2026
26 of 29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

T27F: renderer-neutral UI plan IR and trusted widget catalog in .t27 (ui_plan.t27)

1 participant