fig:lint — read-only structure and flow audit (violations only)
Part of a plugin. The scripts this skill runs ship beside it under ${CLAUDE_PLUGIN_ROOT}. If that path does not resolve, this file was installed on its own — stop and say the plugin itself is needed (claude plugin install fig@byjunyoung), rather than improvising what the scripts do.
Takes one design page and audits structure (placement), flow (arrows), and component usage in a single pass, reporting only violations. Detection is separated from repair, the way a design lint should be — this skill only detects; fig:prep fixes structure and fig:arrows fixes flow.
Why it exists: to keep verification logic in one place behind one gate. Whatever touched the canvas — prep, arrows, a duplicated form, a manual edit — running this one skill at the end covers all of it. When verification is scattered across the working skills you get a blind spot shaped like "I wasn't using that skill this time, so the audit never ran." Removing that blind spot is the reason it is separate.
Prerequisites: zero writes. use_figma is used only for read-only scripts that inspect node properties — return a report, never mutate a node. Load figma:figma-use before calling use_figma.
When to invoke
- "lint this", "audit the design", "check for rule violations" — on its own
- As the mandatory final gate for fig:prep and fig:arrows — always confirm through this right after creating, duplicating, moving frames, or generating and syncing arrows
- Always when clone or move was involved — the main cause of page-level orphans and out-of-bounds frames, and an isolated screenshot will not catch either
- Always right after building a screen by duplicating another — component default residue carried over by the copy is caught by this audit alone; the eye keeps missing it
When NOT to invoke
- Actually fixing, tidying, or stubbing placeholders →
/fig:prep
- Creating or syncing arrows →
/fig:arrows
- Auditing token (variable) bindings →
/fig:tokens
- Checking and applying work into the canonical page →
/fig:sync
- Just understanding the structure →
/fig:read
Inputs
figma_url (required): the page to audit. For several pages, split the calls per page and run them in parallel
scope (optional): structure only / flow only / components only. Omitted, all three run
reference_page (optional): an existing production page to derive component usage conventions from. Omitted, the page in the same file that uses the most of the same components is chosen
Where the rules come from — the config file (same source as fig:prep and fig:sync)
Naming patterns, the state list, excluded sections, tolerances, and statistical thresholds all come from figma-conventions.yaml, which is the single source. No values are written into this document.
python3 ${CLAUDE_PLUGIN_ROOT}/_common/scripts/lib/resolve-config.py --js <fileKey>
The layers merge bottom-up: bundled defaults → ~/.claude/figma-conventions.yaml → ./figma-conventions.yaml. If it ran on bundled defaults alone, say so in the report.
- A
null pattern means that check is skipped. This is what keeps a file with no convention from having every frame reported — "not knowing the rule" and "breaking the rule" are different things
- On an unfamiliar file with no config at all, run
/fig:setup first to infer the conventions and draft one
- If the team keeps a written guide, point
guide_source at it. It is not fetched on every run — it is an input absorbed once to help fill the config
Checked regardless of config: frame membership, out-of-bounds frames, section overlap, variant stacking, library sibling overlap, arrow geometry, pass-through, orphans. Coordinates and parentage are geometry, not convention, so they hold true with or without a written rule.
What it checks
A. Structure (placement) — fig:prep's territory
| Item |
Basis |
Frequency |
| Frame membership |
A screen frame's parent.type === "SECTION" on strict pages. Sitting directly on the page means the absorb step was missed — clone() and createFrame default their parent to currentPage, so anything not absorbed leaks out here |
★ high |
| Out of bounds |
A frame outside its own section's bbox. Usually an absolute coordinate written where a section-relative one belongs — a frame that became page-level and shot off to a stray absolute position |
★ high |
| Frame overlap |
Screen frames within one section whose bboxes intersect (excluding the deliberate adjacency of a variant stack) |
medium |
| Section overlap |
Section rectangles intersecting horizontally and vertically at once — an unambiguous violation. Usually a section stretched to fit a placeholder that then invades the row beside or below |
medium |
| Naming violation |
Mismatch against naming.frame_pattern / naming.section_pattern (skipped when null) |
low |
| Ordering mismatch |
A section's NN. number disagreeing with its position on the canvas (row-major) |
low |
| Missing required state ✋ |
Missing states per [screen] group against naming.required_states — unless that state is managed on a common page and the screen's Default carries a reference annotation to it (see fig:prep, "repeated common elements") |
low |
| Split state variants |
Variants of one [screen] broken apart in a column by an unrelated frame wedged between parent and variant, so the [state] dashed line passes through it |
medium |
| Variant stacking |
Variants of one component set whose bboxes intersect. A set with no auto-layout drops a new variant on the last one's coordinates, so several states render as a single component — nothing about it looks broken, the top variant draws fine and the rest are simply not visible |
★ high |
| Default layer names |
Layers inside a screen still carrying Figma's own Frame 427 / Group 12 name. Layers inside a component instance are excluded — they belong to the library that made the component, cannot be renamed on this page, and are fixed in that file instead |
low |
| Library sibling overlap |
A set or component overlapping the one placed beside it in the same section. Frame overlap above looks at frames only, so a component page passes that check while its components bury each other — a set that gained a variant or a layout runs past the gap it was placed with |
medium |
B. Flow (arrows) — fig:arrows's territory
| Item |
Basis |
| Transition arrow geometry |
Start point within arrows.audit.edge_tolerance of the source edge / arrowhead within arrows.audit.gap_range of the target edge / the final segment perpendicular to the target edge, so the head enters head-on / no segment passing through an unrelated frame / both source and target frames exist (orphan) |
| Labels |
The pill above its own arrow in z-order / clearing the arrowhead by more than arrows.audit.label_clear / not covering another arrow's segment |
| Coverage orphans |
Every screen frame appears at least once in a --> or a [state] (zero orphans) |
[state] dashed lines |
Two vertices, vertical, no elbows / name order top to bottom / endpoints within arrows.audit.edge_tolerance of the source's bottom edge and the target's top edge / no other frame between them |
C. Component usage (default residue) — fig:prep's territory
An instance that was newly placed or duplicated carries the component's defaults with it. A display toggle that is on in the library — an icon slot, a supplementary area, a caption — stays on even where that screen does not use it, and renders an empty slot with nothing in it. At zoomed-out scale it is barely visible, so eye inspection keeps letting it through. This is caught by measurement alone.
| Item |
Basis |
Frequency |
| Boolean default deviation |
An instance's boolean property on the working page differing from the convention that same component follows on existing production screens in the same file |
★ high |
| Empty display toggle |
Same axis — a slot, caption, or supplementary area switched on with nothing in it |
medium |
The basis is not a written rule but how the same file actually uses the component — the same principle as preferring existing assets. A property whose value also varies across production screens is a per-use choice, not a violation, and is not reported; only properties overwhelmingly settled one way count as deviation. The thresholds are component_audit.min_samples and dominance.
The ✋ mark — a check the script does not do
Missing required state, marked ✋ above, is not done by the audit script. Not because it can't be, but because it shouldn't be — which states are required depends on whether the screen is a list, a form, or a search, and that call requires reading the screen's content. Faking it with a regex produces "if the name contains 'list' it's a list screen", which only adds false positives.
So this item is judged by the agent, in step 3. Read the target screen as a screenshot, settle its type, compare against the matching list in naming.required_states, and write the missing states into the report separately. If the type is ambiguous, do not decide — record it as "type unclear". The grade turns on exactly that: a settled type makes a missing state blocking, an unclear one makes it a warning.
Keep the script's violations and this judgement separate in the report. They rest on different kinds of evidence.
Severity — what blocks a handoff
Every violation carries one of two grades. The grade never changes what is audited, only how it is reported — every check still runs, and everything found is still listed.
- blocking — engineering reading the file as it stands would build the wrong thing. A screen that cannot be seen, a coordinate that left its section, a connection that points somewhere it does not go, a slot switched on with nothing in it
- warning — the file reads correctly and is merely untidy. Names, numbering, a few pixels of tolerance
The grade is read off the tag the script already emits, so it is a lookup and not a second judgement.
| In the output |
Grade |
Why |
[duplicate name] |
blocking |
A name lookup catches only one of them, so nothing else in the run can be trusted — narrow and re-run before reading the rest |
[membership] |
blocking |
Not in a section. Handoff marks status on sections, so the screen is not in the handover at all |
[bounds] |
blocking |
The frame left its own section |
[frame overlap] |
blocking |
Screens cover each other — the one behind is invisible |
[section overlap] |
blocking |
Which section a frame belongs to becomes ambiguous |
[variant stack] |
blocking |
Variants sit on one another and only the top one renders |
[library overlap] |
blocking |
Components bury each other on a library page |
[split state] |
blocking |
An unrelated frame is wedged in, so the state chain runs through it |
[arrow] … orphan · … passes through |
blocking |
The source or target does not exist, or the line crosses an unrelated frame and reads as connecting to it |
[state] … orphan · … passes through |
blocking |
The same two, on a state chain |
[coverage] orphan frame |
blocking |
A screen nothing reaches — engineering cannot tell how the user gets there |
[default] |
blocking |
The instance carries a library default this screen does not use, and it ships as an empty slot |
| Missing required state ✋, type settled |
blocking |
A state nobody designed is a state engineering invents |
[naming] · [order] |
warning |
Reads correctly, filed untidily |
[layer name] |
warning |
Reads fine to whoever drew it, and to nobody else |
[arrow] · [state] geometry — edge offset, target gap, entry direction, elbows, name order |
warning |
The connection is right; the drawing is off |
[label] — z-order, clearance, covering a line |
warning |
Legibility, not meaning |
| Missing required state ✋, type unclear |
warning |
Undecided is not the same as missing, and the report has to say which |
Three of these are close calls placed deliberately rather than obviously: [section overlap] (ambiguity, not invisibility), [label] covers (a label can be read against the wrong arrow), and [arrow] arrowhead parallel (the entry direction is wrong, the connection is not). If a file argues otherwise, move it and say so in the report.
Who reads which. /fig:handoff gates on blocking alone — how a file is filed is not a reason to withhold work engineering can build. /fig:prep and /fig:arrows call this skill right after writing and still need a full PASS: the person who would fix the warning is the one standing there.
Procedure
- Settle the target page and resolve the config. Take fileKey and page from the URL and get
CFG via resolve-config.py --js <fileKey>. For several pages, split per page and call in parallel — one setCurrentPageAsync per script, per the figma-use rule.
- Run the read-only audits — the scripts below (A structure / B flow / C components) and collect violations. Zero node changes. C is two stages: derive conventions from a reference page (C-1), then compare against the working page (C-2). Different pages, so different calls.
- Judge required states (✋) and do the second visual pass — settle the screen type (list, form, search) from a screenshot and compare against
naming.required_states. Then, if a violation is suspected or clone/move was involved, confirm with a screenshot of the whole section node, not an isolated frame. Isolated renders cannot catch parent or position errors, so they are never grounds for a PASS.
- Report — violating nodes by category, a one-line reason each, its grade (see "Severity"), and which skill fixes it (structure → fig:prep, flow → fig:arrows). Blocking first, warnings beneath them, never interleaved. Close on a line of its own with both counts —
blocking: N · warning: N. Zero of both is PASS. Zero blocking with warnings still open is GATE PASS — enough for /fig:handoff, not enough for fig:prep or fig:arrows.
Running it
The audit code lives in ${CLAUDE_PLUGIN_ROOT}/_common/scripts/. Snippets are not copied into this document — two copies means one gets fixed and they drift apart.
${CLAUDE_PLUGIN_ROOT}/_common/scripts/audit-struct.js A. structure
${CLAUDE_PLUGIN_ROOT}/_common/scripts/audit-flow.js B. flow
${CLAUDE_PLUGIN_ROOT}/_common/scripts/audit-component.js C. components (MODE=collect / compare)
All three are called the same way.
- Get the single
const CFG = {...}; line from resolve-config.py --js <fileKey>
- Concatenate that line and the whole script, and hand it to
use_figma. If the target page is not the first page, prepend one setCurrentPageAsync line (once per script)
- The return value is either an array of violations or
STRUCT PASS · FLOW PASS · COMPONENT PASS
For several pages, split the calls per page and issue them in one message so they run in parallel. One script never switches pages twice.
C is two stages and its calls are split. MODE="collect" derives STAT from the reference page; MODE="compare" checks the working page against it. If one reference page does not yield enough samples, take one form-heavy page and one list-heavy page and sum their STAT.
After editing a script, check its syntax with scripts/lib/check.sh. use_figma wraps scripts in an async function, so top-level await and return are both legal — a combination node --check alone cannot accept. check.sh wraps them the same way before checking.
Constraints
- Never substitute eye inspection for the component default check (C) — empty icon slots and empty supplementary areas are invisible at zoomed-out scale. Always run C after building a screen by duplication, or after duplicating a field. Repairs go through fig:prep as a "handle the first match, then re-query" while loop — changing an instance property invalidates sibling handles
- Grading never narrows the audit — every check runs and everything found is reported whatever its grade. The grade tells the reader what to do, and only /fig:handoff acts on it
- Zero writes —
use_figma is read-only (return only). Never create or change a node. Repairs go to fig:prep or fig:arrows behind their own go
- Never PASS on an isolated screenshot alone — measured metadata is the first pass, a whole-section screenshot the second. A frame rendered by itself cannot show a parent or canvas-position error
- Convention-dependent checks (naming and the like) are only decided when the config has a pattern. On
null, skip the check; if it ran on bundled defaults, say so in the report
- Excluded sections are treated three different ways — dropped from the audit, dropped from coverage, but kept as pass-through targets. If a line actually crosses one, it is a broken line whether the section is excluded or not. The list is
pages.exclude_sections
- Duplicate frame names make a name lookup return only one of them. Report
[duplicate name] first, then narrow to a single section and re-run
- States managed on a common page are not counted as per-screen omissions or orphans (see fig:prep, "repeated common elements") — (1) a screen whose Default carries a reference annotation to the common page counts that state as covered, (2) canonical state frames on a page matching
naming.common_page_pattern are expected not to appear in per-screen flow, so they are dropped from coverage. If the setting is null, this exception does not apply
Notes
- fig:prep and fig:arrows contain no audit code. They call this skill as a gate right after writing, and handle only the repair side of what it finds
- This skill only answers "what is wrong". The reasoning for "why it should be placed or connected that way" lives in fig:prep, fig:arrows, and the comments in the config file
1---2name: lint3description: Audits a Figma design page read-only — frame membership and bounds, section overlap, naming, arrow geometry and entry direction and pass-through, coverage orphans, [state] dashed links, variants stacked on one another inside a component set, and component default residue carried over by duplication. Reports violations and writes nothing. Each one comes back graded blocking or warning, which is what decides whether a section can be handed to engineering. Rules come from figma-conventions.yaml and the audit code lives in one place under ${CLAUDE_PLUGIN_ROOT}/_common/scripts. It is the single gate that absorbs the checks fig:prep and fig:arrows would otherwise each carry, and it is called again right after those skills write. Triggers - "/fig:lint", "lint this page", "audit the design", "검증해줘", "규칙 위반 검사해줘", "피그마 검수", "화면 복제 후 검수".4---56# fig:lint — read-only structure and flow audit (violations only)78**Part of a plugin.** The scripts this skill runs ship beside it under `${CLAUDE_PLUGIN_ROOT}`. If that path does not resolve, this file was installed on its own — stop and say the plugin itself is needed (`claude plugin install fig@byjunyoung`), rather than improvising what the scripts do.910Takes one design page and **audits structure (placement), flow (arrows), and component usage in a single pass**, reporting only violations. Detection is separated from repair, the way a design lint should be — this skill **only detects**; fig:prep fixes structure and fig:arrows fixes flow.1112**Why it exists**: to keep verification logic in **one place behind one gate**. Whatever touched the canvas — prep, arrows, a duplicated form, a manual edit — running this one skill at the end covers all of it. When verification is scattered across the working skills you get a blind spot shaped like "I wasn't using that skill this time, so the audit never ran." Removing that blind spot is the reason it is separate.1314**Prerequisites**: **zero writes.** `use_figma` is used only for **read-only scripts** that inspect node properties — `return` a report, never mutate a node. Load `figma:figma-use` before calling `use_figma`.1516## When to invoke1718- "lint this", "audit the design", "check for rule violations" — on its own19- **As the mandatory final gate for fig:prep and fig:arrows** — always confirm through this right after creating, duplicating, moving frames, or generating and syncing arrows20- **Always when clone or move was involved** — the main cause of page-level orphans and out-of-bounds frames, and an isolated screenshot will not catch either21- **Always right after building a screen by duplicating another** — component default residue carried over by the copy is caught by this audit alone; the eye keeps missing it2223## When NOT to invoke2425- Actually fixing, tidying, or stubbing placeholders → `/fig:prep`26- Creating or syncing arrows → `/fig:arrows`27- Auditing token (variable) bindings → `/fig:tokens`28- Checking and applying work into the canonical page → `/fig:sync`29- Just understanding the structure → `/fig:read`3031## Inputs3233- `figma_url` (required): the page to audit. For several pages, split the calls per page and run them in parallel34- `scope` (optional): structure only / flow only / components only. Omitted, all three run35- `reference_page` (optional): an existing production page to derive component usage conventions from. Omitted, the page in the same file that uses the most of the same components is chosen3637## Where the rules come from — the config file (same source as fig:prep and fig:sync)3839Naming patterns, the state list, excluded sections, tolerances, and statistical thresholds all come from **`figma-conventions.yaml`**, which is the single source. No values are written into this document.4041```42python3 ${CLAUDE_PLUGIN_ROOT}/_common/scripts/lib/resolve-config.py --js <fileKey>43```4445The layers merge bottom-up: bundled defaults → `~/.claude/figma-conventions.yaml` → `./figma-conventions.yaml`. If it ran on bundled defaults alone, **say so in the report**.4647- **A `null` pattern means that check is skipped.** This is what keeps a file with no convention from having every frame reported — "not knowing the rule" and "breaking the rule" are different things48- On an unfamiliar file with no config at all, run `/fig:setup` first to infer the conventions and draft one49- If the team keeps a written guide, point `guide_source` at it. **It is not fetched on every run** — it is an input absorbed once to help fill the config5051**Checked regardless of config**: frame membership, out-of-bounds frames, section overlap, variant stacking, library sibling overlap, arrow geometry, pass-through, orphans. Coordinates and parentage are geometry, not convention, so they hold true with or without a written rule.5253## What it checks5455### A. Structure (placement) — fig:prep's territory5657| Item | Basis | Frequency |58|---|---|---|59| **Frame membership** | A screen frame's `parent.type === "SECTION"` on strict pages. **Sitting directly on the page means the absorb step was missed** — `clone()` and `createFrame` default their parent to currentPage, so anything not absorbed leaks out here | ★ high |60| **Out of bounds** | A frame outside its own section's bbox. Usually an absolute coordinate written where a section-relative one belongs — a frame that became page-level and shot off to a stray absolute position | ★ high |61| **Frame overlap** | Screen frames within one section whose bboxes intersect (excluding the deliberate adjacency of a variant stack) | medium |62| Section overlap | Section rectangles intersecting horizontally and vertically at once — an unambiguous violation. Usually a section stretched to fit a placeholder that then invades the row beside or below | medium |63| Naming violation | Mismatch against `naming.frame_pattern` / `naming.section_pattern` (skipped when `null`) | low |64| Ordering mismatch | A section's `NN.` number disagreeing with its position on the canvas (row-major) | low |65| Missing required state ✋ | Missing states per `[screen]` group against `naming.required_states` — **unless that state is managed on a common page and the screen's Default carries a reference annotation to it** (see fig:prep, "repeated common elements") | low |66| Split state variants | Variants of one `[screen]` broken apart in a column by an unrelated frame wedged between parent and variant, so the `[state]` dashed line passes through it | medium |67| **Variant stacking** | Variants of one component set whose bboxes intersect. A set with no auto-layout drops a new variant on the last one's coordinates, so several states render as a single component — nothing about it looks broken, the top variant draws fine and the rest are simply not visible | ★ high |68| Default layer names | Layers inside a screen still carrying Figma's own `Frame 427` / `Group 12` name. **Layers inside a component instance are excluded** — they belong to the library that made the component, cannot be renamed on this page, and are fixed in that file instead | low |69| Library sibling overlap | A set or component overlapping the one placed beside it in the same section. Frame overlap above looks at frames only, so a component page passes that check while its components bury each other — a set that gained a variant or a layout runs past the gap it was placed with | medium |7071### B. Flow (arrows) — fig:arrows's territory7273| Item | Basis |74|---|---|75| Transition arrow geometry | Start point within `arrows.audit.edge_tolerance` of the source edge / arrowhead within `arrows.audit.gap_range` of the target edge / **the final segment perpendicular to the target edge, so the head enters head-on** / no segment passing through an unrelated frame / both source and target frames exist (orphan) |76| Labels | The pill above its own arrow in z-order / clearing the arrowhead by more than `arrows.audit.label_clear` / not covering another arrow's segment |77| Coverage orphans | Every screen frame appears at least once in a `-->` or a `[state]` (zero orphans) |78| `[state]` dashed lines | Two vertices, vertical, no elbows / name order top to bottom / endpoints within `arrows.audit.edge_tolerance` of the source's bottom edge and the target's top edge / no other frame between them |7980### C. Component usage (default residue) — fig:prep's territory8182An instance that was newly placed or duplicated **carries the component's defaults with it.** A display toggle that is on in the library — an icon slot, a supplementary area, a caption — stays on even where that screen does not use it, and renders an empty slot with nothing in it. At zoomed-out scale it is barely visible, so eye inspection keeps letting it through. **This is caught by measurement alone.**8384| Item | Basis | Frequency |85|---|---|---|86| **Boolean default deviation** | An instance's boolean property on the working page differing from **the convention that same component follows on existing production screens in the same file** | ★ high |87| Empty display toggle | Same axis — a slot, caption, or supplementary area switched on with nothing in it | medium |8889The basis is not a written rule but **how the same file actually uses the component** — the same principle as preferring existing assets. A property whose value also varies across production screens is a per-use choice, not a violation, and is not reported; only properties overwhelmingly settled one way count as deviation. The thresholds are `component_audit.min_samples` and `dominance`.9091### The ✋ mark — a check the script does not do9293**Missing required state**, marked `✋` above, is not done by the audit script. Not because it can't be, but because **it shouldn't be** — which states are required depends on whether the screen is a list, a form, or a search, and that call requires reading the screen's content. Faking it with a regex produces "if the name contains 'list' it's a list screen", which only adds false positives.9495So this item is **judged by the agent, in step 3.** Read the target screen as a screenshot, settle its type, compare against the matching list in `naming.required_states`, and write the missing states into the report separately. If the type is ambiguous, do not decide — record it as "type unclear". **The grade turns on exactly that**: a settled type makes a missing state blocking, an unclear one makes it a warning.9697Keep **the script's violations and this judgement separate** in the report. They rest on different kinds of evidence.9899## Severity — what blocks a handoff100101Every violation carries one of two grades. **The grade never changes what is audited, only how it is reported** — every check still runs, and everything found is still listed.102103- **blocking** — engineering reading the file as it stands would build the wrong thing. A screen that cannot be seen, a coordinate that left its section, a connection that points somewhere it does not go, a slot switched on with nothing in it104- **warning** — the file reads correctly and is merely untidy. Names, numbering, a few pixels of tolerance105106The grade is read off the tag the script already emits, so it is a lookup and not a second judgement.107108| In the output | Grade | Why |109|---|---|---|110| `[duplicate name]` | blocking | A name lookup catches only one of them, so nothing else in the run can be trusted — narrow and re-run before reading the rest |111| `[membership]` | blocking | Not in a section. Handoff marks status on sections, so the screen is not in the handover at all |112| `[bounds]` | blocking | The frame left its own section |113| `[frame overlap]` | blocking | Screens cover each other — the one behind is invisible |114| `[section overlap]` | blocking | Which section a frame belongs to becomes ambiguous |115| `[variant stack]` | blocking | Variants sit on one another and only the top one renders |116| `[library overlap]` | blocking | Components bury each other on a library page |117| `[split state]` | blocking | An unrelated frame is wedged in, so the state chain runs through it |118| `[arrow] … orphan` · `… passes through` | blocking | The source or target does not exist, or the line crosses an unrelated frame and reads as connecting to it |119| `[state] … orphan` · `… passes through` | blocking | The same two, on a state chain |120| `[coverage] orphan frame` | blocking | A screen nothing reaches — engineering cannot tell how the user gets there |121| `[default]` | blocking | The instance carries a library default this screen does not use, and it ships as an empty slot |122| Missing required state ✋, **type settled** | blocking | A state nobody designed is a state engineering invents |123| `[naming]` · `[order]` | warning | Reads correctly, filed untidily |124| `[layer name]` | warning | Reads fine to whoever drew it, and to nobody else |125| `[arrow]` · `[state]` geometry — edge offset, target gap, entry direction, elbows, name order | warning | The connection is right; the drawing is off |126| `[label]` — z-order, clearance, covering a line | warning | Legibility, not meaning |127| Missing required state ✋, **type unclear** | warning | Undecided is not the same as missing, and the report has to say which |128129Three of these are close calls placed deliberately rather than obviously: `[section overlap]` (ambiguity, not invisibility), `[label] covers` (a label can be read against the wrong arrow), and `[arrow] arrowhead parallel` (the entry direction is wrong, the connection is not). If a file argues otherwise, move it and say so in the report.130131**Who reads which.** `/fig:handoff` gates on blocking alone — how a file is filed is not a reason to withhold work engineering can build. `/fig:prep` and `/fig:arrows` call this skill right after writing and still need a full `PASS`: the person who would fix the warning is the one standing there.132133134## Procedure1351361. **Settle the target page and resolve the config.** Take fileKey and page from the URL and get `CFG` via `resolve-config.py --js <fileKey>`. For several pages, split per page and **call in parallel** — one `setCurrentPageAsync` per script, per the figma-use rule.1372. **Run the read-only audits** — the scripts below (A structure / B flow / C components) and collect violations. Zero node changes. C is two stages: derive conventions from a reference page (C-1), then compare against the working page (C-2). Different pages, so different calls.1383. **Judge required states (✋) and do the second visual pass** — settle the screen type (list, form, search) from a screenshot and compare against `naming.required_states`. Then, if a violation is suspected or clone/move was involved, confirm with **a screenshot of the whole section node**, not an isolated frame. Isolated renders cannot catch parent or position errors, so they are never grounds for a PASS.1394. **Report** — violating nodes by category, a one-line reason each, its **grade** (see "Severity"), and **which skill fixes it** (structure → fig:prep, flow → fig:arrows). Blocking first, warnings beneath them, never interleaved. Close on a line of its own with both counts — `blocking: N · warning: N`. Zero of both is `PASS`. Zero blocking with warnings still open is `GATE PASS` — enough for /fig:handoff, not enough for fig:prep or fig:arrows.140141## Running it142143The audit code lives in `${CLAUDE_PLUGIN_ROOT}/_common/scripts/`. Snippets are not copied into this document — two copies means one gets fixed and they drift apart.144145```146${CLAUDE_PLUGIN_ROOT}/_common/scripts/audit-struct.js A. structure147${CLAUDE_PLUGIN_ROOT}/_common/scripts/audit-flow.js B. flow148${CLAUDE_PLUGIN_ROOT}/_common/scripts/audit-component.js C. components (MODE=collect / compare)149```150151All three are called the same way.1521531. Get the single `const CFG = {...};` line from `resolve-config.py --js <fileKey>`1542. Concatenate that line and the whole script, and hand it to `use_figma`. If the target page is not the first page, prepend one `setCurrentPageAsync` line (once per script)1553. The return value is either an array of violations or `STRUCT PASS` · `FLOW PASS` · `COMPONENT PASS`156157For several pages, split the calls per page and **issue them in one message so they run in parallel**. One script never switches pages twice.158159**C is two stages and its calls are split.** `MODE="collect"` derives STAT from the reference page; `MODE="compare"` checks the working page against it. If one reference page does not yield enough samples, take one form-heavy page and one list-heavy page and sum their STAT.160161After editing a script, check its syntax with `scripts/lib/check.sh`. `use_figma` wraps scripts in an async function, so top-level `await` and `return` are both legal — a combination `node --check` alone cannot accept. check.sh wraps them the same way before checking.162163## Constraints164165- **Never substitute eye inspection for the component default check (C)** — empty icon slots and empty supplementary areas are invisible at zoomed-out scale. Always run C after building a screen by duplication, or after duplicating a field. Repairs go through fig:prep as a **"handle the first match, then re-query" while loop** — changing an instance property invalidates sibling handles166- **Grading never narrows the audit** — every check runs and everything found is reported whatever its grade. The grade tells the reader what to do, and only /fig:handoff acts on it167- **Zero writes** — `use_figma` is read-only (`return` only). Never create or change a node. Repairs go to fig:prep or fig:arrows behind their own go168- **Never PASS on an isolated screenshot alone** — measured metadata is the first pass, a whole-section screenshot the second. A frame rendered by itself cannot show a parent or canvas-position error169- Convention-dependent checks (naming and the like) are only decided when the config has a pattern. On `null`, skip the check; if it ran on bundled defaults, say so in the report170- **Excluded sections are treated three different ways** — dropped from the audit, dropped from coverage, but **kept as pass-through targets.** If a line actually crosses one, it is a broken line whether the section is excluded or not. The list is `pages.exclude_sections`171- Duplicate frame names make a name lookup return only one of them. Report `[duplicate name]` first, then narrow to a single section and re-run172- **States managed on a common page are not counted as per-screen omissions or orphans** (see fig:prep, "repeated common elements") — (1) a screen whose Default carries a reference annotation to the common page counts that state as covered, (2) canonical state frames on a page matching `naming.common_page_pattern` are expected not to appear in per-screen flow, so they are dropped from coverage. If the setting is `null`, this exception does not apply173174## Notes175176- fig:prep and fig:arrows contain no audit code. They call this skill as a gate right after writing, and handle only **the repair side** of what it finds177- This skill only answers "what is wrong". The reasoning for "why it should be placed or connected that way" lives in fig:prep, fig:arrows, and the comments in the config file