Skip to content

Feature: Shaping conventions files, including specs.yaml - #392

Draft
nathanacurtis wants to merge 2 commits into
feat/react-from-specsfrom
feat/revise-conventions-layout
Draft

Feature: Shaping conventions files, including specs.yaml#392
nathanacurtis wants to merge 2 commits into
feat/react-from-specsfrom
feat/revise-conventions-layout

Conversation

@nathanacurtis

Copy link
Copy Markdown
Member

Schema and ADRs only. No transform or CLI code changes, so the loader still reads the old paths — this describes the destination, and the implementation follows separately.

Both ADRs are amended in place rather than superseded, since neither has shipped.

What moves, and why

platforms.<id> holds facts about one implementation. Two members filed under figma were not facts about Figma:

  • states names a prop and an enum value that exist in api.yaml. A transform reading only the spec applies it — the css transform does exactly that, and never opens a Figma file.
  • The prop conventions naming which prop carries an accessible name or a value are the same shape of fact.

Compare the members that genuinely are Figma facts: glyphs.match names a layer pattern, codeOnlyProps.match a frame, subcomponents.scope a page. Those describe the design tool. states does not, and filing it under figma said it did.

Was Now
Conventions.platforms.figma.states Conventions.specs.states
Conventions.platforms.figma.propRoles.accessibleName Conventions.specs.accessibility.label.prop
Conventions.platforms.figma.propRoles.value Conventions.specs.value.prop
Conventions.platforms.figma.propRoles.indeterminate Conventions.specs.value.indeterminate
Conventions.platforms.figma.roleValidation Settings.spec.roleValidation
PropRoleName (deleted — the concepts are named fields now)
# conventions/specs.yaml
states:
  disabled: { prop: isDisabled }
  hover:    { prop: state, value: Hover }
accessibility:
  label:    { prop: a11yLabel }
value:
  prop: progress
  indeterminate: isLooping

Four decisions worth reviewing

specs is a sibling of platforms, not a member of it. primitives is the precedent — it already sits outside for the same reason — and the spec is the hub, so making specs a platform key would name the hub as one of its own spokes. The loader precedent exists too: PRIMITIVES_FILE is already a reserved basename in config/conventions/, so the new file needs a second reserved name and no new mechanism.

indeterminate is not folded into the state concepts. Those are a governed vocabulary — each resolves to a canonical selector, and indeterminate there means a checkbox's mixed state. A progress bar with no known value is a different fact wearing the same word: it suppresses aria-valuenow and has no selector at all. Widening a governed vocabulary to absorb a prop binding is what the governance exists to prevent. It is modelled as value.indeterminate instead — a property of the value, which is what it is.

Prop conventions are objects, not bare names. accessibility.label.prop rather than accessibility.label: a11yLabel, so a convention can gain fields without a break. The known disclosure gap needs one that pairs a label with the state selecting it. This also retires propRoles, a name that borrowed a word meaning something specific and unrelated on anatomy elements.

roleValidation becomes a setting. It describes how strict a run should be — a fact about neither the design tool nor the target — and its on-switch (spec.roles) was already in settings. One feature's two knobs were split across two files.

Also here

conventions/primitives.yamlconventions/figma.primitives.yaml (ADR 073, Decision 5). Its source keys name Figma style properties and its values keys name Figma tokens, so the file is Figma-scoped even though the components it produces are not. This partly reverses Decision 2, whose platform-neutrality reasoning held for the table's output and was over-applied to the file.

Follow-up, not in this PR

The implementation lands separately and must go in one commit, because the loader and its consumers agree on a shape:

  • ConfigLoader — reserve the specs basename, add resolveSpecs, drop three keys from resolvePlatform, change PRIMITIVES_FILE
  • 4 call sites in TransformCommand / AnalyzeCommand
  • ~6 real call sites across react-from-specs and webcomponents-from-specs
  • de-library and eg split figma.yaml in two
  • 12 role docs pages, plus rehoming settings/states.md — it is no longer a setting

TransformerContext.processingStates keeps its name. The config surface moved; the transform surface did not.

The dominant risk is silence. resolvePlatform builds from an explicit allowlist, so an unlisted key is dropped with no error — components lose their state classification, still transform, and look plausible. That mechanism already swallowed propRoles once during this work. Worth a fixture assertion that de-library emits :disabled selectors.

Verification

Types and type-tests compile clean; all three schema files parse. The worktree has no node_modules, so the vitest suite has not run against these changes — worth running once before merge.

nathanacurtis and others added 2 commits September 4, 2026 15:11
… block

`platforms.<id>` holds facts about one implementation, and two members filed
under `figma` were not. `states` names a prop and an enum value that exist in
api.yaml — a transform reading only the spec applies it, and the CSS transform
does exactly that without touching a Figma file. The prop conventions naming
which prop carries an accessible name or a value are the same shape of fact.
Compare the members that genuinely are Figma facts: `glyphs.match` names a layer
pattern, `codeOnlyProps.match` a frame, `subcomponents.scope` a page.

Adds `Conventions.specs`, a sibling of `platforms` and `primitives` rather than
a member of `platforms`. `primitives` is the precedent — it already sits outside
for the same reason — and the spec is the hub, so making `specs` a platform key
would name the hub as one of its own spokes.

    conventions/specs.yaml
    props:
      states: { disabled: { prop: isDisabled } }
      accessibility: { label: { prop: a11yLabel } }
      value: { prop: progress }

Prop conventions are objects, not bare names, so one can gain fields without a
break — the same argument that made `actions` an array of objects. That also
retires `propRoles`, a name that borrowed a word meaning something specific and
unrelated on anatomy elements.

`roleValidation` moves to `Settings.spec`, beside `roles`. It describes how
strict a run should be, which is a fact about neither the design tool nor the
target, and its on-switch was already in settings — one feature's two knobs were
split across two files.

ADRs 067 and 073 are amended in place rather than superseded, since neither has
shipped. Schema and types only; no transform or CLI code changes, so the loader
still reads the old paths until the follow-up lands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…abulary closed

Three revisions to the spec-conventions shape.

The `props` wrapper is gone. Everything the file holds describes props, so the
group earned nothing and read badly against the leaf it contained —
`props.states.disabled.prop` says "prop" twice. A grouping level is worth adding
when a second kind arrives, not before.

`indeterminate` is no longer folded into the state concepts. Those are a governed
vocabulary: each resolves to a canonical selector, and `indeterminate` there means
a checkbox's mixed state. A progress bar with no known value is a different fact
wearing the same word — it suppresses aria-valuenow and has no selector at all.
Widening a governed vocabulary to absorb a prop binding is what the governance
exists to prevent. It is modelled as `value.indeterminate` instead, a property of
the value, which is what it is: the prop that says this value is unknown.

`conventions/primitives.yaml` becomes `conventions/figma.primitives.yaml`. Its
`source` keys name Figma style properties and its `values` keys name Figma tokens,
so the file is Figma-scoped even though the components it produces are not. That
is the same test Decision 4 applies to every conventions file, and leaving it
unqualified said it was scope-free. Recorded as ADR 073 Decision 5, which reverses
part of Decision 2's reasoning — that reasoning held for the table's output and was
over-applied to the file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nathanacurtis nathanacurtis changed the title Move spec-side conventions out of the platform block Further shaping conventions files, including specs.yaml Sep 4, 2026
@nathanacurtis nathanacurtis changed the title Further shaping conventions files, including specs.yaml Feature: Shaping conventions files, including specs.yaml Sep 4, 2026
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.

1 participant