Skip to content

layout: narrow keyed field parts.control by widget (#176) - #177

Merged
timkindberg merged 2 commits into
mainfrom
feat/176-layout-control-widget
Sep 12, 2026
Merged

timkindberg merged 2 commits into
mainfrom
feat/176-layout-control-widget

Conversation

@timkindberg

@timkindberg timkindberg commented Sep 10, 2026 •

Copy link
Copy Markdown
Owner

Reaching a field through layout's keyed door still made you write a c.kind guard, even though the brand already knew the widget. Found this while reviewing #174.

<Default
  of={root.children.username}
  parts={{ control: (c) => <input {...c.attrs} /> }} // no guard now
/>
  • LayoutNode's field branch is a new LayoutField — an EField with parts.control Extracted down to ControlForWidget<TS['fields'][Path]['widget']>
  • ControlOverrideOf already preferred a handle's live parts.control over Core's wide PartsOverrides, so nothing else needed wiring
  • Widget overrides re-narrow for free — useFormTree already re-brands form through ApplyWidgetOverrides
  • field-root-pieces.test.tsx moves its control override back inline: a9a4b24 had extracted KitControl: ControlOverride<'input'> purely to dodge the guard, and reaching the node through the keyed door instead drops both the annotation and the branch
  • Amended ADR 055's consequence bullet, which used to say widget narrowing stays the intercept-rules job

The function intercept door keeps the full union on purpose — it fires for every node so it genuinely can't know more (ADR 029 §5). Unbranded trees keep AnyKindNode. Intercept coverage of parts={{ control }} is unchanged (renderer.test.tsx), as is the ControlOverride<'input'> lesson (intercept.test.tsx, App_16, App_17).

One thing I hadn't noticed until reviewing this: the path-map intercept is a fourth door with the same gap, and its keys don't typo-check either. Filed as #178, not fixed here.

npm run gate green.

Closes #176

timkindberg and others added 2 commits September 10, 2026 18:44
`layout`'s FormShape-keyed field handles returned a plain `EField`, so
`<Default of={root.children.username} parts={{ control }} />` still forced
a `c.kind` guard even though the brand already knew the widget.

`LayoutNode`'s field branch is now `LayoutField`, an `EField` whose
`parts.control` is `Extract`ed to `ControlForWidget<TS['fields'][Path]['widget']>`
— the same archetype the typed-rules door hands `FieldProps`. The floor keeps
the union (a function intercept fires for every node and cannot know more,
ADR 029 §5); unbranded trees keep `AnyKindNode`. Widget overrides re-narrow
for free through the brand `useFormTree` already threads.

Co-authored-by: Cursor <cursoragent@cursor.com>
The narrowing had no consumer — every guard left in the repo is on a door
that correctly keeps the union (function intercept, unbranded runtime tree,
renderer adapters). So move the one test that can use the keyed door.

`a9a4b24` had extracted `KitControl: ControlOverride<'input'>` to get the
`c.kind` branch out of this test, because it reached the node through a
function intercept — `node.path === 'username'` is a runtime check the
compiler cannot lift into the brand. Reached through `layout`'s keyed child
instead, the brand already knows the widget, so the override goes back inline
with no guard and no annotation.

Coverage is unchanged: intercept + `parts={{ control }}` is still pinned by
renderer.test.tsx, and `ControlOverride<'input'>` on the intercept door by
intercept.test.tsx, App_16, and App_17.

Co-authored-by: Cursor <cursoragent@cursor.com>
@timkindberg
timkindberg merged commit 70bd5dd into main Sep 12, 2026
2 checks passed
@timkindberg
timkindberg deleted the feat/176-layout-control-widget branch September 12, 2026 21:20
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.

layout: FormShape-keyed field handles should narrow parts.control by widget

1 participant