Repository navigation
spec(t27f): renderer-neutral UI plan IR and trusted widget catalog (Closes #9962) - #9964
Merged
Merged
Conversation
…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
enabled auto-merge (squash)
October 17, 2026 14:46
Contributor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #9962
Refs gHashTag/trinity#1648 (EPIC: T27F), gHashTag/trinity#1652 (MVP)
What
This PR adds
specs/t27f/ui_plan.t27(modulet27f_ui_plan): the renderer-neutral UI plan IR of #1648, and the trusted widget catalog a plan may name. It is one.t27file 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.
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)givesWIDGET_UNKNOWNfor any other name, and the match is case-sensitive.UiPlan, which hasir_version,catalog_version,complete,state_rev,intent,solver,theme,title,grantedcapabilities, up to 4 declared sources (kind + ref), and up to 8 widget slots (widget, props, binding, action, row/col/span, label).validate_plan()returnsPLAN_OKor 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:SOLVER_MODEL) go through exactly the same gate as rule-built ones (SOLVER_RULES).completeonly after the last widget is written. Until then the plan is refused withERR_INCOMPLETE, even if the part already written would pass.state_rev, the revision of the system description it was solved against.validate_fresh(p, current_rev)returnsERR_STALEbefore anything else, because a stale plan is solved again rather than repaired.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.specs/numeric/gf16.t27, and a formula view bound togf16_decode_to_f32.conformance/gf16_bench_results.json, and a run button.tf3_workspace(): 5 widgets, deliberately shaped differently.conformance/tf3_vectors.jsonwith export granted, a card forspecs/numeric/tf3.t27, a formula view oftf3_to_f32with no action (clipboard is not granted), and a run button.Every source ref is a file in this repository.
same_rules_for_both_formatsapplies six defects to both fixtures throughwith_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:Malformed plans
Unless a row names TF3, each plan changes the GF16 fixture in one place and is rejected with that exact reason:
ERR_UNKNOWN_WIDGETevalERR_UNKNOWN_WIDGETwidget_countERR_UNKNOWN_WIDGETsource_countERR_BINDING_DANGLINGERR_BINDING_KINDERR_BINDING_MISSINGERR_BINDING_KINDERR_SOURCERUN_TESTS/ action id past the catalogERR_ACTION_NOT_ALLOWEDCAP_RUN_LABERR_CAPABILITY_NOT_GRANTEDEXPORT_CSVwithoutCAP_EXPORT_LOCAL(passes once granted)ERR_CAPABILITY_NOT_GRANTEDCAP_READ_METRICSERR_CAPABILITY_NOT_GRANTEDERR_A11Y_LABEL_MISSINGERR_A11Y_LABEL_MISSINGcolumnsprop on the run buttonERR_PROP_NOT_ALLOWEDERR_LAYOUTcompleteunsetERR_INCOMPLETEcompletesetERR_UNKNOWN_WIDGETcompleteunset (passes once set)ERR_INCOMPLETEERR_STALEPositive controls show that each rule is what decides:
validate_planalone ignoresstate_rev.Coverage of the failure cases #1648 lists:
unknown_widget_is_rejectedaction_must_be_allowed_and_grantedinterrupted_generation_is_refusedstale_plan_is_refusedThe 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 head9ae38a926.Negative controls. Each one is a one-token mutation of a copy, with exactly one match:
if (a == ACTION_NONE)->if (a >= ACTION_NONE)same_rules_for_both_formats,action_must_be_allowed_and_granted(11/13)WIDGET_ACTIONS[w as usize] != 0->== 0"text_block"->"eval"inWIDGET_NAMESno_script_widget_in_catalogfails{ return ERR_INCOMPLETE; }->{ return PLAN_OK; }same_rules_for_both_formats,interrupted_generation_is_refused(11/13)p.state_rev != current_rev->==stale_plan_is_refused(12/13)binding[4] = 2->= 1(a spec widget bound to metrics)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 commit57474a4f1. The groups it reports (bit_at,uart_tx_bit_at,led_*,flat_index,is_ascii, ...) are not in this spec.OWNERS
specs/OWNERS.mdis not edited. Thet27f/row describestopic_engine.t27, and another session may move that file out oft27f/, so that row may be rewritten elsewhere. Suggested row text for whoever edits it next:Found along the way (not fixed here: Rust, needs
owner-approved-foreign)The Zig backend lowers
str == strtostd.mem.eqlonly when one side is a literal or astrparameter. A comparison of two indexed[N]strelements, such asWIDGET_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 throughwidget_id(name).Not claimed
completemark a streaming producer would set.specs/t27f/topic_engine.t27is untouched.🤖 Generated with Claude Code