ADR 073-079, 081: Conventions for primitive composition - #363
Closed
nathanacurtis wants to merge 32 commits into
Closed
ADR 073-079, 081: Conventions for primitive composition#363nathanacurtis wants to merge 32 commits into
nathanacurtis wants to merge 32 commits into
Conversation
…to their own package Rebuilds the transform work onto the current release. Three adaptations the original branch predates: clipsContent (ADR-069), configuration read from settings.data.directory (ADR-071), and outputFormat now required on TransformerContext. - cssvars emits library-level CSS custom properties from the fetched library JSON - css expresses inline shadows, blurs and gradient fills that were dropped before - react and stories are consumed from @directededges/react-from-specs Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…vars docs Cherry-picks the webcomponents wiring onto the current release rather than merging feat/webcomponents-from-specs, whose base predates ADR-069 and ADR-071 and would have reinstated clipContent and the removed Config type. - webcomponents and webcomponents-stories come from @directededges/webcomponents-from-specs, consumed like react-from-specs - both are marked experimental in the docs and changelog; the output shape is not yet stable - the imported webcomponents page referred to config.processing.states, which ADR-071 replaced with the figma.states convention - cssvars had a docs page but no nav entry or index row, so it was unreachable Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`--components` narrows the per-component pass, but cssvars produces all of its output in finalize() over the whole data directory, so a single-component run still paid for the full library stylesheet. TransformerContext now carries `scoped`, set when --components is given, and the cssvars transform returns early rather than rebuilding output a scoped run cannot have invalidated. Re-running it unscoped stays the way to pick up token changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Figma frame dimensions include padding; the CSS default excludes it, so every element carrying both a fixed dimension and padding rendered larger than the spec by exactly its padding — a 24px frame with 4px padding measured 32px. Generated stylesheets now set border-box on the block and its descendants. False variant values emitted `[data-x="false"]`, but scaffolds write booleans as presence: the attribute is set to "" when true and omitted when false, never written as "false". Those rules matched nothing, so every false-valued variant's styling was dead. They now emit `:not([data-x])`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…versals Cursor is an affordance Figma has no concept of. It is now inferred from what the stylesheet actually styles: a component that styles a pressed state is a press target and gets `pointer`; one that styles a disabled state gets `not-allowed`. Keying off emitted rules rather than declared states matters — a spec can declare a state whose variant produces no styling at all, which is not evidence of interactivity, and that gave text inputs a pointer. An opacity VARIABLE is authored on a percentage scale (36) while an unbound opacity arrives as the ratio Figma stores (0.36). Emitting the variable bare produced `opacity: 36`, which clamps to 1 and silently discarded the state. Token references are now multiplied into a CSS percentage; raw values are untouched, so the two authoring paths both round-trip. A classified boolean's FALSE value has no concept of its own — it is the negation of the true concept. Those variants were dropped entirely as base/rest state, so an unselected variant and every hover/pressed pairing with it emitted no rule. They now emit `:not([aria-selected="true"])`, with multi-part concepts negated as an AND of nots. A variant layout that exactly reverses a flex parent's children now emits `flex-direction: row-reverse`/`column-reverse` under that variant's selector. This is a visual swap, so DOM order — and reading and tab order — stays as authored. Partial reorders are not expressible this way and are relocated by the emitters instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A negative itemSpacing means children OVERLAP, which `gap` cannot express. The branch that recognised it returned early with a note that child margins were "deferred" — so overlap was never emitted, and every layout declaration after that point was silently dropped for the element too, including FILL translation. Stylesheets now emit a negative margin on each child after the first, along the parent's main axis, and the negative case only skips `gap`. `--get-images` picked the file to query by a hard-coded preference for the `library` alias, so generating from a different source asked the wrong file for its image URLs. A hash present only in the source file came back missing and was reported as Figma failing to return it. The alias now comes from the manifest being generated, which already names its source. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
Brings in ADR-080: PropConfigurationValue and InstanceExample.propConfigurations admit null, so a configuration can state that a nullable prop is unset.
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>
A component's rules are now written three ways from one pass. The class form is unchanged. The host form targets `:host`, with the root's qualifiers inside the functional form — `:host([data-ratio="1:1"])` — so a Web Component can be the root itself and a caller can size it by styling the element. The light-DOM form carries the box-sizing reset for content composed into a custom element, which nothing inside a shadow root can reach: `:host *` stops at the boundary and `::slotted()` reaches only the top level. Without it every composed element computed as content-box and each padded one came out larger than the spec says. Root selectors all resolve through one helper, so the forms cannot drift. Name warnings are suppressed on the second pass — the same names, reported once.
Schema: - TextBinding gains a `content` concept — EGDS Text takes its string as a `text` prop, not children, and GlyphBinding already had the counterpart. No default: absent means children, a name means that prop, null means no content channel. ADR-075 and the conventions docs updated to match. CLI: - ConfigLoader reads config/conventions/<platform>.yaml (ADR-078). The filename is the platform id, so a platform is declared in exactly one file and there is no merge rule. A stray config/conventions.yaml is refused with the migration it needs, not silently ignored. - resolveConventions became a per-platform resolver, applying each concept's default prop name inside a declared binding and folding the platform-level stylesProp into each primitive. - PlatformConventions.ts gives every call site a value for an undeclared platform, restoring the guarantee the required `figma` key used to give. - The React transformer resolves text/glyph/container elements to the bound component, keeping the generated class so existing CSS still applies. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brings the closed transform packages into the branch. Rendering now lives only in specs-from-figma's packages, so the CLI's own React and Stories transformers are deleted rather than kept in step: - packages/cli/src/transforms/React.ts — deleted (react-from-specs) - packages/cli/src/transforms/Stories.ts — deleted (react-from-specs) - packages/cli/src/transforms/react/primitives.ts — deleted, my duplicate of the resolver that belongs in react-from-specs The registry now resolves react, stories, webcomponents, webcomponents-stories and cssvars from the closed packages, which is what the eg workspace's pipeline.yaml has been asking for. This also fixes two things seen in Storybook, both of which were the branch mismatch rather than defects: - `Dot.contract.ts` exported `EgdsPagingCarouselDotDefaults` while the scaffold imported `DotDefaults`. The release branch's Contract.ts prefixed subcomponent symbols with the parent; feat/react-from-specs already emits the bare name. - Story ids moved because the public Stories transformer titled generated stories `Components/…` where the closed one titles them `React/…`. Types/Transformer.ts keeps both sides: dataDirectory and scoped from the feat branch, platform and platformId from this one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A container binding substitutes a component for a box that holds children, so the component's root must be the box those children land in. A layout component that wraps its children in an inner element cannot stand in for one: the container's gap, padding and alignment land on the outer box while the children sit a level deeper, and a wrapper carrying `flex: 1 0 0` inside a `height: fit-content` parent collapses the subtree to zero height. Neither failure raises an error, and neither is expressible in the schema — whether a root hosts its children is a fact about that component's generated markup, not about the conventions naming it. Recorded as a Decision Driver, a new section under Decision 1, a Consequence, and a caution in the conventions docs. ADR-074 Decision 2 is left alone pending a call on whether composition-only resolution should be restated there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The number was claimed on main and the release branch while the ADR branch was in flight, so merging release brought that draft row back alongside the accepted one — the duplicate the accept skill warns about. Same ADR, two tables, two titles. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both commands still produced the single-file layout the loader now refuses, so
`specs init` was scaffolding a workspace the CLI would not read.
- `specs migrate config` writes config/conventions/figma.yaml — everything the
pre-split file declared was a Figma fact — plus commented stubs for react and
web-components. A workspace that generates code will want them, and a file of
pure comments parses to null, so an untouched stub declares nothing. Its
overwrite guard now also refuses when the conventions directory holds files.
- `migrateConfigV1` returns the Figma entry BODY rather than a `{ figma }`
wrapper: the filename is the platform id.
- `specs init` scaffolds the same five files.
- The figma template loses its wrapping key and is de-indented to the root.
Tests: 694 passing. Verified end to end — a v1 workspace migrates, the stubs
parse to null, and the loader reads the result without refusing it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Member
Author
|
Retargeting rather than abandoning: this work now goes to release via Nothing is lost. The branch is unchanged and reopens as a PR into |
nathanacurtis
added a commit
that referenced
this pull request
Sep 4, 2026
* chore: start specs-schema v0.31.0 development * chore: start specs-cli v0.28.0 development * chore: use neutral example identifiers in docs, comments and fixtures (#353) Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * ADR-071: split Config into Conventions, Settings and Pipeline (#352) * docs(adr): draft ADR 071 — separate library conventions from tooling settings Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr): revise ADR 071 — independent term lists, Figma-scoped conventions, artifact layout, plugin analysis Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr): ADR 071 — uniform match/exclude shape for name-based conventions Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr): ADR 071 — block-sequence YAML; rekey conventions without adding capability Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr): ADR 071 — expand states examples from a real catalog declaration Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr): ADR 071 — focus Decision 1 on the two-way split; move plugin evidence to Downstream Impact Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr): ADR 071 — explicit detect setting replaces block-presence on-switch Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr): ADR 071 — no switch; scope and backgroundImage classify as conventions Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr): ADR 071 — two root types, no container; avoids PropConfigurations collision Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr): ADR 071 — output/format swap, figma.naming, settings absorbs workspace members, ResolvedConventions justified Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr): ADR 071 — settings grouped by concern, paths co-located, pipeline artifact Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr): ADR 071 — work vs place; sources folds into data; transformers own no paths Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr): ADR 071 — assets block, analysis derived, targets deferred Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr): ADR 071 — Pipeline type, supporting type placement, schema ref strategy, author, PanelSettings Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr): ADR 071 — one schema file per artifact, workspace schema retired Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr): ADR 071 — retire workspace.schema.json for per-artifact schemas Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(schema)!: split Config into Conventions, Settings and Pipeline (ADR-071) BREAKING CHANGE: Config, ResolvedConfig and DEFAULT_CONFIG are removed. Library facts move to Conventions.figma, run choices to Settings grouped by concern, and declared work to Pipeline. Metadata.config becomes Metadata.conventions + Metadata.settings, and workspace.schema.json is replaced by one schema per authored artifact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr): ADR 071 — record the config/ folder-name rationale Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * adr(071): accept — Conventions, Settings and Pipeline replace Config Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(site): retarget schema pages to Conventions, Settings and Pipeline (ADR-071) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr): use neutral example identifiers Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * docs: restore /pro/thankyou order confirmation page (#355) Ported from the specsplugin-com repo, lost in the site migration. - Left-aligned doc layout with sidebar and table of contents hidden, rather than the centered splash template - Activate your license section split into individual and team subscriptions, drawing the subscription/license distinction - Seat management consolidated into Manage your subscription - Card grids replaced with plain link lists; links made site-relative Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * ADR-071: CLI loads conventions, settings and pipeline (#354) * feat(cli)!: load conventions, settings and pipeline (ADR-071) ConfigLoader reads a config/ directory of three artifacts and still loads a legacy specs.config.yaml, migrating it in memory with a one-time warning. init scaffolds all three files. Commands, bridge and analyzers read the two halves, with a fallback for specs whose metadata predates the split. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(site): regroup nav as Configuration with conventions, settings and pipeline Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(settings): retarget configuration pages to the three artifacts (ADR-071) Each option page names which file it lives in and why, with legacy paths kept as migration notes. Overview retitled Configuration; folders documents the per-concern directories; data-sources renames data to fetch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(cli,guides): retarget to the three-artifact configuration (ADR-071) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(cli): update stale strings in scan manifest and init output Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: legacy config is refused, not migrated in place; add migrate command page Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(cli): refuse pre-split config; add specs migrate config The loader detects a legacy file only to stop — before the fallback path, so a workspace is never generated from defaults it did not declare. Conversion moves to a registry-backed migrate command, keyed by subject and source version. init refuses in a legacy workspace rather than scaffolding defaults over a real configuration. Every refusal names a remedy that works: the discovered file, an explicit --source, or moving a user-level config into the workspace. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(schema): update types README for the split exports Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(schema): align README diagram border Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(schema): add DEFAULT_CONVENTIONS; amend constitution for the split exports The constitution named DEFAULT_CONFIG as the only permitted runtime export, so ADR-071 violated it by shipping two. The rule now permits one defaults constant per configuration artifact, and conventions gain theirs — removing the literals the CLI had been hardcoding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: retarget repo docs to the three-artifact configuration (ADR-071) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(schema,cli)!: default the split spec layout to on One folder per component holding one file per concern is what transform, analyze and render all read, so the layout was not really a per-consumer choice — yet splitComponents, splitConcerns and useSubfolders carried no schema default and both consumers picked false, the one shape nothing downstream can use. DEFAULT_SETTINGS now carries all three as true and ResolvedSettings requires them. The generate flags are renamed for what they do, since each can now only turn a split off: --combine-as-library, --combine-concerns and --no-subfolders replace --split-components, --split-concerns and --use-subfolders. An absent flag defers to the configured value rather than overriding it. specs migrate config writes all three out explicitly, including from a source that configured no output block, so migrating changes where configuration lives without changing what a workspace emits. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(cli,schema): record the configuration split in the changelogs The cli 0.28.0 section was empty, so the breaking change users hit first — specs.config.yaml no longer being read — went unrecorded while the release notes described only the layout default. Adds a Breaking section leading the release, writes up specs migrate as the Added feature it is, and covers config/ discovery, --config taking a directory, init refusing over a legacy workspace, the vocabulary renames, and the removal of user-level ~/.specs/config.yaml. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * feat(schema): carry a closed numeric option set on NumberProp (ADR-072) (#357) Figma has no numeric variant, so a count axis is authored as a VARIANT whose options are the strings "1", "2", "3". Read back, that became an EnumProp — a string type over a numeric domain — and consumers typed the prop "1" | "2" | "3". NumberProp could carry the numeric type but left the range open; EnumProp could carry the closed set but fixed type: string. A numeric variant needs both at once, and neither type provided it. `enum?: number[]` closes that gap, so the option set survives alongside the numeric type — the render direction rebuilds the variant axis from it positionally, so losing it would make the round trip lossy. Additive and optional: a NumberProp without `enum` keeps its present meaning of an open numeric range. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: carry Figma dev status through scan and stop dropping manifest rows (#362) (#364) `scan` collapsed every dev status other than READY_FOR_DEV to NONE, so a COMPLETED component was indistinguishable from an untouched one. The manifest parser then accepted only those two values and silently discarded any other row. Dev statuses are now carried through verbatim, an unrecognized status leaves the row parsed but unselected, and both scan and generate warn on a row they cannot read. Selection is unchanged: generate still builds only READY_FOR_DEV defaults. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(adr): claim numbers 073-079 in the index 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> * docs(adr): claim 080 in the index Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ADR-080: `null` as a Prop Configuration Value (#368) * docs(adr): draft ADR-080 — null as a prop configuration value Widens PropConfigurationValue with a null arm so a configuration can state that a nullable prop is unset, instead of pairing a content value with a visibility boolean that is not a prop of the component being configured. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(schema): null as a prop configuration value (ADR-080) A configuration can now state that a nullable prop is unset. null is a value, not an absence: an absent key inherits from the layer beneath, a null key overrides an inherited value with no value. Without it, a content prop paired with a visibility boolean could only be expressed as both at once — a bound value beside a false flag naming a prop that pairing had already removed from the component's props. InstanceExample.propConfigurations declares its own inline union so it can exclude PropBinding, so it is widened separately. It gains null and keeps that exclusion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(adr): accept ADR-080 All gates pass: tsc build, schema validation 7/7, type tests compile. Moves the index row from Draft to Accepted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(adr): claim ADR 083 in the index Reserves the number for the slot-wrapper collapse ADR drafted on 083-slot-wrapper-collapse, so concurrent ADR authors do not collide. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(adr): draft ADR-083 — collapsing a slot-only wrapper (#378) * docs(adr): draft ADR-083 — collapsing a slot-only wrapper Extends ADR-058's primitive wrapper collapse to a root whose sole anatomy child is a slot, governed by the existing collapsePrimitiveWrapper setting. Context and prior analysis in #377. Claims 083 in the index (082 is taken by origin/082-component-description). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(adr): drop the style eligibility test from ADR-083 Root and slot are both container nodes with the same style signature, so merging them discards nothing and no style needs testing. ADR-058 needs an eligibility list because it merges a container into a `text` or `glyph` leaf, where container styles have nowhere to go — that reason does not carry over. - Eligibility is structural only: a container root with exactly one anatomy child, of type slot, all-or-nothing across variants - The merge keeps the slot's value on any shared key; the slot is the box the children sit in, and the wrapper's layout is an artifact of holding one child - Expansion splits by what a style acts on: parent layout to both boxes, child layout to the slot, everything else to the root `padding` is called out as the one parent-layout key that is not a no-op on a single-child wrapper — applied to both boxes it insets twice. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(adr): correct the padding note in ADR-083 Merge keeps the slot's value on a shared key; values are never summed. A root with padding 8 and a slot with padding 12 collapses to 12. The earlier note framed padding as an exception to the split rule. It is not: a slot carrying padding is an authoring mistake, since padding belongs to the box with the surface and the slot has none. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(schema): collapse a slot-only wrapper (ADR-083) collapsePrimitiveWrapper now covers a root container whose only child is a container bound to the component's sole slot property. Eligibility is the binding, not the anatomy label, and it must hold in every variant. No member is added, removed or retyped — the setting's documented meaning widens and the behaviour belongs to the consuming packages, so this is a PATCH within the unreleased 0.31.0. - types/Settings.ts — Settings.spec and ResolvedSettings.spec doc comments state both shapes and why they are tested differently - schema/settings.schema.json — description widened to match - site docs — the settings page gains the slot shape, its eligibility and a collapsed example; the schema table row is updated - CHANGELOG — one Changed entry under 0.31.0 The collapsed element keeps the slot layer's $extensions['com.figma'], so the source layer stays traceable, matching what ADR-058 already does for a leaf. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(adr): accept ADR-083 All three gates pass: tsc build, schema validation (7/7), and the type tests. Status flipped to ACCEPTED and the INDEX row moved from Draft to Accepted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * Record this release's ADR states in the schema changelog Comparing where the decisions stand meant opening the adr/ directory and reading status lines one file at a time. The changelog already frames the release, so it carries the list: accepted, updated drafts and new drafts, each linking the ADR number beside its current title. States are read from the files rather than from adr/INDEX.md, which goes stale and currently lists 083 under two states at once. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Take the ADR-072 title from the file, backticks and all The heading in 072-numeric-variant-enum.md carries no backticks; the list had borrowed them from INDEX.md, which is the source this section deliberately does not read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * release: @directededges/specs-schema v0.31.0 * release: @directededges/specs-cli v0.28.0 --------- Co-authored-by: Claude Opus 5 <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