---
name: "convention-figma"
type: convention
license: CC BY-NC-SA 4.0
description: Conventions for Figma in Aled's system — which connector writes, the Figma rulebooks that must be loaded before any write (figma-use always; figma-generate-library for component work; figma-generate-design for a page or screen), the house structural bar a Figma build is held to on top of them (auto-layout, bound tokens, proportionate layers, instance reuse, matched naming), how a design-system file is organised (one page per component family, a fixed doc-frame skeleton, every main component inside the frame that documents it, the split trigger), and what it inherits from standard-design-system, convention-aesthetic and the venture's convention-brand instance. Read before any Figma write — a component, a family page, a screen, a library — and before reviewing one (skill-design-parity's Figma structural pass). Sits beside convention-storybook, which holds the code side. Surfaced by APP-614 and APP-897; grown as builds are reviewed.
---

# Figma conventions

How Figma is used in Aled's system. Follows the per-tool convention pattern of `convention-storybook`, `convention-vercel` and `convention-github`: one convention, applied to every Figma file the fleet touches. Owned by Pixel. Deliberately thin — Figma publishes its own rulebooks and the community has written the generic build-quality rules already; this artefact makes loading them non-negotiable and adds only what is ours: the connector, the house bar, the file organisation, and the inheritance from the design system and the aesthetic.

## Why this exists

Figma output from the agents looked approximately right on the surface and was structurally poor underneath — wrong containers, absolute x/y where auto-layout belonged, stacked layers doing what one styled frame would do, hardcoded values where tokens existed. The cost was paid twice: token spend on the build, then manual rework to make the file usable. Aled's framing, 2026-07-20: *if this was execution in code it would be deemed unacceptable* (APP-614). Separately, main components left loose on a page canvas among demo instances were nudged and detached — the plausible origin of the unbound-selector bug (A1-333) and the definite origin of a ten-instance orphan run (A1-336, APP-897). Both failures came from the same absence: nothing on our side bound the rulebooks that already exist, and nothing said where a thing lives in a file.

## The write surface — one connector

One Figma MCP connector is the write surface: the full one, carrying `use_figma`, `create_new_file`, `search_design_system` and the `skill://` rulebook library. A read-only connector (`get_design_context`, `get_screenshot`, `get_metadata`, `get_variable_defs`, `get_figjam`) cannot satisfy the preload rule below and is removed from the Cowork surface (Owner ruling 2026-09-03, APP-614; the config item is carried on APP-965). Where a session finds two Figma connectors attached, the one without `use_figma` is the wrong one — do not treat its presence as write access.

## The typeface must be reachable before any text is authored

The Figma MCP runs server-side. It sees Google fonts, Apple fonts, and fonts uploaded to the **Figma account** — never a font installed locally on the operator's machine, whatever the desktop app shows. `loadFontAsync` on an unreachable face fails and Figma substitutes its default silently, so an agent-built file renders in the substitute while the nodes beside it, authored by hand, record the intended face. The divergence is invisible to the operator: his own machine has the font and renders the file correctly, and it surfaces only when an agent-built component is compared against a hand-built one.

So the order at project setup is fixed: **settle the licence position → upload the face to the Figma account → then let agents author text.** In the other order the library is wholly wrong in a way that renders fine on the author's machine, and correcting it afterwards is a file-wide sweep rather than an edit.

Three checks before the first component is made:

- **The face is reachable from the account, not the desktop.** The account-level upload (avatar → Settings → Account → Your uploaded fonts) is what makes a font reachable by the MCP and therefore by agents; it is a different route from the organisation font upload, which needs the Organization or Enterprise plan. Confirm the face resolves in a file the connector reads, not merely in the desktop font menu.
- **The cut is named, not just the family.** `Helvetica Neue LT Std` and the macOS system `Helvetica Neue` are different cuts of one name, with potentially different metrics. The venture's `convention-brand` instance names the cut; the file uses that cut.
- **A substituted face is a build defect.** A text node rendering in a face nobody chose is the same class as a hardcoded value where a token exists — it fails the structural bar whatever it looks like rendered, and `skill-design-parity`'s Figma structural pass names it.

(Worked case: the `a1.meirionpritchard.ds` library was built by agents while Helvetica Neue existed only on the operator's Mac. Every text node in ② Components, ③ Blocks, ④ Sections and ⑤ Templates — roughly 26 real nodes plus inheriting instances — records Inter, which nobody chose, while the hand-authored nodes record Helvetica Neue. Account-level upload verified working on a Professional plan, 2026-09-08 — APP-1166.)

## Load the rulebooks before every write

No `use_figma` call is made without the matching Figma skill loaded first. The load mechanism is the plugin skill where the Figma plugin is installed (`/figma-use` and siblings), or `get_figma_skill` against the `skill://` URI where it is not (index at `skill://index.json`).

| Work | Load, in order |
| -- | -- |
| Any write at all | `figma-use` — the Plugin API rulebook: auto-layout for any container whose children are structurally related (never `createFrame()` plus absolute x/y), the sizing-mode and append-then-size ordering rules, inspect the file's existing conventions before creating anything, skeleton-first building in small validated steps. |
| A component, variant set, token or library | `figma-generate-library` on top — variables and tokens first, then components with proper variant sets and bound tokens. Required even for a single component. |
| A page, screen, modal or multi-section view | `figma-generate-design` on top — discover the system's components with `search_design_system`, import them, assemble section by section from instances rather than drawing primitives. |
| Mapping repo components to Figma components | `figma-code-connect`. |

The generic build-quality rules are a solved problem and are **pointed at, never copied in**: Figma's own skills above, and the MIT-licensed `senlindesign/claude2figma` (its preflight-produces-a-token-map pattern — enumerate the file's tokens and components before creating a node — and its search → instance → bind → verify loop per section are worth following). Owning a copy of someone else's rulebook is maintenance we do not want; what is ours is below.

## Plugin API footguns that are ours to record

Figma's own rulebooks are pointed at, never copied in. These are the exception the rule allows for: each is a behaviour the upstream doc under-specifies, each was re-derived at cost by one of our own builds, and each renders wrong without erroring.

- **`resize()` resets a frame's *own* sizing modes, not only a child's.** `figma-use` Rule 12c covers `layoutSizingHorizontal` / `layoutSizingVertical` on a child. The same reset applies to the frame's own `primaryAxisSizingMode` / `counterAxisSizingMode`, so `resize()` called after setting `AUTO` pins the frame at its height at that moment and every later append is clipped — `clipsContent` defaults true, `get_metadata` reports the pinned height, and nothing errors. Set the sizing modes **last**, with no `resize()` after. (Worked case: four frames in one `use_figma` session on A1-556 — root, content and two of four table containers — reported at ~194px with ~2,330px of content appended beneath; caught only by the Rule 5 screenshot, since the metadata height was wrong too — APP-1857.)
- **A range-bindable text property comes back as an array, even for one uniform binding.** On TEXT nodes `fontSize`, `fontStyle`, `lineHeight` and `letterSpacing` are returned as arrays even for a uniformly styled run with exactly one binding across the whole text, so a script testing `bv.fontSize && bv.fontSize.id` first, and treating any array as mixed, reports a single genuine uniform binding as mixed or unbound. Only an array holding more than one **distinct** variable id is mixed content. Unwrap, dedupe the ids, and report mixed only on a count above one. This fails in the direction that costs most: a false mixed reading is confident, uniform across every node checked, and plausible enough to reach a ticket's findings. (Worked case: a type-ramp audit's re-verification pass returned mixed for every node it checked, including single-word labels that could not plausibly be mixed; unwrapping and deduping gave the true reading on the same data — APP-1860.)
- **`boundVariables.strokeWeight` is not a key Figma sets.** Bound stroke weight lives on `strokeTopWeight`, `strokeBottomWeight`, `strokeLeftWeight` and `strokeRightWeight`. A scan reading `node.boundVariables.strokeWeight` reads every stroked node as unbound whatever its real state, which is a false systemic-unbound finding rather than a missing one. (Worked case: a "border width unbound across 150+ instances" finding on A1-560, re-verified against the per-side keys as bound almost everywhere sampled — APP-1856.)
- **A `COMPONENT_SET`'s root-frame padding and `itemSpacing` are not a design instance.** They are Figma's "combine as variants" grid scaffold for the editor; the set's root is never placed in a design, only its child `COMPONENT` variants are instanced. A hardcoded-value scan that includes `COMPONENT_SET` in its node-type filter and flags that container padding is a category error, not a finding. (Worked case: three of eight findings on A1-560's token audit were this, and were removed — APP-1856.)
- **`node.boundVariables.fills[i]` (and `.strokes[i]`) is the alias object directly — there is no `.color` key at node level.** The nested form belongs to the individual paint (`node.fills[i].boundVariables.color`), a different location holding the same reference. A scan reading `node.boundVariables.fills[i].color` reads every bound fill as unbound, whatever its real state, and throws nothing. Use one location consistently, and check it against a raw one-node dump with no filtering before trusting it in a scan. The tell is a write call that reports N nodes mutated while the next scan reports the same nodes unbound. (Worked case: A1-642, 2026-10-09 — about 20 fills read back as unbound straight after a successful bind; a raw dump on node 39:231 showed the direct shape. One instance — APP-2200. The two shapes and the node are the filing leg's report; no Figma file is readable from the refine surface, so they are unverified here.)
- **`setBoundVariable` on a TEXT node sets the node-level default only — rebind the range.** It does not rebind the characters already in the node. A first-pass script that called `node.setBoundVariable('fontSize', newVar)` on uniformly styled text left the segment's own `boundVariables.fontSize` on the old variable, the render unchanged, and `node.boundVariables.fontSize` reading back as an array of two distinct ids on a node with one style segment. Use `node.setRangeBoundVariable(0, node.characters.length, field, variable)` and verify with `node.getRangeBoundVariable(...)`, not `node.boundVariables`, which reflects the node default and can disagree with the render. This is the write-time twin of the read-time array bullet above. (Worked case: A1-634, 2026-10-09, `block/b-text` — one extra diagnose-and-fix cycle. One instance — APP-2202; the filing leg's own live read, not re-run by a second pass, and unverified here.)
- **`fontStyle` and `fontWeight` are alternatives on a text range, not a pair.** Binding one clears the other, in either order, and both are valid bindable field names that throw nothing, so a script that binds both keeps whichever went last. Pick one, and read both back after writing. (Same worked case and same caveat as the bullet above — APP-2202.)

