Aesthetic Function

June 12, 2026 · View on GitHub

This document is the authoritative source of truth for system behavior. AI agents and contributors should prioritize this over README.md when resolving questions about runtime boundaries, reconciliation semantics, or architectural constraints.

For a product overview, see README.md.

This document is the source of truth for AF's runtime boundaries, reconciliation semantics, phase invariants, and adapter constraints. It preserves all architectural guarantees that implementations must respect.


Core Identity

A bidirectional Code ↔ Design synchronization system for UI frameworks (React, Vue) and Figma. The pipeline is deterministic by default, using @figma comment markers to extract design intent. An optional LLM-based analyzer can be enabled via feature flag, with automatic fallback to marker parsing on failure.

Architectural Invariants

These invariants are frozen and must not be violated by any future phase:

  1. AF is the only mutation authority. Writes to Figma flow through watcher → server → plugin. No external tool, adapter, or MCP server may write to Figma.
  2. Deterministic precedence: override > marker > ast > code. Implemented in resolveField() / resolveWithPolicy().
  3. Three-runtime boundary: Watcher (reconciliation), Server (relay + persistence + audit), Plugin (mutation executor). No runtime may assume another runtime's responsibilities.
  4. The CLI is a control surface, not a runtime authority. It delegates to watcher/server modules; it does not own reconciliation logic.
  5. Design adapters are read-only unless explicitly allowed. The MCP adapter uses a default-deny tool policy. Write tools are blocked architecturally, not by omission.
  6. Echo suppression prevents feedback loops. AST writes trigger file saves, which re-trigger the watcher. The echo guard cache suppresses duplicate operations.
  7. Reconciliation semantics are frozen at Phase 14F. Later phases add configuration, tooling, and adapters — they do not change how reconciliation resolves.

MVP Freeze (Phase 14F)

As of Phase 14F, the reconciliation system is feature-complete and stable. The figma:reconcile command provides a single entry point for the full Phase 12–14 analysis pipeline, with CI integration via GitHub Actions matrix workflows.

Phase 15 adds a configuration layer, named policy profiles, artifact inspection tooling, and a unified af CLI control surface. No reconciliation semantics changed.

Phase 16 adds a design adapter interface, a Figma Console MCP adapter, and a surface classification metadata layer. Adapters are read-only and non-authoritative. They do not participate in reconciliation decisions. The surface classification layer categorizes adapters along four independent dimensions (surface type, access mode, authority role, stability) without affecting behavior.


What Works Today (Phase 14F)

