Typed Controls Front Door — Implementation Plan
Date: 2026-06-05 18:02:02 +0200 · Progress updated: 2026-06-06 (074 close-out)
Status: ✅ Landed — roadmap complete. Every roadmap feature 065–073 is merged to main: 065-typed-controls-front-door (squash 79ba420), 066-typed-catalog-generation (squash 7706ae1), 067-keyed-reconciliation (squash 28a9674), 068-controls-elmish-command-model (squash 3daed58), 069-design-tokens-penpot (squash 7372b14), 070-typed-controls-migration (squash 843ae65), 071-typed-controls-followups (squash a0253b7), 072-typed-control-catalog-expansion (squash ac90b04), and 073-add-animations (squash 24a79e6). No roadmap feature is still "awaiting" or "Planned". See the Implementation progress section below. The original plan text from §1 onward is retained unedited for provenance.
Feature (proposed → shipped): 065-typed-controls-front-door
Scope: Introduce an additive, compile-time-typed authoring surface (Widget<'msg> + per-control immutable Props records, plus per-control Model/Msg/Effect/update where the control owns ephemeral UI state) that lowers to the existing Control<'msg> IR. Prove it on a six-control reference slice without breaking the shipped FS.Skia.UI.Controls API.
This plan is the merged, source-checked execution of the keystone feature identified in the two prior reports:
docs/reports/2026-06-05-1421-controls-suite-and-penpot-integration-analysis.md(architecture foundation: two-axis model, FuncUI prior art, lowered-IR strategy)docs/reports/2026-06-05-1429-controls-suite-penpot-speckit-plan.md(execution scaffold: variable taxonomy, test plan, evidence artifacts, governance tables)
It deliberately produces only the typed front door for a representative slice. Token/Penpot work, full 47-control migration, keyed reconciliation, and catalog regeneration are explicitly out of scope and sequenced as later features (see §13).
Implementation progress (updated 2026-06-06)
This section records what actually shipped. Everything from §1 onward is the original plan, preserved as written.
Status by roadmap feature
Feature |
State |
Evidence on |
|---|---|---|
065 — Typed controls front door (this plan) |
✅ Merged (squash |
|
066 — Typed catalog generation |
✅ Merged (squash |
|
067 — Internal keyed reconciliation |
✅ Merged (squash |
|
068 — |
✅ Merged (squash |
Branch |
069 — Design tokens + Penpot (DTCG → F#) |
✅ Merged (squash |
A checked-in DTCG-format JSON token document is the single source of truth for the 10 |
070 — Migrate remaining controls to typed Props/MVU |
✅ Merged (squash |
The typed front door extended across the remaining catalog controls: each gained an additive |
071 — Typed-controls follow-ups |
✅ Merged (squash |
Post-migration follow-ups closing residual gaps from |
072 — Typed control catalog expansion |
✅ Merged (squash |
The |
073 — Add animations (motion slice) |
✅ Merged (squash |
The delivered "motion" roadmap item (§13). A representative motion slice: pure interpolation/lowering in |
Open decisions (§12) — resolved as shipped
All five speckit-clarify defaults were adopted and are now reflected in source:
Q1 legacy Attr/*.create kept as a peer (no deprecation); Q2 typed
modules live under the FS.Skia.UI.Controls.Typed namespace (confirmed by the
066 spec and the shipped modules); Q3 AdapterProgram.View stays
Control<'msg> and the Widget.toControl bridge moves into the adapter — now
resolved in 068 via the additive programOfWidget/widgetView view path and the
AdapterCmd Cmd<'msg> bridge (merged, 3daed58); Q4 Widget<'msg> is a sealed wrapper
with a private Lowered field; Q5 TextBox/DataGrid reuse the existing
TextInput/DataGrid models.
Process note
066 was the first feature to exercise the single-source-generation pattern
on the controls surface (joining validation.contract.yml←Routing.fs and
.claude←.agents): one catalogFacts declaration, both catalog artifacts
generated, a drift gate that fails hand-edits. The bump/pack and template-pin
flows that follow a merge are owned by the speckit-merge and
fs-skia-template-update skills respectively — see the Skills section (§16)
for the coverage this surfaced as missing.
1. Objective and success criteria
1.1 Objective
Replace the preferred public authoring path for controls from a weakly typed
Attr<'msg> list keyed by strings into per-control immutable Props records the
F# compiler checks, while keeping the entire downstream pipeline
(Control<'msg> IR → render → layout → diagnostics → event bindings → evidence)
byte-for-byte unchanged.
1.2 Success criteria (acceptance)
A reviewer can confirm the feature is done when all of these hold:
- A new public type
Widget<'msg>exists inFS.Skia.UI.Controls, declared in a curated.fsi. - Six controls expose a typed
Propsrecord +defaults+view(and, for stateful ones,init/update):TextBlock,Button,CheckBox,TextBox,Stack,DataGrid. - Every typed
viewlowers to aControl<'msg>that is structurally equal to what the equivalent legacyControl.create/Attrcall produces today (proven by a parity test, see §10.3). - The legacy
Control.create/Attr/ per-control*.createAPI is unchanged and still compiles, with no behavioral diff in existing tests. ./fake.sh build -t Routeover the branch diff prints thecontrols-public-surfaceescalation; every printed gate passes, includingPackageSurfaceCheckagainst an intentionally-updated surface baseline.- The two evidence artifacts the routing rule already requires exist and are populated:
readiness/typed-controls-front-door.mdandreadiness/package-surface-expectations.md. - No new dependency is added to
FS.Skia.UI.Controls(in particular notFable.Elmish).
1.3 Explicit non-goals
- No removal or deprecation-flagging of the legacy
AttrAPI in this feature (deprecation is a later, separate decision — see §12 open question Q1). - No design-token / Penpot work.
- No keyed VDOM diff/reconciliation.
- No catalog regeneration from the typed source (catalog stays hand-authored as today; typed-catalog generation is feature
066). - No new controls beyond the six slice members; the other 41 stay legacy-only.
2. Grounding: verified current state
All claims below were read from source on 2026-06-05, not from the prior reports.
Fact |
Evidence in repo |
|---|---|
Core IR is string-keyed |
|
Attributes are name-keyed, value union includes escape hatch |
|
A partial "typed" layer already exists but is still string-carrying |
|
Legacy authoring entry points |
|
The render contract is stable and IR-driven |
|
Stateful-control MVU pattern already exists for 3 controls |
|
Elmish dependency split is already correct |
|
Adapter view signature is the integration seam to touch later |
|
Routing already escalates this path and already names the evidence slot |
|
A typed-contract test file already exists (currently asserts the |
|
Test surfaces to extend |
|
Sample to extend for the gallery smoke |
|
Package is a shipped public contract |
|
Key consequence: the routing rule already lists readiness/typed-controls-front-door.md as an expected artifact, so the governance system is pre-wired for this feature. The plan must produce that file (today it is the missing artifact Route --enforce would flag).
3. Architecture decisions
3.1 Two-axis control model (confirmed direction)
Every control is:
Control = (Props : immutable typed record)
- *
Props<'msg>* — the "well-defined variable values" for the control; a closed record. This is the public authoring surface and the compile-time contract. Defaults via adefaultsvalue; modification via recordwith. - MVU — present only for controls that own ephemeral UI state.
TextBlock,Button,CheckBox,Stackare pureProps -> Widget.TextBoxandDataGridcarryModel/Msg/update. - *
Widget<'msg>* — the new public return type of everyview. It is a thin, opaque wrapper that lowers to the existingControl<'msg>IR.
3.2 Widget<'msg> representation — DECISION
Widget<'msg> wraps the lowered IR plus the typed provenance needed for later features, but exposes neither on the .fsi beyond what consumers need:
// Types.fsi (additive)
[<Sealed>]
type Widget<'msg>
module Widget =
val ofControl : Control<'msg> -> Widget<'msg> // internal-leaning escape hatch / bridge
val toControl : Widget<'msg> -> Control<'msg> // lowering accessor used by render + adapter
val render : Theme -> Widget<'msg> -> ControlRenderResult<'msg> // convenience = render (toControl w)
Rationale for a sealed wrapper rather than a bare alias type Widget<'msg> = Control<'msg>:
- Keeps the door open for keyed reconciliation metadata (feature
067) without another public-surface break. - Forces consumers through
Widget.toControlso the lowering seam is explicit and greppable. - The legacy
Control<'msg>API is untouched;WidgetandControlcoexist. (This resolves prior-report open question "shouldControl<'msg>itself become the typed tree" with no — keep them distinct during the preview window.)
Internal field (.fs only): Widget<'msg> = private { Lowered: Control<'msg> }. The record stays internal so the public surface is just the sealed type + module.
3.3 Lowering pipeline (unchanged downstream)
Props<'msg> --(module view)--> Widget<'msg> --(Widget.toControl)--> Control<'msg> --(Control.render)--> ControlRenderResult<'msg>
Nothing in Control.fs, the renderer, layout, diagnostics, accessibility, or evidence changes. Typed view functions are pure constructors that emit the same Attr<'msg> list the legacy builders emit — verified by parity tests.
3.4 Variable taxonomy (applied per Props record)
Each Props record draws its fields from a fixed taxonomy so the six records are consistent and future controls follow the template:
Class |
Meaning |
Example field |
|---|---|---|
Identity |
stable id/key for diffing, events, focus |
|
Content |
text, icon, children |
|
Data |
product-owned values/sources |
|
Behavior |
control behavior not owned by theme |
|
Variant |
semantic style intent |
|
Layout |
sizing/alignment |
|
Theme/style |
token/style references |
|
Accessibility |
role/name/keyboard |
|
Events |
Elmish message callbacks |
|
Rule: every required value is a non-optional field; optional values get defaults via defaults. No optional string event names and no obj payloads in the typed surface.
3.5 MVU contract shape (uniform with existing TextInput)
For stateful controls, mirror the exact shape already shipped in TextInput.fsi:
module TextBox =
val defaults : ControlId -> TextBoxProps<'msg>
val init : TextBoxProps<'msg> -> TextBoxModel * TextBoxEffect list
val update : TextBoxMsg -> TextBoxModel -> TextBoxModel * TextBoxEffect list
val view : TextBoxProps<'msg> -> TextBoxModel -> Widget<'msg>
TextBox reuses the existing TextInputModel/TextInputMsg/TextInputEffect (do not invent a parallel model) — the typed TextBox.view is a thin typed façade over TextInput + the legacy TextBox attrs. DataGrid likewise reuses the existing DataGrid model types. This keeps the feature additive at the model layer too.
4. The reference slice (six controls)
Chosen to exercise every distinct mechanic exactly once:
Control |
Mechanic exercised |
Stateful? |
Reuses existing model |
|---|---|---|---|
|
content-only, pure |
no |
— |
|
command/event + variant ( |
no |
— |
|
boolean state + |
no |
— |
|
text-input runtime, validation |
yes |
|
|
layout composition over |
no |
— |
|
data + bounded visible range runtime |
yes |
existing |
4.1 Illustrative typed surfaces (final API to be fixed in spec)
type ButtonIntent = Primary | Secondary | Danger | Ghost
type ButtonProps<'msg> =
{ Id: ControlId option
Text: string
Enabled: bool
Intent: ButtonIntent
OnClick: 'msg option }
module Button =
val defaults : ButtonProps<'msg>
val view : ButtonProps<'msg> -> Widget<'msg>
type StackOrientation = Vertical | Horizontal
type StackProps<'msg> =
{ Id: ControlId option
Orientation: StackOrientation
Spacing: float
Children: Widget<'msg> list }
module Stack =
val defaults : StackProps<'msg>
val view : StackProps<'msg> -> Widget<'msg>
Authoring stays terse and compiler-checked:
Stack.view
{ Stack.defaults with
Orientation = Vertical
Children =
[ TextBlock.view { TextBlock.defaults with Text = "Sign in" }
Button.view { Button.defaults with Text = "Submit"; Intent = Primary; OnClick = Some Save } ] }
5. Package and file layout
All new files land in src/Controls/ (so they ship in FS.Skia.UI.Controls, no project moves). New compile units, inserted into Controls.fsproj after Control.fs (so Widget can depend on Control) and after TextInput.fs/DataGrid.fs for the stateful façades:
New file |
Contents |
|---|---|
|
sealed |
|
|
|
|
|
|
Naming note: the legacy
module TextBox/module DataGridalready exist inControl.fsi. To avoid collision, the typed modules live under a distinct namespace segment (e.g.FS.Skia.UI.Controls.Typed) or are namedTextBoxWidget/DataGridWidget. Final choice is a spec decision (Q2, §12) — the plan assumesFS.Skia.UI.Controls.Typed.*so the six typed modules can keep the clean namesButton,TextBox, etc. without shadowing legacy ones.
Controls.fsproj <Compile> insertions (order matters in F#):
... Control.fsi/fs ...
Widget.fsi / Widget.fs <- after Control
... Catalog, TextInput, ControlRuntime, Collections, Charts, RichText, DataGrid ...
Widgets/Primitives.fsi / .fs <- after DataGrid (depends only on Widget + Control)
Widgets/TextBoxWidget.fsi / .fs <- after TextInput + Widget
Widgets/DataGridWidget.fsi / .fs <- after DataGrid + Widget
6. Compatibility strategy
- Legacy API: frozen, not touched.
Control.create,Attr.*,Control.standard, and all 47 per-control*.createmodules remain exactly as inControl.fsi/Attributes.fsi. No signatures change, soPackageSurfaceChecksees only additions. - Bridge in both directions.
Widget.ofControllets a consumer drop a legacyControl<'msg>into a typedStack.Childrenlist during migration;Widget.toControllets the renderer and the Elmish adapter consume aWidgettoday without an adapter change (callControl.render (Widget.toControl w)). - Adapter untouched this feature.
ControlsElmish.AdapterProgram.View: 'model -> Control<'msg>stays. Consumers using the typed surface simply finish theirviewwithWidget.toControl. Converging the adapter ontoWidget/Cmd<'msg>is feature068(Q3, §12). - Surface baseline bump is expected and intentional. The feature adds public API, so
PackageSurfaceCheckwill fail until the baseline is regenerated. Regenerating the baseline is an explicit task (T13), reviewed as part of the diff.
7. Governance and routing
7.1 Routing — no new rule needed
The change is confined to src/Controls/**, which already matches rule
controls-public-surface (Routing.fs:131). Running ./fake.sh build -t Route
on the branch will print tier FocusedAuthority and this gate set:
ControlsCatalogCheckControlsInteractionCheckControlsRenderingCheckPackageSurfaceCheckFsiTranscriptsGeneratedProductCheck
No Routing.fs edit is required for the typed front door itself. (Routing edits
start at the token feature.) Because the change also touches public .fsi files,
package-surface gating applies regardless.
7.2 Required evidence artifacts
The rule's ExpectedArtifacts are already:
readiness/typed-controls-front-door.md← produced by this feature (currently missing)readiness/package-surface-expectations.md← produced/updated by this feature
./fake.sh build -t Route --enforce will fail until both exist with content. These live under the feature's spec dir: specs/065-typed-controls-front-door/readiness/.
7.3 Constitution touch-points
- Principle II (visibility in
.fsi): every new typed module needs a curated.fsi; thePropsrecords are public, theWidgetinternal record field is not. - Principle IV (MVU boundary):
TextBox/DataGridtyped façades expose pureinit/updateand reuse the existing effect types; no I/O in updates. - Principle V (synthetic disclosure): if any typed
viewships with placeholder lowering, it must carry the[S]disclosure. The intent here is real lowering (parity-tested), so no[S]should be needed — call this out explicitly in the evidence file.
7.4 Validation order (escalated maintainer-verify path)
Because this is a consumer-contract change (public .fsi), run the serialized
FAKE-backed order sequentially (per AGENTS.md — never concurrently):
./fake.sh build -t Dev./fake.sh build -t GeneratedGuidanceCheck./fake.sh build -t TemplateCheck./fake.sh build -t GeneratedProductCheck./fake.sh build -t EvidenceGraph./fake.sh build -t EvidenceAudit
plus the Route-printed gates (§7.1). Run Route first and only run what it prints; the six-target order above is the escalation this change qualifies for.
8. Catalog impact
catalog.yml (supportedCount: 47) and Catalog.fs are hand-authored today.
The six typed controls correspond to existing catalog rows — no new rows. This
feature does not regenerate the catalog from the typed source (that is feature
066). ControlsCatalogCheck should therefore pass unchanged. The only catalog
action: confirm the six rows' Examples/Tests lists still resolve (they point at
samples/ControlsGallery/Program.fs and the Controls.Tests files we extend).
9. Implementation task breakdown (dependency-ordered)
Tasks are sized for speckit-tasks; IDs are illustrative. [P] = parallelizable
after its dependency.
ID |
Task |
Depends on |
Output |
|---|---|---|---|
T1 |
Failing-first contract tests: assert |
— |
red tests |
T2 |
Author |
T1 |
new compile unit |
T3 |
Wire |
T2 |
green build |
T4 |
|
T3 |
primitive |
T5 |
|
T3 |
primitive |
T6 |
|
T3 |
primitive |
T7 |
|
T3 |
primitive |
T8 [P] |
|
T3 |
stateful |
T9 [P] |
|
T3 |
stateful |
T10 |
Parity tests: each typed |
T4–T9 |
parity proof |
T11 |
Interaction tests: typed |
T4–T9 |
interaction proof |
T12 |
Accessibility + rendering tests for the six typed views at ≥2 viewports |
T4–T9 |
a11y/render proof |
T13 |
Regenerate public-surface baseline; run |
T4–T9 |
surface diff |
T14 |
Extend |
T4–T9 |
gallery smoke |
T15 |
Write |
T10–T13 |
evidence |
T16 |
Run |
all |
gate pass |
Critical path: T1→T2→T3→(T4..T9)→T10→T15→T16. T8/T9 and T4–T7 parallelize after T3.
10. Test plan (mapped to existing files)
10.1 Surface / contract — TypedControlContractTests.fs, PublicSurfaceTests.fs
- Assert
Types.fsi(orWidget.fsi) declaresWidget,Widget.toControl,Widget.ofControl. - Assert each of the six typed modules exposes
defaultsandview(andinit/updateforTextBox/DataGrid). - Assert no
objappears in any new typedPropsfield (grep the new.fsi).
10.2 Failing-first (TDD)
- Each new test in T1/T10/T11 must be committed red first, then made green by the implementation task — matches the repo's failing-first convention.
10.3 Lowering parity — new TypedLoweringTests.fs
The keystone test. For each control, build the same logical control two ways and assert structural equality of the resulting Control<'msg>:
let legacy = Button.create [ Button.text "Submit"; Button.enabled true; Button.onClick Save ] // existing API
let typed = Typed.Button.view { Typed.Button.defaults with Text = "Submit"; OnClick = Some Save } |> Widget.toControl
Expect.equal (normalize typed) (normalize legacy) "typed Button lowers to legacy IR"
normalize sorts attributes by name to ignore ordering. This proves the typed
surface is a faithful façade and protects every downstream test (render, a11y,
diagnostics) without duplicating them.
10.4 Interaction — InteractionTests.fs
- Typed
Button.OnClick = Some msgproduces aControlEventBindingwhose dispatch yieldsmsg. - Typed
CheckBox.OnChangedmaps a toggle event to(bool -> 'msg). TextBox.update/DataGrid.updatevia the typed façade equal the existingTextInput.update/DataGrid.updateresults (delegate, don't fork).
10.5 Accessibility & rendering — AccessibilityTests.fs, RenderingTests.fs
- Each typed view's lowered control carries the same
AccessibilityMetadatathe legacy path does (follows automatically from parity, but assert forButton/CheckBox/TextBox). - Render the six-control typed gallery panel at two viewports; node counts stable.
10.6 Elmish boundary — tests/Elmish.Tests
- A small program whose
viewends inWidget.toControlruns throughAdapterProgramunchanged (proves the bridge needs no adapter edit). - Assert
Controls.fsprojstill has noFable.Elmishreference (dependency-governance guard).
11. Evidence artifacts to produce
Under specs/065-typed-controls-front-door/readiness/:
typed-controls-front-door.md— theWidget/Props design, the six-control slice, lowering-parity results, and an explicit statement that lowering is real (no[S]synthetic disclosure needed). (Required by routing rule.)package-surface-expectations.md— the additive surface delta and the regenerated baseline rationale. (Required by routing rule.)controls-rendering.md— viewport render evidence for the typed panel.typed-lowering-parity.md— the parity-test matrix (six controls × legacy≡typed).
12. Open decisions to resolve in speckit-clarify
These five map to ≤5 speckit-clarify questions; the spec should bake the answers in before speckit-plan.
13. Where this sits in the larger program
This feature is F-α / feature 1 of the merged roadmap. Downstream features
(each its own specs/NNN-*), unchanged from the prior reports' sequencing:
- ✅ 065 — Typed controls front door (this plan) — merged (
79ba420) - ✅ 066 — Typed catalog generation (regenerate
catalog.yml/Catalog.fsfrom the typed registry) — merged (7706ae1) - ✅ 067 — Internal keyed reconciliation (VDOM diff over lowered IR; internal only) — merged (
28a9674) - ✅ 068 —
Controls.Elmishcommand model (Widgetview +Cmd<'msg>alignment) — merged (3daed58) - ✅ 069 — Design tokens + Penpot tokens-first (DTCG JSON → generated F#,
DesignTokenDrift) — merged (7372b14) - ✅ 070 — Migrate remaining controls to typed Props/MVU — merged (
843ae65) - ✅ 071 — Typed-controls follow-ups — merged (
a0253b7) - ✅ 072 — Catalog expansion (typed catalog grown to 51 controls) — merged (
ac90b04) - ✅ 073 — Motion / animations (the delivered "motion" item: representative animation slice —
src/Scene/Animation.*+ ElmishAnimationTick) — merged (24a79e6) - Later — Penpot MCP assist (inspect/draft/provenance), code→design catalog sync; overlays/virtualization breadth; wiring the parked keyed-reconciliation spike (
067) into the render path (seefs-skia-reconciliation)
Sequencing rationale (shared by both prior reports): type the authoring layer first (smallest blast radius, everything else depends on it), wire tokens second, migrate breadth last.
14. Risk register
Risk |
Impact |
Mitigation |
|---|---|---|
Public-surface churn breaks consumers |
|
Additive-only; legacy API frozen; surface baseline reviewed in diff (T13) |
Typed |
Downstream render/a11y tests wouldn't catch a typed-only bug |
Mandatory lowering-parity test per control (§10.3) is the gate |
Module-name collision with legacy |
Compile error or shadowing confusion |
|
|
Principle II violation |
Sealed type + internal record field; |
Elmish dependency creeps into base Controls |
Dependency-governance break |
Guard test asserts |
Scope creep into token/migration work |
Feature never lands |
Hard non-goals (§1.3); six-control slice only |
Missing required evidence artifact |
|
T15 produces both required |
15. Immediate next steps
- Run
speckit-specifyfor065-typed-controls-front-doorusing §3–§4 as the design seed. - Run
speckit-clarifyto resolve Q1–Q5 (§12) and bake answers intospec.md. - Run
speckit-planthenspeckit-tasks; expect the task graph to mirror §9. - Implement T1 (failing contract tests) before any production code.
- Run
./fake.sh build -t Routeand only the gates it prints; on the escalated path, run the serialized six-target order sequentially.
16. Skills: update and add coverage
Update (
074): this backlog is now closed. All three proposed capability skills shipped —fs-skia-typed-controls(070),fs-skia-design-tokens(069), andfs-skia-reconciliation(074/US3) — and the catalog-generation gap was filled by folding it intofsharp-code-generation(C13,074/US1) rather than creating a standalone skill. The original backlog text below is preserved; per-row status is recorded in §16.2/§16.3.
Landing 065/066 introduced two capabilities — a typed authoring front door
and single-source catalog generation — that no current skill covers. The
skill corpus (.agents/skills/*, the 9 capability skills fs-skia-* /
fsharp-* plus the speckit-* workflow skills) has authoring help for
build orchestration, code-generation of governance artifacts, parsing, graph
algorithms, I/O globbing, shell, evidence mode, layout readability, the template,
and generated projects — but nothing teaches a consumer or maintainer how to
author a typed control or how to extend the catalog fact table. This
section is the backlog for closing that gap as the roadmap advances.
16.1 Mechanics for any skill change (do this, not hand-edit .claude)
Skills are generated from a single source: author/edit the canonical
.agents/skills/<name>/SKILL.md, then regenerate the .claude peer with
./fake.sh build -t RefreshSurfaceBaselines. The registry
(build/Governance/Evidence/SkillRegistry.fs) discovers skills dynamically from
each SKILL.md's frontmatter name: — there is no hardcoded skill list to
edit. A skill change routes (via ./fake.sh build -t Route) to the
focused-authority gate set: Dev, SkillSyncCheck, SkillQualityCheck,
SkillContractPathCheck, TemplateUpdateSkillPackageCheck. Never edit the
.claude copy directly — SkillSyncCheck will flag the drift.
16.2 Skills to update (existing)
Skill |
Update |
Why now |
|---|---|---|
|
✅ Done ( |
Shipped in |
|
Already corrected for Feature |
Done 2026-06-05; listed here for completeness. |
Correction (
074): the original §16.2 listed a generated-product "project" skill to update so it would name the typed authoring path as preferred. No such skill exists in the repo skill corpus (.agents/skills/**), so that row is removed. The "typed authoring is the preferred front door" guidance is carried byfs-skia-typed-controls(§16.3), the skill that actually owns typed-control authoring.
16.3 Skills to add (new)
Proposed skill |
Status |
Scope |
Unblocks |
|---|---|---|---|
|
✅ Shipped (landed with |
The workhorse skill. How to author with the typed front door and how to add a new typed control: pick fields from the variable taxonomy (§3.4), write the immutable |
Drove |
|
🔁 Folded into |
(not created standalone) How to extend the |
|
|
✅ Shipped (landed with |
DTCG JSON → generated F# token modules, the |
|
|
✅ Shipped ( |
The keyed VDOM diff over the lowered |
|
16.4 Sequencing
Author fs-skia-typed-controls before starting 070, and
fs-skia-design-tokens before 069 — each new skill should land in the same
feature branch that first needs it, so its guidance is validated against real
work rather than written speculatively. fs-skia-catalog-generation can land
with 071 or be folded into fsharp-code-generation now, since 066 already
provides a complete worked example to document.
17. Sources
Repository (read 2026-06-05, authoritative grounding):
- src/Controls/Types.fsi — Control<'msg>, Attr<'msg>, AttrValue<'msg>, Standard* types
- src/Controls/Control.fsi — legacy authoring modules + Control.render
- src/Controls/Attributes.fsi — Attr builders
- src/Controls/TextInput.fsi — reference MVU contract shape
- src/Controls.Elmish/ControlsElmish.fsi — AdapterProgram.View seam
- src/Controls/Controls.fsproj — compile order, package id/version, dependency set
- src/Controls/Catalog.fs, src/Controls/catalog.yml — 47-control catalog
- build/Governance/Routing.fs:131 — controls-public-surface rule, gates, expected artifacts
- tests/Controls.Tests/* — existing test surfaces to extend
- AGENTS.md, CLAUDE.md — Route-first workflow and serialized six-target order
Prior analysis (merged into this plan):
- docs/reports/2026-06-05-1421-controls-suite-and-penpot-integration-analysis.md
- docs/reports/2026-06-05-1429-controls-suite-penpot-speckit-plan.md
type SealedAttribute = inherit Attribute new: unit -> SealedAttribute + 1 overload member Value: bool
--------------------
new: unit -> SealedAttribute
new: value: bool -> SealedAttribute
val string: value: 'T -> string
--------------------
type string = System.String
val float: value: 'T -> float (requires member op_Explicit)
--------------------
type float = System.Double
--------------------
type float<'Measure> = float