- **Clip content crops leading-trimmed text, and nothing errors.** A text style using cap-height leading trim deliberately shrinks the text node's box to the cap height, so ascenders and descenders paint *outside* it. Any ancestor frame with Clip content on therefore cuts them off, and the type renders shaved rather than wrong — which is why it survives the build and is found on review. So when a trimmed text style is applied, or a frame holding trimmed text is built, **clear Clip content on every ancestor frame in that text's chain, not only its immediate parent** (`clipsContent = false`), and check it again on any file-wide style swap that introduces trim, where the frames needing it are every frame the swap reached rather than the ones authored for it. The counterpart on the record side is `standard-design-system`'s **measurement** section — the line box and trim mechanism per surface — so a type number read off a trimmed box is never applied to an untrimmed one. (Worked case: A1-603, 2026-10-05 — the type ramp samples were rebuilt on the `type/*` styles, which trim to cap height; the frames the samples sit in all had Clip content on, and the samples rendered cropped. The builder did not catch it; Aled found it on review and asked for it to be turned off, and the fix was `clipsContent = false` on 355 frames inside the ramp frame. One instance. Premise unverified — no repo and no Figma file is readable from this refine surface: the property spelling as written, the 355-frame count and the `type/*` style prefix are the filing leg's report, and Clip content's default state on a newly created frame was **not** re-read against `plugin-api-standalone.d.ts`, so this bullet states no default; that flag covers those identifiers only, not the mechanism, which the run demonstrated. APP-2069.)