FeatureStatus
Code → Design (Core Pipeline)
File save detection via chokidar
@figma marker parsing (regex-based, default)
IntentModel → FigmaOperation transformer
Design token resolution
WebSocket + HTTP polling server relay
Figma plugin with SET_TEXT / SET_FILL operations
Live Figma updates on file save
LLM Mode (Optional)
LLM-based intent analyzer (feature flag)
Automatic fallback to markers on LLM failure
Design → Code Capture
DESIGN_CHANGE message from Figma plugin
design-overrides.json persistence
Override reconciliation layer
Override precedence controls
Design → Code Materialization
Patch artifact generation (reviewable diffs)
Marker line updates (direct source edits)
Dry-run mode (default, no writes)
AST Analysis (Read-Only)
Babel-based JSX literal extraction
Component detection (function + arrow)
Marker-to-component anchoring
Diff report: JSX vs Markers vs Overrides
AST Semantic Extraction (Phase 6B/6C)
Semantic color/layout extraction from JSX
AST write feasibility analysis
Token-to-style reverse mapping
Feasibility CLI (feasibility:report)
AST Writes (Phase 7A/7B)
AST-based source mutation via Babel
Marker-line edits via AST
Style object patching
Dry-run mode for AST writes
Reconciliation Policy (Phase 7C)
Unified precedence: override > marker > ast > code
Echo suppression guard (prevents feedback loops)
Resolution summary logging
Variant/State Mapping (Phase 8A)
Component state dimension (base/disabled/hover/pressed)
State-aware marker parsing (state=hover)
State-aware Figma node targeting (NodeName::hover)
Per-state override keys in design-overrides.json
State-aware echo suppression cache
Disabled state inference from AST (disabled={true})
Native Figma Variant Targeting (Phase 8B)
Component Set variant resolution
Variant query parsing (NodeName::state)
State → Figma property mapping
Variant info in DESIGN_CHANGE
DEBUG_LIST_VARIANTS diagnostic
Component Mapping Registry (Phase 8C)
component-map.json for stable node IDs
Automatic map updates on Send Selection
Watcher prefers mapped IDs over names
Plugin resolves id:<nodeId> queries
Idempotent server map merging
Feature Orchestrator (Phase 9A)
Prompt → Code → Figma pipeline
LLM context bundle (AST, intents, overrides, tokens)
Patch artifact generation + validation
State-aware apply routing (markers/overrides for non-base)
Post-Apply Emit (Phase 9B)
Immediate Figma refresh after apply
Watcher suppression (prevents duplicate sends)
Debounced emit for rapid writes
Observability & DX (Phase 9C)
TraceSummary structured logging
Ops-hash based suppression (smarter dedup)
OPERATION_RESULT failure logging
Demo scripts (demo:server, demo:watcher, etc.)
TRACE/TRACE_JSON/TRACE_VERBOSE env vars
Test Stability (Phase 9D)
Deterministic test fixtures
CI guardrails for demos/react-demo-app isolation
CONTRIBUTING.md test policy
Semantic Adapter Architecture (Phase 10A)
Generic SemanticAdapter interface
Adapter registry with priority ordering
Vuetify adapter (v-btn, v-card, v-text-field, v-chip)
Confidence-based prop extraction
Adapter/JSX semantics merging
Fixture-based adapter tests
Ant Design Adapter (Phase 10B)
Import-based component detection
AntD Button, Input, Card, Tag extraction
Semantic hints (antd:primary, antd:danger)
Registry extensibility proof
Component Map Suggestions (Phase 10C)
AST-based suggestion derivation
Adapter-augmented Figma name hints
Explicit-only variant states (markers/overrides)
CLI output (read-only, no file writes)
Existing map entry detection
Component Map Bootstrap (Phase 10D)
Bootstrap artifact generation
Safe apply mode (behind env flags)
Never overwrites existing nodeIds
Atomic file writes
Canonical Token Layer (Phase 10E)
Design-system-agnostic token vocabulary
Vuetify → canonical color mapping
AntD → canonical color mapping
Hex → canonical via design tokens
Spacing normalization (T-shirt sizes)
Extensible hint mapper registry
CLI canonical semantics output
Canonical Resolver + Coverage (Phase 10F)
Canonical → design system value resolution
Color token → hex resolution
Spacing token → pixel resolution
Radius/typography token resolution
Coverage report with gap detection
CLI resolution + coverage output
Resolution Policy + Project Coverage (Phase 10G)
Policy strategies (token-first, token-only, hex-allowed)
Project-level coverage aggregation
canonical:coverage CLI command
Policy violations detection
Strict mode for CI gates
JSON output for automation
Figma Composition Suggestions (Phase 11A)
Read-only Figma composition guidance
Component Set suggestions (missing from map)
Explicit-only variant suggestions (markers/overrides)
Property suggestions (Fill, Auto Layout, Corner Radius)
Token usage suggestions
Coverage gap suggestions
Deterministic, sorted output
CLI integration (ast:report output)
Figma Composition Application (Phase 11B)
Compose operations from suggestions
Opt-in apply mode (dry-run default)
Feature flags (mode, allow-list)
Deterministic opId hashing
Server /compose endpoint
Plugin COMPOSE_OPERATIONS handler
Full audit trail logging
CLI figma:compose command
Figma Property Application (Phase 11C)
Apply properties to existing Figma nodes
Opt-in apply mode (artifact-only default)
Feature flags (mode, dry-run, allow-list)
Property categories (fill, spacing, typography)
Deterministic opId hashing
Server /apply-properties endpoint
Plugin APPLY_PROPERTIES handler
CLI figma:apply command
Variant-aware apply targeting (Phase 11C.1)
Text-descendant resolution for text props
Figma → Code Delta Detection (Phase 12A)
Delta detection: Figma state vs code baseline
Canonical token resolution for both sides
Normalization notes for aliased values
Confidence scoring (high/medium/low)
Per-property delta types (fill, padding, gap)
Batch processing for multiple components
Delta artifact generation
CLI output (read-only, no file writes)
Figma Delta Suggestions (Phase 12B)
Suggestion layer for delta application
Target selection: AST / marker / override / blocked
Non-base state policy (never AST writes)
Base state AST write detection (auto-writable)
Deterministic, sorted output
Suggestion artifact generation
CLI integration (ast:report output)
Figma Delta Application (Phase 12C)
Apply delta suggestions to storage targets
Target routing: AST / marker / override / blocked
Override writes (state-aware keys)
Marker updates (existing markers only)
AST writes (auto-writable literals only)
Confidence threshold filtering
Allow-list based target filtering
Dry-run mode (default, no writes)
Opt-in apply mode (requires env flags)
Delta apply artifact generation
Audit trail logging
CLI figma:delta-apply command
Conflict Surfacing & Preview (Phase 12D)
Conflict detection: AST vs Figma vs markers vs overrides
Conflict types: AST_VS_FIGMA, MARKER_VS_FIGMA, OVERRIDE_VS_FIGMA
Canonical mismatch detection
Non-base state blocking
Low confidence blocking
Conflict evidence with source locations
Policy rule explanations
Conflict artifact generation (.figma-conflicts.json)
CLI conflict preview section
Read-only (no file modifications)
Guided Conflict Resolution (Phase 12E)
Resolution plan generation from conflicts
Resolution actions: APPLY_TO_AST, APPLY_TO_MARKER, APPLY_TO_OVERRIDE, IGNORE, BLOCK
Human-readable reason explanations
Deterministic decision ordering
Resolution artifact generation (.figma-resolution-plan.json)
CLI figma:resolve command
Read-only (produces plan, does NOT apply)
Apply Resolution Plans (Phase 12F)
Execute resolution plans from Phase 12E
Target routing: AST / marker / override / blocked
Opt-in apply mode (artifact-only default)
Multi-flag gate (ON + MODE=apply + DRY_RUN=false)
Allow-list based target filtering
Non-base state AST blocking
Confidence threshold filtering
Decision ID traceability
Apply artifact generation (.figma-resolve-apply.json)
Audit trail logging
CLI figma:resolve-apply command
Post-Apply Verification (Phase 12G)
Verify applied resolutions landed as intended
AST/marker/override target verification
Optional Figma read-only verification
Mismatch and missing detection
Rollback information capture
Verification artifact generation (.figma-verification.json)
CI-safe exit codes (0 = pass, 1 = fail)
CLI figma:verify command
Post-Apply Auto-Verification + CI Gate (Phase 12H)
Automatic verification after apply operations
POST_APPLY_VERIFY environment variable control
Strict mode CI gating (exit 1 on mismatch/missing)
Non-strict mode for advisory-only verification
Bidirectional artifact linking (apply ↔ verify)
Seamless CLI integration (figma:resolve-apply)
Rollback Preview & Safety Envelope (Phase 12I)
Read-only rollback preview generation
Derive rollback actions from verification failures
Deterministic ordering (componentKey → state → property)
Mixed target support (ast, marker, override)
Rollback preview artifact (.figma-rollback-preview.json)
CLI figma:rollback-preview command
Always exit 0 (never fails CI, diagnostic only)
Reconciliation Status Artifact (Phase 12J)
Single deterministic status artifact per file
Auto-discover artifacts from Phases 12F-12I
OverallStatus: CLEAN, APPLIED_UNVERIFIED, VERIFIED_OK, VERIFY_FAILED, ROLLBACK_AVAILABLE
CI Verdict: PASS, WARN, FAIL with exit codes
CLI figma:status command
Human-readable and JSON output formats
Rule-table only, no heuristics
Reconciliation Run Index (Phase 13A)
Read-only indexing of reconciliation artifacts
Indexes all phases: delta, suggestions, conflicts, plans, apply, verify, rollback, status
One-shot snapshot (NOT timeline/history)
Repo-root invariant (works from any working directory)
Deterministic output (sorted, stable, canonical paths)
Metadata extraction (timestamps, modes, counts)
Best-candidate selection for multiple artifacts
Legacy artifact name support
CLI figma:index command
Human-readable and JSON output formats
Design Drift Timeline (Phase 13B)
Append-only run ledger (.figma-run-ledger.json)
Deterministic run ID hashing (djb2)
Longitudinal run history per file
Repo-root invariant (works from any working directory)
Feature flag gated (RECONCILIATION_TIMELINE_ON)
Automatic recording on command completion
CLI figma:timeline command
Human-readable and JSON output formats
Drift Diffs (Phase 13C)
Run-to-run comparison (.figma-drift-diff.json)
Severity classification (info/warn/fail)
Status worsening detection
Metric delta computation
Run selection (latest vs previous, explicit IDs)
Repo-root invariant (works from any working directory)
CLI figma:drift command
Human-readable and JSON output formats
Drift Diff UX Hardening (Phase 13C.1)
Preconditions banner (repo root, source paths, ledger, runs)
Run selection explanation (--explain flag)
Deterministic "no material drift" messaging
Strict exit code (--strict: exit 1 if 'fail' severity)
Presentation ordering (severity → field → deterministic)
Drift Summary Dashboard (Phase 13D)
Aggregated drift dashboard (.figma-drift-dashboard.json)
Stability score (0-100, deterministic rule-table)
Severity counts (info/warn/fail across run window)
Top drift signals (sorted by severity, delta)
CI verdict (PASS/WARN/FAIL) with configurable thresholds
Configurable run window (--limit, --from, --to)
CI strict mode (--strict, exit 1 on FAIL)
Repo-root invariant (works from any working directory)
CLI figma:dashboard command
Human-readable and JSON output formats
Project Drift Dashboard (Phase 13E)
Directory-level drift aggregation (.figma-project-dashboard.json)
File discovery with configurable patterns (**/*.tsx)
Exclusion of node_modules, dist, build, .next, .turbo, .git, coverage
Per-file dashboard status (OK, NO_DATA, ERROR)
Project stability score (average of files with data)
Project verdict (FAIL if any file FAIL, else WARN if any file WARN, else PASS)
Top signals across project (sorted by severity → magnitude → file → key)
Repo-root invariant (works from any working directory)
CI strict mode (--strict, exit 1 on FAIL)
CLI figma:project-dashboard command
Human-readable and JSON output formats
Configurable thresholds (--fail-score, --warn-score, --max-signals)
Environment variable configuration (RECONCILIATION_DASHBOARD_*)
Threshold-based verdict (score < fail → FAIL, fail ≤ score < warn → WARN, ≥ warn → PASS)
Exit code 2 for invalid configuration
CI Gate Summary (Phase 13F)
CI-focused gate command (.figma-ci-gate.json)
Reuses Phase 13E project dashboard data
Trend window from Phase 13B ledgers (default: 5 runs)
Trend direction classification (improving/stable/worsening)
File trend counts (improving, stable, worsening, insufficient data)
Configurable window size (--window, env: RECONCILIATION_CI_WINDOW)
CI strict mode (--strict, exit 1 on FAIL)
Repo-root invariant (works from any working directory)
CLI figma:ci command
Human-readable and JSON output formats
Single-Entry Reconcile CLI (Phase 14A-C)
Single CLI for full reconciliation pipeline
Profile support (local, record, ci)
Bundle artifact generation
CI-friendly output format (--format ci)
Deterministic verdict (PASS/WARN/FAIL)
Git SHA capture for traceability
CI profile always writes bundle
GitHub Actions CI (Phase 14D)
Figma reconcile workflow
PR and main branch triggers
Bundle artifact upload (always)
Structured outputs ($GITHUB_OUTPUT)
Job summary with verdict
Copy/paste CI recipe documented
Multi-Source CI (Phase 14F)
Deterministic source discovery
Manifest file (reconcile.sources.json)
Glob pattern discovery
Deterministic chunking for matrix CI
Aggregated verdict (PASS/WARN/FAIL)
Matrix workflow (figma-reconcile-ci-matrix.yml)
CLI figma:sources command
Observability
Async audit trail logging (sync-log.md)
Project Configuration (Phase 15A)
af.config.json project configuration
Config discovery (cwd → parent dirs → .git root)
Merge precedence: defaults → file → env vars
Zero-config backward compatibility
Config wiring into watcher pipeline (optional param)
Named Policy Profiles (Phase 15B)
designer-first profile (= Phase 14F default)
code-first profile (= if_newer_than_code)
balanced profile (code-first + warn on conflicts)
strict-review profile (block all conflicts)
Profile resolver (RECONCILIATION_POLICY env or config)
Profile wired into watch.ts pipeline
Artifact Inspector (Phase 15D)
Artifact listing (artifacts:list) — all 12 types
Artifact inspection (artifacts:inspect) — type-aware highlights
Lifecycle trace (artifacts:trace) — end-to-end audit trail
Audit log metadata expansion (profile, configSource, policy)
Unified CLI Control Surface (Phase 15C)
af init — Generate af.config.json
af run — Spawn watcher + server as child processes
af reconcile — Delegate to figma:reconcile
af status — Delegate to figma:status
af dashboard — Delegate to figma:dashboard
af dashboard --project — Delegate to figma:project-dashboard
af ci — Delegate to figma:ci
af artifacts list|inspect|trace — Delegate to artifact inspector
Config → env var bridge (no logic duplication)
Surface Classification Metadata (Phase 16A Extension)
SurfaceMetadata types (SurfaceType, AccessMode, AuthorityRole, StabilityLevel)
Optional surfaceMetadata on DesignAdapter and SemanticAdapter
getDesignAdaptersBySurface() read-only query helper
getSemanticAdaptersBySurface() read-only query helper
Figma MCP adapters classified: design / read-only / observational
Semantic adapters classified: runtime / no-mutation / derived
Storybook adapter stub (runtime / read-only / observational)
No reconciliation logic modified

Architecture

The system follows a three-legged stool design with strict runtime boundaries:

┌─────────────────────────────────────────────────────────────────────────────────┐
│                           BIDIRECTIONAL SYNC FLOW                                │
└─────────────────────────────────────────────────────────────────────────────────┘

  CODE → DESIGN:
  ┌─────────────────┐     HTTP POST     ┌─────────────────┐    WebSocket/Poll   ┌─────────────────┐
  │                 │  ─────────────▶   │                 │  ─────────────────▶ │                 │
  │     Watcher     │                   │     Server      │                     │  Figma Plugin   │
  │   (Local Node)  │                   │  (Relay Bridge) │                     │   (Sandbox)     │
  │                 │  ◀─────────────   │                 │  ◀───────────────── │                 │
  └─────────────────┘   DESIGN_CHANGE   └─────────────────┘     Selection Msg   └─────────────────┘
          │                   │                                                          │
          │ watches           │ writes                                                   │ mutates
          ▼                   ▼                                                          ▼
  ┌─────────────────┐ ┌──────────────────┐                                     ┌─────────────────┐
  │  React Source   │ │ design-overrides │                                     │  Figma Document │
  │   (demos/react-demo-app/)   │ │      .json       │                                     │                 │
  └─────────────────┘ └──────────────────┘                                     └─────────────────┘

Data Flow

  1. Code → Design: File Change → Watcher → IntentModel → FigmaOperations → Server → Plugin → Figma
  2. Design → Code: Figma Selection → Plugin → Server → DESIGN_CHANGE → design-overrides.json
  3. Reconciliation: On next file save, overrides are merged with code-derived intents before sending to Figma

Packages

PackageRuntimeResponsibility
@aesthetic-function/watcherLocal Node.jsWatches files, extracts intent (markers or LLM), reconciles with overrides, transforms to operations, sends to server
@aesthetic-function/serverLocal Node.jsHTTP/WebSocket relay bridge, persists design changes to design-overrides.json, audit logging
@aesthetic-function/figma-pluginFigma SandboxReceives operations, executes scene graph mutations, sends selection changes back
@aesthetic-function/sharedSharedProtocol definitions, message types, version constants
@aesthetic-function/cliLocal Node.jsThin CLI control surface (af binary), delegates to watcher/server modules

Runtime Boundaries (Critical)

  • Watcher CAN access disk and LLMs
  • Server CAN access disk and network
  • Figma code.ts CANNOT access disk or network
  • Figma ui.html CAN access network but MUST NOT assume localhost is reachable

Implemented Phases

PhaseDescriptionStatus
Phase 1Protocol + plumbing (WebSocket, HTTP polling, message types)
Phase 2Marker-based + LLM intent parsing
Phase 3LLM analyzer with safety + fallback
Phase 4Audit trail (sync-log.md)
Phase 5ABidirectional sync (Design → Code capture via DESIGN_CHANGE)
Phase 5A.1Override reconciliation layer
Phase 5A.2Override precedence controls (feature flags)
Phase 5BDesign → Code materialization (patch + marker writes)
Phase 6ARead-only AST parsing with Babel
Phase 6BSemantic extraction from JSX (colors, layout)
Phase 6CAST write feasibility analysis
Phase 7AAST-based source mutation
Phase 7BMarker + style patching via AST
Phase 7CReconciliation policy + echo suppression
Phase 8AVariant/state mapping (base/disabled/hover/pressed)
Phase 8BNative Figma Variant Targeting (Component Sets)
Phase 8CComponent Mapping Registry (Stable IDs)
Phase 9AFeature Orchestrator (Prompt → Code → Figma)
Phase 9BOrchestrator "Apply → Re-emit Ops" (Immediate Figma Refresh)
Phase 9CProduction Hardening, Observability & Demo DX
Phase 9DTest Stability & CI Guardrails
Phase 10ASemantic Adapter Architecture + Vuetify Adapter
Phase 10BAnt Design Adapter (Read-Only Semantic Extraction)
Phase 10CComponent Map Bootstrap Suggestions (Read-Only)
Phase 10DComponent Map Bootstrap Artifacts (CLI + Apply Mode)
Phase 10ECanonical Token Layer + Cross-Adapter Normalization
Phase 10FCanonical Resolver + Coverage Reporting
Phase 10GResolution Policy + Project-Level Coverage
Phase 11AFigma Composition Suggestions (Read-Only)
Phase 11BFigma Composition Application (Opt-In, Auditable)
Phase 11CFigma Property Application (Opt-In, Scoped, Auditable)
Phase 12AFigma → Code Delta Detection (Read-Only)
Phase 12BFigma Delta Suggestions (Target Selection)
Phase 12CFigma Delta Application (Opt-In, Auditable)
Phase 12DConflict Surfacing & Resolution Preview (Read-Only)
Phase 12EGuided Conflict Resolution Plans (Read-Only)
Phase 12FApply Resolution Plans (Opt-In, Auditable)
Phase 12GPost-Apply Verification (CI-Safe)
Phase 12HPost-Apply Auto-Verification + CI Gate
Phase 12IRollback Preview & Safety Envelope (Read-Only)
Phase 12JReconciliation Lifecycle Status Artifact
Phase 13AReconciliation Run Index (Read-Only, Deterministic)
Phase 13BDesign Drift Timeline (Append-Only Run Ledger)
Phase 13CDrift Diffs (Run-to-Run Comparison)
Phase 13DDrift Summary Dashboard (Aggregated + CI-friendly)
Phase 13EProject Drift Dashboard (Multi-File Aggregation)
Phase 13FCI Gate Summary + Trend Window (Read-Only)
Phase 14ASingle-Entry Reconcile CLI (figma:reconcile)
Phase 14BProfile Support (local, record, ci)
Phase 14CBundle Artifact Generation + CI Verdict
Phase 14DGitHub Actions CI Workflow
Phase 14EDocumentation Canonicalization
Phase 14FMulti-Source CI (Matrix) + Deterministic Source Discovery
Phase 15AProject Configuration (af.config.json)
Phase 15BNamed Reconciliation Policy Profiles
Phase 15CUnified af CLI Control Surface
Phase 15DArtifact Inspector + Audit Trail Expansion
Phase 16ADesign Adapter Interface (verification-scoped)
Phase 16A.1Surface Classification Metadata (adapter taxonomy)
Phase 16BFigma Console MCP Adapter (read-only)
Phase 16CStorybook MCP Adapter + Cross-Surface Drift Analysis
Phase 16DCross-Surface Drift Normalization Layer

Not Implemented Yet

FeatureStatus
Conflict resolution UI
Layout/spacing operations
Background reconciliation

The reconciliation system is feature-complete through Phase 14F. Key capabilities include:

  • Core Pipeline (1–4): Marker parsing, LLM fallback, audit trail
  • Bidirectional Sync (5A/5B): Design → Code capture and materialization
  • AST Analysis & Writes (6–7): Babel-based extraction, mutation, echo suppression
  • Variant/State Mapping (8A–8C): Component states, Figma variants, stable ID registry
  • Feature Orchestrator (9A–9D): Prompt → Code → Figma pipeline with observability
  • Semantic Adapters (10A–10G): Vuetify, Ant Design, canonical tokens, coverage
  • Figma Composition (11A–11C): Read-only suggestions, controlled application
  • Delta Detection & Resolution (12A–12J): Conflicts, resolution plans, verification, rollback
  • Drift Timeline & Dashboard (13A–13F): Run ledger, drift diffs, project dashboard, CI gate
  • Unified Reconcile CLI (14A–14F): figma:reconcile entry point, profiles (local/record/ci), bundle artifacts, GitHub Actions matrix workflow, multi-source discovery
  • Configuration & Profiles (15A–15B): af.config.json, named policy profiles (designer-first, code-first, balanced, strict-review)
  • CLI & Inspector (15C–15D): Unified af CLI (control surface, not runtime), artifact listing/inspection/trace
  • Design Adapters (16A–16D): Read-only design adapter interface, Figma Console MCP adapter, Storybook MCP adapter, surface classification metadata, cross-surface drift analysis, drift normalization layer

Echo suppression prevents feedback loops when AST writes trigger file save events.


Project Configuration (Phase 15A)

Phase 15A adds a portable project configuration file (af.config.json) that replaces per-invocation environment variables while remaining fully backward compatible.

Configuration File

Create an af.config.json in your project root:

{
  "profile": "designer-first",
  "server": {
    "port": 3001,
    "url": "http://localhost:3001"
  },
  "overrides": {
    "enabled": true,
    "precedence": "always"
  },
  "canonical": {
    "colorStrategy": "token-first",
    "strict": false
  }
}

All fields are optional. Without af.config.json, the system behaves identically to Phase 14F (environment variables + built-in defaults).

Merge Precedence

Configuration values are resolved in order (lowest → highest priority):

  1. Built-in defaults — match existing Phase 14F behavior
  2. af.config.json — version-controlled project settings
  3. Environment variables — always override file values (backward compatible)

Existing env vars (USE_OVERRIDES, OVERRIDES_PRECEDENCE, CANONICAL_STRICT, etc.) continue to work and always take priority over the config file.

Named Policy Profiles (Phase 15B)

Profiles are named presets that map onto the existing resolveField() / resolveWithPolicy() engine. They do not modify resolution logic — they set parameters.

ProfileUSE_OVERRIDESOVERRIDES_PRECEDENCEColorStrategyConflict Action
designer-firsttruealwaystoken-firstapply
code-firsttrueif_newer_than_codetoken-firstapply
balancedtrueif_newer_than_codetoken-firstwarn
strict-reviewtruealwaystoken-onlyblock
  • designer-first is the default and produces identical behavior to Phase 14F
  • code-first maps to existing OVERRIDES_PRECEDENCE=if_newer_than_code behavior
  • balanced adds conflict warnings on top of code-first precedence
  • strict-review blocks all conflicts for human review with strict token policy

Set the profile in af.config.json or via RECONCILIATION_POLICY environment variable:

# Via config file
echo '{"profile": "code-first"}' > af.config.json

# Via environment variable (always overrides config file)
RECONCILIATION_POLICY=code-first pnpm dev:watcher

Artifact Inspector (Phase 15D)

Phase 15D adds read-only inspection tooling for the 12 reconciliation artifact types. All commands are additive — no new artifacts are generated, no reconciliation logic is modified.

CLI Commands:

# List all artifacts for a source file (12 types)
pnpm artifacts:list demos/react-demo-app/src/App.tsx

# Inspect a single artifact with type-aware highlights
pnpm artifacts:inspect design-materializations/demos__react-demo-app__src__App.figma-reconciliation-status.json

# End-to-end lifecycle trace across all artifacts
pnpm artifacts:trace demos/react-demo-app/src/App.tsx

All three commands support --json for machine-readable output and --repo-root for explicit repo root.

Highlight Levels:

IconLevelMeaning
okPassing / verified
warnWarning / dry-run
failFailure / mismatch
·infoInformational

Audit Log Expansion:

The server audit log (sync-log.md) now accepts optional metadata fields when present:

  • profile — active policy profile name
  • configSource — path to the config file used
  • policy — policy settings snapshot (JSON)

These fields are purely additive. Existing callers are unaffected.

Unified CLI Control Surface (Phase 15C)

Phase 15C adds the af binary — a thin CLI control surface that delegates every command to existing watcher/server modules. The CLI does NOT own the runtime, reconciliation logic, or server authority.

Installation:

The CLI lives in packages/cli/. It is currently a workspace-only tool — it must be run from within the aesthetic-function monorepo.

Runtime requirements:

  • Must run from within the monorepo (needs packages/watcher/src/ and packages/server/src/)
  • Requires tsx (installed as a workspace dev dependency)
  • Delegates to .ts source files directly via fork() with --import tsx — no pre-build step
  • Existing pnpm --filter commands continue to work unchanged

In development, run via tsx:

pnpm --filter @aesthetic-function/cli dev -- <command> [args]

Commands:

CommandDelegates ToDescription
af initstandaloneGenerate af.config.json
af runserver + watcher (child processes)Spawn both processes
af reconcile <file>reconciliationReconcile/cliReconcileFull reconciliation pipeline
af status <file>reconciliationStatus/cliStatusReconciliation status
af dashboard <file>reconciliationDashboard/cliDashboardDrift dashboard (file-level)
af dashboard --project <dir>reconciliationProjectDashboard/cliProjectDashboardDrift dashboard (project-level)
af ci [dir]reconciliationCi/cliCiCI gate summary
af artifacts list <file>artifactInspector/cliArtifactListList artifacts
af artifacts inspect <path>artifactInspector/cliArtifactInspectInspect artifact
af artifacts trace <file>artifactInspector/cliArtifactTraceLifecycle trace

Architecture:

  • af init detects project context (framework, existing artifacts) and writes af.config.json — does NOT start the system
  • af run spawns server + watcher as child processes via fork() — it is a launcher, not a runtime
  • All other commands load af.config.json via loadAfConfig(), derive env vars via envBridge.ts, then fork the target watcher CLI module with --import tsx and those env vars
  • The env var names (CANONICAL_COLOR_STRATEGY, OVERRIDES_PRECEDENCE, etc.) match exactly what the watcher/server modules read from process.env
  • Existing pnpm --filter @aesthetic-function/watcher commands continue to work unchanged

The CLI does NOT:

  • Implement reconciliation logic
  • Modify policy behavior
  • Bypass the server
  • Write overrides directly
  • Replace the watcher or server runtime

Variant/State Mapping (Phase 8A)

Phase 8A adds a state dimension to the intent pipeline, enabling synchronization of different component states (base, disabled, hover, pressed) to Figma.

Component States

StateDescriptionFigma Node Name
baseDefault state (no suffix)LoginButton
hoverMouse hover stateLoginButton::hover
disabledDisabled/inactive stateLoginButton::disabled
pressedActive/pressed stateLoginButton::pressed

Marker Syntax

Use the state= attribute to specify component state:

// @figma node=LoginButton fill=#3B82F6 text="Login"
// @figma node=LoginButton state=hover fill=#2563EB text="Login"
// @figma node=LoginButton state=disabled fill=#9CA3AF text="Login"
// @figma node=LoginButton state=pressed fill=#1E40AF text="Login"

Figma Naming Convention

Operations target nodes using a :: suffix for non-base states:

  • Base state: LoginButton (no suffix)
  • Hover state: LoginButton::hover
  • Disabled state: LoginButton::disabled
  • Pressed state: LoginButton::pressed

In your Figma document, name nodes to match this convention or use component variants.

Override Keys

Design overrides support per-state keys:

{
  "LoginButton": {
    "nodeId": "4:7",
    "lastUpdated": "2025-01-15T10:30:00.000Z",
    "fill": "#3B82F6",
    "text": "Sign In"
  },
  "LoginButton::hover": {
    "nodeId": "4:8",
    "lastUpdated": "2025-01-15T10:30:00.000Z",
    "fill": "#2563EB"
  },
  "LoginButton::disabled": {
    "nodeId": "4:9",
    "lastUpdated": "2025-01-15T10:30:00.000Z",
    "fill": "#9CA3AF"
  }
}

AST State Inference

The system can infer disabled state from JSX with high confidence:

<Button disabled={true}>Submit</Button>  // → state: 'disabled'

Note: hover and pressed states cannot be inferred from static JSX and must be explicitly declared via markers.


Native Figma Variant Targeting (Phase 8B)

Phase 8B adds first-class support for Figma Component Sets with variants. Instead of requiring designers to manually name nodes like LoginButton::hover, the plugin can now resolve variant components directly from Component Sets.

How It Works

When an operation targets LoginButton::hover:

  1. Parse variant query: Extract baseName=LoginButton, state=hover
  2. Find Component Set: Search current page for a Component Set named LoginButton
  3. Resolve variant: Find the variant with State=Hover property
  4. Execute operation: Apply changes to the resolved variant component

If no Component Set is found, it falls back to finding a node literally named LoginButton::hover.

Figma Component Set Setup

For variant targeting to work, your Figma document needs:

  1. Component Set named after your component (e.g., LoginButton)
  2. Variants with a State property containing values like:
    • Base or Default (for base state)
    • Hover (for hover state)
    • Disabled (for disabled state)
    • Pressed (for pressed state)

Example Component Set structure in Figma:

LoginButton (Component Set)
├── State=Base (variant component)
├── State=Hover (variant component)
├── State=Disabled (variant component)
└── State=Pressed (variant component)

State Mapping

The plugin maps code states to Figma property values:

Code StateFigma Property Values
baseBase, Default
hoverHover
disabledDisabled
pressedPressed

Property names checked: State, state, Variant (in that order).

Variant Info in DESIGN_CHANGE

When a designer selects a variant component and clicks "Send Selection", the plugin now detects the variant context and includes the NodeName::state format in the message:

{
  "type": "DESIGN_CHANGE",
  "payload": {
    "nodeName": "LoginButton::hover",
    "changes": [{ "changeType": "fill", "value": "#2563EB" }]
  }
}

This enables the reconciliation layer to store overrides with the correct state key.

Diagnostic: DEBUG_LIST_VARIANTS

Set DEBUG_LIST_VARIANTS = true in packages/figma-plugin/src/code.ts to log all Component Sets and their variants on plugin startup:

[Figma Plugin] Found 2 Component Set(s):
  - "LoginButton" with 4 variant(s):
      • State=Base (State=Base)
      • State=Hover (State=Hover)
      • State=Disabled (State=Disabled)
      • State=Pressed (State=Pressed)
  - "IconButton" with 2 variant(s):
      • State=Default (State=Default)
      • State=Hover (State=Hover)

Example Markers

Target specific variants with the standard ::state syntax:

// @figma node=LoginButton fill=#3B82F6 text="Login"
// @figma node=LoginButton::hover fill=#2563EB
// @figma node=LoginButton::disabled fill=#9CA3AF
// @figma node=LoginButton::pressed fill=#1E40AF

The plugin will:

  1. Look for a Component Set named LoginButton
  2. Resolve the variant with the matching state
  3. Apply the fill color to that variant

Component Mapping Registry (Phase 8C)

Phase 8C adds a deterministic mapping registry (component-map.json) that stores stable Figma node IDs for components and their variants. This enables sync targets to remain stable across renames and refactors.

Why Stable IDs?

Figma node IDs remain constant for the lifetime of a node, even if:

  • The node is renamed
  • The node is moved to a different page
  • The Component Set is reorganized

Without stable IDs, renaming a button from "LoginButton" to "SignInButton" would break all sync mappings. With stable IDs, the watcher uses the stored node ID directly.

How It Works

  1. Send Selection: When a designer selects a variant and clicks "Send Selection", the plugin sends both the DESIGN_CHANGE and a map update to the server
  2. Map Storage: The server merges the update into component-map.json at the repo root
  3. Watcher Resolution: On file save, the watcher checks component-map.json for a mapped node ID and uses id:<nodeId> format if found
  4. Plugin Resolution: The plugin resolves id: prefixed queries using figma.getNodeById() for direct lookup

Registry Format

{
  "version": 1,
  "components": {
    "LoginButton": {
      "figma": {
        "componentSetNodeId": "12:34",
        "name": "LoginButton",
        "variants": {
          "base": { "nodeId": "12:35" },
          "hover": { "nodeId": "12:36" },
          "pressed": { "nodeId": "12:37" },
          "disabled": { "nodeId": "12:38" }
        }
      }
    }
  }
}

Resolution Flow

Marker: @figma node=LoginButton::hover

Watcher: Check component-map.json

Found: LoginButton.figma.variants.hover.nodeId = "12:36"

Emit: nodeQuery = "id:12:36"

Plugin: figma.getNodeById("12:36") → direct node access

If no mapping exists, the watcher falls back to name-based resolution (LoginButton::hover).

Git Configuration

By default, component-map.json is gitignored (local state). To commit it for team sharing:

# Remove from .gitignore or add exception
echo '!component-map.json' >> .gitignore

# Commit the map
git add component-map.json
git commit -m "Add component mapping for stable Figma IDs"

Pros of committing:

  • Team members get the same stable mappings
  • Mappings survive local file deletion

Pros of ignoring:

  • Each developer has their own Figma document
  • No merge conflicts on mapping files

Environment Variable

Control component map usage:

# Disable component map resolution
USE_COMPONENT_MAP=false pnpm watcher

# Enable explicitly (default if file exists)
USE_COMPONENT_MAP=true pnpm watcher

Fallback Behavior

  • If component-map.json doesn't exist: name-based resolution only
  • If component exists but variant missing: warns and falls back to name
  • If id: query fails (node deleted): warns and tries name-based resolution
  • If multiple Component Sets share a name: fails loudly (ambiguous)

AST Analysis (Phase 6A)

Phase 6A adds read-only AST-based analysis using Babel to extract literal semantics from JSX. This enables deeper code understanding and produces structured reports for diffing against markers and design overrides.

What It Extracts

CategoryExamples
JSX Text Literals<h1>Welcome</h1> → "Welcome"
JSX Prop Literals<button disabled={true}> → disabled: true
Inline Style Literalsstyle={{ backgroundColor: "#FF0000" }} → backgroundColor: "#FF0000"

Scope: Literals only. No inference from variables, no className parsing, no evaluation.

Marker Anchoring

The AST analyzer maps @figma markers to components:

  1. Finds all // @figma node=... markers with their line numbers
  2. For each marker, finds the nearest following exported component
  3. Extracts text and fill literals from that component
  4. Produces an anchored report linking markers to actual JSX content

CLI Report

Run a diff report on any TSX file:

pnpm --filter @aesthetic-function/watcher ast:report demos/react-demo-app/src/App.tsx

Output includes:

  • Marker Summary: Nodes and their text/fill values from markers
  • AST Anchored Summary: Marker → Component → Extracted literals
  • Diff: JSX vs Marker: Mismatches between JSX code and marker declarations
  • Diff: JSX vs Overrides: Mismatches between JSX code and design-overrides.json

Example output:

AST REPORT
File: demos/react-demo-app/src/App.tsx

============================================================
MARKER SUMMARY
============================================================
  [L25] LoginButton
    text: "Login"
    fill: #883BF5

============================================================
AST ANCHORED SUMMARY
============================================================
  [L25] LoginButton
    → LoginButton (L26-43)
    text: ["Sign In"]
    fills: [#3B82F6]

============================================================
DIFF: JSX vs MARKER
============================================================
  ✗ LoginButton.text
    JSX: Sign In
    Marker: Login
  ✗ LoginButton.fill
    JSX: #3B82F6
    Marker: #883BF5

Summary: 3 markers, 4 components, 4 mismatches

Use Cases

  • Auditing: See drift between markers and actual code
  • Validation: Ensure markers match component content
  • Pre-commit checks: Detect undeclared changes before sync

AST Semantic Extraction (Phase 6B/6C)

Phase 6B adds deeper semantic extraction from JSX to understand colors, layout, and style patterns. Phase 6C adds feasibility analysis to determine which values can be safely mutated.

Semantic Categories

CategoryExtraction
Fill ColorsInline backgroundColor, color from style objects
Layoutgap, padding, margin from style objects
Token MappingReverse-map hex values to design tokens

Feasibility Analysis

The feasibility analyzer determines which AST nodes can be safely modified:

FeasibilityMeaning
LITERAL_EASYDirect string/number literal, safe to modify
PROPERTY_SPREADStyle object needs spread handling
DYNAMIC_UNSAFEExpression/variable, cannot safely modify
NOT_FOUNDTarget not found in AST

CLI Report

pnpm --filter @aesthetic-function/watcher feasibility:report demos/react-demo-app/src/App.tsx

AST Writes (Phase 7A/7B)

Phase 7A/7B implements AST-based source mutation using Babel. This enables direct modification of JSX literals and style objects while preserving formatting.

What It Can Mutate

TargetExample
Marker text// @figma node=X text="Old"text="New"
Marker fill// @figma node=X fill=#OLDfill=#NEW
Style propertiesstyle={{ backgroundColor: "#OLD" }}"#NEW"
JSX text<Button>Old</Button><Button>New</Button>

Dry-Run Mode

By default, AST writes are in dry-run mode (MATERIALIZE_DRY_RUN=true):

AST Write: Would write the following changes:
  File: demos/react-demo-app/src/App.tsx
  Changes:
    - LoginButton.text: "Login" → "Sign In"
    - LoginButton.fill: "#3B82F6" → "#FF5500"

AST Write Mode

Set MATERIALIZE_MODE=ast and MATERIALIZE_DRY_RUN=false for actual writes.


Reconciliation Policy (Phase 7C)

Phase 7C defines a unified precedence policy to prevent "source of truth fights" between markers, AST values, and design overrides.

Precedence Order

When determining the final value for a field:

1. override (design-overrides.json)  ← Highest priority
2. marker   (@figma comment)
3. ast      (JSX literal extracted by Babel)
4. code     (fallback)               ← Lowest priority

Resolution Example

LoginButton.text resolution:
  code:     "Submit" (from JSX)
  marker:   "Login"  (from @figma comment)
  override: "Sign In" (from design-overrides.json)
  → WINNER: "Sign In" (source: override)

Echo Suppression

After an AST write modifies a file, the watcher may re-trigger from the file save. Echo suppression prevents sending duplicate operations:

  1. When AST writes LoginButton.text = "Sign In", record in cache
  2. When file save triggers re-parse, check cache
  3. If same value detected within TTL (5s default), suppress operation
  4. After TTL expires, allow new operations

Configuration

VariableDefaultDescription
ECHO_GUARDtrueEnable echo suppression
ECHO_GUARD_TTL_MS5000Cache TTL in milliseconds
OVERRIDES_PRECEDENCEalwaysalways or if_newer_than_code

Override System

What is design-overrides.json?

When a designer makes changes in Figma and sends them back via the plugin's "Send Selection" feature, those changes are captured in design-overrides.json at the repository root.

{
  "LoginButton": {
    "nodeId": "4:7",
    "lastUpdated": "2025-01-15T10:30:00.000Z",
    "text": "Sign In",
    "fill": "#3B82F6"
  }
}

When is it written?

The server writes to this file when it receives a DESIGN_CHANGE message from the Figma plugin. Each entry captures:

  • nodeId: The Figma node ID
  • lastUpdated: ISO timestamp of when the change was captured
  • text: Text content (if applicable)
  • fill: Fill color (if applicable)

How does reconciliation work?

On each file save, the watcher:

  1. Extracts intents from code (markers or LLM)
  2. Loads design-overrides.json (if it exists)
  3. For each intent, checks if an override exists for that node name
  4. Applies the override values (text, fill) on top of code-derived values
  5. Sends the merged result to Figma

Override Precedence

By default, overrides always win over code values. This ensures designer intent is preserved.

You can control this behavior with environment variables (see below).

⚠️ Warning: If code edits appear "stuck" (changes not reflected in Figma), check whether an override exists for that node. Delete or edit design-overrides.json to reset.


Design → Code Materialization (Phase 5B)

Materialization converts design overrides into reviewable code artifacts. This is an optional feature that must be explicitly enabled.

Materialization Modes

ModeOutputDescription
offNoneDefault. No materialization occurs.
patchdesign-materializations/*.patch.jsonGenerates JSON patch artifacts for review
markersUpdated @figma marker linesEdits source files directly

Patch Mode

Generates a reviewable artifact without modifying source files:

{
  "file": "demos/react-demo-app/src/App.tsx",
  "generatedAt": "2025-12-14T00:00:00.000Z",
  "changes": [
    {
      "node": "LoginButton",
      "before": { "text": "Login", "fill": "Primary/Blue500" },
      "after": { "text": "Sign in", "fill": "#FF5500" },
      "source": "design-overrides.json",
      "nodeId": "4:7"
    }
  ]
}

Markers Mode

Updates existing @figma marker lines in source files:

  • Replaces text="..." and fill=... values
  • Only modifies lines that already contain // @figma node=<NodeName>
  • Does not insert new markers (unapplied overrides are logged)
  • Preserves formatting and indentation

Safety Defaults

  • Dry-run by default: MATERIALIZE_DRY_RUN=true means no files are written
  • Atomic writes: Uses temp file + rename for safe writes
  • Clear logging: Materialize: mode=patch dryRun=true changes=2 unapplied=1

Environment Variables

VariableDefaultDescription
USE_LLM_ANALYZERfalseEnable LLM-based intent parsing
LLM_ANALYZE_ALLfalseAnalyze files without @figma markers
LLM_PROVIDERopenaiLLM provider (openai or anthropic)
OPENAI_API_KEYOpenAI API key (required for openai provider)
ANTHROPIC_API_KEYAnthropic API key (required for anthropic provider)
OPENAI_MODELgpt-4oOpenAI model name
ANTHROPIC_MODELclaude-3-5-sonnet-20241022Anthropic model name
USE_OVERRIDEStrueEnable design override reconciliation
OVERRIDES_PRECEDENCEalwaysalways or if_newer_than_code
MATERIALIZE_MODEoffoff, patch, or markers
MATERIALIZE_ONdesign_changedesign_change or file_save
MATERIALIZE_DRY_RUNtrueWhen true, log changes without writing files
AST_WRITE_MODEoffoff, patch, or write — Controls AST-based code writes
AST_WRITE_DRY_RUNtrueWhen true, log what would change without writing. Set to false to actually write.
AST_WRITE_ALLOWSET_TEXT,SET_FILL,SET_LAYOUTComma-separated list of allowed write operations
POST_APPLY_EMITfalseImmediately emit to Figma after Feature Orchestrator apply
POST_APPLY_EMIT_DEBOUNCE_MS200Debounce delay for post-apply emit (ms)
ENABLE_AUDIT_LOGfalseEnable sync-log.md audit trail
ECHO_GUARDtrueEnable echo suppression after AST writes
ECHO_GUARD_TTL_MS5000Echo cache TTL in milliseconds
TRACEtrueEnable structured trace logging
TRACE_JSONfalseOutput traces as JSON (one per line)
TRACE_VERBOSEfalseInclude detailed timings and resolution info

Examples

# Disable overrides entirely (code always wins)
USE_OVERRIDES=false pnpm dev:watcher

# Only apply overrides newer than the source file
OVERRIDES_PRECEDENCE=if_newer_than_code pnpm dev:watcher

# Enable LLM mode with audit logging
USE_LLM_ANALYZER=true ENABLE_AUDIT_LOG=true OPENAI_API_KEY=sk-... pnpm dev:watcher

# Patch-only materialization (dry run - see what would change)
MATERIALIZE_MODE=patch MATERIALIZE_ON=file_save pnpm dev:watcher

# Patch-only materialization (write artifacts)
MATERIALIZE_MODE=patch MATERIALIZE_ON=file_save MATERIALIZE_DRY_RUN=false pnpm dev:watcher

# Marker edits (dry run)
MATERIALIZE_MODE=markers MATERIALIZE_ON=file_save pnpm dev:watcher

# Marker edits (write actual changes to source files)
MATERIALIZE_MODE=markers MATERIALIZE_ON=file_save MATERIALIZE_DRY_RUN=false pnpm dev:watcher

# Feature Orchestrator: Generate a patch artifact (dry-run by default)
pnpm --filter @aesthetic-function/watcher feature \
  --file demos/react-demo-app/src/App.tsx \
  --component LoginButton \
  --state hover \
  --prompt "Make the hover state button use the success green token"

# Feature Orchestrator: Preview what would be applied
AST_WRITE_MODE=write pnpm --filter @aesthetic-function/watcher feature \
  --file demos/react-demo-app/src/App.tsx \
  --prompt "Change the Card title to 'Welcome'" \
  --apply --dry-run

# Feature Orchestrator: Actually apply the patch to source code
AST_WRITE_MODE=write AST_WRITE_DRY_RUN=false pnpm --filter @aesthetic-function/watcher feature \
  --file demos/react-demo-app/src/App.tsx \
  --prompt "Change the Card title to 'Welcome'" \
  --apply

# Or use the --no-dry-run CLI flag (same as AST_WRITE_DRY_RUN=false)
AST_WRITE_MODE=write pnpm --filter @aesthetic-function/watcher feature \
  --file demos/react-demo-app/src/App.tsx \
  --prompt "Change the Card title to 'Welcome'" \
  --apply --no-dry-run

# Feature Orchestrator: Apply AND immediately push to Figma (Phase 9B)
POST_APPLY_EMIT=true AST_WRITE_MODE=write AST_WRITE_DRY_RUN=false \
  pnpm --filter @aesthetic-function/watcher feature \
  --file demos/react-demo-app/src/App.tsx \
  --component LoginButton \
  --state hover \
  --prompt "Make the hover state button use the success green token and change its label to 'Continue'" \
  --apply

Precedence Modes

ModeBehavior
alwaysOverrides always win over code values (default)
if_newer_than_codeOnly apply overrides where lastUpdated > file mtime

Fast Demo: Immediate Figma Refresh (Phase 9B)

This section shows how to use the Feature Orchestrator with immediate Figma updates.

Prerequisites

  1. Start the server (with optional tunnel for remote Figma access):

    pnpm dev:server
    
  2. Start the watcher (for normal file-change sync):

    pnpm dev:watcher
    
  3. Load the Figma plugin:

    • Open Figma Desktop
    • Run the plugin from Plugins > Development > Import plugin from manifest...
    • Point to packages/figma-plugin/manifest.json
  4. Ensure your Figma document has matching nodes:

    • Create a frame named LoginButton (or LoginButton::hover for state variants)
    • Alternatively, create a Component Set named LoginButton with State=Hover variant

Run Feature Orchestrator with POST_APPLY_EMIT

POST_APPLY_EMIT=true AST_WRITE_MODE=write AST_WRITE_DRY_RUN=false \
  pnpm --filter @aesthetic-function/watcher feature \
  --file demos/react-demo-app/src/App.tsx \
  --component LoginButton \
  --state hover \
  --prompt "Make the hover state button use the success green token and change its label to 'Continue'" \
  --apply

What Happens

  1. LLM generates changes: The orchestrator calls the LLM with the prompt and code context
  2. State-aware apply: Hover state changes go to markers/overrides (not base JSX)
  3. Post-apply emit: Immediately after apply, the system:
    • Reads the updated file from disk
    • Parses the intent model
    • Applies reconciliation and overrides
    • Transforms to FigmaOperations
    • Sends to the server
  4. Figma updates: Connected Figma plugin receives operations and updates the document

Expected Output

[Orchestrator] Processing feature request for demos/react-demo-app/src/App.tsx
[Orchestrator] Prompt: "Make the hover state button use the success green token..."
...
[Orchestrator] Successfully applied changes
[Orchestrator] Post-apply emit: enabled
[PostApplyEmit] Starting emit for demos/react-demo-app/src/App.tsx
[PostApplyEmit] Component: LoginButton, State: hover
[PostApplyEmit] Using marker parser...
[PostApplyEmit] Generated 2 operation(s)
[PostApplyEmit] ✓ Sent 2 ops (1 client(s))
[Orchestrator] Post-apply emit: ops=2 sent=true clients=1

Watcher Suppression

When POST_APPLY_EMIT is enabled:

  • The feature orchestrator records the file path after emit
  • The watcher detects the file change but suppresses the duplicate send
  • Logs show: [Watcher] Suppressed: demos/react-demo-app/src/App.tsx (recently emitted by Feature Orchestrator)

This prevents double-updates to Figma.


Observability (Phase 9C)

Phase 9C adds production-grade observability with structured logging to answer "what happened and why?" when debugging sync issues.

TraceSummary

Every pipeline run (watcher or orchestrator) generates a TraceSummary with:

  • requestId: Unique identifier for tracing
  • source: Origin (watcher, feature-emit-marker, feature-emit-llm)
  • parseMode: How intents were extracted (markers, llm, ast)
  • intentsCount: Number of intents found
  • opsCount: Number of Figma operations generated
  • resolution: Override/marker/ast precedence details
  • emit: Whether operations were sent and to how many clients

Example Trace Output

[Trace] requestId=feature-emit-1234 file=demos/react-demo-app/src/App.tsx parse=markers intents=4 ops=6
[Trace] resolution: override=2 marker=2 ast=0 code=2 map: used=true mappedOps=6
[Trace] emit: enabled=true sent=true clients=1 suppressedWatcher=true

Trace Environment Variables

VariableDefaultDescription
TRACEtrueEnable trace logging (human-readable)
TRACE_JSONfalseOutput traces as JSON (for log aggregation)
TRACE_VERBOSEfalseInclude detailed timings and resolution info

Usage

# Default human-readable traces
TRACE=true pnpm dev:watcher

# JSON output for log aggregation
TRACE_JSON=true pnpm dev:watcher

# Verbose mode with timing details
TRACE_VERBOSE=true pnpm dev:watcher

Suppression Logging

The enhanced suppression system logs decisions:

[Trace] [suppression] demos/react-demo-app/src/App.tsx: SUPPRESSED (same-ops)
[Trace] [suppression] demos/react-demo-app/src/App.tsx: not suppressed (different-ops)

The ops-hash comparison prevents suppression of genuinely different changes that happen within the TTL window.


Semantic Adapter Architecture (Phase 10A/10B/10C)

Phase 10A introduces a generic adapter system for extracting semantic intent from framework-specific UI components. Phase 10B adds the Ant Design adapter, proving the registry is framework-agnostic. Phase 10C adds read-only component map bootstrap suggestions, helping users bootstrap component-map.json for new projects. This allows the system to understand Vuetify, Ant Design, MUI, Chakra, and other UI frameworks without contaminating the core AST analysis pipeline.

Architecture Overview

┌─────────────────────────────────────────────────────────────────┐
│                     AST Analysis Pipeline                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  JSX Source ─→ Babel Parse ─→ Generic Extraction                 │
│                                     │                             │
│                                     ▼                             │
│                            ┌────────────────┐                    │
│                            │ Adapter Registry│                    │
│                            └────────────────┘                    │
│                                     │                             │
│                    ┌────────────────┼────────────────┐           │
│                    ▼                ▼                ▼           │
│              ┌──────────┐    ┌──────────┐    ┌──────────┐       │
│              │ Vuetify  │    │  Antd    │    │   MUI    │       │
│              │ Adapter  │    │ Adapter  │    │ Adapter  │       │
│              └──────────┘    └──────────┘    └──────────┘       │
│                    │                │                │           │
│                    └────────────────┼────────────────┘           │
│                                     ▼                             │
│                            Merged Semantics                       │
│                                     │                             │
│                                     ▼                             │
│                        ┌─────────────────────┐                   │
│                        │ Suggestion Generator│  (Phase 10C)      │
│                        │   (READ-ONLY)       │                   │
│                        └─────────────────────┘                   │
│                                     │                             │
│                                     ▼                             │
│                      Component Map Suggestions                    │
│                        (CLI output only)                          │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

Supported Frameworks (Phase 10A/10B)

FrameworkComponentsDetectionStatus
Vuetifyv-btn, v-card, v-text-field, v-chipTag name (v-*)
Ant DesignButton, Input, Card, TagImport source (antd)
MUI--Planned
Chakra UI--Planned

Vuetify Adapter

The Vuetify adapter extracts semantics from Vuetify's v-* components:

ComponentExtracted Semantics
v-btncolor → fills, disabled, size, variant
v-cardwidth, height, title, subtitle, elevation
v-text-fieldlabel → placeholder, disabled
v-chipcolor → fills, variant

Ant Design Adapter (Phase 10B)

The Ant Design adapter uses import-based detection - components are only recognized if imported from antd or antd/es/*. This proves the adapter system is framework-agnostic.

ComponentExtracted Semantics
Buttontype → fills (antd:primary, etc.), danger, disabled, children
Inputplaceholder, disabled
Cardtitle
Tagcolor → fills (antd:color:green, etc.), children

Key Differences from Vuetify:

  • Uses semantic hints (antd:primary) instead of hex colors
  • Detection via import source, not tag name
  • PascalCase component names (Button, not v-btn)

Component Map Suggestions (Phase 10C)

Phase 10C adds read-only suggestions for bootstrapping component-map.json. When analyzing a file, the system derives suggested entries based on AST anchors and adapter semantics.

Key Characteristics:

  • READ-ONLY: Suggestions are printed to CLI only, no files are written
  • No Figma node IDs: Code-only analysis, users must map to Figma manually
  • Explicit-only variant states: Variant suggestions (variantStatesSuggested) are derived ONLY from explicit sources:
    • @figma state=X markers in code (e.g., // @figma node="Button" state=hover)
    • design-overrides.json keys with ::state suffix (e.g., "LoginButton::disabled")
    • Never inferred from semantics (disabled boolean, hover style hints, etc.)
  • Adapter-aware naming: Uses framework metadata for better Figma name suggestions

Example CLI Output:

=== COMPONENT MAP SUGGESTIONS (READ-ONLY) ===
  NOTE: These suggestions are READ-ONLY. To use them, manually
  add entries to component-map.json or use Figma plugin "Send Selection".

  NEW (not in component-map.json):
    components/LoginButton
      → Suggested name: "Ant Design Button"
      → Variants: [hover]  (explicit: state=hover marker found)
      → Source: combined (antd)
      → Reason: Ant Design adapter: Button

    components/InfoCard
      → Suggested name: "Ant Design Card"
      → Variants: []
      → Source: combined (antd)
      → Reason: Ant Design adapter: Card

  Summary: 2 new, 0 existing, 0 skipped

Suggestion Sources:

SourceDescription
ast-anchorDerived from AST analysis only, no adapter match
combinedAST anchor + adapter semantics

Component Map Bootstrap Artifacts (Phase 10D)

Phase 10D adds a safe, review-first workflow to bootstrap component-map.json from Phase 10C suggestions. It generates deterministic, auditable artifacts that humans can review before applying.

Why It Exists

  • Safe by default: Generates review artifacts only, never auto-writes component-map.json
  • Deterministic: Same inputs produce identical outputs (testable, auditable)
  • Explicit-only: Respects Phase 10C rule — variant states from markers/overrides only
  • Never overwrites node IDs: Existing Figma mappings are always preserved

How to Run

pnpm --filter @aesthetic-function/watcher map:bootstrap demos/react-demo-app/src/App.tsx

Output:

  1. Writes artifact to: design-materializations/<file>.component-map-bootstrap.json
  2. Prints terminal summary with counts and manual fields

Artifact Format

{
  "version": 1,
  "generatedAt": "2025-12-20T12:00:00.000Z",
  "file": "demos/react-demo-app/src/App.tsx",
  "policy": {
    "variantStates": "explicit-only",
    "writes": "artifact-only"
  },
  "proposed": [
    {
      "componentKey": "auth/LoginButton",
      "figmaNameSuggestion": "Login Button",
      "variantStatesSuggested": ["hover"],
      "status": "new",
      "diff": {
        "before": null,
        "after": {
          "version": 2,
          "components": {
            "auth/LoginButton": {
              "figma": {
                "name": "Login Button",
                "componentSetNodeId": null,
                "variants": {
                  "base": { "nodeId": null },
                  "hover": { "nodeId": null }
                }
              }
            }
          }
        }
      },
      "manualFields": [
        "figma.componentSetNodeId",
        "figma.variants.base.nodeId",
        "figma.variants.hover.nodeId"
      ],
      "reason": "AST anchor: LoginButton + Ant Design adapter"
    }
  ],
  "skipped": []
}

How to Use the Artifact

  1. Review proposed entries in the artifact JSON
  2. Copy entries you want into component-map.json
  3. Fill in node IDs manually using Figma plugin "Send Selection"
  4. The manualFields array tells you exactly what needs filling

Enable Apply Mode (Advanced)

To let the CLI merge entries into component-map.json:

# Dry run (shows what would change, doesn't modify)
MAP_BOOTSTRAP_MODE=apply pnpm --filter @aesthetic-function/watcher map:bootstrap demos/react-demo-app/src/App.tsx

# Actually apply (creates scaffolding, never fills node IDs)
MAP_BOOTSTRAP_MODE=apply MAP_BOOTSTRAP_DRY_RUN=false pnpm --filter @aesthetic-function/watcher map:bootstrap demos/react-demo-app/src/App.tsx

Environment Variables:

VariableValuesDefaultDescription
MAP_BOOTSTRAP_MODEartifact | applyartifactWhat to write
MAP_BOOTSTRAP_DRY_RUNtrue | falsetruePrevent actual writes

What's Never Auto-Filled

Even in apply mode, these fields are never auto-filled:

  • figma.componentSetNodeId — Requires Figma plugin
  • figma.variants.*.nodeId — Requires Figma plugin
  • Existing node IDs are never overwritten

Merge Rules

ScenarioBehavior
New componentAdd entry with null nodeIds
Existing component, same configSkip (already present)
Existing component, new variantsAdd variant keys only
Existing component, different nameMark as "manual decision required"
Existing nodeIdsNever overwritten

Canonical Token Layer (Phase 10E)

Phase 10E introduces a design-system-agnostic semantic token layer that normalizes adapter-specific values (Vuetify, Ant Design) and generic JSX values into portable canonical tokens.

Why It Exists

  • Cross-adapter portability: Same canonical tokens regardless of UI library
  • Figma mapping consistency: Canonical tokens → consistent Figma component mappings
  • Future MCP negotiation: Standard vocabulary for AI-driven design token exchange
  • Observability: Clear audit trail showing raw values → canonical tokens

Canonical Token Vocabulary

The canonical layer uses a hierarchical token naming convention:

color.primary      ← Vuetify "primary", AntD type="primary", hex #3B82F6
color.danger       ← Vuetify "error", AntD danger={true}
color.success      ← Vuetify "success", hex #10B981
color.warning      ← Vuetify "warning", hex #F59E0B
color.neutral.100  ← AntD type="default"

space.none         ← 0px
space.xs           ← 1-4px
space.sm           ← 5-8px
space.md           ← 9-16px
space.lg           ← 17-24px
space.xl           ← 25-32px
space.2xl          ← 33-48px
space.3xl          ← 49px+

How Normalization Works

┌──────────────────────┐     ┌─────────────────────┐     ┌──────────────────┐
│  Adapter Semantics   │────▶│  Canonical Layer    │────▶│ Normalized Output│
│  (antd:primary)      │     │  (normalize.ts)     │     │ (color.primary)  │
└──────────────────────┘     └─────────────────────┘     └──────────────────┘
         ▲                            │
         │                            │ Hint Mappers
┌──────────────────────┐              │ (per-adapter)
│  Generic JSX         │              ▼
│  (#3B82F6)           │────▶ ┌───────────────────┐
└──────────────────────┘      │ Design Token Match│
                              │ (designTokens.ts) │
                              └───────────────────┘
  1. Adapter hints → Look up in adapter-specific hint mapper
  2. Hex colors → Check design tokens for known values
  3. Unknown values → Preserved as raw + note generated

CLI Output

The ast:report command now shows canonical semantics:

============================================================
CANONICAL SEMANTICS (Phase 10E)
============================================================
  LoginButton:
    fill: color.primary (confidence=high, source=vuetify)
      raw: #1976D2
  DisabledButton:
    fill: color.danger (confidence=high, source=vuetify)
      raw: #FF5252
  RegularButton:
    fill: color.primary (confidence=high, source=generic-jsx)
      raw: #3B82F6

  Summary: 7 canonical fields, 0 raw fields, 0 notes

Normalization Notes

When values can't be mapped to canonical tokens, notes explain why:

Note TypeMeaning
unmapped_color_hexHex color not in design tokens
unmapped_adapter_hintAdapter hint has no canonical mapping
ambiguous_mappingMultiple possible canonical tokens
raw_value_preservedValue kept as-is (informational)

Extensibility: Custom Hint Mappers

Future adapters can register custom hint mappings:

import { registerCanonicalHintMapper } from './tokens/canonical/index.js';

// Register MUI hint mapper
registerCanonicalHintMapper('mui', (hint: string) => {
  if (hint === 'mui:primary') return 'color.primary';
  if (hint === 'mui:error') return 'color.danger';
  return null;
});

Supported Mappings

Vuetify → Canonical:

VuetifyCanonical
primarycolor.primary
secondarycolor.secondary
successcolor.success
errorcolor.danger
warningcolor.warning
infocolor.info
red, blue, etc.color.red, color.blue, etc.

Ant Design → Canonical:

AntDCanonical
antd:primarycolor.primary
antd:dangercolor.danger
antd:defaultcolor.neutral.100
antd:linkcolor.primary

Design Token Hex → Canonical:

HexToken NameCanonical
#3B82F6Primary/Blue500color.primary
#10B981Success/Green500color.success
#EF4444Error/Red500color.danger
#F59E0BWarning/Yellow500color.warning

Scope & Constraints

Phase 10E is read-only and does not:

  • Modify JSX/TSX source files
  • Write markers or overrides
  • Emit Figma operations
  • Change watcher sync behavior
  • Affect server/plugin/protocol

Canonical Resolver + Coverage Report (Phase 10F)

Phase 10F builds on the canonical layer (10E) by resolving canonical tokens to concrete design system values (hex colors, pixel measurements) and producing a deterministic coverage report.

Why It Exists

  • Bridge canonical → concrete: Convert abstract tokens to actual values for Figma operations
  • Coverage visibility: See exactly which canonical tokens resolve and which have gaps
  • Adapter-agnostic: Same resolution logic for Vuetify, AntD, and generic JSX
  • Deterministic: Same canonical input always produces same resolution output

How Resolution Works

┌──────────────────────┐     ┌─────────────────────┐     ┌──────────────────┐
│  Canonical Semantics │────▶│  Canonical Resolver │────▶│  Resolved Values │
│  (color.primary)     │     │  (resolve.ts)       │     │  (#3B82F6)       │
└──────────────────────┘     └─────────────────────┘     └──────────────────┘


                              ┌──────────────────┐
                              │ Coverage Report  │
                              │ (totals, gaps)   │
                              └──────────────────┘
  1. Color resolution: Canonical token → design token name → hex value
  2. Spacing resolution: Canonical token → pixel value (8-point grid)
  3. Radius resolution: Canonical token → pixel value
  4. Typography resolution: Canonical token → fontSize/fontWeight values

CLI Output

The ast:report command now includes resolution and coverage:

============================================================
CANONICAL RESOLUTION (Phase 10F)
============================================================
  LoginButton:
    fill: color.primary → #3B82F6
  DisabledButton:
    fill: color.danger → #EF4444
  CardComponent:
    padding: space.lg → 24px
    borderRadius: radius.md → 8px

  Coverage: 4/4 resolved (100%)

When tokens cannot be resolved, notes explain why:

  UnknownComponent:
    fill: color.custom → (unresolved)
      ⚠ Canonical token "color.custom" not mapped to design system
    gap: space.weird → (unresolved)
      ⚠ Spacing token "space.weird" not found in scale

  Coverage: 0/2 resolved (0%)

Resolution Scales

Spacing Scale (8-point grid):

CanonicalPixels
space.none0
space.xs4
space.sm8
space.md16
space.lg24
space.xl32
space.2xl48
space.3xl64

Radius Scale:

CanonicalPixels
radius.none0
radius.sm4
radius.md8
radius.lg16
radius.full9999

Typography Scale:

CanonicalValue
text.size.xs12px
text.size.sm14px
text.size.md16px
text.size.lg18px
text.size.xl20px
text.size.2xl24px
text.weight.light300
text.weight.normal400
text.weight.medium500
text.weight.semibold600
text.weight.bold700

Coverage Report Structure

interface CoverageReport {
  totals: {
    canonicalFields: number;  // Total fields analyzed
    resolved: number;         // Successfully resolved
    unresolved: number;       // Gaps (not resolved)
  };
  byCategory: {
    colors: CategoryCoverage;
    spacing: CategoryCoverage;
    radius: CategoryCoverage;
    typography: CategoryCoverage;
  };
  gaps: CoverageGap[];  // List of unresolved tokens with notes
}

Scope & Constraints

Phase 10F is read-only and does not:

  • Modify JSX/TSX source files
  • Write markers or overrides
  • Emit Figma operations
  • Change watcher sync behavior
  • Affect server/plugin/protocol

Resolution Policy + Project Coverage (Phase 10G)

Phase 10G adds policy controls and project-level coverage reporting for canonical resolution.

Why It Exists

  • Policy flexibility: Different teams have different strictness requirements
  • CI integration: Optional strict mode fails builds on policy violations
  • Project visibility: Aggregate coverage across all source files
  • Actionable gaps: See which canonical tokens need attention

CLI Command

# Scan a directory for TSX files
pnpm --filter @aesthetic-function/watcher canonical:coverage src

# JSON output for automation
pnpm --filter @aesthetic-function/watcher canonical:coverage src --json

# Strict mode (fails CI if violations exist)
CANONICAL_STRICT=true pnpm --filter @aesthetic-function/watcher canonical:coverage src

Environment Variables

VariableValuesDefaultDescription
CANONICAL_STRICTtrue, falsefalseEnable strict mode (violations fail CI)
CANONICAL_COLOR_STRATEGYtoken-first, hex-allowed, token-onlytoken-firstColor resolution policy
CANONICAL_SPACING_SCALE8pt, token-only, custom8ptSpacing scale policy
CANONICAL_RADIUS_SCALEdefault, token-only, customdefaultRadius scale policy
CANONICAL_TYPOGRAPHY_SCALEdefault, token-only, customdefaultTypography scale policy

Policy Strategies

Color Strategies:

StrategyBehavior
token-firstPrefer tokens, allow raw hex passthrough (default)
hex-allowedSame as token-first, no violations for hex
token-onlyRaw hex values are policy violations

Other Strategies:

StrategyBehavior
8pt / defaultUse default scales (default)
token-onlyOnly canonical tokens allowed, raw values are violations
customUse custom scale from config (future)

CLI Output

┌─────────────────────────────────────────────────────────────────┐
│           PROJECT CANONICAL COVERAGE (Phase 10G)                │
├─────────────────────────────────────────────────────────────────┤
│ Files scanned:    3                                             │
│ Components:       47                                            │
│ Canonical fields: 11                                            │
│ Resolved:         10                                            │
│ Unresolved:       1                                             │
│ Coverage:         91%                                           │
├─────────────────────────────────────────────────────────────────┤
│ Policy:           color=token-first, spacing=8pt, ...           │
├─────────────────────────────────────────────────────────────────┤
│ Top Gaps:                                                       │
│   color.info (colors): 1 file(s)                                │
├─────────────────────────────────────────────────────────────────┤
│ ℹ️  CI: strict mode disabled (violations are informational)     │
└─────────────────────────────────────────────────────────────────┘

CI Gate (Optional)

To enable strict mode in CI, add to your CI config:

# Example GitHub Actions
- name: Check canonical coverage
  run: |
    CANONICAL_STRICT=true pnpm --filter @aesthetic-function/watcher canonical:coverage src

This will fail the build if any policy violations exist.

Scope & Constraints

Phase 10G is read-only and does not:

  • Modify JSX/TSX source files
  • Write markers or overrides
  • Emit Figma operations
  • Change watcher sync behavior
  • Affect server/plugin/protocol

Figma Composition Suggestions (Phase 11A)

Phase 11A adds read-only Figma composition guidance that translates canonical design semantics into actionable suggestions for Figma.

This phase answers: "Given what the code semantically expresses, what should exist in Figma?"

It does NOT create anything—only describes it deterministically.

Suggestion Types

TypeDescriptionExample
component-setSuggest creating a new Component Set"Create LoginButton for componentKey auth/LoginButton"
variantSuggest adding a variant (explicit-only)"Component Set LoginButton should include variant hover"
propertySuggest Figma property mapping"Component uses color.primary → map to Fill style"
token-usageSuggest using design token"Use token color.primary instead of hard-coded #3B82F6"
coverage-gapHighlight missing coverage"No spacing token resolved for space.xl"

Suggestion Rules

Component Set Suggestions

  • Component has canonical semantics
  • Component NOT in component-map.json
  • → Suggest creating a Component Set

Variant Suggestions (Explicit Only)

  • ONLY from @figma state=X markers
  • ONLY from design-overrides.json ::state keys
  • NEVER inferred from disabled booleans or hover styles

Property Suggestions

  • From canonical semantics:
    • color → Fill property
    • spacing → Auto Layout Gap/Padding
    • radius → Corner Radius
    • typography → Text Style

Token Usage Suggestions

  • When canonical tokens resolve cleanly with high confidence
  • Suggests using token instead of hard-coded values

Coverage Gap Suggestions

  • From Phase 10F/10G gaps
  • From policy violations

CLI Output

The ast:report command now includes Figma composition suggestions:

============================================================
FIGMA COMPOSITION SUGGESTIONS (Phase 11A)
============================================================
  [NEW COMPONENT SET]
    - Login Button (LoginButton)

  [VARIANTS]
    - LoginButton: [hover, disabled]

  [TOKEN USAGE]
    - Fill → color.primary
    - Auto Layout → space.md

  [PROPERTIES]
    - [LoginButton] Fill: color.primary
    - [Container] Auto Layout Gap: space.lg

  [COVERAGE GAPS]
    - Missing typography token for text.size.3xl

  Summary: 8 suggestions
    By type: 1 component-sets, 2 variants, 2 properties, 2 token-usage, 1 coverage-gaps
    By source: 7 canonical, 0 adapter, 1 policy, 0 coverage

Suggestion Sources

SourceDescription
canonicalFrom canonical semantic normalization (Phase 10E)
adapterFrom framework adapter semantics (Phase 10A)
policyFrom resolution policy analysis (Phase 10G)
coverageFrom coverage gap detection (Phase 10F)

Scope & Constraints

Phase 11A is read-only and does NOT:

  • Modify TSX/JSX source files
  • Write markers or overrides
  • Modify component-map.json
  • Emit Figma operations
  • Call materializers
  • Change orchestrator behavior
  • Change canonical normalization or resolution logic

Figma Composition Application (Phase 11B)

Phase 11B transforms Phase 11A's read-only suggestions into actionable compose operations that can be applied to Figma with explicit opt-in and full audit logging.

Overview

Building on Phase 11A suggestions, this phase provides:

  1. Compose Operations - Deterministic, auditable operations for Figma modifications
  2. Opt-In Apply Mode - Dry-run by default, apply only with explicit flags
  3. Feature Flags - Fine-grained control over what can be composed
  4. Audit Trail - Full logging of all compose operations to sync-log.md
  5. CLI Integration - figma:compose command for automation

Compose Operation Types

TypeDescriptionPayload
ENSURE_COMPONENT_SETCreate missing Component Set{ name }
ENSURE_VARIANTAdd variant to Component Set{ setName, variantProps }
ENSURE_PROPERTY_DEFDefine property on Component Set{ setName, propertyName, propertyType, defaultValue? }

Feature Flags

FlagValuesDefaultDescription
FIGMA_COMPOSE_ONtrue/falsefalseMaster enable switch
FIGMA_COMPOSE_MODEoff/dry-run/applyoffExecution mode
FIGMA_COMPOSE_ALLOWcomma-separated(all allowed)Operation type allow-list
FIGMA_COMPOSE_SERVERURLhttp://localhost:3001Server endpoint

Allow-list values: component-set, variant, property

CLI Usage

# Generate compose artifact (dry-run, no Figma changes)
pnpm --filter @aesthetic-function/watcher figma:compose demos/react-demo-app/src/App.tsx

# Apply operations to Figma (requires FIGMA_COMPOSE_ON=true, server + plugin running)
FIGMA_COMPOSE_ON=true FIGMA_COMPOSE_MODE=apply \
  pnpm --filter @aesthetic-function/watcher figma:compose demos/react-demo-app/src/App.tsx --apply

# Restrict to only component-set operations
FIGMA_COMPOSE_ALLOW=component-set \
  pnpm --filter @aesthetic-function/watcher figma:compose demos/react-demo-app/src/App.tsx --apply

Compose Artifact

Artifacts are written to design-materializations/ with naming:

<file-path>.compose.json

Example artifact structure:

{
  "sourceFile": "demos/react-demo-app/src/App.tsx",
  "generatedAt": "2025-01-15T10:30:00.000Z",
  "mode": "dry-run",
  "operations": [
    {
      "opId": "abc123def",
      "type": "ENSURE_COMPONENT_SET",
      "componentKey": "PrimaryButton",
      "figmaName": "PrimaryButton",
      "payload": { "name": "PrimaryButton" },
      "reason": "Component in code missing from component-map",
      "source": "figma-suggestions"
    }
  ],
  "summary": {
    "total": 1,
    "byType": { "ENSURE_COMPONENT_SET": 1 }
  }
}

Deterministic Operation IDs

Each operation receives a deterministic opId generated via djb2 hash of:

  • Operation type
  • Component key
  • Figma name
  • Stringified payload

This ensures:

  • Idempotency - Same inputs produce same opId
  • Deduplication - Operations can be compared across runs
  • Traceability - Operations can be tracked through the system

Server Endpoint

The /compose endpoint accepts compose artifacts:

curl -X POST http://localhost:3001/compose \
  -H "Content-Type: application/json" \
  -d @design-materializations/demos__react-demo-app__src__App.compose.json

The endpoint:

  1. Validates the artifact structure
  2. Logs the request to sync-log.md via logCompose()
  3. Broadcasts COMPOSE_OPERATIONS to connected Figma plugins
  4. Returns the number of operations dispatched

Plugin Behavior

When the Figma plugin receives COMPOSE_OPERATIONS:

  1. ENSURE_COMPONENT_SET - Searches for existing Component Set by name, creates if missing using combineAsVariants()
  2. ENSURE_VARIANT - Finds target Component Set, checks for existing variant by props, adds if missing
  3. ENSURE_PROPERTY_DEF - Adds component property definition if not present

The plugin returns COMPOSE_RESULT with per-operation success/failure status.

Audit Logging

All compose operations are logged to sync-log.md:

| 2025-01-15T10:30:00.000Z | compose | demos/react-demo-app/src/App.tsx | dry-run | 3 ops | ENSURE_COMPONENT_SET,ENSURE_VARIANT |

Fields logged:

  • Timestamp
  • Event type (compose)
  • Source file
  • Mode (dry-run or apply)
  • Operation count
  • Operation types

Safety Guarantees

  1. Dry-run by default - No Figma changes without explicit --apply flag
  2. Master switch - FIGMA_COMPOSE_ON must be true for server dispatch
  3. Allow-list - FIGMA_COMPOSE_ALLOW restricts operation types
  4. Idempotent - Operations check for existing elements before creating
  5. Auditable - Full trail in sync-log.md and compose artifacts

Figma Property Application (Phase 11C)

Phase 11C applies resolved canonical semantics (from Phase 10F/10G) to existing Figma structures. Unlike Phase 11B which creates new Component Sets and variants, this phase only applies properties to nodes that already exist.

Overview

Building on the canonical resolution pipeline:

  1. Property Application - Apply colors, spacing, and typography to existing nodes
  2. Explicit Targeting - Only nodes with stable IDs in component-map.json
  3. Category Allow-List - Fine-grained control over property types
  4. Opt-In Apply Mode - Artifact-only by default
  5. Audit Trail - Full logging of all apply operations
  6. Variant-Aware Targeting - Ops target variant nodeIds, never Component Sets (Phase 11C.1)
  7. Text-Descendant Resolution - Text props auto-resolve to TEXT node descendants (Phase 11C.1)

Variant-Aware Targeting (Phase 11C.1)

Apply operations target the correct variant nodeId rather than the Component Set:

  • Component Sets are containers and should never receive visual properties directly
  • When ComponentKey::state is specified (e.g., LoginButton::hover), the variant nodeId for that state is used
  • getVariantNodeId(componentMap, componentKey, state?) returns { nodeId, state, fromVariant }
  • If no variant exists for the requested state, a missing-variant-id violation is created (no fallback to Component Set)
  • State name fallback: basedefault for compatibility

Text-Descendant Resolution (Phase 11C.1)

Text properties (fontSize, fontWeight, textColor) require a TEXT node, but apply ops often target containers:

  • Plugin auto-resolution: findTextDescendant(node) finds the first TEXT node within a container (depth-first)
  • If the target node is not a TEXT node, the plugin searches for a TEXT descendant
  • Console logs show which TEXT node was actually modified: Applied fontSize to TEXT node 23:45 (target was 23:27)
  • Falls back gracefully if no TEXT node is found

Property Types

PropertyCategoryFigma Operation
fillfillSet background/foreground color
textColorfillSet text fill color
paddingspacingSet Auto Layout padding
gapspacingSet Auto Layout item spacing
widthspacingSet node width (if already defined)
heightspacingSet node height (if already defined)
fontSizetypographySet text font size
fontWeighttypographySet text font weight

Feature Flags

FlagValuesDefaultDescription
FIGMA_APPLY_ONtrue/falsefalseMaster enable switch
FIGMA_APPLY_MODEartifact/applyartifactExecution mode
FIGMA_APPLY_DRY_RUNtrue/falsetrueDry-run mode (apply only)
FIGMA_APPLY_ALLOWcomma-separated(none)Property category allow-list
FIGMA_APPLY_SERVERURLhttp://localhost:3001Server endpoint
FIGMA_APPLY_MIN_CONFIDENCElow/medium/highhighMinimum confidence threshold

Allow-list values: fill, spacing, typography

CLI Usage

# Generate apply artifact (artifact-only, no Figma changes)
pnpm --filter @aesthetic-function/watcher figma:apply demos/react-demo-app/src/App.tsx

# Apply properties to Figma (requires full opt-in)
FIGMA_APPLY_ON=true FIGMA_APPLY_MODE=apply FIGMA_APPLY_DRY_RUN=false \
  FIGMA_APPLY_ALLOW=fill,spacing \
  pnpm --filter @aesthetic-function/watcher figma:apply demos/react-demo-app/src/App.tsx --apply

# Verbose output
pnpm --filter @aesthetic-function/watcher figma:apply demos/react-demo-app/src/App.tsx --verbose

Apply Artifact

Artifacts are written to design-materializations/ with naming:

<file-path>.figma-apply.json

Example artifact structure:

{
  "version": "1.0",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "sourceFile": "demos/react-demo-app/src/App.tsx",
  "mode": "artifact",
  "dryRun": true,
  "operations": [
    {
      "opId": "apply-abc123",
      "nodeId": "CS:btn-123",
      "componentKey": "Button",
      "property": "fill",
      "to": "#3498db",
      "canonicalSource": "color.primary.500",
      "confidence": "high",
      "source": "canonical-resolution",
      "reason": "Apply resolved color color.primary.500"
    }
  ],
  "violations": [
    {
      "type": "property-not-allowed",
      "componentKey": "Card",
      "property": "padding",
      "message": "Property category 'spacing' not in FIGMA_APPLY_ALLOW"
    }
  ],
  "summary": {
    "totalOperations": 1,
    "byProperty": { "fill": 1 },
    "totalViolations": 1,
    "byViolationType": { "property-not-allowed": 1 }
  }
}

Policy Violations

Operations may be rejected for several reasons:

Violation TypeDescription
missing-node-idNode not found in component-map.json
property-not-allowedProperty category not in FIGMA_APPLY_ALLOW
no-canonical-sourceNo canonical token for this property
low-confidenceConfidence below FIGMA_APPLY_MIN_CONFIDENCE
value-unchangedCurrent value matches target (idempotency)

Server Endpoint

The /apply-properties endpoint accepts apply operations:

curl -X POST http://localhost:3001/apply-properties \
  -H "Content-Type: application/json" \
  -d '{"operations": [...], "mode": "apply"}'

The endpoint:

  1. Validates the request
  2. Logs to sync-log.md via logApplyProperties()
  3. In dry-run mode: returns operations without sending to plugin
  4. In apply mode: broadcasts APPLY_PROPERTIES to connected plugins
  5. Returns operation results

Plugin Behavior

When the Figma plugin receives APPLY_PROPERTIES:

  1. Locate Node - Uses figma.getNodeById() with the nodeId
  2. Validate Node Type - Ensures node supports the property
  3. Apply Property - Sets the appropriate property value:
    • fill: Sets fills array with solid color
    • textColor: Sets fills on TextNode
    • padding/gap: Sets Auto Layout properties
    • fontSize/fontWeight: Sets text style properties
  4. Return Result - Reports success/failure per operation

The plugin returns APPLY_PROPERTIES_RESULT with per-operation status.

Audit Logging

Apply operations are logged to sync-log.md:

| 2025-01-15T10:30:00.000Z | apply-properties | demos/react-demo-app/src/App.tsx | apply | 3 ops | fill(2),fontSize(1) |

Safety Guarantees

  1. Artifact-only by default - Mode defaults to 'artifact'
  2. Dry-run by default - FIGMA_APPLY_DRY_RUN defaults to true
  3. Master switch - FIGMA_APPLY_ON must be true for server dispatch
  4. Category allow-list - FIGMA_APPLY_ALLOW restricts property types
  5. Explicit targeting only - Only nodes in component-map.json
  6. Confidence threshold - FIGMA_APPLY_MIN_CONFIDENCE filters low-confidence
  7. Idempotent - Repeated runs produce same opIds, no duplicate changes
  8. Auditable - Full trail in sync-log.md and apply artifacts

Differences from Phase 11B

AspectPhase 11B (Compose)Phase 11C (Apply)
PurposeCreate new structureApply properties to existing
TargetMissing Component Sets/VariantsExisting nodes with stable IDs
OperationsENSURE_*Property assignments
Allow-listOperation typesProperty categories
Creates nodesYesNo

Guided Conflict Resolution (Phase 12D/12E)

Phase 12D/12E introduces a guided resolution layer that surfaces conflicts between Figma and code, then proposes explicit, auditable resolution plans.

Key Principles

  • No automatic resolution - Plans are proposals, not executions
  • No silent writes - All changes require explicit approval
  • No inference beyond existing policy - Uses same rules as Phase 12B/12C
  • Everything explicit, reviewable, and reversible

Conflict Types (Phase 12D)

TypeDescription
AST_VS_FIGMAAST literal differs from Figma value
MARKER_VS_FIGMAMarker attribute differs from Figma value
OVERRIDE_VS_FIGMAOverride entry differs from Figma value
CANONICAL_MISMATCHRaw values match but canonical tokens differ
UNMAPPED_VARIANTVariant nodeId not found in component map
NON_BASE_STATE_BLOCKEDNon-base state without explicit data
LOW_CONFIDENCE_BLOCKEDDelta confidence too low for auto-apply

Resolution Actions (Phase 12E)

ActionDescription
APPLY_TO_ASTWrite value directly to AST literal
APPLY_TO_MARKERUpdate @figma marker line
APPLY_TO_OVERRIDEWrite to design-overrides.json
IGNORESkip this conflict (user must decide)
BLOCKCannot resolve automatically

Default Resolution Rules

Conflict ScenarioDefault Resolution
AST auto-writable base stateAPPLY_TO_AST
Non-base state with explicit markerAPPLY_TO_MARKER
Non-base state with existing overrideAPPLY_TO_OVERRIDE
Non-base state, no explicit dataBLOCK
Unsafe AST writeAPPLY_TO_OVERRIDE
Canonical mismatchIGNORE
Low confidenceBLOCK

CLI Commands

# Generate resolution plan for a file
pnpm --filter @aesthetic-function/watcher figma:resolve demos/react-demo-app/src/App.tsx

# Full report including conflict preview (in ast:report)
pnpm --filter @aesthetic-function/watcher ast:report demos/react-demo-app/src/App.tsx

CLI Output

=== CONFLICT RESOLUTION PLAN (Phase 12E) ===

  Component: LoginButton
  State: hover

    Property: fill
    Conflict: LoginButton::hover::fill
    Suggested Resolution: APPLY_TO_MARKER
    Reason: Non-base state (hover) with explicit marker present

  Summary:
    - Apply to AST: 1
    - Apply to Marker: 2
    - Apply to Override: 0
    - Ignored: 1
    - Blocked: 1

  Resolution plan written to:
    design-materializations/demos__react-demo-app__src__App.figma-resolution-plan.json

Artifact Files

File PatternPhaseContent
*.figma-conflicts.json12DConflict analysis with evidence
*.figma-resolution-plan.json12EResolution decisions with reasons

Example Resolution Plan Artifact

{
  "version": "1.0",
  "source": "figma-resolution-plan",
  "generatedAt": "2025-01-01T00:00:00.000Z",
  "sourceFile": "demos/react-demo-app/src/App.tsx",
  "summary": {
    "applyAst": 1,
    "applyMarker": 2,
    "applyOverride": 0,
    "ignored": 0,
    "blocked": 1
  },
  "decisions": [
    {
      "componentKey": "LoginButton",
      "targetState": "base",
      "property": "fill",
      "action": "APPLY_TO_AST",
      "reason": "Base state with auto-writable fill literal in AST",
      "sourceConflictId": "LoginButton::base::fill"
    }
  ]
}

What Phase 12E Does NOT Do

  • Does NOT modify TSX/JSX - Resolution plans are proposals only
  • Does NOT modify markers - No automatic marker updates
  • Does NOT modify design-overrides.json - No override writes
  • Does NOT emit Figma operations - Read-only analysis
  • Does NOT apply changes - Phase 12F is the executor

Apply Resolution Plans (Phase 12F)

Phase 12F provides the controlled execution layer that takes Phase 12E resolution plans and applies them to the correct targets.

Key Principles

  • Artifact-only by default - No mutations without explicit opt-in
  • Multi-flag gate - Requires all three flags: ON + MODE=apply + DRY_RUN=false
  • Allow-list enforcement - Only enabled targets can be written
  • Non-base state AST blocking - Hover/pressed/disabled states never get AST writes
  • Deterministic execution - Same plan → same operations → same artifacts
  • Full traceability - Decision IDs link artifacts to source conflicts

Environment Variables

VariableDefaultValuesDescription
FIGMA_RESOLVE_APPLY_ONfalsetrue/falseMaster switch
FIGMA_RESOLVE_APPLY_MODEartifactartifact/applyArtifact-only or apply
FIGMA_RESOLVE_APPLY_DRY_RUNtruetrue/falseDry-run mode
FIGMA_RESOLVE_APPLY_ALLOWast,marker,overrideCSV listAllowed targets
FIGMA_RESOLVE_APPLY_MIN_CONFIDENCEhighlow/medium/highMin confidence
FIGMA_RESOLVE_PLAN_PATH(auto)File pathCustom plan path

CLI Commands

# Preview what would be applied (artifact-only, default)
pnpm --filter @aesthetic-function/watcher figma:resolve-apply demos/react-demo-app/src/App.tsx

# Apply with explicit flags (still respects env var gates)
pnpm --filter @aesthetic-function/watcher figma:resolve-apply demos/react-demo-app/src/App.tsx --apply

# Full apply (all flags enabled)
FIGMA_RESOLVE_APPLY_ON=true FIGMA_RESOLVE_APPLY_MODE=apply FIGMA_RESOLVE_APPLY_DRY_RUN=false \
  pnpm --filter @aesthetic-function/watcher figma:resolve-apply demos/react-demo-app/src/App.tsx --apply

# Apply from custom plan artifact
pnpm --filter @aesthetic-function/watcher figma:resolve-apply demos/react-demo-app/src/App.tsx \
  --from .aesthetic-function/artifacts/my-custom-plan.json

# Filter by component or state
pnpm --filter @aesthetic-function/watcher figma:resolve-apply demos/react-demo-app/src/App.tsx \
  --component LoginButton --state base

Apply Result Statuses

StatusDescription
appliedSuccessfully written to target
noopTarget already matches intended value
skippedSkipped due to IGNORE/BLOCK or filter
blockedPolicy/precondition prevented apply
failedAttempted but errored

Professional Runbook

Complete workflow for resolving conflicts:

# 1. Generate conflict report and resolution plan
pnpm --filter @aesthetic-function/watcher figma:resolve demos/react-demo-app/src/App.tsx

# 2. Review the generated plan artifact
cat .aesthetic-function/artifacts/demos/react-demo-app/App.figma-resolution-plan.json

# 3. Preview what would be applied (artifact-only)
pnpm --filter @aesthetic-function/watcher figma:resolve-apply demos/react-demo-app/src/App.tsx

# 4. Apply with full flags (when ready)
FIGMA_RESOLVE_APPLY_ON=true \
FIGMA_RESOLVE_APPLY_MODE=apply \
FIGMA_RESOLVE_APPLY_DRY_RUN=false \
FIGMA_RESOLVE_APPLY_ALLOW=override,marker \
  pnpm --filter @aesthetic-function/watcher figma:resolve-apply demos/react-demo-app/src/App.tsx --apply

# 5. Verify applied changes
git diff demos/react-demo-app/src/App.tsx
cat .aesthetic-function/design-overrides.json

Artifact Files

File PatternPhaseContent
*.figma-resolution-plan.json12EResolution decisions with reasons
*.figma-resolve-apply.json12FApply results with decision IDs

Example Apply Artifact

{
  "version": "1.0",
  "source": "figma-resolution-apply",
  "generatedAt": "2025-01-01T00:00:00.000Z",
  "sourceFile": "demos/react-demo-app/src/App.tsx",
  "planPath": ".aesthetic-function/artifacts/demos/react-demo-app/App.figma-resolution-plan.json",
  "mode": "apply",
  "dryRun": false,
  "summary": {
    "decisionsTotal": 4,
    "attempted": 4,
    "applied": 2,
    "noop": 1,
    "skipped": 0,
    "blocked": 0,
    "failed": 1
  },
  "results": [
    {
      "decisionId": "a1b2c3d4e5f6g7h8",
      "componentKey": "LoginButton",
      "targetState": "base",
      "property": "fill",
      "action": "APPLY_TO_AST",
      "target": "ast",
      "success": true,
      "status": "applied",
      "appliedValue": "#FF0000"
    }
  ]
}

Post-Apply Verification (Phase 12G)

Phase 12G provides the verification layer that confirms whether applied resolution plans landed as intended.

Key Principles

  • Verification-only - No mutations, only observes and records
  • Apply ≠ Trust - System proves it did what it claimed
  • Detect drift - Identifies partial failures or external changes
  • CI-safe - Exit codes suitable for CI gates
  • Rollback preparation - Captures previous values for future use

Environment Variables

VariableDefaultValuesDescription
FIGMA_VERIFY_INCLUDE_FIGMAfalsetrue/falseInclude Figma verification (read-only)
FIGMA_VERIFY_ALWAYS_WRITE_ARTIFACTfalsetrue/falseWrite artifact even on success
FIGMA_SERVER_URLhttp://localhost:3001URLServer for Figma queries

CLI Commands

# Verify most recent apply (auto-discovers artifacts)
pnpm --filter @aesthetic-function/watcher figma:verify demos/react-demo-app/src/App.tsx

# Verify specific apply artifact
pnpm --filter @aesthetic-function/watcher figma:verify demos/react-demo-app/src/App.tsx \
  --apply-artifact .aesthetic-function/artifacts/demos/react-demo-app/App.figma-resolve-apply.json

# Always write verification artifact (even on success)
pnpm --filter @aesthetic-function/watcher figma:verify demos/react-demo-app/src/App.tsx --always-write

# Include Figma verification (requires server running)
pnpm --filter @aesthetic-function/watcher figma:verify demos/react-demo-app/src/App.tsx --include-figma

Exit Codes

CodeMeaningCI Behavior
0All verified or skippedPass
1Any mismatch, missing, or blockedFail

Verification Statuses

StatusDescription
verifiedActual value matches expected
mismatchValues differ (drift detected)
missingTarget not found (file, marker, or override)
skippedVerification not applicable
blockedCould not verify (network error, etc.)

Verification Targets

TargetWhat is Verified
astRe-read source file and check JSX value at expected location
markerParse markers and verify property value
overrideParse design-overrides.json and check property
figmaQuery Figma server for current value (optional)

Professional Runbook

Complete workflow with verification:

# 1. Generate and apply resolution plan
pnpm --filter @aesthetic-function/watcher figma:resolve demos/react-demo-app/src/App.tsx
FIGMA_RESOLVE_APPLY_ON=true \
FIGMA_RESOLVE_APPLY_MODE=apply \
FIGMA_RESOLVE_APPLY_DRY_RUN=false \
  pnpm --filter @aesthetic-function/watcher figma:resolve-apply demos/react-demo-app/src/App.tsx --apply

# 2. Verify the apply succeeded
pnpm --filter @aesthetic-function/watcher figma:verify demos/react-demo-app/src/App.tsx

# 3. Check verification artifact if failures
cat .aesthetic-function/artifacts/demos/react-demo-app/App.figma-verification.json

# 4. Use in CI (exit code gates the build)
pnpm --filter @aesthetic-function/watcher figma:verify demos/react-demo-app/src/App.tsx || exit 1

Artifact Files

File PatternPhaseContent
*.figma-resolve-apply.json12FApply results with decision IDs
*.figma-verification.json12GVerification results with evidence

Example Verification Artifact

{
  "version": 1,
  "timestamp": "2025-01-01T00:00:00.000Z",
  "file": "demos/react-demo-app/src/App.tsx",
  "items": [
    {
      "decisionId": "a1b2c3d4e5f6g7h8",
      "componentKey": "LoginButton",
      "targetState": "base",
      "property": "fill",
      "target": "ast",
      "status": "verified",
      "expected": "#FF0000",
      "actual": "#FF0000"
    },
    {
      "decisionId": "b2c3d4e5f6g7h8i9",
      "componentKey": "LoginButton",
      "targetState": "hover",
      "property": "fill",
      "target": "marker",
      "status": "mismatch",
      "expected": "#CC0000",
      "actual": "#00FF00",
      "evidence": {
        "markerText": "// @figma node=LoginButton::hover fill=#00FF00",
        "location": "demos/react-demo-app/src/App.tsx:42"
      }
    }
  ],
  "summary": {
    "total": 2,
    "verified": 1,
    "mismatch": 1,
    "missing": 0,
    "skipped": 0,
    "blocked": 0
  },
  "rollbackInfo": {
    "a1b2c3d4e5f6g7h8": {
      "previousValue": "#0000FF",
      "target": "ast",
      "location": "demos/react-demo-app/src/App.tsx:35"
    }
  }
}

Post-Apply Auto-Verification + CI Gate (Phase 12H)

Phase 12H introduces an orchestration layer that automatically runs verification after apply operations when explicitly enabled. This creates a seamless apply → verify → exit-code pipeline for CI integration.

Key Principles

  • Orchestration + Policy only - No new mutation capabilities beyond Phase 12F
  • Opt-in via environment variable - Nothing changes unless explicitly enabled
  • CI-safe - Clear exit semantics for CI gates
  • Artifact linking - Bidirectional linking between apply and verification artifacts

Environment Variables

VariableDefaultValuesDescription
POST_APPLY_VERIFYfalsetrue/falseEnable automatic verification after apply
POST_APPLY_VERIFY_INCLUDE_FIGMAfalsetrue/falseInclude Figma verification (read-only)
POST_APPLY_VERIFY_STRICTtruetrue/falseExit 1 on mismatch/missing (CI gate)

When Verification Runs

Verification only runs when all conditions are met:

  1. POST_APPLY_VERIFY=true
  2. FIGMA_RESOLVE_APPLY_MODE=apply (not artifact-only)
  3. FIGMA_RESOLVE_APPLY_DRY_RUN=false (actual mutations occurred)

If any condition is not met, verification is skipped with exit code 0.

Exit Codes

ScenarioExit CodeDescription
Verification passed0All items verified or skipped
Verification not enabled0POST_APPLY_VERIFY=false
Artifact-only mode0Mode is 'artifact', no verification
Dry-run mode0DRY_RUN=true, no verification
Strict mode + mismatch1Found mismatches with strict=true
Strict mode + missing1Found missing targets with strict=true
Non-strict mode + failure0Advisory only, no CI failure

CLI Usage

Post-apply verification integrates seamlessly with figma:resolve-apply:

# Apply without verification (default)
FIGMA_RESOLVE_APPLY_ON=true \
FIGMA_RESOLVE_APPLY_MODE=apply \
FIGMA_RESOLVE_APPLY_DRY_RUN=false \
  pnpm --filter @aesthetic-function/watcher figma:resolve-apply demos/react-demo-app/src/App.tsx --apply

# Apply WITH automatic verification (CI mode)
FIGMA_RESOLVE_APPLY_ON=true \
FIGMA_RESOLVE_APPLY_MODE=apply \
FIGMA_RESOLVE_APPLY_DRY_RUN=false \
POST_APPLY_VERIFY=true \
  pnpm --filter @aesthetic-function/watcher figma:resolve-apply demos/react-demo-app/src/App.tsx --apply

# Apply with verification but don't fail CI on mismatches (advisory)
FIGMA_RESOLVE_APPLY_ON=true \
FIGMA_RESOLVE_APPLY_MODE=apply \
FIGMA_RESOLVE_APPLY_DRY_RUN=false \
POST_APPLY_VERIFY=true \
POST_APPLY_VERIFY_STRICT=false \
  pnpm --filter @aesthetic-function/watcher figma:resolve-apply demos/react-demo-app/src/App.tsx --apply

# Include Figma verification (requires server)
POST_APPLY_VERIFY=true \
POST_APPLY_VERIFY_INCLUDE_FIGMA=true \
  pnpm --filter @aesthetic-function/watcher figma:resolve-apply demos/react-demo-app/src/App.tsx --apply

Artifact Linking

When post-apply verification runs, artifacts are linked:

Apply Artifact includes verification path:

{
  "version": "1.0",
  "source": "figma-resolution-apply",
  "verificationArtifactPath": "design-materializations/src__App.figma-verification.json",
  ...
}

Verification Artifact includes apply path:

{
  "version": "1.0",
  "source": "figma-verification",
  "applyArtifactPath": "design-materializations/src__App.figma-resolve-apply.json",
  ...
}

CI Integration Example

# GitHub Actions example
jobs:
  sync-design:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Apply resolution plan with verification
        run: |
          FIGMA_RESOLVE_APPLY_ON=true \
          FIGMA_RESOLVE_APPLY_MODE=apply \
          FIGMA_RESOLVE_APPLY_DRY_RUN=false \
          POST_APPLY_VERIFY=true \
          POST_APPLY_VERIFY_STRICT=true \
            pnpm --filter @aesthetic-function/watcher figma:resolve-apply src/App.tsx --apply
        # CI will fail if verification finds mismatches

      - name: Upload verification artifacts
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: verification-report
          path: design-materializations/*.figma-verification.json

CLI Output

When verification runs:

--- Post-Apply Verification (Phase 12H) ---
  enabled:      YES
  includeFigma: NO
  strict:       YES (exit 1 on mismatch)

POST-APPLY VERIFICATION
=======================
  Status: PASSED ✓

  ✓ Verified: 5
  ⚠ Mismatch: 0
  ✗ Missing:  0
  ⏭ Skipped:  1
  ⊘ Blocked:  0

  Artifact: design-materializations/src__App.figma-verification.json

  Exit Code: 0

When verification is skipped:

--- Post-Apply Verification (Phase 12H) ---
  Status: SKIPPED
  Reason: POST_APPLY_VERIFY is not enabled

Rollback Preview & Safety Envelope (Phase 12I)

Phase 12I introduces a read-only rollback preview layer that shows exactly what would be undone if a verification failure were to trigger a rollback — without performing any rollback.

This phase completes the safety triangle:

Apply → Verify → Rollback Preview

Rollback execution itself is explicitly out of scope.

Key Principles

  • Read-only only - No mutations, only observes and records
  • Deterministic - Same inputs → same outputs
  • Explicit - No automatic behavior
  • Diagnostic - Improves human confidence and CI auditability
  • Always exit 0 - Never fails CI (purely informational)

CLI Commands

# Generate rollback preview (auto-discovers artifacts)
pnpm --filter @aesthetic-function/watcher figma:rollback-preview demos/react-demo-app/src/App.tsx

# Use specific artifact paths
pnpm --filter @aesthetic-function/watcher figma:rollback-preview demos/react-demo-app/src/App.tsx \
  --apply-artifact design-materializations/custom-apply.json \
  --verify-artifact design-materializations/custom-verify.json

CLI Output

=== ROLLBACK PREVIEW (Phase 12I) ===

LoginButton::hover
  fill:
    applied  → #00FF00
    previous → #2563EB
    target   → override
    reason   → mismatch

Summary:
  Total rollback actions: 1
  Targets: override (1)

When no rollback is needed:

=== ROLLBACK PREVIEW (Phase 12I) ===

No rollback actions needed.
All verification items passed or were skipped.

Rollback Preview Artifact

Pattern: design-materializations/<file>.figma-rollback-preview.json

Only written when rollback actions exist.

{
  "version": "1.0",
  "source": "figma-rollback-preview",
  "sourceFile": "src/App.tsx",
  "timestamp": "2025-01-01T00:00:00.000Z",
  "applyArtifactPath": "design-materializations/src__App.figma-resolve-apply.json",
  "verificationArtifactPath": "design-materializations/src__App.figma-verification.json",
  "actions": [
    {
      "actionId": "a1b2c3d4e5f6g7h8",
      "target": "override",
      "componentKey": "LoginButton",
      "targetState": "hover",
      "property": "fill",
      "appliedValue": "#00FF00",
      "previousValue": "#2563EB",
      "sourceApplyOpId": "x1y2z3w4...",
      "verificationStatus": "mismatch",
      "reason": "Verification mismatch: expected #00FF00, observed #FF0000"
    }
  ],
  "summary": {
    "total": 1,
    "byTarget": { "override": 1 },
    "byProperty": { "fill": 1 }
  }
}

Professional Runbook

Complete workflow with rollback preview:

# 1. Generate and apply resolution plan
pnpm --filter @aesthetic-function/watcher figma:resolve demos/react-demo-app/src/App.tsx
FIGMA_RESOLVE_APPLY_ON=true \
FIGMA_RESOLVE_APPLY_MODE=apply \
FIGMA_RESOLVE_APPLY_DRY_RUN=false \
  pnpm --filter @aesthetic-function/watcher figma:resolve-apply demos/react-demo-app/src/App.tsx --apply

# 2. Verify the apply succeeded
pnpm --filter @aesthetic-function/watcher figma:verify demos/react-demo-app/src/App.tsx

# 3. If verification failed, preview what would be rolled back
pnpm --filter @aesthetic-function/watcher figma:rollback-preview demos/react-demo-app/src/App.tsx

# 4. Review rollback preview artifact
cat design-materializations/demos__react-demo-app__src__App.figma-rollback-preview.json

Safety Triangle

PhasePurposeExit Code
12F: ApplyExecute resolution plan0 or 1
12G: VerifyConfirm apply succeeded0 or 1
12I: Rollback PreviewShow what would be undoneAlways 0
12J: StatusSummarize reconciliation lifecycle0 (PASS/WARN) or 1 (FAIL)

Reconciliation Status Artifact (Phase 12J)

Phase 12J completes the reconciliation lifecycle by producing a single, deterministic, human- and CI-readable artifact summarizing the end-to-end reconciliation state for any source file.

Key Question

"What is the reconciliation status of this file, right now?"

The figma:status command answers this by:

  1. Auto-discovering artifacts from Phases 12F-12I
  2. Computing status using a fixed rule-table (no heuristics)
  3. Producing a deterministic verdict and exit code

Status Values

OverallStatusMeaningCiVerdictExit Code
CLEANNo reconciliation artifacts foundPASS0
APPLIED_UNVERIFIEDApply attempted, verification not runWARN0
VERIFIED_OKApply + verification both succeededPASS0
VERIFY_FAILEDVerification failed, no rollback previewFAIL1
ROLLBACK_AVAILABLEVerification failed, rollback preview existsFAIL1
INCOMPLETEMissing or inconsistent artifactsWARN0

Status Determination Rules

ConditionoverallStatusciVerdict
No apply, no deltasCLEANPASS
Apply attempted, no verifyAPPLIED_UNVERIFIEDWARN
Apply + verify successVERIFIED_OKPASS
Verify failed, rollback preview existsROLLBACK_AVAILABLEFAIL
Verify failed, no rollback previewVERIFY_FAILEDFAIL
Missing or inconsistent artifactsINCOMPLETEWARN

CLI Usage

# Check status of a file
pnpm --filter @aesthetic-function/watcher figma:status demos/react-demo-app/src/App.tsx

# JSON output
pnpm --filter @aesthetic-function/watcher figma:status demos/react-demo-app/src/App.tsx --json

# Write status artifact (only if non-CLEAN)
pnpm --filter @aesthetic-function/watcher figma:status demos/react-demo-app/src/App.tsx --write

Example Output

✅ Reconciliation Status: ✓ VERIFIED_OK

Source: demos/react-demo-app/src/App.tsx
Timestamp: 2025-01-15T12:00:00.000Z

Phases:
  • Apply: ✓ 5 operation(s)
  • Verify: ✓ 0 mismatch(es), 0 missing
  • Rollback Preview: not generated

Explanation: Apply succeeded and verification passed.

CI Verdict: PASS
Exit Code: 0

Status Artifact

When --write is provided and status is non-CLEAN:

{
  "version": "1.0",
  "sourceFile": "demos/react-demo-app/src/App.tsx",
  "timestamp": "2025-01-15T12:00:00.000Z",
  "phases": {
    "apply": {
      "attempted": true,
      "dryRun": false,
      "success": true,
      "operationCount": 5
    },
    "verify": {
      "attempted": true,
      "success": true,
      "mismatchCount": 0,
      "missingCount": 0
    }
  },
  "overallStatus": "VERIFIED_OK",
  "ciVerdict": "PASS",
  "explanation": "Apply succeeded and verification passed."
}

Complete Workflow

# 1. Generate and apply resolution plan
FIGMA_RESOLVE_APPLY_ON=true \
FIGMA_RESOLVE_APPLY_MODE=apply \
FIGMA_RESOLVE_APPLY_DRY_RUN=false \
  pnpm --filter @aesthetic-function/watcher figma:resolve-apply demos/react-demo-app/src/App.tsx --apply

# 2. Verify the apply succeeded
pnpm --filter @aesthetic-function/watcher figma:verify demos/react-demo-app/src/App.tsx

# 3. If verification failed, preview what would be rolled back
pnpm --filter @aesthetic-function/watcher figma:rollback-preview demos/react-demo-app/src/App.tsx

# 4. Get comprehensive status summary
pnpm --filter @aesthetic-function/watcher figma:status demos/react-demo-app/src/App.tsx --write

# 5. Use status exit code in CI
if pnpm --filter @aesthetic-function/watcher figma:status demos/react-demo-app/src/App.tsx; then
  echo "Reconciliation OK"
else
  echo "Reconciliation FAILED - review status artifact"
fi

CI Integration

# .github/workflows/figma-reconciliation.yml
- name: Check Reconciliation Status
  run: |
    pnpm --filter @aesthetic-function/watcher figma:status ${{ matrix.file }} --write
  # Exit 1 on FAIL verdict, 0 on PASS/WARN

Guarantees

  • Deterministic: Same artifacts → same status, always
  • Rule-table only: No heuristics, no inference
  • Read-only: Reads artifacts, never mutates them
  • Single artifact: One status file per source file
  • CI-friendly: Exit codes map directly to verdict

Non-Goals (Explicit)

  • ❌ Executing rollback
  • ❌ Auto-rollback
  • ❌ Background watchers
  • ❌ Implicit behavior
  • ❌ Any mutation

Reconciliation Run Index (Phase 13A)

Phase 13A provides a single, deterministic "run index" artifact that summarizes what reconciliation artifacts exist for a given source file and their key metadata. This is NOT a timeline/history feature (that is Phase 13B). This phase is one-shot indexing of current/latest artifacts.

Key Question

"What reconciliation artifacts exist for this file, and what are their key details?"

The figma:index command answers this by:

  1. Auto-discovering all artifacts from Phases 12A-12J
  2. Extracting key metadata (timestamps, modes, counts)
  3. Producing a deterministic, sorted index

Indexed Artifact Types

Artifact TypePhaseFile Pattern
delta12A*.figma-delta.json
deltaSuggestions12B*.figma-delta-suggestions.json
conflicts12D*.figma-conflicts.json
resolutionPlan12E*.figma-resolution-plan.json
resolutionApply12F*.figma-resolution-apply.json
verification12G*.figma-verification.json
rollbackPreview12I*.figma-rollback-preview.json
status12J*.figma-reconciliation-status.json

CLI Usage

# Index artifacts for a file
pnpm --filter @aesthetic-function/watcher figma:index demos/react-demo-app/src/App.tsx

# JSON output
pnpm --filter @aesthetic-function/watcher figma:index demos/react-demo-app/src/App.tsx --json

# Write run index artifact
pnpm --filter @aesthetic-function/watcher figma:index demos/react-demo-app/src/App.tsx --write

# Verbose mode (show discovery paths)
pnpm --filter @aesthetic-function/watcher figma:index demos/react-demo-app/src/App.tsx --verbose

Example Output

=== FIGMA RUN INDEX (Phase 13A) ===
Repo Root: /path/to/repo
Source: demos/react-demo-app/src/App.tsx (canonical)

Artifacts:
  ✗ delta
  ✗ delta-suggestions
  ✓ conflicts (3 conflicts, 1 blocked) 2025-12-30T10:00:00.000Z
  ✓ resolution-plan (2 decisions) 2025-12-30T11:00:00.000Z
  ✓ resolution-apply (1 op, dry-run) 2025-12-30T12:00:00.000Z
  ✗ verification
  ✗ rollback-preview
  ✗ status

Notes: none

Run Index Artifact

Pattern: design-materializations/<file>.figma-run-index.json

{
  "version": "1.0",
  "repoRoot": "/abs/path/to/repo",
  "sourceFile": "demos/react-demo-app/src/App.tsx",
  "generatedAt": "2025-12-30T18:10:00.000Z",
  "artifacts": {
    "delta": { "found": false },
    "deltaSuggestions": { "found": false },
    "conflicts": {
      "found": true,
      "path": "design-materializations/demos__react-demo-app__src__App.figma-conflicts.json",
      "timestamp": "2025-12-30T10:00:00.000Z",
      "summary": { "conflicts": 3, "blocked": 1 }
    },
    "resolutionPlan": {
      "found": true,
      "path": "design-materializations/demos__react-demo-app__src__App.figma-resolution-plan.json",
      "timestamp": "2025-12-30T11:00:00.000Z",
      "summary": { "decisions": 2 }
    },
    "resolutionApply": {
      "found": true,
      "path": "design-materializations/demos__react-demo-app__src__App.figma-resolution-apply.json",
      "timestamp": "2025-12-30T12:00:00.000Z",
      "summary": { "ops": 1, "dryRun": true, "applied": 0, "skipped": 1, "failed": 0 }
    },
    "verification": { "found": false },
    "rollbackPreview": { "found": false },
    "status": { "found": false }
  },
  "notes": []
}

Summary Fields by Artifact Type

ArtifactSummary Fields
deltadeltas
deltaSuggestionssuggestions
conflictsconflicts, blocked
resolutionPlandecisions
resolutionApplyops, dryRun, applied, skipped, failed
verificationverified, mismatch, missing
rollbackPreviewactions
statusoverallStatus, ciVerdict

Best-Candidate Selection

If multiple matching artifacts exist for a type (e.g., legacy and current naming):

  1. Prefer canonical current naming
  2. If still multiple, pick newest by timestamp inside artifact
  3. Fallback to file mtime if timestamp field missing
  4. Log a warning in notes array

Repo-Root Invariance

The command behaves identically when run from:

  • Repository root
  • packages/watcher
  • Any subdirectory

All input paths (relative, ../, absolute) normalize to the same canonical form.

Relation to Other Phases

PhasePurpose
12J: StatusSingle lifecycle status (CLEAN, VERIFIED_OK, etc.)
13A: IndexSnapshot of all artifact presence + metadata
13B: Timeline (future)Historical run-over-run tracking

Guarantees

  • Deterministic: Same artifacts → same index output, always
  • Read-only: Never generates, applies, or mutates artifacts
  • Repo-root invariant: Works identically from any working directory
  • Single artifact: One index file per source file
  • Always exit 0: Read-only indexing never fails CI

Design Drift Timeline (Phase 13B)

Phase 13B turns the one-shot index from Phase 13A into longitudinal history. Every reconciliation command run (apply, verify, resolve-apply, etc.) is recorded as an immutable entry in an append-only ledger.

Key Question

"What reconciliation runs have occurred for this file over time, and what was the artifact state at each run?"

This enables:

  • Drift detection: Tracking how design/code synchronization evolves
  • Debugging: Understanding what happened before a failure
  • Audit trails: Recording who/what/when for compliance

Feature Flag

Recording is off by default and gated by:

RECONCILIATION_TIMELINE_ON=true

When enabled, runs are automatically recorded after successful completion of:

  • figma:apply
  • figma:verify
  • figma:resolve-apply
  • figma:rollback-preview
  • figma:status
  • figma:index

Run Ledger Artifact

Pattern: design-materializations/<file>.figma-run-ledger.json

{
  "version": 1,
  "sourceFile": "demos/react-demo-app/src/App.tsx",
  "runs": [
    {
      "runId": "abc12345",
      "sourceFile": "demos/react-demo-app/src/App.tsx",
      "timestamp": "2025-12-30T10:00:00.000Z",
      "cwd": "/repo",
      "repoRoot": "/repo",
      "command": "figma:status",
      "artifacts": {
        "conflicts": "design-materializations/demos__react-demo-app__src__App.figma-conflicts.json"
      },
      "summary": {
        "conflicts": 3,
        "decisions": 2
      }
    },
    {
      "runId": "def67890",
      "sourceFile": "demos/react-demo-app/src/App.tsx",
      "timestamp": "2025-12-30T11:00:00.000Z",
      "cwd": "/repo/packages/watcher",
      "repoRoot": "/repo",
      "command": "figma:apply",
      "mode": "artifact",
      "artifacts": {},
      "summary": {}
    }
  ]
}

Run ID Generation

Run IDs are deterministic 8-character hex strings generated via djb2 hash:

runId = djb2(sourceFile + timestamp + command + sortedArtifactPaths)

This ensures:

  • Same inputs → same ID (deterministic)
  • Different inputs → different ID (unique per run)
  • Collision-resistant for practical purposes

CLI Usage

# View timeline for a file (read-only mode)
pnpm --filter @aesthetic-function/watcher figma:timeline demos/react-demo-app/src/App.tsx

# JSON output
pnpm --filter @aesthetic-function/watcher figma:timeline demos/react-demo-app/src/App.tsx --json

# Limit results
pnpm --filter @aesthetic-function/watcher figma:timeline demos/react-demo-app/src/App.tsx --limit 5

# Verbose mode (show artifact paths)
pnpm --filter @aesthetic-function/watcher figma:timeline demos/react-demo-app/src/App.tsx --verbose

# Explicitly record a run to the ledger (requires feature flag)
RECONCILIATION_TIMELINE_ON=true pnpm --filter @aesthetic-function/watcher figma:timeline demos/react-demo-app/src/App.tsx --record

# Force write ledger artifact (for testing only)
pnpm --filter @aesthetic-function/watcher figma:timeline demos/react-demo-app/src/App.tsx --write

Recording Runs (Phase 13B.1)

Recording is explicit and opt-in. No command automatically records runs to the ledger.

To record a run, BOTH conditions must be met:

  1. The --record flag is present
  2. The RECONCILIATION_TIMELINE_ON=true environment variable is set
# This will record a run:
RECONCILIATION_TIMELINE_ON=true pnpm figma:timeline demos/react-demo-app/src/App.tsx --record

# This will NOT record (no feature flag):
pnpm figma:timeline demos/react-demo-app/src/App.tsx --record
# Output: ⚠️  Recording disabled: RECONCILIATION_TIMELINE_ON is not set to "true"

# This will NOT record (no --record flag):
RECONCILIATION_TIMELINE_ON=true pnpm figma:timeline demos/react-demo-app/src/App.tsx
# Output: ℹ️ Read-only mode (use --record to append a run)

Example Output

=== FIGMA RUN TIMELINE (Phase 13B) ===
Repo Root: /path/to/repo
Source: demos/react-demo-app/src/App.tsx (canonical)

Runs (newest first, showing 3 of 3):

[def67890] 2025-12-30 11:00:00
  Command: figma:apply
  Mode: artifact
  Summary: (none)

[abc12345] 2025-12-30 10:00:00
  Command: figma:status
  Summary: 3 conflicts, 2 decisions

[xyz99999] 2025-12-29 15:30:00
  Command: figma:verify
  Summary: 5 verified, 1 mismatch, 0 missing

Relation to Phase 13A

PhasePurposeType
13A: IndexCurrent artifact snapshotOne-shot
13B: TimelineHistorical run ledgerAppend-only

Phase 13B uses Phase 13A's artifact discovery to populate each run entry's artifacts and summary fields.

Guarantees

  • Append-only: Runs are only ever added, never modified or deleted
  • Deterministic IDs: Same run → same runId
  • Repo-root invariant: Works identically from any working directory
  • Explicit recording: Recording ONLY when --record flag + RECONCILIATION_TIMELINE_ON=true
  • No implicit side effects: Other commands never record runs; user must explicitly opt-in
  • Read-only default: Timeline CLI reads ledger unless --record is specified

Drift Diffs (Phase 13C)

Phase 13C adds run-to-run comparison capability, turning the timeline ledger from Phase 13B into actionable drift detection. It compares two runs and summarizes what changed between them.

Key Question

"What changed between this run and the previous run?"

This enables:

  • Regression detection: Catching status worsening (VERIFIED_OK → VERIFY_FAILED)
  • Trend analysis: Tracking verification mismatches over time
  • Debugging: Understanding what triggered a failure

Drift Diff Artifact

Pattern: design-materializations/<file>.figma-drift-diff.json

{
  "version": "1.0",
  "sourceFile": "demos/react-demo-app/src/App.tsx",
  "fromRunId": "abc12345",
  "toRunId": "def67890",
  "generatedAt": "2025-12-30T12:00:00.000Z",
  "summary": {
    "totalChanges": 2,
    "infoCount": 1,
    "warnCount": 0,
    "failCount": 1,
    "insufficientHistory": false,
    "message": "1 regression(s), 0 warning(s), 1 info change(s)"
  },
  "changes": [
    {
      "field": "overallStatus",
      "from": "VERIFIED_OK",
      "to": "VERIFY_FAILED",
      "severity": "fail",
      "reason": "Status worsened from VERIFIED_OK to VERIFY_FAILED"
    },
    {
      "field": "verifyMismatch",
      "from": 0,
      "to": 2,
      "delta": 2,
      "severity": "fail",
      "reason": "Verification mismatches increased by 2"
    }
  ],
  "from": { "runId": "abc12345", "timestamp": "...", "command": "...", ... },
  "to": { "runId": "def67890", "timestamp": "...", "command": "...", ... }
}

Severity Rules

ChangeSeverityCondition
Status worseningfailTo VERIFY_FAILED or worse
Status worseningwarnTo APPLIED_UNVERIFIED
verifyMismatch increasefailAny increase > 0
verifyMissing increasefailAny increase > 0
conflictsTotal increasewarnAny increase > 0
deltasTotal increasewarnAny increase > 0
applyDryRun toggleinfoAny change
Status improvinginfoLevel decreasing
CLEAN ↔ VERIFIED_OKinfoBoth good states

CLI Usage

# Compare latest vs previous run
pnpm --filter @aesthetic-function/watcher figma:drift demos/react-demo-app/src/App.tsx

# JSON output
pnpm --filter @aesthetic-function/watcher figma:drift demos/react-demo-app/src/App.tsx --json

# Compare specific runs
pnpm --filter @aesthetic-function/watcher figma:drift demos/react-demo-app/src/App.tsx --from abc12345 --to def67890

# Write artifact to disk
pnpm --filter @aesthetic-function/watcher figma:drift demos/react-demo-app/src/App.tsx --write

# Verbose mode (show artifact paths and reasons)
pnpm --filter @aesthetic-function/watcher figma:drift demos/react-demo-app/src/App.tsx --verbose

# Explain run selection (Phase 13C.1)
pnpm --filter @aesthetic-function/watcher figma:drift demos/react-demo-app/src/App.tsx --explain

# Strict mode for CI: exit 1 if any 'fail' severity (Phase 13C.1)
pnpm --filter @aesthetic-function/watcher figma:drift demos/react-demo-app/src/App.tsx --strict

Preconditions Banner (Phase 13C.1)

Every drift command now prints a preconditions header before results:

=== DRIFT DIFF PRECONDITIONS ===
Repo Root: /path/to/repo
Source (input): ../../demos/react-demo-app/src/App.tsx
Source (canonical): demos/react-demo-app/src/App.tsx
Ledger: ✓ found
Run selection:
  from: abc12345 (2025-12-30T10:00:00.000Z)
  to:   def67890 (2025-12-30T11:00:00.000Z)

Run Selection Explanation (--explain)

The --explain flag provides detailed reasoning about run selection:

pnpm figma:drift demos/react-demo-app/src/App.tsx --explain

Output:

=== RUN SELECTION EXPLANATION ===
From Run:
  Method: previous
  Reason: Auto-selected as second-to-last run (default comparison baseline)
  Explicit: no (auto-selected)

To Run:
  Method: latest
  Reason: Auto-selected as latest run (most recent in ledger)
  Explicit: no (auto-selected)

Candidate Validation (Phase 13C.2)

Before computing drift, candidates are validated to ensure meaningful comparison:

Run States

Each run is classified based on its artifacts:

StateDescriptionHas VerificationHas Apply
VERIFIED_OKRun completed with verification-
VERIFIED_MISMATCHVerified but found mismatches-
APPLY_ONLYApplied changes but never verified
INCOMPLETEHas some artifacts but not verification or apply
EMPTYNo reconciliation artifacts

Comparison Classes

The comparison class indicates the confidence level of the drift analysis:

ClassDescriptionBehavior
FULLBoth runs verified✓ High confidence
PARTIALOne run verified⚠️ Warning printed
WEAKNeither run verified⚠️ Warning printed, --strict fails
INVALIDMissing artifacts❌ Not meaningful, --strict fails

Example Output

=== CANDIDATE CLASSIFICATION (Phase 13C.2) ===
From Run: abc12345
  State: VERIFIED_OK
  Artifacts: status, verification
To Run: def67890
  State: APPLY_ONLY
  Artifacts: resolutionApply
Comparison Class: PARTIAL

⚠️ Drift comparison is PARTIAL
Reason: to run has not been verified
Results may reflect incomplete reconciliation

Strict Mode Enforcement

With --strict, the command exits with code 1 when:

  • Comparison class is INVALID or WEAK
  • Any drift item has severity fail

No Material Drift Message (Phase 13C.1)

When no significant changes exist (empty changes or all 'info' with zero deltas):

✓ No material drift detected between runs.

Example Output

=== DRIFT DIFF PRECONDITIONS ===
Repo Root: /path/to/repo
Source (input): demos/react-demo-app/src/App.tsx
Source (canonical): demos/react-demo-app/src/App.tsx
Ledger: ✓ found
Run selection:
  from: abc12345 (2025-12-30T10:00:00.000Z)
  to:   def67890 (2025-12-30T11:00:00.000Z)

=== FIGMA DRIFT DIFF (Phase 13C) ===
Repo Root: /path/to/repo
Source: demos/react-demo-app/src/App.tsx (canonical)

Comparing: [abc12345] → [def67890]

From:
  Run ID: abc12345
  Timestamp: 2025-12-30T10:00:00.000Z
  Command: figma:status
  Status: VERIFIED_OK

To:
  Run ID: def67890
  Timestamp: 2025-12-30T11:00:00.000Z
  Command: figma:status
  Status: VERIFY_FAILED

Summary: 1 regression(s), 0 warning(s), 1 info change(s)

Changes (2):
  [FAIL] overallStatus: VERIFIED_OK → VERIFY_FAILED
  [FAIL] verifyMismatch: 0 → 2 (+2)

Run Selection

By default, compares the latest run vs the previous run:

  • Default: runs[n-2] (from) vs runs[n-1] (to)
  • Explicit: Use --from and/or --to with run IDs

If fewer than 2 runs exist, produces an artifact with insufficientHistory: true.

Relation to Other Phases

PhasePurposeType
13A: IndexCurrent artifact snapshotOne-shot
13B: TimelineHistorical run ledgerAppend-only
13C: DriftRun-to-run comparisonRead-only diff

Guarantees

  • Deterministic: Same runs → same diff output
  • Deterministic ordering: Changes sorted by severity (fail > warn > info), then field name
  • Read-only: Never modifies any artifacts or source files
  • Repo-root invariant: Works identically from any working directory
  • Preconditions visible: Always shows source paths, ledger status, run selection
  • Exit code 0: Default success (even with insufficient history)
  • Exit code 1: With --strict, if any drift item has 'fail' severity
  • Exit code 2: Usage error (invalid arguments)

Drift Summary Dashboard (Phase 13D)

Phase 13D adds an aggregated dashboard that summarizes drift across multiple reconciliation runs. It provides an "at a glance" view of design drift over time for a source file, with a stability score, severity counts, top drift signals, and a CI-friendly verdict.

Artifact

design-materializations/<file>.figma-drift-dashboard.json

Example path: design-materializations/demos__react-demo-app__src__App.figma-drift-dashboard.json

Structure

{
  "version": 1,
  "generatedAt": "2025-12-30T12:00:00.000Z",
  "repoRoot": "/path/to/repo",
  "sourceFile": "demos/react-demo-app/src/App.tsx",
  "runWindow": {
    "limit": 10,
    "fromRunId": null,
    "toRunId": null
  },
  "counts": {
    "runsConsidered": 5,
    "bySeverity": {
      "info": 3,
      "warn": 1,
      "fail": 0
    }
  },
  "stabilityScore": {
    "value": 84,
    "rationale": [
      "-10 (1 warn-severity drift)",
      "-6 (3 info-severity drifts)"
    ]
  },
  "topSignals": [
    {
      "key": "conflicts.total",
      "label": "Conflicts",
      "delta": 2,
      "from": 0,
      "to": 2,
      "severity": "warn"
    }
  ],
  "recentRuns": [
    {
      "runId": "def67890",
      "timestamp": "2025-12-30T11:00:00.000Z",
      "command": "figma:status",
      "overallStatus": "VERIFIED_OK",
      "driftSeverity": "info",
      "highlights": ["Status: VERIFIED_OK", "Drift: info"]
    }
  ],
  "ciVerdict": "WARN",
  "exitCode": 0,
  "explanation": "1 warn-severity drift detected"
}

Stability Score

The stability score starts at 100 and deducts points based on drift severity:

SeverityDeduction
fail-25 per event
warn-10 per event
info-2 per event

The score is clamped to the range 0-100.

CI Verdict

VerdictCondition
PASSNo significant drift detected
WARNWarn-severity drifts but no failures
FAILFail-severity drift or threshold exceeded

Thresholds

Configurable via environment variables or CLI flags:

ThresholdDefaultEnvironment Variable
Fail on fail severitytrueDASHBOARD_FAIL_ON_FAIL_SEVERITY
Max fail count1DASHBOARD_MAX_FAIL
Max warn countnoneDASHBOARD_MAX_WARN
Max verify mismatch increasenoneDASHBOARD_MAX_VERIFY_MISMATCH_INCREASE
Max conflict increasenoneDASHBOARD_MAX_CONFLICT_INCREASE

Exit Code

  • exit 0: Default behavior (even for WARN or FAIL verdicts)
  • exit 1: Only when --strict flag or DASHBOARD_CI_STRICT=true AND verdict is FAIL

CLI Usage

# Basic usage
pnpm --filter @aesthetic-function/watcher figma:dashboard demos/react-demo-app/src/App.tsx

# JSON output
pnpm --filter @aesthetic-function/watcher figma:dashboard demos/react-demo-app/src/App.tsx --json

# Custom run window
pnpm --filter @aesthetic-function/watcher figma:dashboard demos/react-demo-app/src/App.tsx --limit 20

# Specific run range
pnpm --filter @aesthetic-function/watcher figma:dashboard demos/react-demo-app/src/App.tsx --from abc12345 --to def67890

# Write artifact to disk
pnpm --filter @aesthetic-function/watcher figma:dashboard demos/react-demo-app/src/App.tsx --write

# CI strict mode (exit 1 on FAIL)
pnpm --filter @aesthetic-function/watcher figma:dashboard demos/react-demo-app/src/App.tsx --strict

# Verbose mode (show rationale and highlights)
pnpm --filter @aesthetic-function/watcher figma:dashboard demos/react-demo-app/src/App.tsx --verbose

Example Output

=== FIGMA DRIFT DASHBOARD (Phase 13D) ===
Repo Root: /path/to/repo
Source: demos/react-demo-app/src/App.tsx (canonical)
Generated: 2025-12-30T12:00:00.000Z

Run Window:
  Runs considered: 5
  Limit: 10

Drift Counts:
  Fail: 0
  Warn: 1
  Info: 3

Stability Score:
  ████████░░ 84/100

Top Signals (1):
  [WARN] Conflicts: +2 (0 → 2)

Recent Runs (newest first, 5):
  1. [def67890] figma:status - VERIFIED_OK (info)
  2. [abc12345] figma:apply - APPLIED_UNVERIFIED
  3. [xyz99999] figma:verify - VERIFIED_OK
  4. [pqr88888] figma:status - CLEAN
  5. [mno77777] figma:apply - APPLIED_UNVERIFIED

CI Verdict:
  ⚠ WARN
  1 warn-severity drift detected
  Exit code: 0

Relation to Other Phases

PhasePurposeType
13A: IndexCurrent artifact snapshotOne-shot
13B: TimelineHistorical run ledgerAppend-only
13C: DriftRun-to-run comparisonRead-only diff
13D: DashboardAggregated summaryRead-only

Guarantees

  • Deterministic: Same runs → same dashboard output
  • Read-only: Never modifies any artifacts or source files
  • Repo-root invariant: Works identically from any working directory
  • CI-friendly: Configurable exit codes for build gates
  • Feature-flagged: RECONCILIATION_DASHBOARD_ON (default: true)

Project Drift Dashboard (Phase 13E)

Phase 13E adds directory-level aggregation of Phase 13D dashboards. It scans a directory for source files, computes or loads dashboards for each file, and produces a project-level summary with an overall verdict, stability score, and top drift signals across all files.

Artifact

design-materializations/<scanRoot>.figma-project-dashboard.json

Example path: design-materializations/demos__react-demo-app__src.figma-project-dashboard.json

Structure

{
  "version": 1,
  "generatedAt": "2025-12-30T12:00:00.000Z",
  "repoRoot": "/path/to/repo",
  "scanRoot": "demos/react-demo-app/src",
  "filePattern": "**/*.tsx",
  "counts": {
    "totalFiles": 10,
    "filesWithData": 8,
    "filesNoData": 2,
    "filesWithErrors": 0,
    "byVerdict": { "PASS": 5, "WARN": 2, "FAIL": 1 },
    "bySeverity": { "fail": 3, "warn": 5, "info": 12 }
  },
  "stabilityScore": {
    "value": 72,
    "filesIncluded": 8,
    "filesExcluded": 2
  },
  "topSignals": [
    {
      "key": "conflicts.total",
      "label": "Conflicts",
      "delta": 2,
      "from": 0,
      "to": 2,
      "severity": "warn",
      "magnitude": 2,
      "sourceFile": "demos/react-demo-app/src/Card.tsx"
    }
  ],
  "files": [
    {
      "sourceFile": "demos/react-demo-app/src/App.tsx",
      "status": "OK",
      "verdict": "PASS",
      "stabilityScore": 84,
      "runsConsidered": 5,
      "severityCounts": { "fail": 0, "warn": 1, "info": 3 }
    }
  ],
  "projectVerdict": "FAIL",
  "exitCode": 0,
  "explanation": "1 file with FAIL verdict"
}

