// Use when working with figmaclaw-generated data (figma/*.md pages, _census.md, ds_catalog.json, token sidecars) or modifying figmaclaw itself. Covers the four-layer data contract (frontmatter / body / manifest / file-scope registries), invariant classes (BP/SC/FM/CL/W/CR/KS/TS/CW/LW/HE/TC/TS-S/REG/PP/NC/HSH/SI/MIG/AUTH/ERR/WF), design decisions D1-D15, refresh-trigger ladder, and the failure-mode catalog F1-F21. Authoritative for "is this change safe?" questions.
[HINT] Download the complete skill directory including SKILL.md and all related files
name
figmaclaw canon
description
Use when working with figmaclaw-generated data (figma/*.md pages, _census.md, ds_catalog.json, token sidecars) or modifying figmaclaw itself. Covers the four-layer data contract (frontmatter / body / manifest / file-scope registries), invariant classes (BP/SC/FM/CL/W/CR/KS/TS/CW/LW/HE/TC/TS-S/REG/PP/NC/HSH/SI/MIG/AUTH/ERR/WF), design decisions D1-D15, refresh-trigger ladder, and the failure-mode catalog F1-F24. Authoritative for "is this change safe?" questions.
figmaclaw canon โ invariants and design decisions
Status: authoritative. This skill is the single source of truth for figmaclaw's data contract, invariants, and design decisions. Every other doc in figmaclaw's docs/ directory either feeds into this one (historical context, deeper rationale) or is superseded by it. Module docstrings, commit-message conventions, CLAUDE.md (figmaclaw), and consumer-repo agent fragments cross-link here for the actual rule.
When you change figmaclaw's behavior, the order of operations is: (1) update this skill, (2) update tests, (3) write code. If the canon doesn't authorize the behavior, don't ship it.
Consumers: this skill is bundled with the figmaclaw plugin. Invoke it as figmaclaw:figmaclaw canon from any consumer repo โ you do not need to read figmaclaw's CLAUDE.md separately.
figmaclaw invariants exist to protect expensive, hard-won knowledge while still allowing cheap, incremental refresh. A correct change preserves human/LLM-authored data, never silently discards generated evidence that consumers rely on, knows the cost tier of every action, and can answer what is stale, why it is stale, and what minimum refresh will make it current.
Use this philosophy to decide whether a proposed rule belongs in canon:
Protect hard-won data. Bodies, enrichment state, human choices, generated registries used by consumers, and migration evidence must not be lost silently. If data is costly to recreate, preserve it; if it is recomputable, make the recomputation path explicit.
Work incrementally. Prefer the cheapest sufficient refresh tier. Do not make "get one current answer" require a full pull, screenshots, LLM enrichment, or repo-wide churn when a file-scope or page-scope refresh is enough.
Know freshness. Every cache consumer must be able to answer whether its input is current. If it is stale, it should name what is stale, why it is stale, and the smallest action that will make it current.
Keep authority clear. Separate source truth, generated cache, observed usage, advisory suggestions, and prose. Do not let observation, fallback state, or seeded bridge data masquerade as authoritative Figma truth.
Recover by preserving provenance. When workflows race, APIs fail, or schemas change, recovery should restore a state that could have been produced from source data. Do not text-merge generated cache snapshots or leave legacy generated artifacts orphaned.
Each invariant below should serve one of these goals. If a proposed invariant only records a temporary implementation preference, put it in tests, docs, or the review checklist instead of canon.
1. Four-layer data contract
figmaclaw's data is organized in four layers. Every artifact figmaclaw writes belongs to exactly one of them, and each layer has a different authority and a different writer.
Layer
Authority
Recomputable?
Who writes it
Examples
Frontmatter (in page .md files)
Source of truth for page identity, structure, and enrichment state.
No โ losing it loses the page's identity in git.
Authoritative cache of file-level Figma data. Recomputable from file-scoped Figma registry readers.
Yes โ figmaclaw variables and figmaclaw census reproduce them without page, screenshot, or LLM work.
figmaclaw CLI (dedicated subcommands).
Variable catalog, published component-set census.
The law:
Frontmatter is the index of what exists on a page. Use it to make enrichment decisions cheaply (no API calls).
Body is prose. No Python code, no CLI command, no agent tool may parse prose or use prose as source of truth. Code may inspect canonical generated headings/tables only through the canonical walkers named in CW-1. No parse_page_summary(). No parse_section_intros(). No ad hoc regex over body tables.
Manifest is engineering cache. Treat it as recomputable; never store load-bearing information in it that isn't reproducible from the API.
File-scope registries are file-scope answers cached in committed files for cross-tool consumption (suggest-tokens, agent skills, CI). They are recomputable from file-scoped Figma registry readers and must remain so.
The full body-preservation argument and the full manifest-vs-frontmatter argument are derived from this contract. See ยง4 BP, FM, ยง4 W, ยง5 D3, D4, D11.
2. Storage-tier table
Every artifact figmaclaw produces, what it caches, when it refreshes, who writes it, and whether losing it is recoverable.
Published/importable component sets (name, key, page, updated). Stable content hash over (name, key) pairs. Local unpublished component definitions are page structure and render to components/*.md; they are not census entries.
figmaclaw census standalone. pull fetches component sets for component_set_keys, but does not write _census.md.
figmaclaw variables standalone, and opportunistic during pull when file version changed and repo config is Enterprise
commands/variables.py, token_catalog.py
yes (from any complete, file-scoped variable-registry reader available in the install: Figma MCP by default, REST /variables/local only when [tool.figmaclaw] license_type = "enterprise"; seeded:* entries fill the gap when no authoritative reader is available)
pull when file version changed AND page hash changed
pull_logic.py, figma_render.py
yes
Page tokens (raw/stale usage)
figma/{slug}/pages/*.tokens.json
Per-frame (property, classification, value) aggregates with count. Schema v2.
pull when page hash changed
pull_logic.py, token_scan.py
yes (re-walk page)
Page body
figma/{slug}/pages/*.md body
LLM-authored prose.
claude-run (LLM enrichment, downstream of pull)
LLM via write-body; figmaclaw never overwrites
no โ protected by BP-1..6
Component section body
figma/{slug}/components/*.md body
LLM-authored prose for DS sections.
claude-run
Same as above.
no โ same
page_hash is not stored in .md frontmatter; only enriched_hash and enriched_frame_hashes are. Current hashes live in the manifest (see D9).
3. Refresh-trigger ladder
When a sync runs, figmaclaw cascades through these checks. Higher tiers gate lower tiers โ i.e. if a higher tier shows "no change," the work below is skipped.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Tier 1 File-version meta (`?depth=1`) โ
โ stored.version == api_version โ skip whole file โ
โ else: โ
โโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Tier 1.5 File-scope registry refresh โ
โ complete variable-registry reader โ catalog โ
โ (TC-1, TC-5) โ
โ `get_component_sets(file_key)` โ component_set_keys โ
โ for component markdown. `pull` does not write census. โ
โโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Tier 2 Per-page hash (computed from node tree) โ
โ stored.page_hash == computed โ skip page โ
โ else: re-render frontmatter + token sidecar โ
โโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Tier 3 Per-frame hash (depth-1 children) โ
โ frontmatter.enriched_frame_hashes[f] == computed โ
โ โ that frame's body row is already current โ
โ Drives surgical re-enrichment; see D2, D7, D10. โ
โโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Content hash (registry-internal, e.g. census) โ
โ stable hash over (name, key) pairs โ
โ rewrite the artifact only when hash changes โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ Schema-version forced refresh โ
โ When CURRENT_PULL_SCHEMA_VERSION bumps, files at older versions โ
โ get re-rendered even if Figma is unchanged. Bypass-flag budget โ
โ rule applies (W-3 / anti-loop dim 1 corollary). โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ Webhook + cron fallbacks โ
โ Webhook โ incremental, file-scoped, fires on Figma save. โ
โ Cron / `--force` โ catch-all for missed webhooks. โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
The Tier 1.5 slot is the architectural fix for failure-mode F1: file-scope content (variables, components, styles) refreshes once per changed file, decoupled from per-page hashing. See D11 and the audit in issue #128.
4. Invariant classes
Each class names a category of always-true property. The IDs are stable; tests and PR review reference them by ID. New invariants get appended; nothing is renumbered.
BP โ Body preservation
The .md body is LLM-authored prose. Producing it costs Figma screenshots + LLM inference + human review. Losing it silently is unacceptable.
ID
Invariant
Enforced by
BP-1
sync on an existing file preserves the body byte-for-byte.
tests/test_body_preservation.py::test_bp1_*
BP-2
pull_file on an existing file preserves the body byte-for-byte.
test_bp2_*
BP-3
set-flows on an existing file preserves the body byte-for-byte.
test_bp3_*
BP-4
update_page_frontmatter() preserves the body byte-for-byte.
test_bp4_*
BP-5
scaffold_page() is never called on existing files by sync or pull.
When a writer DOES write, the resulting file must round-trip through its own reader (e.g. census's _existing_hash(out_path) == content_hash, commit f56eb17).
runtime assertion in writer + golden test
W-3
Bypass-flag budget rule. Any flag that bypasses the page-hash check (--force, schema_stale) must NOT consume the max_pages budget for pages it processes. Otherwise the while pull loop never terminates. Schema-only upgrades are the canonical example.
tests/test_*budget*, commit 5612e2b
Rationale. figmaclaw runs in a CI loop. Any unconditional write โ even a timestamp โ lands in a git commit, triggers Claude enrichment, and wastes budget. Idempotency is the foundation of every other invariant in this file.
CR โ Cross-run discipline
Anti-loop dim 1.
ID
Invariant
Enforced by
CR-1
Any guard that prevents retries within a run must be paired with an invariant that prevents retries across runs. Test shape: state_0 := fixture; run code โ state_1; run code โ state_2; assert state_2 does not re-select work state_1 already addressed.
tests/test_*cross_run*
CR-2
A consumer of a recomputable cache must detect staleness explicitly. If the cache's source_version is older than the upstream's current version (or absent), the consumer exits non-zero with an actionable message; it does NOT produce results from a stale cache. Applies to suggest-tokens reading ds_catalog.json and any analogous reader.
tests/test_*staleness*
KS โ Frame-keyed key-set
Anti-loop dim 2.
ID
Invariant
Enforced by
KS-1
For every frame-keyed dict in frontmatter (enriched_frame_hashes, raw_frames, raw_tokens, frame_sections, unresolvable_frames), keys(d) โ frames must hold after every write. Enforced centrally in figma_render._build_frontmatter (single chokepoint) and defensively on parse by FigmaPageFrontmatter._cap_unresolvable_frames_to_frames.
tests/test_frontmatter_key_set_invariant.py
If you add a new frame-keyed field: extend _build_frontmatter to prune it; add a test asserting orphan keys are dropped on write. Do not rely on callers to pre-prune.
TS โ Terminal-state for LLM-dispatched work
Anti-loop dim 3.
ID
Invariant
Enforced by
TS-1
Every "pending" state has a terminal counterpart. If the LLM can produce an output that says "I cannot resolve this right now" (e.g. (no screenshot available)), that output must be recordable as a tombstone so we don't re-dispatch the same question on the next run. Tombstones auto-invalidate when the underlying content hash changes.
tombstone-protocol tests
If you introduce a new "soft-done" row marker, design the tombstone protocol in the same PR.
CW โ Canonical walker reuse
Anti-loop dim 4.
ID
Invariant
Enforced by
CW-1
Body frame-row iteration has exactly one canonical implementation: body_validation.iter_body_frame_rows. Fence-aware, exact rendered-header matching, yields BodyFrameRow pydantic models with line_index and node_id. Use it for any code that inspects or mutates canonical body frame tables.
tests/test_body_validation.py
Don't re-implement is_table_separator / parse_frame_row / fence tracking in new code. Don't copy loops from existing walkers. Don't import re in pull / claude-run / write-body code to match row shapes.
Use iter_body_frame_rows for row-by-row work; body_frame_node_ids (a thin projection) when you only need the node-id list; section_line_ranges / parse_sections for section-level work.
LW โ Log-writer auto-heal
Anti-loop dim 5.
ID
Invariant
Enforced by
LW-1
A log writer that emits "WARN โฆ skipped until file is fixed" but never fixes the file silently loses data every run forever. Acceptable resolutions, in order of preference: (a) Migrate in place on the first mismatch; (b) Auto-archive and reset โ rename the prior file to <name>.bak.<UTC-timestamp><ext> and start a fresh schema-v1 log, emit a human-readable error line; (c) Hard-fail for critical writers where silently resetting would corrupt load-bearing state.
tests/test_*log_writer*
LW-2
Schema migrations for sidecars and catalogs follow the same rule. v1 โ v2 either migrates content forward (preserving any human-set fields like fix_variable_id) or auto-archives. Never silently overwrite.
The test shape that pins this: run 1 with bad input triggers the heal; run 2 with no new bad input does NOT re-emit the error.
HE โ Heal-at-entry
Anti-loop dim 6.
ID
Invariant
Enforced by
HE-1
Every selection / entry boundary that reads a page .md calls normalize_page_file (or goes through something that does, e.g. claude_run.enrichment_info) as its first step. New entry points register in _HEALING_ENTRY_POINTS in the parametric test.
tests/test_entry_point_heals.py
A structural invariant that fires only when WE write a file leaves files written by older figmaclaw, hand-edits, or merge-conflict resolutions in violating state. Heal on encounter is the chokepoint.
TC โ Token catalog
The catalog (.figma-sync/ds_catalog.json) is the file-scope authoritative answer to "what design tokens does this Figma file's variable system define?".
ID
Invariant
Enforced by
TC-1 โ Authoritative source.
Catalog is built from a complete Figma file-scope variable-registry reader with explicit capability gating. Provider choice and call shape are implementation details; the contract is completeness for the file. It is never built from page-walk observation. It enumerates every variable Figma defines for the file, including ones never bound to any node. Page walks may produce usage facts; they MUST NOT add a variable to the catalog or set its definitional fields.
tests/test_token_catalog.py::test_tc1_*
TC-2 โ Complete identity.
Every variable entry stores: library_hash, collection_id, name, resolved_type (COLOR/FLOAT/STRING/BOOLEAN), values_by_mode, scopes, code_syntax, alias_of, source (figma_api/figma_mcp/seeded:*/observed). No definitional field is "set later by another code path."
tests/test_token_catalog.py::test_tc2_*
TC-3 โ No dead fields.
Every field on CatalogVariable and CatalogLibrary has exactly one canonical writer. CI fails if a model field is declared but no source location writes it.
source-scan meta-test in tests/test_dead_fields.py
TC-4 โ Mode-aware storage.
Variables store values_by_mode: dict[mode_id, value], never flattened to a single value. Readers that need a single value pick an explicit default_mode_id from the library entry. There is no implicit last-write-wins.
tests/test_token_catalog.py::test_tc4_*
TC-5 โ Refresh is page-independent.
The catalog refresh code path takes file_key and a FigmaClient only. It never reads or writes any pages/*.md, never consults enriched_hash, page_hash, or frame_hashes. It joins pull at Tier 1.5 (file-version-gated, once per changed file) โ at the same tier as get_component_sets.
tests/test_variables_command.py::test_tc5_*
TC-6 โ Cheap subcommand exists.
figmaclaw variables --file-key <key> refreshes the catalog without touching pages, screenshots, sidecars, or LLM-authored body. Runtime must scale with file-scoped registry reads, not with page count or frame count.
smoke test
TC-7 โ Observability of staleness.
Each library entry records source_version (Figma file version at fetch) and fetched_at. Consumers compare the entry's source_version against manifest.files[k].version to decide if the cache is current.
tests/test_token_catalog.py::test_tc7_*
TC-8 โ Idempotent writes.
save_catalog skips writes when only fetched_at would change (W-1 applied to the catalog). Source-scan meta-test pins this.
tests/test_catalog_idempotency.py
TC-9 โ Schema upgrades migrate, never drop.
Schema bumps either migrate forward in place (preserving seeded:* entries and any human-set fields) or auto-archive to ds_catalog.bak.<UTC>.json and start fresh. Never silently overwrite or warn-and-skip. (LW-2 applied to the catalog.)
tests/test_catalog_migration.py
TC-10 โ Current unavailable markers use bounded retry backoff.
When figmaclaw variables --source auto records source: unavailable for the current Figma file version, it may also record an unavailable_retry_after timestamp. Later default runs skip that file only until the retry deadline, instead of re-running MCP and re-emitting the same fallback errors every CI tick. Missing/expired deadlines, --force, and explicit readers retry; --source rest additionally requires license_type = "enterprise".
Non-Enterprise figmaclaw variables --source auto may use MCP for authoritative variables, but it must not blindly treat every tool failure as transient. Readers must distinguish file-type ineligibility, provider capability/read-only denial, and transient failure. Ineligible files are not sent through the local-variable MCP export path; capability/read-only denials are scoped to the file/tool and are not immediately retried. The resulting unavailable marker remains version-scoped and bounded by TC-10, not permanent poison.
TC-12 โ Registry provenance includes source-system lifecycle.
Every file-scope registry row must be joinable to its source Figma file and source system lifecycle. CatalogLibrary entries carry source_file_key, optional source project id/name, and source lifecycle (active, archived, or implicit unknown). Component census files carry the same source context in frontmatter when known. Archived/legacy libraries are preserved as migration evidence, but readers must not treat them as the default current design-system target unless explicitly selected.
figma_api โ populated from /variables/local. Authoritative.
figma_mcp โ populated from Figma MCP plugin-runtime variable export. Authoritative fallback when REST variables scope is unavailable.
seeded:css โ imported from a CSS export by seed_catalog.py or equivalent. Bridge state when no authoritative variable reader is available.
seeded:manual โ hand-added (e.g. border/width tokens not present in CSS).
observed โ variable ID was seen as a boundVariables reference but the definition was never resolved. Legacy / graceful-degradation only; new code MUST NOT produce these.
TS-S โ Token sidecar
Per-page *.tokens.json files are the page-scope answer to "what raw or stale token usage exists on this page?".
ID
Invariant
Enforced by
TS-S-1 โ Aggregation.
Sidecar issues are aggregated by (property, classification, value) per frame with a count field (schema v2). Per-node detail is dropped โ the sidecar is consumption-shaped, not observation-shaped.
tests/test_compact_sidecar.py
TS-S-2 โ Sparse output.
Only frames with raw > 0 or stale > 0 appear in the sidecar. A frame with all-valid bindings is absent.
same
TS-S-3 โ Sum preservation.
Sum of count across aggregated entries equals the input issue count โ no data loss in aggregation.
same
TS-S-4 โ Hex derivation.
hex is derived from current_value for color properties only; None for numeric.
same
TS-S-5 โ fix_variable_id survives migration.
Schema migration (v1 โ v2 โ vN) preserves human-set or suggest-tokens-set fix_variable_id values. (LW-2 applied to sidecars.)
tests/test_sidecar_migration.py
TS-S-6 โ Backfill.
If a page .md exists but its sidecar is missing or schema-stale, the next pull writes it even if page content is unchanged (commit e100631, 6a666ac).
tests/test_sidecar_backfill.py
TS-S-7 โ Lifecycle.
Sidecars are pruned alongside their parent .md when pages disappear (prune_utils.py).
tests/test_prune_utils.py
REG โ File-registry state
File-scope registries are committed cache artifacts. Code and agents must be able to tell the difference between "not checked" and "checked, empty."
ID
Name
Description
Rationale
Proof
REG-1
Explicit registry state
For every tracked file registry, figmaclaw must distinguish three states: not probed, probed-empty, and probed-with-entries. A missing registry artifact is unknown; it is never proof that the upstream registry is empty. An explicit figmaclaw census --file-key <key> probe persists probed-empty component state as _census.md with component_set_count: 0.
Consumer repos use registry files to answer source-of-truth questions. If absence means both "not emitted" and "empty," agents and automation can incorrectly conclude that Figma has no published components or variables. Persisting explicit empty probes keeps high-signal product files quiet while letting important DS files carry a durable "probed empty" fact.
Tap In / LSN had zero published component sets, but missing _census.md was ambiguous. Evidence: PR #129 comment, tests/test_census.py::test_census_reports_empty_registry_for_explicit_file_key.
PP โ Pull terminal state
Every manifest page entry is a promise about generated repo state. A pull may skip a page intentionally, but it must not silently remember a page that produced no artifact.
ID
Name
Description
Rationale
Proof
PP-1
No silent partial pull
Every non-skipped manifest page entry must end in a terminal data state: either md_path is present, or component_md_paths is non-empty, or the page is explicitly skipped with reason. The shape md_path: null with empty component_md_paths is invalid.
This prevents a stable manifest hash from hiding missing markdown forever. Component-only pages are valid, but only when their component markdown paths exist.
PR #129 found 215 pages across linear-git stuck in the invalid shape, including 9 design-system pages. Evidence: PR #129 H2 comment, commit 2c3dc08, tests/test_doctor.py::test_doctor_surfaces_partial_pull_pages.
NC โ Node coverage parity
figmaclaw has multiple walkers over the Figma node tree. They must agree on what counts as renderable input.
ID
Name
Description
Rationale
Proof
NC-1
Rendered unit coverage parity
The page parser, renderer, page-hash walker, frame-hash walker, manifest writer, and prune/migration logic must cover the same renderable Figma unit classes: supported FRAME, COMPONENT, and COMPONENT_SET nodes, whether top-level or section-wrapped.
If one subsystem sees a node and another ignores it, figmaclaw can write stale hashes, miss markdown files, or fail to prune generated artifacts. Coverage parity is what makes hash gating and generated output trustworthy.
Top-level COMPONENT_SET pages caused partial pulls until parser/hash coverage was aligned. Evidence: commit 379aa41, commit 3cabadc, tests/test_top_level_component_pages.py, tests/test_pr_129_adversarial.py.
NC-2
Mixed section preservation
A Figma SECTION containing both screen frames and component sets must preserve both classes in generated output. One supported child class must not cause another supported child class to disappear.
Figma sections are containers, not exclusive type declarations. Treating a mixed section as only "screens" or only "components" silently drops source data.
Mixed SECTION pages dropped component sets until sibling screen/component sections were emitted. Evidence: commit 33e5c03, tests/test_pr_129_adversarial.py::test_mixed_section_with_frames_and_component_sets_emits_both_outputs.
HSH โ Hash coverage
Hash gates are only safe when they cover all source identity that affects generated output.
ID
Name
Description
Rationale
Proof
HSH-1
Hash covers rendered identity
Any visible Figma node identity that affects generated markdown must contribute to the relevant page and unit hashes. Invisible nodes stay excluded, and sibling order remains order-insensitive unless order itself is rendered.
Tier 2 and surgical enrichment depend on hashes being a complete summary of rendered source identity. If markdown can change while the hash stays stable, figmaclaw will skip required work.
Adding/renaming variants inside an existing COMPONENT_SET originally did not change page hashes or frame hashes. Evidence: commit 3cabadc, Agent A report H8/H9, tests/test_pull_logic.py::test_schema_upgrade_v8_to_v9_picks_up_variant_changes.
SI โ Synthetic identity
Synthetic nodes are allowed only when Figma has no natural grouping node for output that figmaclaw must render. Once synthetic identity reaches paths or manifest state, it must be as collision-resistant as real source identity.
ID
Name
Description
Rationale
Proof
SI-1
Source-scoped synthetic identity
Synthetic sections, node IDs, and generated paths must encode enough source identity that two different source pages or sections cannot generate the same repo path.
A synthetic ID is not just an in-memory convenience; it becomes persistent manifest and filesystem identity. Generic synthetic IDs create last-writer-wins corruption.
Top-level component pages initially wrote every synthetic component section to components/ungrouped-components-ungrouped-components.md. Evidence: commit a384b47, tests/test_pr_129_adversarial.py::test_synthetic_component_section_path_unique_across_two_real_pages.
MIG โ Generated-artifact migration
Generated artifacts are recomputable, but stale generated artifacts still mislead consumers until they are removed or migrated.
ID
Name
Description
Rationale
Proof
MIG-1
Own legacy generated names
When figmaclaw changes generated path schemes or schema versions, it must still narrowly recognize, migrate, or prune legacy generated artifacts from previous schemes.
Otherwise old generated files become permanent orphans: committed, stale, and not owned by the current manifest. Generated does not mean harmless once it is in git.
The legacy pre-H6 synthetic file lacked a numeric node suffix, so generated-file detection skipped it. Evidence: commit 2fcd38b, Agent A report H10, tests/test_pr_129_adversarial.py::test_legacy_ungrouped_components_file_is_generated.
AUTH โ Authority claims
Commands must not overstate what their data proves.
ID
Name
Description
Rationale
Proof
AUTH-1
Authority claims require authoritative sources
Any command or workflow that claims design-token coverage, or applies irreversible/token-writing decisions, must require catalog entries from authoritative sources (figma_api or figma_mcp) or explicitly refuse/degrade output. observed, seeded:*, and unavailable entries may support bridge/suggestion workflows only when the output is labeled as non-authoritative.
Observation proves only usage; seeded data is a bridge; unavailable proves absence of access. Treating those as authoritative causes automation to make token decisions from incomplete evidence, while still allowing safe advisory workflows such as seeded suggestions.
Retry suppression is part of the data contract whenever fallback readers exist. The scope of a cached failure must match the scope of the evidence.
ID
Name
Description
Rationale
Proof
ERR-1
Persistent and transient failures do not share cache semantics
Retry suppression may be cached across files or runs only for persistent configuration absence, such as missing credentials or a REST variables token that Figma explicitly says lacks file_variables:read. Enterprise-only REST readers must be gated by license_type = "enterprise" so non-Enterprise installs do not perform hopeless probes. Per-file or transient API/MCP failures must remain scoped and retryable for later files/runs.
A transient reader error is not evidence that the reader is unavailable everywhere. Caching it globally silently downgrades unrelated files to unavailable and loses authoritative data. Conversely, retrying a known unavailable Enterprise-only endpoint wastes CI time before the real fallback.
Live linear-git CI exposed a transient MCP read-only error that poisoned later files until the cache semantics were split. Evidence: PR #129 comment, commit 19cd1c0, tests/test_mcp_variable_export.py, tests/test_variables_command.py::test_variables_command_auto_skips_rest_variables_without_enterprise_license.
ERR-2
Slow upstream work is scoped, bounded, and observable
Any CI loop that reads external source state must bound each independently retryable unit of work and report timeout or failure at that same scope. A slow file, page, registry probe, or provider call may make that unit stale for this run, but it must not hang the whole process, hide the failure, or block unrelated downstream work that can still run against the last committed state.
Mirrors are eventually consistent. A transiently slow upstream object is not evidence that the repository is unsyncable. Bounded, scoped failure preserves progress, keeps downstream generated registries running, and leaves the slow unit retryable on a later run.
linear-git PR #26 marination while fixing issue #143: a changed Figma file repeatedly held sync until runner shutdown and prevented downstream registry jobs. Covered by pull timeout tests and checkpoint-loop timeout tests.
WF โ Workflow recovery
Reusable workflows write generated cache artifacts in shared git branches. Their recovery behavior must preserve the "generated from source" contract.
ID
Name
Description
Rationale
Proof
WF-1
Replay deterministic generated artifacts
When a workflow push is rejected for deterministic generated artifacts, recovery must recompute those artifacts from the latest remote source state instead of text-merging stale generated JSON/markdown.
Generated artifacts are cache snapshots. Text-merging two snapshots can create a state that was never generated from any Figma/Linear source. Reset-and-replay preserves determinism and avoids cache corruption.
Agent prompts and skills that ask LLMs/humans to commit generated or enriched Figma data must not prescribe `git push
git pull && git push` or any equivalent merge-pull retry. If the work is deterministic generated cache, workflows reset and replay it under WF-1. If the work is LLM/human-authored body, the prompt stops on rejected push and lets a human or orchestrator choose the integration strategy.
WF-3
Stateful jobs start from current publication target
Any reusable workflow that selects, mutates, commits, or pushes repo state must refresh from the current publication-target head before doing that work. The normal same-branch case must be installable without host-specific branch plumbing; explicit target overrides are reserved for workflows that intentionally publish somewhere else. A dispatch-time checkout is only a starting snapshot and may already be stale by the time stateful work begins.
Work selection from stale git state causes agents to process deleted/moved files, generators to backfill obsolete paths, or push recovery to reason from the wrong base. Refreshing the target head before selection preserves the source-of-truth branch boundary without merging, while keeping host YAML minimal.
A scheduled linear-git enrichment job selected figma/migrations/... from a pre-merge checkout after the branch had moved migration receipts to figma_migrations/. Covered by workflow template invariant tests.
WF-4
No-op jobs do not publish
A workflow that produced no local commits must not publish merely to prove the branch is current. Before pushing, generated registry workflows must verify that they have publishable commits relative to the current target head; no-op runs exit successfully.
A no-op job has no state to publish. Letting it push from a stale checkout turns unrelated branch movement into a fake failure and can trigger unnecessary recovery work.
linear-git PR #25 marination showed census had no changes but attempted to push after enrichment advanced the branch. Covered by no-op publication guard tests.
WF-5
Expensive authored work is durable
LLM/human-authored body commits must never exist only in a doomed runner checkout after a rejected push, cancellation, or failed publish. Authored-work publication must avoid canceling in-flight work that may already have produced local commits, must not reset/replay non-deterministic prose, and must make any unpublished commits recoverable before exit.
Authored prose is expensive and non-deterministic. It cannot be recreated by rerunning a generator over source truth, and killing or resetting it loses work.
Issue #143; covered by authored-publication serialization and unpublished-commit preservation tests.
WF-6
Publication policy matches artifact class
Every reusable workflow that mutates git state must declare and implement a push recovery path matching its output: deterministic generated cache may reset/replay under WF-1; no-op generated runs exit under WF-4; authored body work serializes or preserves under WF-5. Do not apply one branch-race recipe to all writers.
Branch pushes are serialized transactions, but figmaclaw work is not homogeneous. Treating generated cache, generated source mirror, and authored body prose the same either corrupts generated state or loses expensive authored work.
Issue #143; host workflow templates keep cheap registry jobs independent while serializing authored enrichment publication.
WF-7
Concurrency scopes match publication target
Caller-side concurrency groups must be scoped to the branch or other publication target they protect, unless the protected resource is intentionally repo-global. Same-target writers may debounce or serialize according to artifact class; different-target runs must not cancel or queue each other merely because they use the same reusable workflow.
GitHub concurrency groups are repository-scoped, while workflow publication usually mutates one branch head. Cross-target cancellation drops valid marination or feature-branch work that cannot race the default branch.
linear-git PR #26 marination of figmaclaw issue #143: scheduled default-branch variables canceled PR-branch variables during Push. Covered by branch-scoping workflow invariant tests.
Generated-registry publishers must distinguish three cases before pushing: no local registry change, remote movement unrelated to that registry, and remote movement that may invalidate the registry snapshot. No-op runs exit without publishing; unrelated movement may be integrated without regenerating the registry; same-registry movement must regenerate from the latest source state. All rejected pushes retry through the same policy, never by text-merging generated snapshots.
Generated registries are deterministic cache snapshots, but branch heads also receive unrelated authored or generated commits. Treating every rejection as either fatal or full replay wastes CI and still races; text-merging generated cache remains invalid under WF-1.
linear-git PR #26 marination: variables replayed once after rejection, then failed on a second final push after unrelated page prose moved the branch. Covered by generated-registry workflow invariant tests.
5. Design decisions
D1..D10 are carried forward verbatim from frontmatter-v2-plan.md. D11..D14 resolve the audit in issue #128. D15 records source-lifecycle provenance for migration work.
D1: Descriptions out of frontmatter
Decision:frames: stores only node IDs (list), not descriptions (dict). Descriptions live exclusively in the body.
Why: Descriptions are LLM prose. Storing them in frontmatter created duplication (frontmatter AND body), sync drift, and double work (set-frames + write-body). Frontmatter is a machine index โ what exists and what changed, not what things look like.
Tradeoff:parse_frame_descriptions() goes away. Any tool that needs descriptions reads the body tables or calls the LLM. Machines need IDs and hashes; humans and LLMs need prose.
D2: Per-frame content hashes for surgical enrichment
Decision: Compute a content hash per frame (depth-1 children: names, types, text content, component IDs). Store enriched hashes in frontmatter, current hashes in manifest. Diff to find exactly which frames changed.
Why: A 500-frame page where 2 frames changed should re-enrich 2 frames, not 500. Without per-frame tracking, any structural change triggers full-page re-enrichment (~$15). With it: 2 screenshots, ~$0.10.
Why depth-1: Catches meaningful changes (elements added/removed, text changed, component swapped) while ignoring noise (position shifts, style tweaks). Descriptions rarely become stale from a color change.
D3: Enrichment state in frontmatter, not manifest
Decision:enriched_hash, enriched_at, enriched_frame_hashes live in the .md file's frontmatter. Manifest only holds sync cache (page_hash, frame_hashes).
Why:
Self-contained: inspect reads one file to check enrichment status. No manifest dependency.
No merge conflicts: concurrent jobs on different pages never conflict. Manifest is a single file โ two writers = merge conflict.
No single point of failure: manifest corruption loses cache (recomputable). Enrichment state survives because each page carries its own.
Portable: rename, move, or copy a page to another repo โ enrichment state travels with it.
Git-friendly: each page's enrichment history is in its own git blame.
If the manifest is deleted, sync re-fetches everything on the next run. Zero data loss. If frontmatter is deleted, we lose the page's identity and enrichment history.
D11 extends this axis: file-scope registries (catalog, census) belong in the cache layer because they are recomputable from file-scoped Figma registry readers. They do NOT belong in frontmatter.
D5: mark-enriched as separate command
Decision:write-body writes body only. mark-enriched snapshots hashes. Two separate commands, two separate concerns.
Why:write-body might be used to fix a typo. Coupling it with hash snapshotting would mark a page as fully enriched when it isn't. The enrichment pipeline calls both in sequence; manual edits call only write-body.
D6: Exit codes for errors only
Decision: All commands exit 0 on success. Exit 2 for actual errors (not a figmaclaw file, missing manifest, etc.). Business logic status (needs_enrichment, missing_descriptions) is in the JSON output, never in exit codes.
Why: Exit 1 conventionally means error. Using it for "needs enrichment" breaks set -e scripts and CI step semantics.
D7: Frame hash excludes position/size/style
Decision:compute_frame_hash hashes child names, types, text content, and component references. It ignores absolute position, size, fills, strokes, effects, opacity.
Why: Descriptions say "login screen with email input and Sign In button." Moving the button 10px doesn't make that stale. Changing the button text from "Sign In" to "Log In" does.
D8: Command naming โ verbs match semantics
Command
Verb
Why
sync
sync
Synchronizes structure from Figma to local.
pull
pull
Pulls all tracked files (git analogy).
census
(noun)
Snapshot.
variables
(noun)
Same shape as census โ file-scope registry snapshot.
write-body
write
LLM is authoring prose.
mark-enriched / mark-stale
mark
Sets a flag/state.
inspect
inspect
Read-only state examination.
set-flows
set
Writes a specific field value.
screenshots
(noun)
Downloads artifacts.
suggest-tokens
suggest
Annotates with non-binding suggestions.
fix-tokens (future)
fix
Applies suggestions back to Figma.
D9: Current hashes in manifest, not frontmatter
Decision: Current frame hashes (frame_hashes) live in the manifest only. Enriched frame hashes (enriched_frame_hashes) live in frontmatter only.
Why: Per D4, current is cache, enriched is state. Duplicating in two places bloats large pages (~10KB for 500 frames) and increases git churn.
Fallback: If manifest is missing, treat all frames as stale (safe, triggers full re-enrichment).
D10: Section-level enrichment via per-frame hash aggregation
Decision: No per-section hashes or timestamps in frontmatter. Section staleness is computed at runtime by mapping stale frames to sections (body parsing via parse_sections()).
Why: Per-frame hashes already exist. Computing "which sections are stale" is a join of manifest.frame_hashes ร enriched_frame_hashes ร parse_sections(). Adding per-section hashes would be redundant aggregation.
mark-enriched remains page-level. Cannot call after each section โ that would mark other still-stale sections as current.
D11: File-scope cached registries are a peer of page-scope state
Decision: Variables, components, styles, and any other file-level Figma data live in dedicated, file-scope cached registries. They refresh at Tier 1.5 of the ladder (once per file when version changes), independent of per-page hashing.
Why: Variables (and components) change independently of any page's content. Before this decision, the catalog was gated on per-page hash invalidation, which meant a Figma file rename of gray/100 โ gray/100-default could change the file's version without changing any page's hash, and the local catalog would never refresh โ even though every consumer of "what tokens does this DS define?" expected an answer. Census already established this pattern for components (commit 13b0146); D11 generalises it.
Tradeoff: A new tier in the refresh ladder. Acceptable โ the normal cost is one cheap REST call per changed file for component sets. REST variables are Enterprise-only and must stay behind license_type = "enterprise"; default figmaclaw variables --source auto uses Figma MCP for the catalog refresh instead.
D12: Library identity is data-derived, never hardcoded
Decision: No library hash constants in figmaclaw source. Library identity comes from the file's variable-registry response (/variables/local or MCP export), or, for an unknown library, from the catalog's libraries map populated by other tracked files.
Why: The previous DS_LIB_HASH = "778120a4..." and OLD_LIB_PREFIX = "a3972cba" constants in token_scan.py coupled a general-purpose tool to one customer's setup, in direct violation of the "general-purpose open-source" rule in CLAUDE.md. Worse, any unknown library (including a new DS coming online) was silently classified valid by the conservative fallback โ making the catalog answer "yes, this binding is fine" even when it had no idea what library the binding pointed at.
How:classify_variable_id(var_id, *, libraries) becomes a pure function over data. The libraries argument is the catalog's libraries dict. Variables resolving to a known library get the library's name; variables resolving to an unknown library get an explicit unknown_library classification (with the lib hash in the issue) โ not silent valid.
D13: The catalog stores definitions; sidecars store usage
Decision: Two distinct data structures:
ds_catalog.json โ definitions. Authoritative answer to "what variables exist?". Source: Figma variable registry via REST or MCP. Never observation.
*.tokens.json sidecars โ usage. Per-page answer to "what bindings and raw values exist on this page?". Source: page walks.
Why: The original ccdd2d7 design collapsed both into observation. The catalog inherited mode-blindness, name-blindness, and library-confusion as a result; see failure modes F2..F5 in ยง6.
Implication for merge_bindings. It stops writing definitional fields (hex, numeric_value, name). It records only observed_on properties and (new) usage_count per (file_key, variable_id). Definitions come exclusively from the variables refresh code path.
D14: SEEDED entries are first-class via a source field
Decision: Each catalog variable carries a source field with values figma_api, figma_mcp, seeded:css, seeded:manual, or observed. Advisory readers such as suggest-tokens may use seeded bridge entries as labeled, non-authoritative match candidates. Any workflow that claims authoritative coverage or applies token writes must require figma_api / figma_mcp definitions (AUTH-1); observed, seeded:*, and unavailable are not authoritative.
Why: Until every customer team has Enterprise scope file_variables:read, CSS-derived seeds remain the only path for many tokens. The seed_catalog.py script in linear-git is the precedent (commit 157cd98d6). Treating SEEDED:* IDs as a permanent first-class case (not a temporary workaround) means the schema is honest about its sources, and fix-tokens can refuse to apply a seeded:* ID until it's resolved to a real Figma variable ID by a future runtime resolution step.
Tradeoff: Catalog readers must be tolerant of multiple sources for the same value. suggest-tokens flags ambiguity when both a figma_api and a seeded:css candidate match the same hex, and its output remains advisory unless backed by authoritative definitions.
D15: Source lifecycle is provenance, not authority
Decision: File-scope token and component registries record source-system lifecycle separately from reader authority. source=figma_mcp / figma_api answers "is this definition complete and authoritative?" while source_lifecycle=archived answers "is the originating Figma system historical?" A registry can therefore be authoritative-but-archived, unavailable-but-archived, or authoritative-and-active.
Why: Migration work often needs old DS tokens/components as evidence for mapping legacy designs to the current system. Losing archived entries destroys useful migration context; letting archived entries silently compete as current targets corrupts suggestions. Keeping lifecycle as provenance lets callers explicitly include historical systems when auditing, and exclude them by default when applying fixes.
6. Failure-mode catalog
Each row records a failure mode that has actually occurred (or that we shipped a near-miss for) and the invariants that preclude it. Cite by ID in PR review.
ID
Failure mode
Origin
Precluded by
F1
Page-hash gating of file-level data. Variables and other file-scope content change independently of page node trees, but the catalog refresh was inside the per-page loop. Page skips โ catalog stale.
Issue #128 audit, Apr 2026.
D11, TC-5
F2
Observation-only catalog. Catalog only contained variables figmaclaw happened to encounter as boundVariables.<prop>.id. Primitives, unused tokens, alias targets, and variables on un-pulled pages were invisible.
ccdd2d7 design choice ("zero additional API calls โ piggybacks on get_page() data"). Right call for "annotate raw bindings on pull"; wrong call for "tell me what tokens the DS defines."
D13, TC-1
F3
Dead model fields. CatalogVariable.name shipped in JSON but had no writer. The 22 names on disk were a hand-seeded artifact (22dcd48f1 in linear-git), not figmaclaw output.
Same audit.
TC-3
F4
Mode-blind catalog. merge_bindings was last-write-wins on a single value field. Per-mode variables collapsed silently.
Same audit.
TC-4
F5
Silent staleness in consumers. suggest-tokens reported no_match indistinguishably whether the DS doesn't define a token or the catalog hasn't seen it.
Same audit.
TC-7, CR-2
F6
Coupled cost โ no cheap path. "Get me current token names" required a multi-hour --force pull.
Same audit.
TC-6
F7
Catalog was neither truth nor cache. Lived in .figma-sync/ (cache layer per D4) but wasn't recomputable without re-walking every page.
Same audit.
D11, W-1
F8
Hardcoded DS library identity in a general-purpose tool. DS_LIB_HASH / OLD_LIB_PREFIX constants in token_scan.py:33-34 coupled figmaclaw to one customer's library hashes. Unknown libraries were silently classified valid.
Visible in the source from the original ccdd2d7.
D12
F9
suggest-tokens has no terminal application. Tool annotates sidecars with fix_variable_id candidates; nothing applies them. Annotations accumulate as disk state and become stale before any consumer acts on them.
Audit confirmed: every sidecar in the linear-git consumer has suggested_at: null, no fix_variable_id populated.
Future fix-tokens RFC; tracked separately.
F10
Schema migration silently drops user-set data. e100631's sidecar v1 โ v2 migration rewrites the file. Once suggest-tokens runs, human-set fix_variable_id values would be lost on the next migration.
Latent; doesn't bite today because suggest-tokens isn't in CI.
LW-2, TS-S-5
F11
Registry absence misread as empty registry. A file with no _census.md could be read as "no published components" even when the file had never been explicitly probed or emitted.
Tap In / LSN component audit during PR #129.
REG-1
F12
Silent partial pull. A page entry reached stable manifest state with md_path=null, component_md_paths=[], and page_hash=sha256("[]")[:16]; future pulls skipped it forever.
PR #129 H2; 215 entries in linear-git.
PP-1, NC-1, HSH-1
F13
Walker coverage mismatch. Parser/renderer/hash logic disagreed about top-level or section-wrapped COMPONENT / COMPONENT_SET nodes, so some code paths saw real content and others treated the page as empty.
PR #129 H2/H9.
NC-1, HSH-1
F14
Mixed section data loss. A SECTION containing both frames and component sets rendered one class and silently dropped the other.
PR #129 H11.
NC-2
F15
Rendered component variant staleness. Adding or renaming a visible variant inside an existing COMPONENT_SET did not change page or frame hashes, so generated variant tables stayed stale.
PR #129 H8/H9.
HSH-1
F16
Synthetic path collision. Multiple pages with top-level components generated the same synthetic component-section path; later pages overwrote earlier pages' component markdown.
PR #129 H6.
SI-1
F17
Legacy generated orphan. A generated file from an old path scheme was no longer recognized as generated and survived pruning forever.
PR #129 H10.
MIG-1
F18
Green but non-authoritative catalog. CI could succeed with source=unavailable or observation-only token data while downstream tasks interpreted the catalog as current DS truth.
PR #129 variables proof lane.
AUTH-1, TC-1, TC-7
F19
Transient failure poisoned later files. A per-file MCP export error was cached like missing credentials and suppressed authoritative fallback for unrelated later files.
PR #129 live linear-git run.
ERR-1
F20
Merged generated cache snapshot. Rejected workflow pushes attempted merge-pull recovery for generated JSON, allowing conflict states that were not the output of a deterministic generator over current source.
PR #129 variables workflow lane.
WF-1
F21
MCP read-only denial treated as transient. use_figma can run in contexts where Figma refuses file mutation, even if figmaclaw's JavaScript is intended to read variables. Retrying that error burns CI minutes and repeats the same unavailable marker.
linear-git PR #140 marination: FigJam files and an archived Figma file returned Operation attempted to modify the file while in read-only mode.
TC-11, ERR-1
F22
Stateful workflow selected work from a stale dispatch snapshot. Downstream enrichment checked out the scheduled run's old SHA and selected a file path that had already been moved on the branch.
linear-git scheduled run 25456720296, after PR #23 moved migration receipts from figma/migrations/ to figma_migrations/.
WF-3
F23
No-op, generated, or expensive workflow entered the wrong branch-race recovery lane. A generated no-op job tried to publish from stale state, expensive authored work could be canceled or left only in a runner checkout after losing a branch-head race, repo-wide concurrency canceled work for an unrelated target branch, and generated-registry recovery retried in a way that could still lose the next unrelated branch movement.
linear-git PR #25 marination exposed the no-op publication half; PR #26 marination exposed cross-target cancellation and generated-registry retry gaps while issue #143 was being fixed.
WF-4, WF-5, WF-6, WF-7, WF-8
F24
Slow upstream unit became a sync poison pill. A changed upstream object could occupy sync until the runner killed the job. The whole workflow failed before downstream jobs that did not depend on fresh data from that object could run.
linear-git PR #26 marination repeatedly showed one changed Figma file holding sync until runner shutdown, skipping census and variables.
ERR-2
Each row was either repeated more than once or had a near-miss before being canonized. New rows are appended; nothing is renumbered.
7. Document index
Every doc that exists, what it owns, where it points to canon.
Doc
What it owns now
Relationship to canon
skills/figmaclaw-canon/SKILL.md
This document โ invariants, design decisions, refresh ladder, failure modes. Bundled as the figmaclaw:figmaclaw canon skill so consumer repos can invoke it without reading figmaclaw's CLAUDE.md.
Authoritative.
docs/figmaclaw-canon.md
Pointer stub at canon's old location, redirecting to the skill above.
Pointer only.
docs/figmaclaw-md-format.md
Authoritative reference for .md file format: frontmatter schema, body structure, single-line flow YAML rule, command-by-command table.
Format spec. References canon for the underlying data contract; canon ยง1 references it back for format details.
docs/body-preservation-invariants.md
Detailed test-by-test invariant list for BP, SC, FM, CL classes.
Invariant detail. Canon ยง4 names the invariants and links here for the test mapping.
docs/body-preservation-design.md
Historical: why we renamed enrichโsync, render_pageโscaffold_page. Implementation notes.
Historical. Superseded by canon for current rules.
docs/frontmatter-v2-plan.md
Original D1..D10 design decisions; per-frame hash design; enrichment flow.
Historical RFC. Decisions D1..D10 are canonized in ยง5. The rest is implementation notes โ keep for archaeology.
docs/sync-observability.md
SYNC_OBS and SYNC_OBS_PULL event taxonomy; artifact upload structure.
Operational reference. Canon ยง3 references it for the refresh ladder's observability hooks.
docs/token-auth-and-rotation.md
API key handling, secret rotation runbook.
Operational. Canon TC-6 references it for figmaclaw variables auth path.
docs/giant-section-strategies.md
Section-mode enrichment for pages with too many frames for a single LLM dispatch.
Operational. Tied to D10 (section-level enrichment).
Operational. Standards stay there; invariants live here.
README.md
User-facing intro and install.
User-facing. Not affected by canon.
INSTALL.md
Developer install instructions.
Same.
Consumer-repo fragment (e.g. linear-git's .agents/AGENTS.md) cross-references canon ยง1 (data contract) and ยง4-TC (catalog invariants) for any agent doing token migration work.
8. Anti-pattern checklist for PR review
Use this checklist when reviewing any PR that touches figmaclaw's data model, catalog, sidecars, or enrichment loop. Each item is a question you should be able to answer "yes" to before approving.
Cross-cutting
Does this PR add a loop-break or selector? If yes, is there a cross-run test (CR-1)?
Does this PR touch frontmatter fields? If yes, are frame-keyed dicts pruned at the _build_frontmatter chokepoint and covered by key-set tests (KS-1)?
Does this PR introduce a new LLM row marker? If yes, is the tombstone protocol designed in the same PR (TS-1)?
Does this PR walk the body? If yes, does it use iter_body_frame_rows / section_line_ranges (CW-1)?
Does this PR touch a log or schema writer? If yes, does it auto-heal or hard-fail on schema drift (LW-1, LW-2)?
Does this PR add a new selection / entry boundary that reads a page .md? If yes, is it registered in _HEALING_ENTRY_POINTS and does it call normalize_page_file (HE-1)?
Does this PR add a writer? If yes, does it strip timestamps before comparing existing content (W-1) and round-trip-assert after writing (W-2)?
Does this PR add or consume a file-scope registry? If yes, can callers distinguish not-probed, probed-empty, and probed-with-entries (REG-1)?
Does this PR change manifest page entries or pull skipping? If yes, can every non-skipped page reach a terminal data state (PP-1)?
Does this PR add or change a Figma node walker? If yes, does parser/renderer/hash/manifest/prune coverage stay in parity (NC-1), including mixed sections (NC-2)?
Does this PR change generated markdown content? If yes, do page and unit hashes include every visible source identity that can affect that output (HSH-1)?
Does this PR create synthetic nodes or paths? If yes, are they source-scoped and collision-proof at the filesystem path layer (SI-1)?
Does this PR change generated path schemes or schema versions? If yes, are legacy generated names still narrowly recognized, migrated, or pruned (MIG-1)?
Does this PR cache an API/MCP failure or suppress retries? If yes, is the cache scoped only to evidence that is persistent at that scope (ERR-1)?
Does this PR add an external source-reader call in a CI loop? If yes, is the independently retryable unit bounded and observable so one slow source object cannot starve unrelated work (ERR-2)?
Does this PR recover from rejected pushes of generated artifacts? If yes, does it reset/replay deterministic generation instead of text-merging generated cache snapshots (WF-1)?
Does this PR edit prompts or skills that tell agents to push commits? If yes, do they avoid merge-pull retry recipes and preserve the authored-vs-generated recovery split (WF-2)?
Does this PR add or edit a stateful reusable workflow? If yes, does it refresh from the current publication target before selecting or writing repo state, while keeping the normal same-branch install path free of host-specific branch plumbing (WF-3)?
Does this PR add or edit a workflow git push? If yes, do no-op generated runs exit before pushing (WF-4), and does the recovery path match the artifact class (WF-6)?
Does this PR add or edit generated-registry publication? If yes, does it distinguish no-op, unrelated remote movement, and same-registry invalidation without text-merging generated snapshots (WF-8)?
Does this PR add or edit expensive LLM/human-authored publication? If yes, are in-flight runs non-canceling and are unpublished commits preserved before exit (WF-5)?
Does this PR add or edit caller-side concurrency? If yes, are groups scoped to the publication target unless the protected resource is intentionally repo-global (WF-7)?
Token catalog and sidecar specifically
Does this PR add a field to CatalogVariable or CatalogLibrary? If yes, is there a writer for it and does the dead-fields meta-test cover it (TC-3)?
Does this PR write to the catalog? If yes, is the source field set correctly (figma_api, seeded:*, never observed for new code) (D14, TC-2)?
Does this PR write file-scope registries? If yes, do token libraries and component census output preserve source_file_key and source lifecycle so archived systems remain explicit evidence (TC-12, D15)?
Does this PR change how the catalog is refreshed? If yes, is the refresh page-independent (TC-5)? Does it hit Tier 1.5, not Tier 2?
Does this PR touch classify_variable_id? If yes, is library identity passed in as data, never read from a hardcoded constant (D12)?
Does this PR change the sidecar schema? If yes, does the migration preserve fix_variable_id (LW-2, TS-S-5)?
Does this PR add a consumer of the catalog? If yes, does it CR-2 staleness-check before producing results?
Does this PR claim token coverage or apply token-writing decisions? If yes, does it require authoritative figma_api / figma_mcp data or explicitly refuse/degrade output (AUTH-1)?
If any answer is "no" or "not sure," the answer is not ready to merge.
Bumping the canon. New invariants get appended (next ID in the class), never renumbered. New design decisions get the next D-number. Failure modes get the next F-number. This document's IDs are referenced by tests, commit messages, and PR reviews โ stability of IDs is itself an invariant.