Several of these came from a script hand-written against assumed Plugin API shape. Check a property name against `plugin-api-standalone.d.ts`, or against one live read, before a scan's finding is reported as systemic.

## The structural bar — the house additions

Stated as properties a finished build either has or lacks, so a review can name the one it lacks. Qualitative on first draft; numeric thresholds wait until enough reviewed builds exist to calibrate them (APP-614).

- **Auto-layout for every container whose children are structurally related.** Absolute positioning is for a genuinely free-placed element, not for a row, a stack or a grid.
- **Tokens bound, not typed.** Fills, strokes, spacing, radius and type are bound to the file's variables or styles wherever the venture's system defines one. A hardcoded value where a token exists is a defect, whatever it looks like rendered. **Illustrative marks are exempt.** A mark that is bespoke artwork (the nav marks, the spots) is exempt from token binding: its stroke weights, corner radii and sizes are optical tuning, and an unbound value on one is a recorded exemption, never a defect. Binding stays optional for a mark (a fill that must follow a theme can still bind a colour variable) and is never scored against it. Its check is that it renders identically to its exported SVG, which is the source of truth where the mark ships as one, and a sweep lists a mark as exempt rather than failing it. The exemption covers marks only: UI-structural icons (`assets/icon/*`: close, drag-handle, selector) stay in the token system. The exemption is stated on the mark's asset record (`standard-design-system`, *The asset record*), because an exempt value nobody wrote down reads the same as a missed binding. (Owner ruling, Aled, 2026-10-09: illustrative marks are exempt — APP-2204. Premise unverified: that the nav marks are consumed in code as exported SVGs rests on the titles of A1-124 and A1-204, no repo was read, and the 4px pupil stroke and 3.14px star stroke quoted by the filing leg were read from the Figma file by that leg and not re-read here; that flag covers those two points only, not the ruling.)
- **Layer count proportionate to the result.** No stack of layers achieving what one frame with applied styles would do. Depth and count that a reader cannot account for from the rendered result are a defect.
- **Instances over redrawn primitives.** Where a library component exists, the build places an instance of it. Drawing the shape again is a fork of the system.
- **Naming matched to the file.** Read the file's existing convention (`figma-use` §9) and follow it; the lexical scheme itself is a trial instance held in `standard-design-system` and is not scored.
- **Every main component contained** — inside the doc frame that documents it, on the page for its family (*File organisation* below). A main loose on the canvas is a defect, not a convenience.