File Discovery

The scanner searches for **/*.tsx files in the scan root, excluding:

  • node_modules/
  • dist/
  • build/
  • .next/
  • .turbo/
  • .git/
  • coverage/

Files are processed in lexicographically sorted order for determinism.

Per-File Dashboard Loading

For each discovered file, the project dashboard:

  1. Attempts to load an existing Phase 13D dashboard artifact
  2. If no artifact exists, attempts to compute a dashboard in-memory (requires run ledger)
  3. If no data is available, records the file as NO_DATA
  4. On any error, records the file as ERROR with the error message

Project Verdict

The project verdict is the worst verdict across all files:

ConditionProject Verdict
Any file has FAIL verdictFAIL
No FAIL but any file has WARN verdictWARN
All files are PASS or NO_DATAPASS

Project Stability Score

The project stability score is the average of file stability scores, excluding files with NO_DATA or ERROR status:

projectScore = sum(file.stabilityScore) / filesWithData

Top Signals

Project-level signals are merged from all file dashboards and sorted deterministically:

  1. Severity (fail → warn → info)
  2. Magnitude (descending absolute delta)
  3. File path (ascending lexicographic)
  4. Signal key (ascending lexicographic)

CLI Usage

# Basic usage (scan demos/react-demo-app/src)
pnpm --filter @aesthetic-function/watcher figma:project-dashboard demos/react-demo-app/src

# JSON output
pnpm --filter @aesthetic-function/watcher figma:project-dashboard demos/react-demo-app/src --json

# Limit top signals
pnpm --filter @aesthetic-function/watcher figma:project-dashboard demos/react-demo-app/src --limit 10

# Write artifact to disk
pnpm --filter @aesthetic-function/watcher figma:project-dashboard demos/react-demo-app/src --write

# CI strict mode (exit 1 on FAIL)
pnpm --filter @aesthetic-function/watcher figma:project-dashboard demos/react-demo-app/src --strict

# Custom repo root
pnpm --filter @aesthetic-function/watcher figma:project-dashboard demos/react-demo-app/src --repo-root /path/to/repo

# Verbose mode (show all files)
pnpm --filter @aesthetic-function/watcher figma:project-dashboard demos/react-demo-app/src --verbose

# Custom thresholds
pnpm --filter @aesthetic-function/watcher figma:project-dashboard demos/react-demo-app/src --fail-score 50 --warn-score 70

# Limit max signals in output
pnpm --filter @aesthetic-function/watcher figma:project-dashboard demos/react-demo-app/src --max-signals 5

Threshold Configuration (Phase 13E.1)

The project dashboard uses configurable score thresholds to determine the verdict. Thresholds can be set via CLI flags or environment variables, with CLI taking precedence.

Threshold Precedence

  1. CLI flags (highest priority)
  2. Environment variables
  3. Built-in defaults (lowest priority)

Configuration Options

CLI FlagEnvironment VariableDefaultDescription
--fail-scoreRECONCILIATION_DASHBOARD_FAIL_SCORE60Scores below this are FAIL
--warn-scoreRECONCILIATION_DASHBOARD_WARN_SCORE80Scores below this (but ≥ fail) are WARN
--max-signalsRECONCILIATION_DASHBOARD_MAX_SIGNALS10Maximum signals shown in output

Verdict Rules

ConditionVerdict
score < failScoreFAIL
failScore ≤ score < warnScoreWARN
score ≥ warnScorePASS

Invariant

The invariant failScore < warnScore is enforced at resolution time. Invalid configurations exit with code 2:

# Error: fail-score must be less than warn-score
pnpm figma:project-dashboard demos/react-demo-app/src --fail-score 80 --warn-score 70
# Exit code: 2

Environment Variable Examples

# Set thresholds via environment
export RECONCILIATION_DASHBOARD_FAIL_SCORE=50
export RECONCILIATION_DASHBOARD_WARN_SCORE=75
export RECONCILIATION_DASHBOARD_MAX_SIGNALS=20

pnpm figma:project-dashboard demos/react-demo-app/src

Example Output

=== FIGMA PROJECT DASHBOARD (Phase 13E) ===
Repo Root: /path/to/repo
Scan Root: demos/react-demo-app/src (canonical)
File Pattern: **/*.tsx
Generated: 2025-12-30T12:00:00.000Z

