ADR 073-079, 081: Conventions for primitive composition - #363
Open
nathanacurtis wants to merge 17 commits into
Open
ADR 073-079, 081: Conventions for primitive composition#363nathanacurtis wants to merge 17 commits into
nathanacurtis wants to merge 17 commits into
Conversation
Five draft ADRs extending Conventions with spec-to-code primitive bindings, so a text, glyph, or container layer in a composition resolves to the design system's designated component per platform. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nathanacurtis
commented
Aug 30, 2026
Revisions from review of ADR-073: - Figma becomes a key in `conventions.platforms` rather than a sibling namespace. `Conventions` is unreleased, so the move is free - Platform ids name implementations and stay flat: `react` and `web-components` are peers, not children of a `web` family - `image` joins the primitive vocabulary, triggered by a non-null `Styles.backgroundImage`, so the layer-fill half of ADR-063 reaches a designated component per platform - The boundary rule is restated as read-side vs write-side, a property of a member rather than of a platform Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nathanacurtis
commented
Aug 30, 2026
…ills as styling Revisions from review of ADR-075, plus the backgroundImage correction: - `styleProps` becomes `props`; `stylePropName` becomes `stylesProp` - `props` is closed per primitive: text maps textColor and typography, glyph maps fillColor and content, container maps layoutMode. Everything else is passed styling, enforced by additionalProperties: false - Glyph size comes from sizing and layout styling, not a prop - Glyph `content` maps to a `name` prop by default - Unmatched prop values fall back to the component's own default - A container's backgroundImage always stays styling; `image` is removed from the primitive vocabulary and the designated image component gets a per-platform code name at `platforms.<id>.images.component` - ADR-076 no longer hoists `props`, since the closed sets are disjoint Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tive - `props` keys name concepts, not spec members: `color` on both text and glyph (fed by textColor and fillColor), `content` on glyph, `direction` on container. Decouples the vocabulary from Styles - Defaults become the concept's own name where no survey settles it, so glyph content defaults to `content` rather than React's `name` - ADR-077 grounds `images.component` in the primitive-vs-attribute distinction: text, glyph and container are node kinds; an image is a paint on a node, so it needs its own convention. Removes the hedge that the member might be droppable Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The pipeline runs both ways on both kinds of platform — code is read to produce specs, and specs are written to produce Figma — so direction of travel never distinguished the two member groups. What does: - Encoding members say how a platform expresses something the spec models explicitly (name patterns, variant-prop classifications, containers) - Vocabulary members say which of a platform's components implements a spec primitive Both apply to any platform and in either direction. `images.match` and `images.component` are now both classed as vocabulary — one question answered in two languages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
config/conventions/<platform>.yaml, discovered by convention, composing into the single Conventions map ADR-073 defines. Filename is the platform id, so a platform is declared in exactly one file and no merge rule is needed. The single-file form stays valid; both present is an error. No type changes — composition is a loader concern. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… (ADR-079) Since ADR-073 made Conventions a platform-keyed map, metadata.conventions has embedded every platform in the workspace. That is a defect, not just noise: the drift check ADR-071 built the member for compares the whole object, so a Compose vocabulary change marks every Figma-generated spec as drifted. Metadata now carries the single platform entry that produced the spec, in the same shape as the artifact so the drift comparison stays direct, with maxProperties: 1 enforcing it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The single-file config/conventions.yaml is introduced by the unreleased CLI 0.28.0 — npm's latest is 0.27.0, and 0.28.0's own breaking change is the move from specs.config.yaml to config/. It has never reached a workspace outside this repo, so supporting it alongside the directory form would preserve compatibility with something that never existed. Directory only: one discovery path, no both-present error, no precedence rule. `specs migrate config` emits the directory form, so no workspace lands on a layout it would later migrate off. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Researched against the current branches and recorded in the two ADRs that cause it — ADR-073 for the namespace move, ADR-078 for the file layout. Notable findings: - specs-plugin-2 takes a single line; settingsToSpecConfig is its only translation point from panel fields to SpecConfig - specs-from-figma is wide but shallow: 42 call sites across 13 files, every one the same repoint of an object passed down from Component - The CLI is the awkward one: 7 of its 24 sites are user-facing validation messages that quote conventions.figma.* paths, and analyzers/Keys.ts reads the path out of a spec's metadata rather than configuration - bridge/server.ts reads config/conventions.yaml by literal path, bypassing ConfigLoader. Left behind it fails silently rather than erroring - conventions.schema.json lists figma in required, so the schema change is not purely additive within that file Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Promotes two items from "action required" into decided positions: Decision 5 — bridge/server.ts consolidates onto ConfigLoader. Its resolveSources() reads config/conventions.yaml and config/settings.yaml by literal path with its own pre-split specs.config.yaml fallback, so the two read paths actively disagree: ConfigLoader refuses an unmigrated workspace and the bridge serves it. Consolidating removes the duplication, the divergence and a failure mode that is silent by construction. Decision 6 — the conventions template splits per platform, and specs init scaffolds config/conventions/figma.yaml alone. Which implementations a workspace targets is not knowable at init, and a commented placeholder would claim a platform id no generator reads. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nces Reverts them out of Options Considered. Neither is a decision this ADR weighs alternatives for — they are effects of the layout change on code that reads it, so they belong in Downstream Impact and Consequences. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nathanacurtis
added a commit
that referenced
this pull request
Aug 31, 2026
The ADRs drafted in PR #363 never had their index rows cherry-picked onto main, so the index topped out at 070 and the next author would have claimed 073 again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nathanacurtis
added a commit
that referenced
this pull request
Aug 31, 2026
The ADRs drafted in PR #363 never had their index rows cherry-picked onto main, so the index topped out at 070 and the next author would have claimed 073 again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each platform states the width it shows components at, in its own config/conventions/<platform>.yaml. A Figma frame, a Storybook canvas and a device preview are different canvases, so the value is a member of PlatformConventions rather than a single workspace-wide number. Names a third category of PlatformConventions member — presentation — alongside the encoding and vocabulary groups ADR-077 defined, and cross-references it there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Renames the member: it governs the root of any render — a component, an instanceExample, a slotContentExample, a story — not examples alone, so "example" under-described it. Nothing below a root is affected; a parent sizes its children. - Drops the invented third category of PlatformConventions member and reverts the cross-reference added to ADR-077. The member is added without a classification claim. - Settles the absent case: the schema declares no default at any level, and each rendering tool falls back to 375. A resolved default cannot reach a platform with no conventions file at all (ADR-078), so the number belongs in the tools. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The member applies only when a root's layoutSizingHorizontal is FILL, and the number is the width of a container the renderer creates for that root to fill — not the instance's own width. Fixed and hugging roots are untouched, so the member can never override what a design states. Renamed from defaultInstanceWidth. Constitution VI rule 2: no code-platform consensus exists, so the name follows Compose's Modifier.fillMaxWidth(), and it matches the FILL value layoutSizingHorizontal already carries. No height member is defined; Decision 5B records why. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Conventions become a platform-keyed map in which figma is one implementation
among react, web-components and swiftui, and the shape gains the vocabulary
members a code generator needs.
- Conventions.platforms replaces Conventions.figma; PlatformConventions is one
permissive shape for every platform, and the root of a single
config/conventions/<id>.yaml so one file validates standalone (073, 078)
- PrimitiveKind and TextBinding/GlyphBinding/ContainerBinding bind text, glyph
and container to a platform's own component, resolved at emit time (074, 075)
- ContainerBinding.component takes a LayoutMode-keyed map; a platform-level
stylesProp baselines what each primitive overrides and folds in on
resolution (076)
- images.component names the image component on a code platform, beside the
match naming it in Figma (077)
- MetadataConventions narrows a spec's recorded conventions to the producing
platform (079)
- defaultFillWidth states the container width for a fill-width root (081)
DEFAULT_CONVENTIONS becomes {} — every default it carried belongs inside a
declared platform entry.
Schema package only. specs-from-figma and the CLI follow in that order, and
ADR-078's loader is CLI work not done here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The command's steps are types/, schema/, tests and docs, and the constitution it gates against is the schema package's — but nothing said so, and an ADR whose Downstream Impact table names the CLI reads as an invitation to follow it there. States the boundary, and the order consumers are updated in: schema → specs-from-figma → cli, since the CLI depends on specs-from-figma and cannot be verified against types the engine has not yet adopted. Records that an ADR with no schema-package surface is reported as outstanding rather than chased, and that leaving a consumer uncompilable is expected rather than a reason to widen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7 tasks
…rimitive list
ADR-073's Notes claimed a missing platform entry 'gets no naming at all, which
is the same statement NONE made'. It is not: ADR-071 settled that naming,
slotConstraints and inferNumberProps are defaulted and only convention blocks
are not, and it held because figma was a required key. A platform-keyed map
makes every key optional, so the guarantee had to move rather than lapse.
It moves to resolution, where ADR-071 already put every other default: a
resolver produces a complete entry for any platform it is asked about, declared
or not. ResolvedPlatformConventions requires the three members for that reason.
DEFAULT_CONVENTIONS stays {} because a map has no fixed key to populate — no new
exported constant, and no constitution amendment.
Also:
- ContainerBinding.component drops minProperties on the keyed map. A partial map
is normal and an empty one is inert; the schema was rejecting what the type
allowed, which Constitution I calls drift.
- PrimitiveKind is derived from PrimitiveBindings instead of written out beside
it, so the vocabulary and the block enforcing it are one list.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Conventions become platform-keyed.
figmastops being the only namespace and becomes one implementation key amongreact,web-componentsandswiftui— because this pipeline reads Figma to produce specs and writes specs to produce Figma, so Figma is a peer rather than a special case. On top of that axis, the shape gains the vocabulary a code generator needs: which of a platform's own components meanstext,glyph, orcontainer.The ADRs
conventions.platformsreplacesconventions.figma; keys name implementations, not platform families, and stay flatPrimitiveKindis the bindable subset ofElementTypepropsis a closed, concept-keyed map onto a component's props; unmapped styling routes tostylesPropLayoutMode-keyed map; onlystylesProphoists to the platformimages.componentnames the image component in code, beside thematchnaming it in Figma — an image is an attribute, not a node kind, so it stays out of the primitive vocabularyconfig/conventions/; the filename is the platform id, so there is no merge rule and no single-file formmetadata.conventionsrecords only the producing platform, not every platform the workspace configuresdefaultFillWidth— the container width a platform gives a root that resizes to fill its parentWhat is implemented here
packages/schemaonly. The order for this ecosystem is schema →specs-from-figma→ CLI, since the CLI depends onspecs-from-figma; this PR is the first step.types/Conventions.ts—Conventions.platforms,PlatformConventions,PrimitiveKind,TextBinding/GlyphBinding/ContainerBindingand their resolved forms,MetadataConventions,stylesProp,defaultFillWidth.DEFAULT_CONVENTIONSbecomes{}: every default it carried belongs inside a declared platform entry, and no platform being declared is a statement no default can supply.types/Metadata.ts—conventionsretyped toMetadataConventions.schema/conventions.schema.json— rewritten.PlatformConventionsdoubles as the standalone per-file root ADR-078 needs, so a singleconfig/conventions/<id>.yamlvalidates on its own and the two forms cannot drift.MetadataConventionsis the same definition undermaxProperties: 1.schema/component.schema.json—metadata.conventionsnow referencesMetadataConventions.propsvocabularies, theLayoutMode-keyed container, the metadata narrowing, anddefaultFillWidth.schema/conventions.mdrestructured aroundplatforms,primitives,stylesPropanddefaultFillWidth; 30 pages updated to drop thefigma:YAML wrapper (the filename carries the id now) and repoint paths and anchors.CHANGELOG.md— unreleased 0.31.0 entries amended where the reshape invalidated them, rather than contradicted by new ones.Gates:
tsc -p tsconfig.build.jsonclean,validate-schema.sh7/7, all 14.test-d.tscompile,packages/schematests 13/13.Version: stays
0.31.0.Conventionsis@since 0.31.0and unpublished — npm's latest is0.30.0— so reshaping it inside the release that introduces it breaks no published contract. That reasoning expires the moment 0.31.0 ships.Not done here
config/conventions.yamlare all CLI work. The schema-side half (the standalone per-file definition) is done.specs-from-figmaand the CLI. Both readconventions.figmaand will not compile against these types until updated, in that order.workspace.schema.json, which ADR-071 already removed. Stale row; nothing to change.figma.images-style prose headings. They read correctly as platform-qualified concept names, but thefigma.prefix no longer appears in the file itself.🤖 Generated with Claude Code