## File organisation — a design-system file

How a design-system Figma file is laid out. Agreed on the a1.meirionpritchard.ds pilot (A1-336, modifier family) and stated once here so every subsequent family inherits it rather than re-deriving it from instinct (Owner ruling 2026-09-03, APP-897).

**One page per component family**, holding everything for that family — the main components *and* their documentation. No separate "Docs — x" pages. The page takes the family's parent-component name.

**A fixed doc-frame skeleton, the same for every family, in this order:**

`doc/overview` → `doc/config` → `doc/parent` → `doc/components` → `doc/sub-components` → `doc/examples`

The three middle frames descend the family — parent, then components, then sub-components. `doc/config` carries the family's `component.config/v1` record (`standard-design-system`). A family that does not yet need a frame leaves it empty rather than omitting it, so the skeleton reads the same on every page.

**A doc frame holds specification only — findings and open items go on the ticket, never into the file.** An audit finding, a FINDINGS block, an "open" note or a question for the Owner written into `doc/config` or onto a foundations page has nothing sweeping it: nothing re-reads it, nothing closes it, and it goes stale inside the one artefact that is supposed to read as the current specification. So findings, open questions and progress notes live on the ticket (or in chat), and the frame carries only what the component or foundation **is**. (Owner ruling D104, 2026-10-05, `log.meirion-decisions.2026-W41` — an F1–F3 block and two audit paragraphs written during the A1-604 cell build were ruled clutter to be removed, and findings ruled to belong on the ticket. Distinct from APP-1858, which settled where a token-audit **frame** is placed; this settles what may go inside one. APP-2121.)