DASHBOARD THRESHOLDS:
  Fail:  < 60
  Warn:  < 80
  Pass:  ≥ 80
  Max signals: 10

File Counts:
  Total: 10
  With data: 8
  No data: 2
  Errors: 0

By Verdict:
  PASS: 5
  WARN: 2
  FAIL: 1

Severity Counts (all files):
  Fail: 3
  Warn: 5
  Info: 12

Project Stability Score:
  ███████░░░ 72/100 (8 files)

Top Signals (3):
  [FAIL] Card.tsx: Conflicts: +2 (0 → 2)
  [WARN] App.tsx: Status worsened: CLEAN → APPLIED_UNVERIFIED
  [INFO] Button.tsx: Verify mismatches: +1 (0 → 1)

Project Verdict:
  ✗ FAIL
  Score 72: Below fail threshold (60). Project has failing drift signals.
  Exit code: 0

Exit Codes

Exit CodeCondition
0Default behavior (even for WARN or FAIL verdicts)
1--strict flag or PROJECT_DASHBOARD_CI_STRICT=true AND verdict is FAIL
2Invalid configuration (e.g., --fail-score >= --warn-score)

Relation to Other Phases

PhaseScopePurpose
13D: DashboardPer-fileAggregated drift summary for one file
13E: Project DashboardDirectoryAggregated summary across many files

