Summary
Consolidate the runtime adapter implementations for the processing engine into specs-from-figma, alongside the FigmaNodes/FigmaFoundations interfaces and the existing REST adapters. Today the plugin adapters (FigmaPluginNodes, FigmaPluginFoundations) live in specs-plugin-2, split from their REST counterparts and the interfaces they implement. This split has produced a recurring, hard-to-catch class of bug.
Why
The engine has two planes of node access:
- Lookup/scanning — goes through the shared
FigmaNodes abstraction (getPageSiblings, getNodeById, …). Interface + FigmaRestNodes live in specs-from-figma; FigmaPluginNodes lives in specs-plugin-2.
- Tree traversal / property access — engine code (e.g.
SlotDetector) walks raw node trees and reads runtime-specific properties directly (.componentId, .children, getMainComponentAsync). This plane is not abstracted, and the node shape differs by runtime (REST JSON vs live Figma SceneNode).
Because the plugin adapter lives in a different repo and is never exercised by specs-from-figma's tests (which mock nodes in the REST shape), plane-2 divergences ship green in the engine and break only at runtime in the plugin. Two instances of exactly this:
SlotDetector gated instance recursion on .componentId (REST-only) → the plugin never descended into instances, capturing no nested slot content. (Just fixed by falling back to getMainComponentAsync().id.)
getPageSiblings/scan results needed mainComponentId populated on plugin INSTANCE nodes → earlier fix in FigmaPluginNodes.
Proposal
Move the plugin adapters into specs-from-figma:
specs-from-figma/src/Runtime/Nodes/{interfaces, FigmaRestNodes, FigmaPluginNodes}
specs-from-figma/src/Runtime/Foundations/{interface, FigmaRest…, FigmaPlugin…}
specs-plugin-2 imports them from @specs-from-figma/Runtime/... instead of owning them. The plugin still owns its UI, controller, output rendering, and build pipeline.
This is safe given the project's constraints: specs-from-figma is private; plugin-only code leaking into the obfuscated package consumed by specs-cli is acceptable; and the plugin already builds directly from specs-from-figma source (tsconfig alias), so no new coupling is introduced.
Benefits
- Both adapters sit next to the interface they implement — adding/changing an interface method makes the missing-impl divergence obvious.
specs-from-figma's test suite can (and should) exercise the plugin-shaped path, closing the "green in engine, broken in plugin" gap.
- The plugin adapters become first-class — the current
@ts-expect-error reach-ins into specs-from-figma privates go away.
Scope / acceptance
- Move
FigmaPluginNodes + FigmaPluginFoundations into specs-from-figma, carrying their current (latest) logic (note: these files have changes on the examples-slots-instances branch not yet in release — preserve those).
- Rewire
specs-plugin-2 imports; delete the @ts-expect-error reach-ins.
- Add
@figma/plugin-typings as a specs-from-figma devDependency.
- Keep
figma.* references inside methods only (never module top-level) so the CLI/REST build can bundle the plugin adapter without evaluating figma. Preserve the node-builtin-stub invariant (build-plugin.js stubs node:crypto/etc).
- Engine traversal tests run against both a REST-shaped and a plugin-shaped fixture.
Stretch (the deeper fix)
Once both adapters live together, route raw node-property access in plane-2 through the adapter (e.g. componentId(node), children(node), type(node)) so traversal code never does (child as any).componentId again — or normalize nodes into a canonical internal tree at the runtime boundary.
Notes
- Larger refactor, intentionally scoped outside the examples feature work (ADR-047/048/050).
- To be done on a branch off the release branch; reconcile with the adapter changes currently on
examples-slots-instances.
Summary
Consolidate the runtime adapter implementations for the processing engine into
specs-from-figma, alongside theFigmaNodes/FigmaFoundationsinterfaces and the existing REST adapters. Today the plugin adapters (FigmaPluginNodes,FigmaPluginFoundations) live inspecs-plugin-2, split from their REST counterparts and the interfaces they implement. This split has produced a recurring, hard-to-catch class of bug.Why
The engine has two planes of node access:
FigmaNodesabstraction (getPageSiblings,getNodeById, …). Interface +FigmaRestNodeslive inspecs-from-figma;FigmaPluginNodeslives inspecs-plugin-2.SlotDetector) walks raw node trees and reads runtime-specific properties directly (.componentId,.children,getMainComponentAsync). This plane is not abstracted, and the node shape differs by runtime (REST JSON vs live FigmaSceneNode).Because the plugin adapter lives in a different repo and is never exercised by
specs-from-figma's tests (which mock nodes in the REST shape), plane-2 divergences ship green in the engine and break only at runtime in the plugin. Two instances of exactly this:SlotDetectorgated instance recursion on.componentId(REST-only) → the plugin never descended into instances, capturing no nested slot content. (Just fixed by falling back togetMainComponentAsync().id.)getPageSiblings/scan results neededmainComponentIdpopulated on plugin INSTANCE nodes → earlier fix inFigmaPluginNodes.Proposal
Move the plugin adapters into
specs-from-figma:specs-plugin-2imports them from@specs-from-figma/Runtime/...instead of owning them. The plugin still owns its UI, controller, output rendering, and build pipeline.This is safe given the project's constraints:
specs-from-figmais private; plugin-only code leaking into the obfuscated package consumed byspecs-cliis acceptable; and the plugin already builds directly fromspecs-from-figmasource (tsconfig alias), so no new coupling is introduced.Benefits
specs-from-figma's test suite can (and should) exercise the plugin-shaped path, closing the "green in engine, broken in plugin" gap.@ts-expect-errorreach-ins intospecs-from-figmaprivates go away.Scope / acceptance
FigmaPluginNodes+FigmaPluginFoundationsintospecs-from-figma, carrying their current (latest) logic (note: these files have changes on theexamples-slots-instancesbranch not yet in release — preserve those).specs-plugin-2imports; delete the@ts-expect-errorreach-ins.@figma/plugin-typingsas aspecs-from-figmadevDependency.figma.*references inside methods only (never module top-level) so the CLI/REST build can bundle the plugin adapter without evaluatingfigma. Preserve the node-builtin-stub invariant (build-plugin.js stubsnode:crypto/etc).Stretch (the deeper fix)
Once both adapters live together, route raw node-property access in plane-2 through the adapter (e.g.
componentId(node),children(node),type(node)) so traversal code never does(child as any).componentIdagain — or normalize nodes into a canonical internal tree at the runtime boundary.Notes
examples-slots-instances.