**The main components live inside the doc frame that documents them.** The variant set sits inside `doc/components`; a sub-component sits inside `doc/sub-components`. There is no separate library region: the frame that explains a part is the frame that holds it, so there is one place to look and nothing sits loose on the canvas. This is the load-bearing rule — it removes the mechanism by which loose mains are nudged and detached. A main with no frame to live in is a signal the doc skeleton is incomplete, not a licence to drop it on the canvas.

**A part lives on the page for its layer.** Sub-components live with the family that owns them — they carry state and behaviour and only mean anything inside their parent. Assets (icons, marks) live on the assets page — pure marks, no behaviour, free to be used anywhere. Filing follows what a thing *is*, not who currently uses it; a corollary is that an asset is never named for one consumer's usage (A1-343). An assets page carries the same skeleton, with its marks in `doc/components`, but its `doc/config` frame holds the asset record (`standard-design-system`, *The asset record*) and not `component.config/v1`, because variants, API, responsive modes and token binding mostly do not apply to a mark. (APP-2205.)

**A `doc/config` or doc-skeleton fix is sized as author-from-scratch until a live read says otherwise.** A criterion such as "each family page's `doc/config` describes its own component(s), with the real Storybook title and code path" reads as a one-row correction and is not. Where the frame holds another family's copy or a placeholder, meeting it means a full `component.config/v1` record for that frame, and a family with no `doc/*` frames needs the whole six-frame skeleton built, including locating its `COMPONENT_SET` and reparenting it into `doc/components`. So a dispatch or sizing read costs such a ticket from the number of frames it names, not from its criterion's wording, and opens one named frame before it costs the rest. (Worked case: A1-640, 2026-10-09 — ten write targets, seven rewrites and three full skeletons, against a criterion that read as small; the filing leg put the gap at roughly an order of magnitude. One instance — APP-2198, with its duplicate APP-2199. The counts are the filing leg's report and unverified here. Whether `skill-fleet-dispatch`'s sizing pass should also spot-check a named frame is not ruled: one instance, and this paragraph is read at build time.)

**A doc frame is sized to what it documents**, not to a fixed page width — `doc/parent` at 1632px to carry a 1376px part, the rest at 1280px. One system-wide width set by the widest part trades a ragged lane for a lot of empty frame.

**The split trigger.** A family gets its own page when a sub-component starts being used outside its parent, or when a family exceeds about six doc frames. Not on instinct. Page-per-component does not survive forty components; page-per-family does, and this is when a family breaks out.

**Frame naming waits on the trial.** The `doc/…` slash-path form above is the pilot's working form and follows whatever `standard-design-system`'s naming trial returns on APP-613 (the A1-231 verdict). The structure — the page model, the skeleton and its order, the containment rule, the split trigger — is canon now; the lexical form inherits the verdict and is not fixed here.

**A foundations or token-audit artefact is a single flat frame, not the skeleton.** The skeleton above descends a family — parent, components, sub-components — and a token or foundations audit has none of that structure, so it takes the second shape: **one flat frame**, placed on the file's existing foundations page, reusing the file's established doc-frame chrome (header band over a content area, matching type styles) so it reads as part of the file rather than as a visitor. **And it is positioned against a read taken immediately before the write, not against the read the build opened with.** The "position away from (0,0)" rule assumes a single writer: two sessions building on one page each find it empty at the moment they check, both default their new top-level frame to (0,0), and the second frame is invisible underneath the first with nothing erroring. So re-read the page's current children in the same call that positions the frame, and offset past the rightmost sibling. (Worked cases: A1-556, A1-557 and A1-560 each derived this placement independently and landed side by side on the same page with no overlap, which is what makes the pattern worth writing down rather than merely plausible; and on the same page A1-560's frame and A1-556's `tokens/colour` frame were built in parallel and stacked exactly at (0,0), found only when the Owner asked whether the frame had made it onto the page — APP-1858, APP-1856.)

**No maintained template yet.** A new family starts by copying the skeleton from a neighbouring family page. A published Figma template is a tooling commitment someone maintains; it is adopted once the pattern has been used twice and has stopped moving (Owner ruling 2026-09-03, APP-897). Record its pointer here when it exists.