Guarantees

  • Deterministic: Same files → same output (sorted, stable)
  • Read-only: Never modifies any source files or artifacts
  • Repo-root invariant: Works identically from any working directory
  • CI-friendly: Configurable exit codes for build gates

CI Gate Summary (Phase 13F)

Phase 13F adds a CI-focused command that computes a pass/warn/fail decision from Phase 13E project dashboard data, with a small trend window derived from Phase 13B ledgers.

Artifact

design-materializations/<scanRoot>.figma-ci-gate.json

Example path: design-materializations/demos__react-demo-app__src.figma-ci-gate.json

Structure

{
  "version": 1,
  "generatedAt": "2025-12-30T12:00:00.000Z",
  "repoRoot": "/path/to/repo",
  "scanRoot": "demos/react-demo-app/src",
  "filePattern": "**/*.tsx",
  "counts": {
    "totalFiles": 10,
    "filesWithData": 8,
    "filesNoData": 2,
    "filesWithErrors": 0,
    "byVerdict": { "pass": 5, "warn": 2, "fail": 1 },
    "bySeverity": { "fail": 3, "warn": 5, "info": 12 }
  },
  "stabilityScore": {
    "value": 72,
    "filesIncluded": 8,
    "filesExcluded": 2
  },
  "trend": {
    "improving": 3,
    "stable": 4,
    "worsening": 1,
    "insufficientData": 2,
    "windowSize": 5,
    "files": [
      {
        "sourceFile": "demos/react-demo-app/src/App.tsx",
        "runsInWindow": 5,
        "direction": "improving",
        "startScore": 80,
        "endScore": 90,
        "scoreDelta": 10
      }
    ]
  },
  "topSignals": [...],
  "files": [...],
  "verdict": "FAIL",
  "exitCode": 0,
  "explanation": "1 file with FAIL verdict"
}

