Repository navigation
Plugin 7.0.0 is a major release because every existing document asks to
be updated once. EXTRACTOR_VERSION moves from '3' to '4' for Figma
slots, so a component doc built by 6.0.0 or earlier reads "Rebuild
needed". Annotate in Dev Mode, motion variables and the Motion section,
Figma slots, placeholders for empty writing sections and the Library's
incremental source check all ship here, with Foundation Context v5 schema
5.2.0 and Component Context v5 schema 5.4.0. It is the first plugin built
and attested by release-plugin.yml. The proxy it needs deploys from
main ahead of it, and spec-layer@0.13.0 is latest on npm, which a
pulled Markdown page needs before it says what the canvas says about a
slot.
The CLI portion of this section ships as 0.12.0, a minor release because
pull, status and list gain --json, and because --help and
--version now print to stdout and exit 0. It is also the first CLI that can
pull a library with an EASING variable: 0.11.0 does not know the easing
token type, refuses the Foundation as failing schema validation, and asks for
a republish that cannot help. A plugin build that exports easing tokens must
not reach the listing before 0.12.0 is latest on npm. A repository on 0.11.0
re-projects once on its first pull with 0.12.0 through the
manifest.cliVersion check. 0.12.0 is the first version published by the
release workflow, so it is the first to carry npm provenance.
The CLI changes merged after 0.12.0 ship as 0.13.0, a minor release: pulled
Markdown prints each Figma slot's facts (preferred components, "only
preferred", the limits and default content) where 0.12.0 lists a slot with
its type alone, and spec-layer skill says what a slot asks of code (Figma
slots, under Added). A plugin build that documents slots should not reach the
listing before 0.13.0 is latest on npm, so a pulled Markdown page says what
the canvas says.
Added
-
Annotate in Dev Mode. A Library row's menu and Settings, Export can now write a
component doc back into Figma's own fields, so Dev Mode and the REST API
show what the canvas shows. The Usage text the doc shows (Overview, When to
use, When not to use) goes into the component description with one
provenance line, and is cleared again once the doc no longer has it. When a
file link is saved in Settings, the documentation link opens the doc's
Section, follows it when an Update rebuilds the Section, and is cleared when
the doc is deleted. When the doc draws its Anatomy legend, each top-level
part gets a native annotation numbered as the legend numbers it, with
padding, spacing, radius and type pinned as live properties, so no number is
ever typed; an annotation on a part the legend no longer numbers is removed,
and a doc whose parts changed since it was built places no annotation until
it is updated. Every run you start asks first and names AI text nobody has
edited: the confirmation says what it writes in one line and lists each
caution as its own point, naming which kinds of value (description, link,
annotations) a change made in Figma keeps. A description, link or annotation set, changed or removed in Figma
is kept, and replacing it is a separate, labelled step that also names AI
text: it is offered once, when a run finds a value newly changed in
Figma, and again from a component's Library row; a run over every
component names what it kept in a toast instead. A part shown by a
property is labelled "Shown when … is true", as the legend says. A record
of what was written lives in the component's plugin data,
and the serializer reads a description or link that matches it as absent,
so an annotated doc never reports drift against its own words and the AI
prompt is never fed the sync's own text. A description a person edits in
Figma becomes theirs and reads as source like any other, without the
provenance line. A record from a newer build is left untouched. Nothing it
writes enters a hash or an artifact, andEXTRACTOR_VERSIONis unchanged.
An optional switch annotates again after an Update, only for components
already annotated by hand, never writes text that was edited in Figma or
written with AI, and names a component it held back once, not on every
Update. Whether Figma's MCP server or Make reads the descriptions is not
verified. -
Motion variables in the Foundation. Figma's
TIMINGandEASING
variable types (Plugin API update 133) are read asdurationtokens in
seconds and a neweasingtoken type (a named preset, a cubic bezier, a
spring bounce, or hold). Foundation rows read "0.3 s" or the easing name,
and a cubic bezier draws its curve. Foundation Context v5 moves to schema
5.2.0 (foundation-5.2.0.json); the envelope sits outside the content
hash, so no artifact identity moves for a file without these variables.
DTCG projectsdurationand a cubic beziercubicBezier; a preset,
spring or hold is omitted and named inreport.json, never expanded to
numbers the API did not return. Before this, aTIMINGvariable exported
as a unitlessnumberand anEASINGvariable read "Not resolved".
@figma/plugin-typingsmoves to 1.140.0. -
Motion section for variant transitions. Prototype interactions that
change a component set's variant (hover, press, click, delay) appear as a
Motion section after States, with From, To, Trigger, Transition, Duration
and Easing, and asmotion.transitionsin the YAML brief, Component
Context v5 (since schema 5.3.0), Markdown and the AI
profile. ACHANGE_TOoutside the set is reported as
transition-target-outside-set. The section is marked Beta in the
section picker; existing documents gain it when it is ticked on. The
motionkey exists only when a component has transitions, so every
document without them keeps a byte-identical hash: the content hash of an
affected document moves on its own, which is what drift detection
reports. (The release still movesEXTRACTOR_VERSIONto'4', for Figma
slots, below.) An easing on a prototype
transition that Figma stores as a physical spring (mass, stiffness,
damping) is reported as not supported, never converted to a bounce,
because the conversion is Figma's own and not available to a plugin. -
Figma slots. A component's slot properties and slot layers are read
and documented. Each slot is a property underapi.slotswith
type: slot, and Figma's own facts about it: the designer's
description, the preferred components (named from the instances inside
the component, then the file's own components, and never over the
network, so a drift check does not depend on library access), "only
preferred", the minimum and maximum, "display empty
by default" and "stretch child on insert". Each slot layer is one anatomy
part carryingslot(its property) anddefault_content, what it holds
in the default variant. Default content is an example, so it is no longer
documented as anatomy parts, related components, token bindings or
unbound values; a slot inside a nested instance keeps its content, which
is fixed content of the component. On canvas, Properties reads Type
Slot with the preferred names, "only these", the limits and the
default content, and the Anatomy legend marks each slot. A preferred key
the document cannot name is counted, never named, and reported as
unresolved-slot-content; a slot linked to no property is reported as
unlinked-slot; a property type the extractor does not know is left out
and reported asunsupported-property-type. The Library's change list
itemizes each slot change. Component Context v5 schema 5.4.0
(component-5.4.0.json) gainsanatomy_node.slot,
anatomy_node.default_contentand a typedapi.slotsentry. 5.3.0,
the motion contract above, was published on spec-layer.com before
slots and stays there unchanged; no release writes it.
EXTRACTOR_VERSIONmoves to'4', so every document built by 6.0.0
reads Rebuild needed once. Annotate in Dev Mode never
pins a corner radius on a slot, which Figma refuses. Eachapi.slotsslot
entry also carriesdefault_content, so the Markdown page prints the same
Values and Default text as the canvas, adds a Description column only for
a slot's own description, and says in Anatomy what each slot holds. A
published library versions a slot by direction: narrowing what may go in
it (only preferred turned on, a raised minimum, a lowered or new maximum, a
preferred component removed while only preferred is on) is major,
widening is minor, and its default content and display settings are
patch; each is a property change in the history, named by its facet, so a
6.0.0 plugin's history pane still reads it. The AI writing prompt gets
each slot's facts in the same words. An instance swap property now reads
"Instance swap" in Markdown rather than "InstanceSwap". The agent guide
spec-layer skillwrites, and the plugin's skill download, say what a
slot asks of code: build it as the framework's children or a named slot,
accept only the preferred components when only preferred is on, keep to
the limits, and treat default content as an example, never as fixed
markup. The guide uses the YAML keys or the Markdown page's words,
whichever format the components were written in.
Changed
-
CI's dependency review exempts
@figma/plugin-typingsfrom the licence
allow-list. The package ships a verbatim MIT licence but declares it as the
non-SPDX string "MIT License", which GitHub reports as
LicenseRef-bad-mit-license, so every version bump failed the gate. The
terms are unchanged; only the metadata is exempted. -
The component screen's main button says Update docs (and
Updating docs… while it runs) when the component already has a doc,
in place of Replace docs. It runs the same build as the Library's Update. -
Pro AI writing moves to Claude Sonnet 5.5. The proxy now assigns
claude-sonnet-5-5to a proved Pro license, still at
output_config.effort: low, in place ofclaude-sonnet-5. The per-token
price is the same. The free plan stays on Claude Haiku 4.5
(claude-haiku-4-5), which is still the current Haiku. The plugin never
names a model, so this needs only a proxy deploy, and every 6.0.0 plugin
picks it up. A 5.1.0 plugin's legacy request still names its own Haiku
model and is forwarded unchanged. An answer the proxy stored before the
deploy is replayed for an unchanged request until it expires, within 24
hours. -
The
spec-layerCLI is now published from this repository's release
workflow through npm trusted publishing, so each version from the next
release on carries an npm provenance attestation that names the commit and
workflow run it was built from.npm audit signatureschecks it. Plugin
releases attach the built plugin as a zip with a GitHub build provenance
attestation (gh attestation verify). -
The proxy's license routes refuse a request body over 4 KB with
413,
like the other routes that read a body. Every proxy response carries
X-Content-Type-Options: nosniff, and an internal error's log line
carries the first 2,000 characters of its stack; the response stays
generic. -
The proxy's per-IP rate limits now hold across every isolate in a
Cloudflare location: behind each isolate's own window sits the Workers
Rate Limiting binding, at the same limits (60 a minute for prose, quota,
dry runs and pulls; 20 for license, publish and rotate). Before, each
isolate counted alone, so a burst spread across isolates was not limited.
Bundles are hashed on publish with the runtime's native SHA-256. -
spec-layer pull,statusandlisttake--json, which prints one
JSON object instead of the text: the command, its exit code, its own
fields (state, version, publish date, the files written), and the text
lines asstdoutandstderrarrays. Exit codes do not change.
spec-layer --helpand--versionnow print to stdout and exit 0; before,
both printed the usage to stderr and exited 1. -
pullandstatusnow retry a dropped connection, a timeout, a5xxor
a429twice with a short backoff, honouring the server'sRetry-After
up to 30 seconds, and say so on stderr.SPEC_LAYER_RETRIES=0turns this
off. A failed connection names the system error (ENOTFOUND, a
certificate error) and, whenHTTPS_PROXYis set without
NODE_USE_ENV_PROXY=1or a certificate is not trusted, what to set.
Requests carry aUser-Agentwith the CLI and Node versions, and a
response over 64 MB is refused instead of read. -
When the free AI writing allowance is used up and AI writing is on, the
component screen now says so before you build: one line under the switch
reads that no free AI uses are left until the reset date and that sections
marked AI will be drawn as placeholders, with an Upgrade to Pro action.
Before, the header knew and the build went ahead, and the placeholders
arrived as news in a toast afterwards. -
"Detach this doc" in a Library row menu has its own glyph. It shared one
with "View this doc on canvas". -
The Library's Update all progress line reads "Updating docs" and leaves the
number to the count beside the bar, which said "0 of 3" while the label
said "doc 1 of 3". -
Quick search no longer repeats the page name on every row when every result
comes from the same page. -
Coming back to the Library from another tab no longer re-runs the source
check on every visit. The plugin now watches the current page for edits
and re-checks only when something changed since the last scan; an
unchanged file sends no extraction work at all. When a check does run it
goes one document at a time in the order the rows are shown, pauses when
you leave the Library and resumes when you return, and shares one
variable and style lookup cache across the whole pass instead of one per
document. One line under the filters shows a running check's progress
("Checking for source changes", a bar, "3 of 14") and, once it
finishes, when the rows were last checked ("Checked just now", "Checked
4 min ago", "Checked at 10:42"). It keeps the same height in every
state, so a check starting or finishing no longer moves the list, and
the progress no longer floats over the last rows; Update and Update all
docs keep their progress above the footer. A doc built by an older
version of the plugin reads "Rebuild needed" as soon as the rows appear,
with no check to wait for, so the rebuild banner no longer drops in
above the list partway through, and those docs skip a source read that
could only ever say that. Refresh library still forces a full check. A
variable rename, addition, or deletion also counts as a change. A spacing or radius
(number) variable value edit also counts, since a component's layout
summary carries the resolved numbers. Any style edit counts, a paint,
text, effect or grid value edit included, whichever page you make it
on, so a Foundation row that shows a style re-checks after it. A color,
string or boolean variable value edit does not, because no component row
carries those values and Figma sends no event for variables, and the
Foundation row that shows them keeps its last result until the next
change or Refresh library. The plugin watches layer edits on the page you
have open only, so layer edits by others on any other page are not seen,
visited or not.
While the plugin is asking whether anything changed, Update, Update all
docs and Refresh library are disabled for that moment. A source check
still running when a new scan starts no longer sets a row from a read
taken before that scan; each reply carries the pass it was asked for. No
extraction output changes. -
Free AI writing is now a flat 20 generations per UTC calendar month. It
was 20 in the first 30 days after first use and then 10 a month; the
first-sight boost window is gone from the proxy's quota engine, and its
two stored fields (firstSeen,boostUsed) are dropped when a counter
loads. Every generation was already counted by month, so a free user
keeps this month's count and moves straight to the new limit, with no
migration. This is a proxy change only: the plugin reads the limit from
the proxy's quota headers, so no plugin release is needed. Publishing
limits are unchanged. -
Empty writing sections are drawn as placeholders instead of left out.
When a selected writing section has no prose (AI writing off, the monthly
allowance spent, or a failed request), the doc now draws it inside a dashed
box marked Placeholder, with guidance on what to write, in the section's
own shape: When to use, Do and don't, Keyboard, Pointer and touch, Semantics
and focus, Content, and Overview when the component has no Figma
description. Type over the guidance on the canvas and the next Update keeps
it and drops the box. Guidance is never stored as prose, so Copy for AI,
the YAML and Markdown outputs and publishing never see it. Sections built
from the spec that have nothing in it are still left out. The result line
now reads "Added placeholders for When to use, Keyboard. Fill them in on
the canvas.", and the allowance note says sections that needed AI were
added as placeholders. No hash orEXTRACTOR_VERSIONchange.Text typed into a placeholder is recorded as written by a person, and
stays recorded after later Updates. The brief's and the Component Context
v5 artifact'sguidelinesblock lists those fields underauthored, after
origin: generated, or readsorigin: authoredwhen a person wrote every
field in it; a field built partly from AI text, such as Interactions with a
typed Keyboard row beside AI-written Pointer bullets, is not listed. The
Markdown marks a section a person wrote "Written on the Figma canvas, not
generated by AI." in place of the AI line. A rebuild never asks AI to
rewrite a key a person wrote, Keyboard included. Thespec-layerskill
guide describes both markers. Output for docs without typed-in text is
byte for byte unchanged, andguidelinesstays outside every hash, so no
schema file changes. The documentation site's description ofguidelines
lives outside this repository and still has to be updated there. -
Faster builds, Updates and selections. Clicking from layer to layer
inside a component set no longer reads the whole set again: a selection
that resolves to the component the panel already shows, unchanged, sends
nothing, so the panel also keeps the variants and sections you picked. It
reads again after a layer edit on the current page, any style edit, or a
variable change. Edits by others on other pages are not seen, the same
limit the Library's check has. The first selection reads the component and
the file's variables at the same time instead of one after the other.An Update no longer re-checks every doc in the file when it finishes. The
docs it rebuilt are in sync, and every other row keeps the result of its
last check, since an Update never changes a source; "Checked ..." keeps
that check's time. Refresh library still checks every row. Update all now
rebuilds each doc without reading the whole registry for it, and shares
one variable lookup cache, one Foundation read and one contrast report
across the run.Token chips resolve each token once per build instead of once per variant
card, and a token table resolves all of its tokens together. A Foundation
frame loads its text-style fonts at once. Telling whether a doc has hand
edits finds its tagged layers with one search instead of asking every
layer, so the Library scan and every build spend less time there. None of
this changes a hash orEXTRACTOR_VERSION. -
Removed code nothing reached: the Markdown token tables left in the
extractor after the 1.x Markdown specification went,narrowFoundation
and thenot-in-scopereference status only it could produce, and four
test-only v5 helpers. The extractor and the CLI now share one canonical
JSON serializer; every hash is byte for byte unchanged.
Fixed
-
A component with a Figma slot property no longer stops its Properties
section from building. The extractor did not know Figma'sSLOT
property type, so the property reached the canvas with no type, and
Figma refuses an empty text value. A baseline saved before this fix lists
the change as "type not recognised changed to slot". -
Undo in Figma now reverts one Spec Layer action at a time. Plugin edits
were never closed as undo steps, so one Undo after building a second doc
could take the first doc away too. Each build, Foundation build, update,
detach and remove is now its own step. -
Keyboard focus stays where it was when the plugin redraws on its own (a
build finishing, the allowance loading, a selection change); before, it
fell back to the page and a screen reader lost its place. The plugin also
follows a change of Figma's light or dark theme while it is open, and an
invalid first version on Publish is read out with its hint. -
An error no part of the plugin expected is now logged to the plugin
console and shown as a toast, instead of stopping the action silently. -
A
spec-layer pullthat could not move its new output into place (on
Windows, a file held open by a virus scanner or indexer) deleted the
previous.speclayer/first and left none. The previous copy now moves
aside first and moves back when the swap fails. Rotating the pull key
over a loose-permissionspeclayer.local.jsonno longer leaves the new
key readable for a moment: the file is made0600before the key is
written.speclayer.jsonis written atomically. -
AI writing no longer fails after the text was already written. When the
proxy wrote an answer but could not record it against the allowance, it
answered with an error, and every retry for the next three minutes was
told a generation was still pending. The answer now comes back, is not
counted, and a retry runs at once. -
A Pro license check now gives up on Lemon Squeezy after 10 seconds and
falls back to the last good result within the 5-day grace window. Before,
a request waiting on a Lemon Squeezy call that never answered stayed open
until Cloudflare ended it. -
When Anthropic is overloaded or rate limited (429, 503 or 529), the proxy
now answers503 upstream_busyand passes on Anthropic'sRetry-After
when there is one, instead of a502that read as broken. The plugin
shows the same message for both. -
A damaged license cache entry is now treated as missing and replaced,
instead of failing every request for that key with a 500. -
Clicking inside the component you just documented, then pressing Create
docs again, asked for a new AI draft and spent another free AI use. The
draft is now kept for that component as long as it reads exactly as it
did when the draft was written. -
The Measurements card, its value chips, and the "No variant" cell in a
matrix kept rounded corners under the Sharp and Round corner styles. They
now follow the theme like the Anatomy card beside them. -
"Contact support" on the License screen opened the homepage. A key the
service has turned off now says which address to email and the button,
Email support, opens your mail client with the subject filled in. -
A component build that fails now stays on screen as a banner above the
footer until the next Create docs or selection, the way a Publish error
already does, instead of a five-second toast. This covers a failure before
anything reaches the canvas too (no section picked). Reselecting the same
component keeps the banner, and selecting another layer clears it. If the
selection changes right as the build fails, for example because a layer
was deleted during it, the failure arrives as a toast instead. -
The Publish screen said "Checking…" three times while it read the file's
identity. The header pill and the footer button say it; the Version block
waits. -
Building a Foundation frame no longer fails with "in set_maxWidth: Can
only set maxWidth on auto layout nodes and their children". 6.0.0 capped
two text nodes before appending them to their auto-layout parent, which
Figma refuses: a code-syntax identifier chip, so any collection with a
variable that defines code syntax, and the block label on a frame that
holds both layouts. Both caps are now set after the append, and the
test stub throws Figma's own error when a min or max size is set outside
auto layout, so the suite catches this order from now on.
Before publishing
- The manual Figma pass in
packages/plugin/TESTING.mdran on this build (the zip attached here) and is recorded below. -
npm run check:site-livepassed against https://spec-layer.com (the scheduled Site live run counts). - The proxy this build needs is deployed to production.
- Any CLI version this build needs is
lateston npm. - The Figma Community listing is updated to 7.0.0 last, after all of the above.
Install from the Figma Community listing, or import manifest.json from the attached zip under Plugins → Development → Import plugin from manifest. gh attestation verify <zip> --repo sandroleks/spec-layer checks the zip was built by this repository's release workflow.