Figma's Dev Status has three values a designer can set: READY_FOR_DEV, COMPLETED, and none. The CLI recognizes only READY_FOR_DEV; COMPLETED — set when the design work is finished, arguably a stronger curation signal than "ready for dev" — is silently collapsed to NONE and becomes indistinguishable from a component nobody has touched. A second, closed vocabulary in the manifest parser then makes any attempt to record the real status silently lossy.
Package:@directededges/specs-cli — found on 0.26.0, still present on 0.27.0 (current release; verified in the shipped bundle 2026-08-29).
The string COMPLETED does not appear anywhere in dist/.
1. scan discards every dev status except READY_FOR_DEV
FigmaFileDiscovery.findAllComponents() (dist/specs.js, two occurrences):
devStatus: node.devStatus?.type==="READY_FOR_DEV" ? "READY_FOR_DEV" : "NONE"
In our library file that is 39 COMPLETED components against 4 READY_FOR_DEV, so a manifest-driven generate selected 4 of 43. Note specs fetch is fine — the payload it writes carries COMPLETED correctly; only the scan-side mapping drops it.
2. The manifest parser's closed vocabulary silently drops rows
varROW_REGEX=/^\|\s*\[([x])\]\s*\|\s*(.+?)\s*\|\s*(\d+:\d+)\s*\|\s*(COMPONENT_SET|COMPONENT)\s*\|\s*(READY_FOR_DEV|NONE)\s*\|/i;
ROW_REGEX accepts only (READY_FOR_DEV|NONE) in the status column, and generate in manifest mode (ManifestParserV2.parse → .filter(c => c.included)) runs every row through it. A row it cannot parse is dropped, not merely unselected — and the ✓ Loaded manifest: N components (M selected) line counts only survivors, so the run looks healthy.
That makes the manifest silently lossy for any status value the CLI does not itself emit, and it is what turns (1) from "a mapping gap" into a metered spending decision applied to the wrong set with no error.
Reproduction, verified against the shipped regex:
| [x] | Text Field | 9313:18494 | COMPONENT_SET | COMPLETED | DROPPED
| [x] | Text Field | 9313:18494 | COMPONENT_SET | NONE | COMPLETED | PARSED (extra cell ignored)
Suggested fix
- Carry Figma's
devStatus.type through verbatim instead of mapping to a closed two-value set. - In the manifest parser, treat unknown status values as unselected rather than unparseable.
- Warn when a manifest row fails to parse instead of dropping it silently.
Our current workaround
We wrap specs scan in a script that re-reads the fetched payload and appends a sixth column carrying the real Figma status, leaving column 5 in the CLI's own vocabulary so ROW_REGEX still parses the row (it is not anchored to end-of-line). All of that machinery can be deleted the day the CLI carries the status through itself.
Two smaller behaviours, observed on 0.26.0 (not re-checked on 0.27.0)
- Without
--split-components, a per-component generate loop writes one shared library.yaml and overwrites it wholesale per invocation — billing a metered generation each and keeping only the last. - Generating a renamed component writes a new derived filename without removing the old spec for the same
nodeId; on a case-insensitive filesystem the two can differ only in case.
Figma's Dev Status has three values a designer can set:
READY_FOR_DEV,COMPLETED, and none. The CLI recognizes onlyREADY_FOR_DEV;COMPLETED— set when the design work is finished, arguably a stronger curation signal than "ready for dev" — is silently collapsed toNONEand becomes indistinguishable from a component nobody has touched. A second, closed vocabulary in the manifest parser then makes any attempt to record the real status silently lossy.Package:
@directededges/specs-cli— found on0.26.0, still present on0.27.0(current release; verified in the shipped bundle 2026-08-29).The string
COMPLETEDdoes not appear anywhere indist/.1.
scandiscards every dev status exceptREADY_FOR_DEVFigmaFileDiscovery.findAllComponents()(dist/specs.js, two occurrences):In our library file that is 39
COMPLETEDcomponents against 4READY_FOR_DEV, so a manifest-drivengenerateselected 4 of 43. Notespecs fetchis fine — the payload it writes carriesCOMPLETEDcorrectly; only the scan-side mapping drops it.2. The manifest parser's closed vocabulary silently drops rows
ROW_REGEXaccepts only(READY_FOR_DEV|NONE)in the status column, andgeneratein manifest mode (ManifestParserV2.parse→.filter(c => c.included)) runs every row through it. A row it cannot parse is dropped, not merely unselected — and the✓ Loaded manifest: N components (M selected)line counts only survivors, so the run looks healthy.That makes the manifest silently lossy for any status value the CLI does not itself emit, and it is what turns (1) from "a mapping gap" into a metered spending decision applied to the wrong set with no error.
Reproduction, verified against the shipped regex:
Suggested fix
devStatus.typethrough verbatim instead of mapping to a closed two-value set.Our current workaround
We wrap
specs scanin a script that re-reads the fetched payload and appends a sixth column carrying the real Figma status, leaving column 5 in the CLI's own vocabulary soROW_REGEXstill parses the row (it is not anchored to end-of-line). All of that machinery can be deleted the day the CLI carries the status through itself.Two smaller behaviours, observed on 0.26.0 (not re-checked on 0.27.0)
--split-components, a per-componentgenerateloop writes one sharedlibrary.yamland overwrites it wholesale per invocation — billing a metered generation each and keeping only the last.nodeId; on a case-insensitive filesystem the two can differ only in case.