Trend Window

The trend summary looks at the last N runs (default: 5) for each file to determine direction:

DirectionCondition
improvingScore increased by ≥5 points
stableScore changed by <5 points
worseningScore decreased by ≥5 points

Files with fewer than 2 runs in the window are marked as "insufficient data".

CLI Usage

# Basic usage
pnpm --filter @aesthetic-function/watcher figma:ci demos/react-demo-app/src

# JSON output
pnpm --filter @aesthetic-function/watcher figma:ci demos/react-demo-app/src --json

# Custom trend window
pnpm --filter @aesthetic-function/watcher figma:ci demos/react-demo-app/src --window 10

# Write artifact to disk
pnpm --filter @aesthetic-function/watcher figma:ci demos/react-demo-app/src --write

# CI strict mode (exit 1 on FAIL)
pnpm --filter @aesthetic-function/watcher figma:ci demos/react-demo-app/src --strict

# Verbose mode (show all files and trends)
pnpm --filter @aesthetic-function/watcher figma:ci demos/react-demo-app/src --verbose

Example Output

=== FIGMA CI GATE (Phase 13F) ===
Repo Root: /path/to/repo
Scan Root: demos/react-demo-app/src
Generated: 2025-12-30T12:00:00.000Z

Files:
  Total discovered: 10
  With data: 8
  No data: 2
  Errors: 0

