lint · git:20260826.e976a84 · 2026-08-26 · sha256 ab02700e4cff24b2

lint git:20260826.e976a84A

Immutable. This exact content is served forever at /api/v1/blob/ab02700e4cff24b2.

---
name: lint
description: 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, and component default residue carried over by duplication. Reports violations and writes nothing. 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", "검증해줘", "규칙 위반 검사해줘", "피그마 검수", "화면 복제 후 검수".
allowed-tools: AskUserQuestion, Bash, mcp__plugin_figma_figma__use_figma, mcp__plugin_figma_figma__get_metadata, mcp__plugin_figma_figma__get_screenshot, mcp__claude_ai_Notion__notion-fetch
---

# fig:lint — read-only structure and flow audit (violations only)

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, 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 |

### 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".

Keep **the script's violations and this judgement separate** in the report. They rest on different kinds of evidence.

## Procedure

1. **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.
2. **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.
3. **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.
4. **Report** — violating nodes by category, a one-line reason each, and **which skill fixes it** (structure → fig:prep, flow → fig:arrows). Zero violations is `PASS`.

## 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.

1. Get the single `const CFG = {...};` line from `resolve-config.py --js <fileKey>`
2. 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)
3. 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
- **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