**A main component with no page at all is invisible to a page-based sweep, and can still be load-bearing.** Figma keeps a main alive as long as one instance references it, so a `COMPONENT` can sit with `parent === null` — on no page, inside no frame, reachable only through its instances. A sweep built on `page.findAllWithCriteria(...)` per page can never reach one, and the containment rule above does not catch it either: it is not loose on a canvas, it is nowhere. So a file-wide pass — a font or style swap, a token rebind, a rename, a parity count — does not finish at the end of the page walk. It then enumerates every unique `instance.mainComponent` in the document, flags each whose `.parent` is null, and fixes or files those too. They are not safely assumed dead: in the one file swept this way, 5 orphaned mains turned up and one of them, `components/hero-bar-v1`, is composed *inside* `sections/s-header` — the live canonical section, still used across templates and example frames — with its title text still on the pre-swap system font and one nested instance carrying an unbound 84px line-height literal that matched no other instance of the same text. Neither would ever have surfaced from a page sweep. (Instance: `a1.meirionpritchard.ds`, the A1-511 font-family sweep, 2026-09-24 — found only on a second, cross-referencing pass — APP-1751. The enumeration needs a script: `get_metadata` takes a node ID and cannot list parentless mains, so the count of 5 is that sweep's reading, not a connector re-read.)

## Inheritance

Three layers stack, and this convention states only what the layers below do not:

- **`standard-design-system`** — the token and component bar (architecture, three-way naming, hierarchy, binding discipline, the component config record). A Figma build is held to it exactly as a code build is.
- **`convention-aesthetic`** and the venture's **`convention-brand`** instance — the visual bar: taste, references, anti-patterns, and the palette and faces a render must never fall back from to a tool default.
- **This convention** — the Figma-specific mechanics: connector, preload, structural bar, file organisation.

`convention-storybook` holds the code side and the three build modes; which side is the reference for a given scope is the scope's declared mode, and this convention applies to the Figma side in every mode.

## Review

`skill-design-parity` carries the **Figma structural pass** — a read-only review of a finished Figma build against the structural bar above, reporting in plain language which properties the build lacks, so a bad build bounces to its builder rather than being hand-reworked. Enforcement is **review only** for now: a write-time check that fires on every change is not adopted until the review pass has calibrated what "structurally poor" means on our files (Owner ruling 2026-09-03, APP-614). Remediation of a file already built badly — converting flat absolute-positioned frames to auto-layout in place — is a separate capability with no canon home yet; name it when it is needed rather than assuming the review pass covers it.

**Read side: resolve a nested instance to its main component before accepting a visual-similarity claim.** The bar above governs what a build *has*. This governs what a reader may *conclude*. A structural claim about a file — "the label and the caption come from the same modifier cell", "these two rows are the same component", "this block reuses the shared selector" — is verified by **main-component node ID**, never by looking at the frame. Read the nested instances with `get_metadata` and compare the main components they resolve to.

Two components that look alike in a frame may be one main used twice or two different mains that happen to render the same, and **only the node ID settles which.** The two cases have opposite consequences and no visual difference: one is the system being reused, the other is a fork of the system that will drift the moment either side is edited. Accepting the resemblance records the fork as reuse — the same class of invisible defect as a substituted face or a hardcoded value where a token exists, and invisible for the same reason: the render is correct either way.

So the obligation is on the reader, not the builder, and it runs before the claim leaves the file. A claim traced to node IDs can be repeated in a ticket, a `skill-design-parity` report or a build. A claim resting on visual similarity is an observation, and is written as one — "these look like the same part" — not asserted as structure.

**And the evidence is the node's *type*, not only its ID.** "X is an instance of Y" asserts two things — that X is an `INSTANCE` at all, and which main it resolves to — and `get_metadata` returns both, so a finding quotes both per usage: the node ID, and what the node is (`INSTANCE` of a named main, or `TEXT`, or `FRAME`). A plain text node styled to look like a part of the system is the commonest false positive here, and it reads identically in the frame. **Where a usage's node cannot be located, that usage is written `unverified` — never generalised from a sibling that was located.** One confirmed instance of a shared main says nothing about a second usage, and a confirm leg that admits it could not find a node and generalises anyway hands the next build a false premise in the ticket's own words. So a build leg whose ticket premise *is* a Figma-structure claim re-reads that one node's type before building: a single `get_metadata` call, against the cost of a build and a revert. Quote the date the node was read with the ID — IDs go stale as a file is reworked, and a stale ID is indistinguishable from a wrong one.

(Worked case: told that a block's label and caption were derived from the same modifier cell, the `.selector`, `modifier-name` and `grabber` instances on both were traced with `get_metadata` and confirmed to resolve to the same main component, `39:1905`. The claim held — but nothing in the rendered frame showed that it did, and the same frame would have looked identical had it not. 2026-09-22, `meirionpritchard-com` — APP-1663.)

(The negative case, on the same main. A confirm-only leg reported that Split's `secondary` and BText's caption were instances of that same `text/caption` modifier cell, `39:1905`, while admitting in the same finding that `b-split`'s own Figma node could not be located. A build was written and shipped on the finding — `LabelBar` wiring, CSS, new baselines, PR #172 — and reverted once the Owner pointed out that both are plain text nodes. Cost: one build-and-revert cycle, two baseline rounds, and Owner review time. Re-read on 2026-09-25 through the Figma connector on `a1.meirionpritchard.ds` (`PgD9RVk9v7BRyNTPulV64T`): `39:1905` resolves as a component, and `316:2959` resolves as a text node, not an instance — the claim was false as stated, and one `get_metadata` call would have said so before the build. The second usage the finding cited, `299:2940`, no longer resolves in the file at all, which is its own argument for dating a quoted node ID. 2026-09-23 — APP-1720.)

**And where the claim is about a mark's *shape*, read the node's pixels — the connector has a route that needs no egress.** The read-side rules above settle **identity** and **type** from `get_metadata`. A glyph, icon or logo port raises a question neither answers — *is this the same form?* — and the obvious route to the source's own render is `download_assets`, which returns short-lived **URLs** and therefore needs an outbound fetch a sandbox can block. The route that does not: **`get_screenshot` with `enableBase64Response: true`**, which appends the node's render inline as base64 beside the URL, and whose own schema names exactly this case — set it true "ONLY if the agent cannot fetch URLs (no shell access, no HTTP client, or a sandboxed environment that blocks outbound requests)". Raise `maxDimension` where the detail is fine; the response reports `original_width`/`original_height` beside the rendered size, so a re-request at a higher dimension is a decision rather than a guess. Then compare **painted form against painted form** — arm or segment count, angles, extents, stroke — rather than reading the render by eye: a symmetry claim made by looking at a source screenshot is a visual-similarity claim, and the first rule of this section already says what those are worth. **And where the mark is a vector, the raster is the weaker instrument — the Plugin API returns the path itself as text.** A 20x20 render cannot carry a sub-two-pixel round-ended arm, so a pixel diff against it can read clean on a glyph that is the wrong form. `use_figma` executes Plugin API JavaScript against the file, so a **read-only** script — the node's vector paths, or an SVG-string export for a symbol — returns the exact path data as a string in the response, with no outbound fetch and nothing touching the proxy-blocked asset host. For any glyph, icon or logo port, read the path and paste it verbatim into the component rather than fitting bars to a render; keep the screenshot route for marks that are not vectors, and afterwards as the corroborating render. The preload rule above binds unchanged — `figma-use` is loaded before the call — and the script writes nothing. A byte comparison of the Figma string against the committed constant is then an exact check rather than a tolerance. (Worked case: A1-581, rounds 2 to 5, 2026-10-04 to 2026-10-07 — a hand-drawn six-arm polygon set, then eight flat-capped bars fitted to a 20x20 raster, which the review leg accepted at round 3 on a mean pixel diff of 0.018 and the Owner then rejected by eye as not right at all. One read-only call returned the exact path; pasted verbatim, the comparison against the committed constant was identical, and the render differed from Figma's own SVG export by at most three of 255 at eight times scale. One instance — APP-2183. That `use_figma` executes Plugin API JavaScript was read from the live connector schema on 2026-10-09 and is verified; the specific Plugin API members, the character count, the pixel figures and the node's vector type are the filing leg's report and are **unverified**, no Figma file being readable from the refine surface — that flag covers those five only.) **So "the vector was unreachable" is not a finding on this surface** — it is a route not taken, and a port passed or bounced on a shape claim made without it is resting on the eye. (`skill-qa`, *A bounce bar stated as a size*, carries the QA-side bar. Worked case: A1-581 / PR #221, 2026-10-04 — both legs believed the Figma vector unreachable because the asset download is proxy-blocked, and a hand-authored **6-arm** asterisk passed a box-sized bounce bar against Figma's **8-arm** `icon/selector-3` (151:228); APP-2022. The `enableBase64Response` behaviour and the URL-plus-curl default were read from the **live connector schema** on 2026-10-05 and are verified. The proxy block on the asset-download route, the PR number and the node ID are the filing leg's report and are **unverified** here, no Figma file or repo being readable from the refine surface; that flag covers those three only.)

**And where the claim is about *how many* — "the only instance", "no other usages", "all instances" — the count is taken document-wide or it is not taken.** The three rules above settle a single node's **identity**, its **type** and its **shape**. A completeness claim is a different assertion in kind: it is a claim about everything the reader did *not* look at, so the enumeration is the only thing that can check it, and the intuitive check — search the page the usage would obviously live on, or the pages the claim's author happened to be working in — under-counts in exactly the case that matters. An instance nested inside **another instance's override chain**, on a page nobody would associate with the component, is invisible to that search and is not invisible to a per-page `findAllWithCriteria({ types: ["INSTANCE"] })` walk that resolves each instance's main component. This is the *instance*-side twin of the orphaned-main enumeration under *File organisation* above and it takes the same sweep, so state the count with the date it was taken. The failure is quiet in the ordinary case — most "only instance" claims on a small component really are complete — and expensive in the rare one, because a count is what a rebind, a detach or a deletion is decided on, and a count repeated out of a ticket is indistinguishable from one that was enumerated. **So an unenumerated count is an observation, not a finding**, and is written as one — "the instances I found are X and Y" — never as "the only instance". (Worked case: A1-631, 2026-10-08 — the exec hand-off's "the only instance in the file" claim was re-checked document-wide rather than taken on its word, and a second instance was found, nested on a template page inside two levels of composition. No defect followed — the second instance inherited the fix's bound fill with no override, so the ticket's own definition of done held either way — which is the argument rather than against it: the undercount was free this time and the mechanism that produced it was not. Same family as APP-1751 on the main side; same section as APP-1720 by a different mechanism. Premise unverified — no Figma file is readable from the refine surface, so the node identifiers, the page name and the nesting chain are the filing leg's report, and the async accessor's exact spelling was not read against the Plugin API types, where this convention's own attested form one section above is `instance.mainComponent`; that flag covers those identifiers only, not the mechanism, which this convention already prescribes for the main side. APP-2190.)

## Quick checklist before a Figma write

- [ ] Writing through the connector that carries `use_figma`?
- [ ] The venture's face — the named cut — uploaded to the Figma account and resolving through the connector, before any text node is authored?
- [ ] `figma-use` loaded — and `figma-generate-library` or `figma-generate-design` on top where the work is a component or a page?
- [ ] The file's tokens and components enumerated before creating anything, and its existing naming read?
- [ ] Every structurally related container an auto-layout; every value that has a token bound to it; instances placed where a library component exists?
- [ ] For a design-system file: the family's page carries the six-frame skeleton in order, and every main sits inside the frame that documents it?
- [ ] The build reviewed against the bar (`skill-design-parity`, Figma structural pass) before it reaches Aled?
- [ ] For a file-wide pass (a style swap, a rebind, a rename): after the page walk, every unique `instance.mainComponent` in the document enumerated, and every `parent === null` main brought into scope?