Verdict Breakdown:
  PASS: 5
  WARN: 2
  FAIL: 1

Project Stability Score:
  ███████░░░ 72/100

Trend Summary (window: 5 runs):
  ↑ Improving: 3
  → Stable: 4
  ↓ Worsening: 1
  ? Insufficient data: 2

CI Verdict:
  ✗ FAIL
  1 file with FAIL verdict
  Exit code: 0

Exit Code

  • exit 0: Default behavior (even for WARN or FAIL verdicts)
  • exit 1: Only when --strict flag or RECONCILIATION_CI_STRICT=true AND verdict is FAIL

Environment Variables

VariableDefaultDescription
RECONCILIATION_CI_STRICTfalseExit 1 on FAIL verdict
RECONCILIATION_CI_WINDOW5Trend window size (runs per file)
DASHBOARD_LIMIT10Max runs to consider per file for dashboard

Relation to Other Phases

PhasePurpose
13B: TimelineProvides run ledger for trend computation
13D: DashboardPer-file drift aggregation
13E: Project DashboardMulti-file aggregation (reused by 13F)
13F: CI GateCI-focused gate with trend window

Guarantees

  • Deterministic: Same inputs → same output
  • Read-only: Never modifies any source files or artifacts
  • Repo-root invariant: Works identically from any working directory
  • CI-friendly: Only --strict can cause non-zero exit

Color Mapping

Vuetify semantic colors are mapped to hex values:

Vuetify ColorHex Value
primary#1976D2
success#4CAF50
error#FF5252
warning#FB8C00
info#2196F3

Confidence Levels

Prop TypeConfidenceExample
String literalhighcolor="primary"
Static templatehighcolor={`primary`}
Boolean shorthandhighdisabled
Variable/expressionlowcolor={buttonColor}

SemanticAdapter Interface

Custom adapters implement this interface:

interface SemanticAdapter {
  readonly id: string;           // e.g., 'vuetify'
  readonly displayName: string;  // e.g., 'Vuetify'
  readonly priority?: number;    // Lower = runs earlier

  supports(node: JSXElement, ctx: AdapterContext): boolean;
  extract(node: JSXElement, ctx: AdapterContext): AdapterResult;
}

Merge Rules

When adapter semantics are merged with generic JSX semantics:

  1. Adapter wins for fields it explicitly sets
  2. Generic JSX preserved for fields adapter doesn't set
  3. No erasure - adapters cannot remove existing values
  4. Provenance tracked - each value knows its source

CLI Output

The pnpm ast:report command now includes adapter semantics:

=== ADAPTER SEMANTICS (Phase 10A) ===
Components with adapter matches: 3

  LoginButton
    Adapter: Vuetify
    Fields: visual.fills, booleans.disabled
    Confidence: high
    Metadata: { component: 'v-btn', vuetifyColor: 'primary' }

Troubleshooting

Common issues and solutions:

"Figma doesn't update when I save a file"

  1. Check server is running: pnpm dev:server should show "HTTP server listening on http://localhost:3001"
  2. Check watcher is running: pnpm dev:watcher should show "Watcher started"
  3. Check plugin is connected: Plugin UI should show "Connected" status
  4. Check for markers: File must contain // @figma node=... markers
  5. Check node names match: Figma nodes must be named exactly as specified in markers

"Operations sent but node not found in Figma"

Look for this log in the server:

[Server] ✗ OPERATION_RESULT failed for requestId=...
[Server]   Error: Node not found: "LoginButton"

Solutions:

  • Verify node exists with exact name in Figma
  • Use component-map.json for stable ID mapping
  • Enable DEBUG_LIST_VARIANTS=true in plugin to see available nodes

"Duplicate operations sent"

The suppression system should prevent this. If it happens:

  1. Check suppression logs: TRACE=true pnpm dev:watcher
  2. Look for: [Trace] [suppression] entries
  3. If different ops are being incorrectly suppressed, the ops-hash may need debugging

"Override changes not reflected"

  1. Check design-overrides.json exists with the node entry
  2. Verify USE_OVERRIDES=true (default)
  3. Check precedence: OVERRIDES_PRECEDENCE=always means overrides always win
  4. If using if_newer_than_code, ensure override timestamp is newer than file mtime

"LLM mode returns empty intents"

  1. Verify API key is set: OPENAI_API_KEY or ANTHROPIC_API_KEY
  2. Check provider setting: LLM_PROVIDER=openai or anthropic
  3. Look for fallback: [Watcher] LLM failed, falling back to markers
  4. Try with markers first to verify basic pipeline works

Enable Verbose Diagnostics

# Full trace output with timing details
TRACE=true TRACE_VERBOSE=true ENABLE_AUDIT_LOG=true pnpm dev:watcher

Demo Scripts (Phase 9C)

Quick-start scripts for demos and development:

CommandDescription
pnpm demo:serverStart the relay server with pretty logging
pnpm demo:watcherStart the file watcher with trace output
pnpm demo:featureRun the feature orchestrator CLI
pnpm demo:fastStart server + watcher concurrently
pnpm demo:tunnelExpose server via cloudflared for Figma access

Professional Demo Runbook

Setup (5 minutes before demo):

  1. Start server and watcher:

    pnpm demo:fast
    
  2. In a separate terminal, start the tunnel:

    pnpm demo:tunnel
    

    Copy the HTTPS URL.

  3. Open Figma, run the plugin, paste the tunnel URL, click Connect.

  4. Verify connection by saving a file with @figma markers and watching Figma update.

During Demo:

  1. Show Code → Design: Edit a file, save, watch Figma update in real-time.

  2. Show Design → Code: In Figma, select a node, click "Send Selection" in the plugin. Show design-overrides.json updating.

  3. Show Feature Orchestrator:

    pnpm demo:feature --file demos/react-demo-app/src/App.tsx \
      --prompt "Change the Card background to success green"
    

    Show the patch artifact generated.

  4. Show Immediate Apply (Phase 9B):

    POST_APPLY_EMIT=true AST_WRITE_MODE=write AST_WRITE_DRY_RUN=false \
      pnpm demo:feature --file demos/react-demo-app/src/App.tsx \
      --prompt "Change the button text to Submit" --apply
    

    Watch Figma update immediately without waiting for file-save detection.


Reconciliation System (Phase 14)

The reconciliation system provides a unified CLI for analyzing design ↔ code drift. All Phase 12–13 analysis is now reachable through figma:reconcile.

Entry Point

The primary orchestration command is:

pnpm --filter @aesthetic-function/watcher figma:reconcile <source-file> [options]

This single command runs the complete Phase 12–13 read-only analysis sequence:

StepPhaseCommand EquivalentDescription
1. status12Jfigma:statusCompute reconciliation status
2. index13Afigma:indexIndex existing artifacts
3. timeline13Bfigma:timelineLoad/record timeline ledger
4. drift13Cfigma:driftCompute run-to-run drift diffs
5. dashboard13Dfigma:dashboardGenerate drift summary dashboard

Each step is deterministic and read-only by default. The command produces a single bundle artifact that links all outputs.

Profiles (Authoritative Table)

Profiles are deterministic flag presets. CLI flags always override profile defaults.

ProfilestrictrecordwritealwaysWriteBundleIntended Use
localfalsefalsefalsefalseHuman inspection
recordfalsetruetruetrueRun capture
citruefalsefalsetrueCI gate

Key behaviors:

  • local (default): Safe for iterative development. No side effects.
  • record: Captures timeline runs. Requires RECONCILIATION_TIMELINE_ON=true.
  • ci: Strict mode. Always writes bundle artifact for attribution, even though write=false.

Precedence: CLI flags > Profile defaults

# Use ci profile but disable strict mode
figma:reconcile demos/react-demo-app/src/App.tsx --profile ci --no-strict

Output Formats

FormatFlagWhen to Use
human--format human (default)Local development, human inspection
json--format json or --jsonProgrammatic processing, debugging
ci--format ciGitHub Actions, CI pipelines

CI format guarantees stable key=value output for parsing:

✓ VERDICT: PASS

--- CI SUMMARY ---
source=demos/react-demo-app/src/App.tsx
profile=ci
verdict=PASS
ok=true
timestamp=2026-01-05T15:28:19.046Z
git_sha=8a2a519
bundle_path=design-materializations/demos__react-demo-app__src__App.figma-reconcile.json
dashboard_info=1
dashboard_warn=1
dashboard_fail=0
stability_score=88

--- STEPS ---
status=ok
index=ok
timeline=ok
drift=ok
dashboard=ok

reason=All steps completed successfully

Exit Code Contract

Exit CodeMeaningCI Behavior
0PASS or WARNCI passes
1FAIL (strict violation)CI fails
2Usage or configuration errorCI fails

Important: WARN never fails CI. Exit code 0 includes both PASS and WARN verdicts.

Verdict Semantics

VerdictConditionExit Code
PASSAll steps OK, no warnings0
WARNSteps OK but: PARTIAL drift comparison, missing artifacts, dashboard warn > 00
FAILStrict mode AND: dashboard fail > 0, INVALID drift, step failure1

Artifact Model

All artifacts are written to design-materializations/ with deterministic naming:

design-materializations/<source-path>.figma-<artifact-type>.json

Where <source-path> has / replaced with __ and the file extension removed.

Example: demos/react-demo-app/src/App.tsxdemos__react-demo-app__src__App

Artifact Types by Phase:

ArtifactPhaseDescription
.figma-delta.json12AFigma → Code deltas detected
.figma-delta-suggestions.json12BSuggested delta applications
.figma-conflicts.json12DConflict analysis
.figma-resolution-plan.json12EGuided resolution decisions
.figma-resolution-apply.json12FResolution application results
.figma-verification.json12GPost-apply verification
.figma-rollback-preview.json12IRollback preview
.figma-reconciliation-status.json12JOverall status
.figma-run-index.json13ARun artifact index
.figma-run-ledger.json13BAppend-only timeline ledger
.figma-drift-diff.json13CRun-to-run drift diff
.figma-drift-dashboard.json13DDrift summary dashboard
.figma-reconcile.json14ABundle artifact (links all outputs)

Bundle Artifact (figma-reconcile.json):

  • Always written in CI profile (alwaysWriteBundle=true)
  • Contains: version, timestamp, repoRoot, sourceFile, profile, mode, steps, artifacts, overall verdict
  • Includes git SHA for attribution
  • Provides deterministic naming for CI artifact upload

CLI Reference

Usage: figma:reconcile <source-file> [options]

Arguments:
  <source-file>           Source file to reconcile (e.g., demos/react-demo-app/src/App.tsx)

Options:
  --profile <name>        Profile preset: local, record, ci (default: local)
  --repo-root <path>      Repository root (default: auto-detect)
  --format <format>       Output format: human, json, ci (default: human)
  --json                  Output JSON format (shorthand for --format json)
  --write                 Write bundle artifact (overrides profile default)
  --no-write              Do not write bundle artifact (overrides profile default)
  --record                Record timeline run (overrides profile, requires env)
  --strict                Enable strict mode (overrides profile default)
  --verbose, -v           Show step invocations and discovery
  --limit <n>             Limit for dashboard/drift runs (default: 10)
  --help, -h              Show this help message

Environment Variables

VariableDefaultDescription
RECONCILIATION_TIMELINE_ONfalseEnable timeline recording (required for --record)
RECONCILIATION_CI_WINDOW5Window size for CI trend analysis
RECONCILIATION_DASHBOARD_FAIL_SCORE50Score threshold for FAIL verdict
RECONCILIATION_DASHBOARD_WARN_SCORE75Score threshold for WARN verdict

Examples

# Local inspection (default)
pnpm --filter @aesthetic-function/watcher figma:reconcile demos/react-demo-app/src/App.tsx

# CI mode with CI-friendly output
pnpm --filter @aesthetic-function/watcher figma:reconcile demos/react-demo-app/src/App.tsx --profile ci --format ci

# Record a run (requires RECONCILIATION_TIMELINE_ON=true)
RECONCILIATION_TIMELINE_ON=true pnpm --filter @aesthetic-function/watcher figma:reconcile demos/react-demo-app/src/App.tsx --profile record

# JSON output for debugging
pnpm --filter @aesthetic-function/watcher figma:reconcile demos/react-demo-app/src/App.tsx --json --verbose

# From watcher directory
cd packages/watcher
pnpm figma:reconcile demos/react-demo-app/src/App.tsx --profile ci --format ci

Quick Start (Demo)

Prerequisites

  • Node.js ≥ 18
  • pnpm ≥ 9

Install

pnpm install

Start the Server

pnpm dev:server

Server runs at http://localhost:3001 with:

  • WebSocket: ws://localhost:3001/ws
  • HTTP polling: GET /poll
  • Test endpoint: POST /test

Start the Watcher

pnpm dev:watcher

Watches demos/react-demo-app/src/ for changes by default.

Add @figma Markers

In any .tsx or .ts file:

// @figma node=<FigmaNodeName> text="<Text>" fill=<TokenOrHex>

Examples:

// @figma node=LoginButton text="Sign In" fill=Primary/Blue500
// @figma node=ErrorMessage text="Something went wrong" fill=#EF4444
// @figma node=CardBackground fill=Neutral/Gray50

Human Test Walkthrough

1. Start the Server

pnpm dev:server

2. Start the Watcher

pnpm dev:watcher

3. Expose Server via Tunnel (for Figma access)

pnpm tunnel

Copy the tunnel URL (e.g., https://xxx.trycloudflare.com).

4. Connect Figma Plugin

  1. In Figma, run the Aesthetic Function plugin
  2. Enter the tunnel URL
  3. Click "Connect"
  4. Status should show "Connected"

5. Code → Figma Sync

  1. Edit a file in demos/react-demo-app/src/ with @figma markers
  2. Save the file
  3. Watch the Figma document update in real-time

6. Figma → Code Capture

  1. In Figma, select a node (text or frame)
  2. In the plugin, click "Send Selection"
  3. The server logs DESIGN_CHANGE and writes to design-overrides.json

7. Reconciliation Behavior

On the next file save:

  • The watcher loads overrides from design-overrides.json
  • Override values are merged with code-derived intents
  • The merged result is sent to Figma
  • Watcher logs show: Overrides: precedence=always applied=N ignored=M

8. Resetting Overrides

To clear all overrides and let code take precedence:

rm design-overrides.json

Or disable overrides temporarily:

USE_OVERRIDES=false pnpm dev:watcher

CI Integration (Phase 14D)

This section covers GitHub Actions integration. For the complete figma:reconcile reference, see Reconciliation System (Phase 14).

Required Command

pnpm --filter @aesthetic-function/watcher figma:reconcile <source-file> --profile ci --format ci --verbose

GitHub Actions Workflow

The workflow is defined in .github/workflows/figma-reconcile-ci.yml.

Triggers:

  • Push to main branch
  • Pull requests targeting main

Job Outputs (available via $GITHUB_OUTPUT):

  • verdict — PASS, WARN, or FAIL
  • bundle_path — Path to the reconcile bundle artifact
  • stability_score — Stability score (0-100)
  • git_sha — Git commit SHA for traceability

Uploaded Artifacts:

  • figma-reconcile-<run_id>-<sha> — Always uploaded (bundle artifact)
  • figma-reconcile-all-<run_id>-<sha> — Optionally uploaded (all materializations)

Running CI Locally

# From repo root
pnpm --filter @aesthetic-function/watcher figma:reconcile demos/react-demo-app/src/App.tsx --profile ci --format ci --verbose

# Or from watcher directory
cd packages/watcher
pnpm figma:reconcile demos/react-demo-app/src/App.tsx --profile ci --format ci --verbose

Interpreting CI Results

VerdictExit CodeCI BehaviorAction
PASS0PassesMerge with confidence
WARN0PassesReview warnings, but CI passes
FAIL1FailsCI blocks merge; investigate dashboard fail count or INVALID drift

WARN conditions (CI still passes):

  • PARTIAL drift comparison (missing apply/verify artifacts)
  • Dashboard warn count > 0
  • Cold-start conditions (see below)

FAIL conditions (CI blocks merge):

  • Dashboard fail count > 0
  • INVALID drift classification
  • Any step failure in strict mode (excluding cold-start skips)

Cold-Start Handling (Phase 14D.1)

On fresh checkouts or first CI runs, the run ledger (design-materializations/<file>.figma-run-ledger.json) may not exist. This is a normal condition when:

  • Running CI for the first time on a new component
  • Cloning a repo where materializations were gitignored
  • Adding a new source file to the reconciliation matrix

Cold-start behavior:

  • Drift and Dashboard steps are skipped (not failed)
  • Overall verdict is WARN (exit code 0, CI passes)
  • Reason: "Cold-start: skipped drift, dashboard (no ledger data yet)"

Example CI output (cold-start):

⚠ VERDICT: WARN
--- CI SUMMARY ---
source=demos/react-demo-app/src/Card.tsx
profile=ci
verdict=WARN
ok=true
--- STEPS ---
status=ok
index=ok
timeline=ok
drift=ok
dashboard=ok
reason=Cold-start: skipped drift, dashboard (no ledger data yet)
Exit code: 0

Resolution: Run figma:reconcile with --record to populate the ledger. After 2+ recorded runs, drift and dashboard steps will execute normally.


Multi-Source CI (Phase 14F)

Phase 14F extends single-file reconciliation to multiple sources using GitHub Actions matrix strategy. This enables running figma:reconcile across all component files in parallel with aggregated verdict.

Source Discovery

Sources can be specified via:

  1. Manifest file (reconcile.sources.json) — Explicit list of files
  2. Glob patterns — Match files by pattern (e.g., **/*.tsx)
  3. CLI arguments — Direct --source flags

Discovery order: explicit > glob > manifest > default glob (**/*.tsx)

Manifest Format

{
  "version": 1,
  "sources": [
    "demos/react-demo-app/src/App.tsx",
    "demos/react-demo-app/src/Card.tsx"
  ],
  "ignore": [
    "**/internal/**"
  ]
}

CLI Commands

# Discover all sources (outputs JSON)
pnpm figma:sources

# With custom glob
pnpm figma:sources --glob "src/**/*.tsx"

# Get chunk 0 for matrix job
pnpm figma:sources --chunk-size 5 --chunk-index 0

# Output matrix indices for GitHub Actions
pnpm figma:sources --matrix-indices --chunk-size 10

# Just count
pnpm figma:sources --output count

Chunking

Sources are chunked for parallel matrix execution:

  • Default chunk size: 10
  • Deterministic ordering (lexicographic sort)
  • Balanced distribution (avoids tiny final chunks)

Example: 27 sources with chunk size 10 → 3 chunks of [9, 9, 9]

Matrix Workflow

The matrix workflow is defined in .github/workflows/figma-reconcile-ci-matrix.yml.

Jobs:

  1. discover — Find sources, calculate chunks, output matrix indices
  2. reconcile — Matrix job running reconcile for each chunk
  3. aggregate — Combine verdicts, produce overall PASS/WARN/FAIL

Triggers:

  • Pull requests touching .tsx files
  • Manual workflow_dispatch with glob/chunk-size inputs

Aggregation

Verdicts are aggregated with precedence:

  • FAIL > WARN > PASS
  • Any FAIL → overall FAIL
  • Any WARN (no FAIL) → overall WARN
  • All PASS → overall PASS

CLI Reference

FlagDescriptionDefault
--repo-root <path>Repository rootcwd
--glob <pattern>Glob pattern for sources**/*.tsx
--manifest <path>Manifest file pathreconcile.sources.json
--source <path>Explicit source (repeatable)
--ignore <pattern>Ignore pattern (repeatable)node_modules, dist, etc.
--chunk-size <n>Sources per chunk10
--chunk-index <n>Get specific chunk
--matrix-indicesOutput chunk indices onlyfalse
--output <format>Output format: json, list, countjson

Module Structure

packages/watcher/src/reconciliationSources/
├── types.ts        # Type definitions
├── discover.ts     # Source discovery logic
├── chunk.ts        # Chunking logic
├── aggregate.ts    # Verdict aggregation
├── cliSources.ts   # CLI entry point
├── index.ts        # Public exports
└── __tests__/      # Unit tests

Design Tokens

The watcher resolves semantic tokens to hex values:

TokenHex
Primary/Blue500#3B82F6
Success/Green500#10B981
Error/Red500#EF4444
Warning/Yellow500#F59E0B
Neutral/Gray50#F9FAFB
Neutral/Gray900#111827

Raw hex values (e.g., #FF0000) pass through unchanged.


Audit Trail

The server can log all broadcast operations to sync-log.md for debugging and traceability.

Enable Audit Logging

ENABLE_AUDIT_LOG=true pnpm dev:server

Log Format

## [2025-01-15T10:30:00.000Z] [req-abc123] type=APPLY_OPERATIONS source=watcher
file=demos/react-demo-app/src/Card.tsx
ops=2
- node="LoginButton" action=SET_FILL value="#3B82F6"
- node="LoginButton" action=SET_TEXT value="Sign In"

Behavior

  • Default: Audit logging is disabled
  • Async: Logging is non-blocking with an in-memory queue (100ms flush interval)
  • Graceful shutdown: Queue is flushed on SIGINT/SIGTERM

Git Hygiene

The following files are intentionally gitignored as runtime artifacts:

File/DirectoryPurpose
design-overrides.jsonCaptured design changes (local state)
component-map.jsonStable Figma node ID mappings (Phase 8C)
sync-log.mdAudit trail (debug artifact)
design-materializations/Generated patch artifacts (Phase 5B)

These files are machine-local and should not be committed by default. Each developer/environment maintains their own override state. However, component-map.json can optionally be committed for team sharing of stable ID mappings.


Project Structure

aesthetic-function/
├── packages/
│   ├── shared/           # Protocol definitions, shared types
│   ├── watcher/          # Reconciliation engine, AST analysis, adapters
│   │   └── src/
│   │       ├── analyze/  # LLM-based intent analyzer
│   │       ├── ast/      # Babel-based AST parser + feasibility analysis
│   │       ├── parse/    # @figma marker parser
│   │       ├── reconcile/# Override reconciliation + precedence policy
│   │       ├── materialize/ # Design → Code materialization + AST writes
│   │       ├── tokens/   # Design token resolution
│   │       ├── transform/# IntentModel → FigmaOps
│   │       ├── designAdapter/        # Figma + Storybook MCP adapters
│   │       └── crossSurfaceDrift/    # Cross-surface drift analysis engine
│   ├── server/           # WebSocket/HTTP relay, audit logging
│   ├── figma-plugin/     # Figma sandbox plugin (mutation executor)
│   └── cli/              # `af` CLI control surface
├── demos/
│   ├── react-demo-app/   # React reference demo — App.tsx with @figma markers, Storybook stories
│   │   ├── src/
│   │   │   ├── App.tsx       # Sign-in composition panel — sole source of @figma markers
│   │   │   ├── Button.tsx    # SDS-faithful Button (Primary variant, no markers)
│   │   │   ├── Input.tsx     # SDS-faithful Input Field (no markers)
│   │   │   └── Card.tsx      # SDS-faithful Card container (no markers)
│   │   └── .storybook/       # Storybook config (addon-mcp enabled, Components/Button|Input|Card)
│   └── vue-demo-app/     # Vue 3 reference demo — App.vue with @figma markers (read-only analyzer)
│       └── src/
│           ├── App.vue       # Sign-in panel — mirrors React demo markers
│           └── components/   # Card.vue, Button.vue, Input.vue
├── docs/                 # Reference documentation (this file and others)
├── .github/
│   ├── workflows/        # CI workflows (reconciliation matrix)
│   └── instructions/     # AI agent instructions
├── design-overrides.json # (gitignored) Captured design changes
├── component-map.json    # (gitignored) Stable Figma node ID mappings
├── design-materializations/ # (gitignored) Patch artifacts
├── sync-log.md           # (gitignored) Audit trail
└── README.md

Scripts Reference

CommandDescription
pnpm dev:serverStart the relay server
pnpm dev:watcherStart the file watcher
pnpm devStart all packages in parallel
pnpm buildBuild all packages
pnpm testRun all tests
pnpm typecheckRun TypeScript checks
pnpm tunnelExpose server via cloudflared
pnpm test:sendSend test operations
pnpm demo:serverStart server with pretty logging
pnpm demo:watcherStart watcher with trace output
pnpm demo:featureRun feature orchestrator CLI
pnpm demo:fastStart server + watcher concurrently
pnpm demo:tunnelExpose server for Figma access
pnpm --filter @aesthetic-function/watcher ast:report <file>Run AST diff report
pnpm --filter @aesthetic-function/watcher feasibility:report <file>Run feasibility analysis

Reconciliation Commands (Phase 12-14)

CommandDescription
pnpm --filter @aesthetic-function/watcher figma:reconcile <file>Primary entry point — Run full reconciliation analysis
pnpm --filter @aesthetic-function/watcher figma:sourcesDiscover sources for multi-file reconciliation (Phase 14F)
pnpm --filter @aesthetic-function/watcher figma:status <file>Compute reconciliation status (Phase 12J)
pnpm --filter @aesthetic-function/watcher figma:index <file>Index existing artifacts (Phase 13A)
pnpm --filter @aesthetic-function/watcher figma:timeline <file>Show/record timeline ledger (Phase 13B)
pnpm --filter @aesthetic-function/watcher figma:drift <file>Compute drift diffs (Phase 13C)
pnpm --filter @aesthetic-function/watcher figma:dashboard <file>Generate drift dashboard (Phase 13D)
pnpm --filter @aesthetic-function/watcher figma:project-dashboard <dir>Project-level dashboard (Phase 13E)
pnpm --filter @aesthetic-function/watcher figma:ci <dir>CI gate summary (Phase 13F)

Note: figma:reconcile orchestrates all Phase 12-13 steps in sequence. Use individual commands only when debugging specific steps.

Design Adapter Commands (Phase 16A)

CommandDescription
af design pullPull full design data (tokens + components + styles)
af design tokensPull and normalize design tokens to canonical vocabulary
af design inspect <name>Inspect a specific design component
af design inspect --allInspect all design components

Figma Console MCP Adapter Commands (Phase 16B)

CommandDescription
af design screenshotCapture a design screenshot (PNG)
af design screenshot --node <id>Screenshot a specific Figma node
af design screenshot --out file.pngSave screenshot to file
af design componentList all design components
af design component <name>Inspect a specific component

All design adapter commands are read-only — they do not write to Figma or trigger reconciliation. Use --json for machine-readable output, --verbose for trace details.

Phase 16B: Figma Console MCP Adapter (Read-Only)

Phase 16B integrates southleft/figma-console-mcp as a constrained, read-only AF design adapter using the Model Context Protocol (MCP).

Integration boundary — what talks to what:

┌─────────────────────────────────────────────────────────────────────┐
│  AF Watcher (Node.js)                                               │
│                                                                     │
│  FigmaConsoleMCPAdapter                                             │
│    │                                                                │
│    ├─── MCP Client (@modelcontextprotocol/sdk)                      │
│    │      │                                                         │
│    │      ├─── [stdio] spawn `npx figma-console-mcp`  ──┐          │
│    │      │    (preferred: full 92+ tools available)     │          │
│    │      │                                              ▼          │
│    │      └─── [sse] connect to running MCP server  → figma-       │
│    │           (22 read-only tools in SSE mode)       console-mcp  │
│    │                                                     │          │
│    │                                                     ▼          │
│    │                                              Figma REST API    │
│    │                                                                │
│    └─── [rest-fallback] Direct Figma REST API calls                 │
│         (same data as figma-console-mcp, no MCP protocol)           │
└─────────────────────────────────────────────────────────────────────┘

Transport modes (in order of preference):

  1. stdio — AF spawns figma-console-mcp as a child process via npx figma-console-mcp and communicates over stdin/stdout using the MCP stdio transport.
  2. sse — AF connects to an already-running figma-console-mcp SSE server.
  3. rest-fallback — Direct Figma REST API calls when figma-console-mcp is unavailable.

Tool mapping (figma-console-mcp tool → AF adapter method):

MCP ToolAF MethodData
figma_get_variablesgetDesignTokens()Design variables/tokens
figma_get_file_datagetComponent(), getComponents(), getFileData()File tree, nodes, components
figma_get_stylesgetStyles()Published styles
figma_take_screenshotgetScreenshot()Visual capture (PNG)

getComponent(name) — node search behavior:

Searches the Figma file tree recursively across all pages. The MCP path uses depth=3 with verbosity='full' (the MCP tool's maximum depth); the REST fallback uses depth=8. Both are sufficient for typical file structures (components are at depth 2-3). The verbosity='full' parameter is required for the MCP path — the default 'summary' strips componentPropertyDefinitions and all visual properties from nodes.

If the MCP call fails, the adapter falls through to the REST API automatically (not caught by the outer error handler). This ensures Figma metadata is always extracted when credentials are valid.

Matches by exact name (case-insensitive) against any of these node types:

Figma typeAF DesignComponent.type
COMPONENTcomponent
COMPONENT_SETcomponent-set
INSTANCEinstance
FRAME, GROUP, SECTION, TEXTframe

The raw Figma type is preserved in properties.figmaType. A warning is added to the result when the matched node is not a COMPONENT or COMPONENT_SET. getComponents() (list all) retains its original behavior and only returns published COMPONENT/COMPONENT_SET nodes.

MCP response shape: The figma-console MCP figma_get_file_data tool returns components as a count (number), not as a Record<string, ComponentMeta> like the REST API. The adapter guards against this with a type check before using the metadata map. componentPropertyDefinitions is extracted inline from the document tree nodes (available in 'full' verbosity) and enriched from top-level componentSets/components metadata when available.

Architecture:

  • AF acts as an MCP client connecting to figma-console-mcp as the MCP server.
  • Read-only. No write operations. No mutations.
  • AF remains the sole mutation authority (watcher → server → plugin).
  • Default-deny MCP tool policy: only the 14 tools in ALLOWED_MCP_TOOLS may be invoked.
  • All 30+ write tools are explicitly blocked (see BLOCKED_MCP_TOOLS registry).
  • Unclassified tools (not in either list) are also rejected.
  • Graceful degradation: if figma-console-mcp is unavailable, AF falls back to REST.
  • Every adapter result includes transport: metadata for observability.

Blocked capabilities (by AF architecture, not by omission):

  • writeDesign — Design creation is AF plugin's job
  • writeVariables — Token authority belongs to AF reconciliation engine
  • executeDesignCodefigma_execute bypasses AF control plane
  • writeVariableCollections — Collection CRUD is AF's responsibility
  • cloudWriteRelayfigma_pair_plugin would create a second control plane
  • writeFigJam / writeSlides — Outside AF scope

Configuration:

export FIGMA_ACCESS_TOKEN=figd_...
export FIGMA_FILE_KEY=your-file-key

Dependencies:

  • @modelcontextprotocol/sdk — MCP client SDK (in watcher package)
  • figma-console-mcp — External MCP server (not a dependency; spawned via npx or connected via SSE)

Transport validation status:

  • rest-fallback — Fully tested (all 47 adapter tests exercise this path).
  • stdio / sse — Implemented, typechecked, config-tested. Live end-to-end validation against a real figma-console-mcp instance is an integration-time check (not part of unit test suite).

Phase 16B Compliance Summary:

RequirementStatusFile(s)Notes
Real figma-console-mcp integration via MCP protocolfigmaConsoleMCPAdapter.tsUses @modelcontextprotocol/sdk Client + StdioClientTransport to spawn/connect to figma-console-mcp
Read-only constraint (no write operations)BLOCKED_MCP_TOOLS (30+ entries), callMCPTool() guardDefault-deny: blocked tools rejected explicitly, unclassified tools rejected as unknown
Allowed read tools enforced as strict allow-listALLOWED_MCP_TOOLS (14 entries), callMCPTool() guardOnly tools in ALLOWED_TOOL_NAMES are invoked; all others are rejected before client.callTool()
REST fallback when figma-console-mcp unavailablefigmaConsoleMCPAdapter.tsEach method tries MCP first, falls back to direct Figma REST API
Transport observability (which path was used)getActiveTransport(), result warningsEvery result includes transport: mcp-stdio, mcp-sse, or rest-fallback
Integration boundary documentedREADME.md Phase 16B sectionASCII diagram showing AF → MCP → figma-console-mcp → Figma REST
README contradictions resolvedREADME.mdRoadmap table now lists 16A/16B as ✅; "Not Implemented" table updated
Tests pass (all packages)1842 watcher + 50 CLI tests47 adapter-specific tests (37 original + 10 new transport/MCP tests)
Typecheck clean (all packages)shared, server, cli, watcherZero errors across all packages
Branch: no merge, no 16Cfeat/16b-figma-console-mcp-readonly-adapterAll corrections on same branch
Deviation: MCP SDK in watcher (not separate package)⚠️packages/watcher/package.jsonplan.md suggested packages/adapters/figma-mcp/. Kept in watcher for simplicity; can extract later.

Surface Classification Metadata (Phase 16A Extension)

Phase 16A.1 adds a taxonomy/descriptor layer for adapters. This allows AF to categorize external UI surfaces (design tools, runtime views, generators, inspection sources) without introducing any new authority into reconciliation.

This is a classification layer only. It does not affect reconciliation logic, precedence, execution order, or mutation paths.

Design Principle

Surface metadata consists of four independent dimensions that must not be conflated:

DimensionTypeDescription
Surface TypeSurfaceTypeWhat kind of UI surface: design, runtime, generation, inspection
Access ModeAccessModeMutation capability: read-only, no-mutation, internal-write
Authority RoleAuthorityRoleWhether AF treats data as authoritative: external-non-authoritative, internal-authoritative
StabilityStabilityLevelData stability: canonical, derived, observational

Interface

interface SurfaceMetadata {
  surfaceType: SurfaceType;
  accessMode: AccessMode;
  authorityRole: AuthorityRole;
  stability: StabilityLevel;
}

The surfaceMetadata field is optional on both DesignAdapter and SemanticAdapter interfaces to preserve backward compatibility.

Current Classifications

AdapterSurface TypeAccess ModeAuthority RoleStability
FigmaMCPAdapterdesignread-onlyexternal-non-authoritativeobservational
FigmaConsoleMCPAdapterdesignread-onlyexternal-non-authoritativeobservational
VuetifySemanticAdapterruntimeno-mutationexternal-non-authoritativederived
AntdSemanticAdapterruntimeno-mutationexternal-non-authoritativederived
StorybookAdapter (stub)runtimeread-onlyexternal-non-authoritativeobservational

Query Helpers

// Design adapter registry
getDesignAdaptersBySurface('design')    // → [FigmaMCPAdapter, FigmaConsoleMCPAdapter]
getDesignAdaptersBySurface('runtime')   // → [StorybookAdapter]

// Semantic adapter registry
getSemanticAdaptersBySurface('runtime') // → [VuetifySemanticAdapter, AntdSemanticAdapter]

Adapters without surfaceMetadata are excluded from surface-type queries.

Explicit Non-Goals

The following are internal reconciliation inputs and MUST NOT be modeled as adapters:

  • design-overrides.json
  • Marker extraction
  • AST-derived values

These remain governed exclusively by the frozen reconciliation precedence: override > marker > ast > code.

Future Extensibility

New adapters (Penpot, UXPilot, Storybook, etc.) can be classified using the same four dimensions without refactoring:

// Example: future Penpot adapter
readonly surfaceMetadata: SurfaceMetadata = {
  surfaceType: 'design',
  accessMode: 'read-only',
  authorityRole: 'external-non-authoritative',
  stability: 'observational',
};

Cross-Surface Drift: Metadata Extraction (Phase 16C Enhancement)

Cross-surface drift analysis compares component metadata across three surfaces: Figma, Storybook, and Code. Each surface extracts props and variants independently, then analyze.ts compares them.

Figma Surface

The primary metadata source is componentPropertyDefinitions (CPD) from the Figma REST API. When a component or component set defines property definitions, findNodeByName() in the Figma adapter extracts them and passes them through normalization:

Figma REST API → findNodeByName() → NormalizedDesignComponent.componentPropertyDefinitions → buildFigmaSnapshot()

CPD entries are classified by type:

  • VARIANT: Creates a variant axis (e.g., State with options ['Default', 'Hover']) and a corresponding prop
  • TEXT: Creates a text prop (property names like "Label#12:34" are cleaned to "Label")
  • BOOLEAN: Creates a boolean prop
  • INSTANCE_SWAP: Creates an instance-swap prop

When CPD is absent (older Figma files or non-component nodes), buildFigmaSnapshot() falls back to children-based variant inference from normalized variant names.

Storybook Surface

Props are extracted from reactDocgen metadata via extractProps() in storybookAdapter.ts. Variants are inferred by inferVariantAxes() from prop type unions.

Story name fallback: When inferVariantAxes() yields no variants but named stories exist, story names are used as variant candidates. A skip-list filters non-variant names: docs, overview, page, playground, template.

Code Surface

findCodeSurface() in cliCrossSurfaceDrift.ts extracts metadata from TypeScript source:

  1. Props: Regex matches both destructured parameters ({ variant, label }) and interface/type declarations (interface ButtonProps { variant?: ...; label?: ... })
  2. Variants: Line-based union extraction finds all quoted values in union types ('Default' | 'Hover')

Demo Fixture

demos/react-demo-app/src/DemoButton.tsx and demos/react-demo-app/src/stories/DemoButton.stories.tsx provide a minimal component with typed props and matching Storybook stories for verifying drift analysis end-to-end.

Cross-Surface Drift: Normalization Layer (Phase 16D)

Different surfaces use different names for the same concepts, causing false-positive drift findings. Phase 16D adds a deterministic, configurable normalization layer that aligns equivalent concepts before comparison.

Problem

SurfaceVariant AxisText PropLayout Props
FigmaState (VARIANT)text (TEXT)fills, cornerRadius, width, height
Storybookvariantlabel(none)
Codevariantlabel(none)

Without normalization, State vs variant and text vs label appear as false drift, and Figma-only layout properties generate noise.

Solution

normalize.ts in crossSurfaceDrift/ applies three passes to each surface snapshot before comparison:

  1. Design-only filtering (Figma only): Removes layout/visual properties (fills, cornerRadius, width, height, padding, gap, fontSize, fontWeight, textContent) that have no API-level counterpart
  2. Alias normalization (all surfaces): Renames equivalent prop names to a canonical form using a configurable alias map
  3. Deduplication: Merges props that collide after renaming

Default alias rules:

CanonicalAliases
variantstate, variant
labeltext, label

Configuration

Normalization is configured via NormalizationConfig (defined in @aesthetic-function/shared/crossSurfaceDrift):

interface NormalizationConfig {
  propAliases: Array<{ canonical: string; aliases: string[] }>;
  designOnlyFields: { names: string[]; strategy: 'exclude' | 'tag' };
}

The default config (DEFAULT_NORMALIZATION_CONFIG in normalize.ts) handles the DemoButton demo. Custom configs can be passed via DriftAnalysisOptions.normalizationConfig for per-project overrides.

Traceability

Normalization is fully explainable:

  • SurfaceProp.normalizedFrom: When a prop is renamed (e.g., Statevariant), the original name is preserved in normalizedFrom
  • CrossSurfaceDriftReport.normalization: The report includes metadata listing all applied alias rules and excluded design-only props, with surface attribution

Design Decisions

  • Normalization runs before comparison, transforming snapshots rather than patching findings. The existing comparePropInventory and compareVariantCoverage functions require zero changes.
  • Design-only filtering applies only to the Figma surface — if Code/Storybook happens to have a prop named fills, it is not filtered.
  • Alias matching is case-insensitive but the canonical name is always lowercase.
  • The strategy: 'tag' option (alternative to 'exclude') is defined but not yet implemented — it would keep design-only props but mark them for separate "visual drift" reporting.

Protocol Version

Current: 0.1.0

All messages include a protocolVersion field for compatibility checking.