Skip to content

AI WF2Des — I/O Definition

Captured component identity and optional content (September 2026)

Candidate deduplication and metadata adoption require the same pinned snapshot identity (or the same publish key for legacy unpinned records), not a shared name. Different source snapshots keep independent variant axes, keys and defaults. Hash-verified preflight derives root BOOLEAN definitions, text-path visibility bindings and bounded native navigation-action evidence in memory. INSTANCE values are marked as observed, not master defaults, and are sent explicitly when that component is selected. No database migration or registry rewrite is required; the existing component_properties spec field is reused.

An explicit opacity=0 on a node or ancestor excludes its text and native actions from visible evidence, even when a BOOLEAN visibility binding is true. Positive opacity alone does not imply hidden content.

Only proven visible slots can supply duplicate navigation after optional overrides. Hiding source-authored default copy is rejected. A single label-plus-glyph navigation action may be covered only by a captured matching native action; text alone cannot justify deleting artwork. That proof must account for every non-text leaf as the action, its sole glyph, or an explicitly proven empty layout cell. Additional opaque instances/controls inside or outside the button block coverage even when ink flags are absent or false; missing ink evidence is not emptiness. When a native action supplies coverage, its already-proven true BOOLEAN visibility bindings are pinned in the owner's existing component_properties so renderer sample cleanup cannot hide it. Hidden, unknown or ambiguous actions are never enabled to justify coverage. Bound action deduplication abstains if no serializable owner decision can retain those bindings. Atomic, textless artwork can use a non-placeholder component whose primary or secondary semantic kind is media. Compound content remains subject to the scope guards.

A footer before a separate compact full-width action strip may be promoted only when one captured optional-content configuration exactly covers its copy and a neighboring native navigation action. The strip is retained independently; arbitrary following content, fields, unaccounted artwork, ambiguous configurations and hidden actions do not qualify. Chrome deduplication uses the selected variant's Boolean-visible slots, not specimen defaults.

A full-width top bar classified as navigation may use native header ownership when its entire source content is one compact brand cluster already selected as captured brand media. Promotion requires one unique, viewport-compatible header variant with captured brand/header semantics and hash-verified structure containing only one painted graphic, without visible text, independent controls or uncertain visibility bindings. Other source copy/actions, ambiguous variants, explicit unmatched decisions and parent-specific styling retain their source composition. An otherwise unstyled compose decision may be reconciled only under this complete single-brand proof; source pixel height alone does not demand wireframe chrome. This is compound-region ownership, not a new wordmark/text-equivalence claim; asset names alone never authorize it. Evidence is derived in memory; no DB migration or schema addition.

Source-fidelity and evidence guards (September 2026)

The whole-screen planner (screen-review@3) payload includes required_control_owners: bounded source units with their exact member IDs and candidate aliases. These require a painted single-control surface, one short label or one positively identified atomic control glyph, and a compatible registered non-placeholder control/selection candidate. Table cells, content cards, compound controls, neutral decorative descendants inside a known control, unknown extra artwork and unrelated geometry do not qualify. Source state/overrides are carried into the obligation; no match or state is inferred from a name or color. An owner must receive an explicit decision unless an enclosing selected component already supplies it. A descendant-only decision cannot satisfy the owner. Sparse initial-plan review also checks omitted owners, within its existing validation/request budget; it does not launch an extra paid completion loop. Explicit conservative fallback remains valid and preserves source content. Candidate hints never auto-select a component or bypass grounding guards. For these positively identified owners, a composed fallback retains the source height as a minimum on its own emitted surface only. Ancestor/table tracks and native instance dimensions are unchanged; a native replacement continues to own its intrinsic geometry.

Before paid selection, pinned component snapshots must be readable and match their canonical registry lineage hashes. Missing, unsupported, malformed or mismatched evidence fails with an actionable instruction to resync/sweep the component library and start a new generation. Transient storage/access failures remain retryable. Metadata-only capture and design-rule extraction cannot repair missing raw component snapshots.

Free-form emission order and layout mode use the same retained sibling set. When removing a redundant/annotation node removes the last overlap, retained nodes are sorted on their inferred flow axis, with aligned gaps and unchanged layer-path identities. Genuine remaining overlays keep coordinates/z-order; explicit auto-layout keeps authored order.

An optional table_layout: {rows: [{cell_paths, min_height, gap_after?}]} on existing layout_frame/compose nodes defines shared table-row tracks. It is omitted when absent. Only source-grounded table regions with horizontal columns, vertical cell flow, aligned row geometry and complete cell coverage qualify; ambiguous grids and row spans keep source layout. The renderer uses the greatest source minimum/intrinsic cell height for each row, aligns columns through wrappers, and does not stretch native component artwork.

Each row may additionally carry cell_surface_paths: {cell_path: surface_path}, omitted when empty. This names a composed cell's painted surface behind transparent single-child, zero-inset flow wrappers whose source bounds exactly match the cell. The renderer verifies the descendant chain before any table mutation and grows those FRAME surfaces with the row; it never stretches native instances, nested icons, unrelated cards or intentionally inset content. Unmarked cells retain the existing behavior; text can still grow beyond the minimum.

A single table_layout.rows entry may instead address every direct cell of an aligned horizontal table row (including headers). Partial or reordered rows are rejected before mutation. Composed cells grow to the shared height; native instances keep their geometry. auto_layout.min_height is an optional positive source control-height floor, not a cap: wrapped content can grow. It applies only to composed atomic controls, not whole sections. Optional stroke_edges: [top, right, bottom, left] preserves partial container borders; omission retains the historical four-edge behavior. Covered sections contribute no occupied interval to inter-sibling gaps; uncovered whitespace and annotation gaps remain unchanged.

For a registry button, tab, or navigation control with exactly one visible direct TEXT child and a successfully bound source label, the renderer disables inherited specimen ellipsis, retains width/type/inset, and permits vertical growth. Compound controls and source clones are unchanged. Explicitly placeholder-labelled textless artwork is retained but reported as unmatched, not verified as a finished replacement. The label is a warning signal, never authority to delete artwork.

Selection consistency is checked again during deterministic assembly. VARIANT/TEXT property definitions do not prove that surplus content can be hidden; only BOOLEAN visibility controls can defer that check. Numeric value/unit splitting requires sibling slots, a numeric-only value, a nonnumeric unit, and no existing exact whole-string slot match. Identical source controls in one local group must not acquire contradictory selection states; ambiguous conflicts preserve source appearance. Repeated actions may reuse a unique native match only with identical copy, source style/state/structure, and a captured compatible size variant. Textless whole-control replacements without verified represented-text bindings preserve the source instead of adding a duplicate label beside native artwork. These checks add no database fields or model calls.

Same-file wireframe fallback clones the actual source instance (including visibility, nested overrides and state), not its component master's defaults. Selection guards reject passive table content substituted with actions and incompatible repeated-control cardinality; explicit source state outranks component defaults. Missing evidence never authorizes guessing.

Repeated painted table cells are checked as aligned row families before coverage and during deterministic stitching. A captured compact component whose intrinsic height cannot cover the source cell's full painted track cannot replace that whole cell. The height check requires unchanged captured copy and no narrower live width; possible text growth is not treated as evidence of a surface deficit. Equivalent source master/state and surface groups cannot acquire different captured surface treatments merely from differing text labels. Only contradicted selections fall back with diagnostics; compatible native cells, real source-state differences, unpainted/inset badges and unrelated lists remain independently selectable. This guard does not invent missing sibling selections or infer a state from a numeral.

Rejecting a library choice must remain lossless: an atomic source instance or painted/opaque artwork leaf cannot become an empty composed frame, including when the model explicitly asks for composition. Such leaves use the existing source-instance/glyph preservation path and remain visibly flagged as fallback. Containers with decomposable children and plain text retain composition; true empty layout cells retain their source geometry. This is a shared grounding/rendering invariant, not an exception for a screen, node ID, or control name.

WFNode.has_graphic_overrides is additive source evidence, defaulting to false and omitted when false. It is true only for an INSTANCE whose explicit fills, strokes, opacity, inheritFillStyleId, or inheritStrokeStyleId override resolves by exact ID to a visible, painted graphic descendant. Extraction inspects bounded override/descendant sets and does not infer interaction state from gray paint, names, neighboring text, or geometry. Text-only, hidden, missing-target and geometry-only overrides do not set the flag. This preserves evidence otherwise lost when vectors collapse into the IR; old artifacts remain readable without migration. The parse processor release is generation_parse@0.4 / wf_parse@0.4. New runs re-extract older cached trees even when the source hash is unchanged, so missing override evidence is not silently treated as false. Existing immutable run artifacts are not rewritten; no DB migration or rule/component re-extraction is required.

Additive semantic section-layout contract (September 2026)

ScreenPlan.semantic_sections is an optional list, defaulting to empty, of bounded grouping requests {parent_node_id, member_node_ids, role}. The closed role vocabulary is content|form|table|list|actions|header|footer|navigation. IDs refer to the pinned source tree, not output Figma nodes. Members must be contiguous direct siblings, in source order, within one eligible vertical flow. Requests cannot cross grids, absolute-positioned regions or instance boundaries, reorder or drop content, or invent dimensions. Emitted member coverage and order must still match after component selection; invalid/ambiguous requests produce findings and retain source layout instead of granting the model arbitrary tree authority.

Generation saves a deterministic, validated SectionLayoutPlan under the immutable assembly_state.layout_plan. Its version is section-layout@1; its report includes requests, sections, joints, findings and viewport, recording accepted scope, layout authority and fallback decisions. Generation and rendered-output correction use the same application path. Sparse review changes must reconcile against the current selections and persist the resulting plan with the candidate's new assembly-state hash. An unchanged review must reproduce the same spec. A legacy state without layout_plan retains legacy assembly behavior during replay; it is not silently opted into new layout transforms.

ScreenGroupPolicy.viewport_width_px is optional and strictly positive when present. Only this explicitly stated output viewport may override source width. globals.base_viewport_px remains advisory; neither card/content width nor padding implies a viewport. Conflicting applicable policies fall back to source layout with a finding. Rule-backed section joints use the effective boundary spacing, with measured source gaps as fallback. Chrome edges, painted-container padding and component-internal geometry remain protected; the same inset must not be applied both as an outer margin and as inner padding.

A clipped structural wrapper may participate in section flow only when its explicit vertical layout and both source/emitted bounds prove contained, nonoverlapping content without artwork, rounded cropping or fixed/absolute sizing. The clip itself remains unchanged. Independently painted container insets remain protected. Where an applicable section-gap rule proves that neutral nested edge padding overstates the joint, the joint becomes the single spacing owner; unruled whitespace, ambiguous crops and component-internal padding are retained. Native registry instances reached through such proven flow use their own height, not an obsolete source spacer.

The materializer still accepts the existing five spec node kinds. The additive InstanceNode.height_authority="component" is optional and omitted otherwise. Within an eligible semantic vertical flow it lets the component's actual height determine flow, without adding a spacer for its former wireframe footprint. It does not authorize changing component internals or removing intentional source spacing; absent means existing behavior. New writes use index_schema_version=1.4 for the additive table-surface/control-artwork shape. Old documents remain readable; no destructive database migration or new SQS endpoint is required.

Additive rendered-output review contract (September 2026)

Generation pins inputs.llm.screen_review_model_id, screen_review_reasoning_effort and screen_review_enabled at parse time. Screenshot bytes are copied to a run-specific, SHA-256-addressed artifact. Assemble additionally saves spec_hash, assembly_state_hash and artifact_urls.assembly (exact source tree, selections, grouping, drops, slot roles, style bindings, confirmation, annotation markers and screen plan). Existing documents and specs remain readable; missing evidence yields cannot_verify, not guessed state.

Assembly state also pins exact validator section_ids. Review preserves that scope plus grounded additions; missing, malformed or foreign source IDs fail before the model call. Terminal publication I/O failures release only the owned lease for prompt redelivery. The plugin verifies current PG/result attempt and original spec hash before accepting a terminal review, covering the requeue window across the two stores.

Instance represented_text_bindings is optional, omitted when empty, and stores exact source text/identity plus selected component/variant/subtree/source-image/render hashes for a narrowly grounded wordmark representation. Only an actual-render review with successful current-session artwork inspection may create it. Existing hash-pinned bindings survive restitch only while source unit, exact text, component and variant still match. This metadata is not a visual pass and cannot authorize deleting arbitrary source copy.

The binding representation_kind also accepts control_artwork; omission still means wordmark. This authorizes one exact source TEXT label only when actual-render review positively observes that label in a selected whole control's painted artwork, successfully inspects its current pinned variant/subtree, and the source is a single-label control with no additional content-bearing unit. A source classified other additionally requires its own painted single-label flow surface and a registered non-placeholder control/selection kind; the same registered control semantics are required for explicitly classified source controls. Names, square geometry and empty text slots are not equivalence evidence. Hidden/visibility-bound artwork, extra source controls, changed copy, variants or hashes reject the binding. The represented copy remains in the instance metadata. Only the exact generated with_text wrapper holding that instance and bound leftover may collapse after successful binding, so source-control padding is not counted twice; unrelated source/table wrappers remain. Nonvisual initial assembly retains unproven source labels.

The authenticated, project-scoped backend endpoint POST /organizations/{organization_id}/projects/{project_id}/wf2des/{id}/render-review accepts attempt, baseSpecHash, outputNodeId, outputPngBase64, renderManifest, materializerReport and optional parentReviewId. It returns {reviewId}. Both the PG job identity/scope and current result attempt/hash are checked before writing evidence. The evidence is stored under {org}/{project}/wf2des/render-reviews/{encoded-job}/{attempt}/{reviewId}/. PNG limit: 4 MiB, edge 8000, area 32 million pixels. Manifest limit: 512 KiB / 5000 nodes. The manifest contains output_node_id, render_manifest (node/parent IDs, layer path, name, type, bounds and optional text) and the materializer diagnostics.

The wf2des-events queue receives event_type="render_review", tenant IDs, event_run_id, wf2des_id, attempt, base_spec_hash, output_node_id, render_url, render_hash, manifest_url, manifest_hash, and optional parent_review_id. event_run_id is rr- plus SHA-256 of canonical JSON [org,project,job,attempt,baseHash,renderHash,parentOrNull,outputNodeId,manifestHash]. The worker rechecks scope, identity, byte hashes, PNG structure and rooted manifest tree. Only hash-verified pinned registry snapshots/rules are available to component-inspection tools.

Event status uses the existing scoped poll. Terminal result contains review_id, wf2des_id, attempt, parent_review_id, base_spec_hash, output_node_id, verification_only, verdict (pass|needs_changes|cannot_verify) and findings. An initial review may additionally return candidate_spec, candidate_spec_hash, assembly_state_url and assembly_state_hash; inline candidate budget is 1 MiB. A parent review must be this same job/attempt's first correction. A second review is verification-only and never creates another candidate. A corrected spec is not a passed render. The plugin retains the original frame, places at most one adjacent candidate, and captures reinforcement only after the actual render passes. Unavailable, stale, rejected, timed-out and incomplete checks remain visibly unverified.

Actual-render review judges source content/function/state against pinned component and rule evidence, not pixel-identical wireframe styling. A native compound may reorganize covered navigation and legal copy; source-only controls, missing copy, wrong state, duplicates and unrequested functional content remain defects. Existing screen_plan.covered_sections records deterministic navigation ownership as well as grounded planner coverage. Sparse corrections preserve and revalidate these dependencies: changing/removing an owner or its visible variant/properties restores source navigation unless coverage is proven again. Legacy assembly drops recover ownership from their pinned source/selection evidence; unrelated annotation drops are not reinterpreted. No DB migration or new schema field.

Each finding keeps its optional legacy node_id and may additionally carry affected_node_ids: string[] (default empty, at most 64 entries). These are explicit IDs from the pinned source tree, never rendered-output IDs or IDs inferred from prose. Only warning/error findings authorize corrections. The union of their known, non-root IDs defines the existing bounded ancestor/descendant/repeated-family correction scope; unknown IDs and the screen root never widen it. Verification checks every valid affected node, so an existing multi-region issue is not labelled a new regression, but a mixed finding that also names an unrelated region remains a regression. Legacy singular findings and unscoped diagnostic-category comparison remain supported. No source or registry mutation is authorized by this additive review-artifact field.

Render reviews never overwrite a generation or its decision ledger, and emit no PG webhook. Single-flight event claims have a one-hour, tenant-scoped lease with a random owner token; terminal duplicates skip paid work, busy deliveries retry, and late owners cannot overwrite newer status. Status TTL is six hours; result/state artifacts are content-addressed. Document schema version is 1.4; this is additive and requires no destructive migration.

Shared spec additions: text font_family, line_height, line_height_pct, max_lines; instance component_properties (captured optional BOOLEAN values only in this workflow). Optional component_file_key on instances and source_file_key on the spec scope file-local Figma node IDs. Registry references carry their component file; source fallbacks and images carry the pinned wireframe file. Unknown/cross-file local IDs cannot be cloned by coincidence; published component keys are the cross-file resolution path. Absent optional fields remain omitted in legacy serialization, but unresolved legacy raw-source identities fail explicitly. In local development, where Figma does not expose figma.fileKey, the existing explicit user-entered current-file key is an operator assertion, not a key inferred from the spec. The live API key takes precedence when available; a conflicting assertion is rejected. For a wireframe fallback, resolve its source instance from lineage_wf_node_ids in the source file, then follow that instance's main component. Its master's raw node ID may belong to a remote library and must not be looked up as a local source-file node. Materializer reports accept name_fallback, ordinal_fallback, build_error, font_fallback, prop_rejected, unmatched, preserved end-to-end. A missing style binding that retains rendered literal/default values is preserved, not a build failure.

This page is the official I/O contract for the wf2des worker implemented in apps/wf2des/ (guinness-ai-v2). It covers generation plus every discriminated wf2des-events message the single worker serves. Any change to fields, shapes, status values, webhook effects, storage, or failure modes is a contract change and must be reflected in this page and test-case.en.md before merging code. When changing the shape of a DocumentDB document, bump index_schema_version for that collection in schemas/.

Reading order: Overview → Shared Contract Primitives → the generation and internal-event sections (Generation — Parse Phase → Generation — Assemble Phase → wf_parse → rule_process → component_sweep) → Error Handling → Idempotency & Fencing. The Field Reference near the end is for lookups.


Overview

flowchart LR
  subgraph BE["guinness-backend (product control plane — PostgreSQL)"]
    api["Backend wf2des API<br/>POST/GET/confirm/cancel/<br/>placement/feedback + frame-reg"]
    RDB[("PostgreSQL<br/>wf2des row (status/phase/attempt)")]
    DREG[("platform design table<br/>type=component — SHARED registry<br/>(consumed, not a wf2des table)")]
    WH["POST /v1/webhooks/ai-status<br/>(X-API-Key) — generation flip<br/>+ component-registry apply"]
  end
  Q[("SQS wf2des generation queue<br/>(backend-owned)")]
  SQSE[("SQS wf2des-events intake queue<br/>(AI-owned — backend send-only)")]
  EB["EventBridge schedule<br/>(AI-owned) — component resync sweeps"]
  W["apps/wf2des worker<br/>parse · assemble · wf_parse ·<br/>rule_process · component_sweep ·<br/>component_capture · render_review"]
  DDB[("DocumentDB guinness_v2<br/>6 collections: wireframe · design_rule ·<br/>design_component · project_figma_file ·<br/>design_generation_result ·<br/>design_resolution (decision ledger)")]
  S3[("S3<br/>snapshots · diffs ·<br/>{org}/{proj}/wf2des/{id}-{ts}-{result|failed}.json + manifest")]

  api -->|"INSERT wf2des row status='0' phase=parse (row FIRST)"| RDB
  api -->|"SendMessage parse/assemble {wf2des_id, attempt, nonce, input URLs, snapshot scope+hash}"| Q
  api -->|"SendMessage thin event {event_type, refs, URLs}<br/>(rule · frame-reg → wf_parse · resync → component_sweep)"| SQSE
  Q --> W
  SQSE --> W
  EB -->|"invoke (resync sweep)"| W
  W -->|"UPSERT/LWW context docs + design_generation_result (fenced on attempt)"| DDB
  W -->|"PutObject result/failed artifact + manifest"| S3
  W -->|"POST /v1/webhooks/ai-status {job_id, attempt, nonce, status, error?, result_manifest_url?} — GENERATION only (raises on failure)"| WH
  W -->|"component_sweep: sweep manifest (discovered type=component)"| WH
  WH -->|"attempt-guarded flip: wf2des status/phase + result refs + flag_count (one PG txn)"| RDB
  WH -->|"UPSERT design rows type=component (from component_sweep manifest)"| DREG

The three backend recovery sweeps are NOT IMPLEMENTED

This document describes an awaiting-confirm timeout sweep, a stuck-'0' sweep and a stuck-generation sweep (terminal-manifest replay). None of the three exists in the backend today — there is no scheduled job, no EventBridge target and no endpoint for any of them, and nothing scans S3 for terminal manifests. They are stated below as requirements, not as live behaviour. Until they are built, a missed webhook or a failed enqueue leaves a row non-terminal with no automated recovery.

Item Value
Worker apps/wf2des in guinness-ai-v2 (Python 3.12 Lambda container images; handler/service/schemas/repo recipe; partial-batch SQS with ReportBatchItemFailures)
Processing paths Generation (parse + assemble phases) plus six wf2des-events types: frame_registration (→ wf_parse), rule_upload (→ rule_process), resync (→ component_sweep), component_upload (→ scoped component_sweep), component_capture, render_review
Triggers Backend-owned generation SQS queue (parse / assemble); AI-owned wf2des-events intake queue (rule / plugin events); AI-owned EventBridge schedule (component resync only)
Reads DocumentDB guinness_v2 (6 collections) + S3 (snapshots, config) + Admin internal token provider + Figma REST (service-account OAuth app grant (Authorization: Bearer, figma_pat fallback) — sweep, rule_process board node reads / style tokens / image renders, snapshot / memo fallback)
Writes DocumentDB documents + S3 artifacts; generation additionally emits an S3 result manifest
RDB writes None — workers NEVER write PostgreSQL and hold no PG credentials. Every PG effect is backend-side via the ai-status webhook handler (the sole completion-time PG writer) or the confirm/placement/feedback endpoints
Webhook POST /v1/webhooks/ai-status (X-API-Key) — GENERATION only. wf_parse / rule_process are webhook-free; component_sweep emits a separate discovered-component sweep manifest, not the ai-status webhook
LLM calls Only where a step explicitly says so; everything else is deterministic. Parse/wf_parse: roles + intent (fast tier). Assemble: section matching as a K-sample self-consistency vote (strong tier, vision-capable) + whole-screen planning. Rule ingestion (rule_process): comprehension/extraction/consolidation. Registry enrichment and render review use their configured models. The REST sweep (component_sweep) and the component-capture merge remain deterministic — no LLM. The deterministic guards over the vote and the confidence formula are LLM-free
Tenancy Every read and write filters organization_id + project_id. DEFAULT_ORGANIZATION_ID=1 (single-tenant today). Cross-project reads are a deliberate future decision, never a default
DocumentDB guinness_v2 (AWS DocumentDB 5.0), shared platform DB; the 6 wf2des collections are unprefixed peers; env-overridable collection names; index_schema_version per collection

Run matrix — every path the one worker serves. wf2des_id = the run id = the wf2des PostgreSQL row id (generation only). Internal events carry no product row; tracked events use event_run_id and wf2des_event_status.

Run Started by Consumes DocDB writes S3 writes PG effect
Generation — parse Backend-owned generation parse SQS queue (row-first-then-SQS; auto-confirm chains assemble in-invocation) Parse message (wf2des_id, attempt, nonce, snapshot scope + wf_content_hash, pre-collected URLs) wireframe cache doc (LWW) + design_generation_result inputs+parse blocks (CAS on (_id, attempt)) parse.json (immutable) wf2des.phase → '1' awaiting_confirm via ai-status handler (interactive only; none on auto-confirm)
Generation — assemble Backend confirm endpoint (CAS on awaiting_confirm + attempt) or chained in-invocation on auto-confirm Assemble message (wf2des_id, attempt, fresh nonce, confirm decision); pins rehydrated from result doc inputs design_generation_result spec/spec_nodes_flat/selection/validator_report/confidence/placement.design_area (CAS on (_id, attempt)) + design_resolution ledger upserts (score-ratchet fence) …-result.json (or …-failed.json) + optional …-spec.json spill + result manifest wf2des row → status '1' completed + result refs + flag_count, phase cleared, via ai-status handler (one PG txn)
wf_parse wf2des-events intake queue — plugin frame-registration event Registration event (figma_file_key, node_id, backend-captured snapshot URL) wireframe cache doc (LWW) none of its own PG-free
rule_process wf2des-events intake queue — rule-upload event Rule-upload event (design_rule_id, figma_file_key, board_node_ids[]); reads the selected guideline boards via Figma REST (PAT) design_rule immutable revision doc — merged rules + boards[] + extraction provenance (idempotent per (design_rule_id, content_hash) of the MERGED RuleSet) board renders at wf2design/rule-boards/{figma_file_key}/{node_id}.{sha256}.png PG-free
component_sweep EventBridge schedule (AI-owned) or wf2des-events plugin resync event Scheduled invocation (no body) or thin resync event; walks Figma REST itself design_component context docs (upsert on _id + single-flight) + project_figma_file field-level updates content-addressed component snapshots + discovered-component sweep manifest Backend UPSERTs platform design rows type=component from the manifest (registry, not a wf2des-row flip)
component_upload wf2des-events selected-board upload Selected board ids + file key + target registry Scoped component sweep plus optional registry enrichment captures/snapshots may spill to S3 PG-free except normal backend registry-manifest application
component_capture wf2des-events plugin-observed capture Inline captures or exactly one captures_url spill Monotone field-level merge into design/wireframe component registry reads spill when supplied PG-free
render_review wf2des-events plugin actual-render evidence hash-fenced PNG + render manifest, attempt/spec/output identity, optional parent review wf2des_event_status lease/result; does not rewrite the original generation review/candidate assembly artifacts PG-free; plugin alone promotes a verified candidate

Status / phase enums (wf2des PG row — generation only). status: '0' processing · '1' completed · '2' failed · '3' rejected (designer declined the parse; NOT failed) · '4' cancelled. phase is namespaced INSIDE status '0' and is independent of the status codes: '0' parse · '1' awaiting_confirm · '2' assemble — cleared (NULL) once the row reaches a terminal status. Internal runs carry neither.


Shared Contract Primitives

Stated once here; the per-run sections reference these rather than repeat them.

Intake queue — wf2des-events (AI-owned)

The AI-owned wf2des-events queue is a discriminated union (on event_type) of thin event messages {event_type, ...refs, pre-collected S3 URLs}: frame_registration → wf_parse, rule_upload → rule_process, resync → component_sweep, component_upload → scoped component_sweep, component_capture, and render_review. The backend holds SQS send permission only — its only send right on AI-owned infra; the worker processes each event directly and owns the TTL-backed wf2des_event_status record keyed by event_run_id. A scheduled resync may omit that id and is untracked.

Feedback emits no event. Generation is driven by the backend-owned generation SQS queue (parse / assemble), never by wf2des-events or EventBridge. Internal events carry no wf2des row and no durable run ledger — beyond the TTL-backed status record, the output doc's existence + freshness IS the run status.

ai-status webhook envelope + auth (generation only)

Only generation emits a webhook. The worker POSTs:

POST /v1/webhooks/ai-status
Content-Type: application/json
Accept: application/json
X-API-Key: <shared-service-api-key>

Body is a discriminated union on type — a wf2des generation type on the platform ai-status union. The generation envelope:

{
  "job_id": "<wf2des_id>",        // = the run id = the wf2des PG row id
  "attempt": 1,                    // fencing pair member (echoed from the SQS message)
  "nonce": "<per-send token>",     // part of the (job_id, attempt, nonce) dedupe key
  "status": "…",                   // parse-done → phase; succeeded/failed → terminal row flip
  "error": { "message": "…" },     // present on failure only
  "result_manifest_url": "…",      // absent at parse-done; the manifest pointer on succeeded/failed (the handler's row-effect input)
  "manifest_schema_version": 1
}

Auth matrix (relevant surfaces): worker → webhook = X-API-Key (Secrets Manager, timing-safe compare); backend → wf2des-events = SQS send permission; plugin → internal-api wf2des routes = session-issued data-plane token {org_id, project_id, exp} (read+write); backend/MCP → internal-api = shared X-AI-Service-Token (read routes only); worker → Admin token provider = IAM-protected Lambda Function URL + X-AI-Service-Token; service-account Figma REST = provider-issued Authorization: Bearer, with the worker's figma_pat secret as deployment fallback.

The ai-status handler is the sole completion-time PG writer: it dedupes on (job_id, attempt, nonce), validates the manifest's own job_id/job_type/S3-path-prefix against the payload (the envelope discriminator type = wf2des is distinct from the manifest's job_type = generation, the run type), returns 5xx on manifest-read failure, and applies the attempt-guarded wf2des flip in ONE PostgreSQL transaction. wf_parse / rule_process never reach it.

S3 key conventions

Purpose Key Produced by
Content-addressed snapshot (all worker-captured sources) wf2design/snapshots/{figma_file_key}/{node_id}.{content_hash}.json component_sweep (component nodes); backend / trigger snapshot capture (WF frames)
Client-facing result artifact (generation) {org}/{proj}/wf2des/{wf2des_id}-{ts}-result.json (or …-failed.json) Generation — assemble
Generation result manifest (webhook row-effect input) {org}/{proj}/wf2des/{wf2des_id}-{ts}-manifest.json (or …-failed-manifest.json) Generation — assemble
Intermediate artifacts (internal wf2des prefix) {org}/{proj}/wf2des/{wf2des_id}-{ts}-{spec\|parse\|feedback}.json Generation (parse.json, spec.json spill, feedback diffs)
Guideline board render (rule ingestion) wf2design/rule-boards/{figma_file_key}/{node_id}.{sha256}.png rule_process (Figma images API render, PNG scale 2; referenced by design_rule.boards[].image_url)
Project config home {org}/{proj}/wf2des/config.json (referenced by project_figma_file.config_url) Curation / plugin

Example artifact key: 1/3/wf2des/8f14e4…-20260706T093000Z-result.json. Result artifacts are readable only via wf2des-api presigning or the backend's own role.

PostgreSQL isolation (hard platform rule)

Workers NEVER write PostgreSQL and hold no PG credentials. The worker fences its DocDB commits with (wf2des_id, attempt) and reports both back via the webhook; the backend advances the row attempt-guarded. Every PG effect in this document is applied backend-side — by the ai-status webhook handler (generation completion / component-registry apply) or by the confirm / placement / feedback endpoints — never by the worker itself.


Generation — Parse Phase

Extracts a wireframe frame into structured parse artifacts and parks the run at the confirm checkpoint (interactive) or chains straight into assemble (auto-confirm). One-shot Lambda invocation, consumed via the platform partial-batch SQS handler (ReportBatchItemFailures; service.py:process_record, schemas/ as the contract surface).

Consumes / Input

Backend-published generation SQS payload. The backend writes the wf2des PG row first (status '0', phase parse), captures the trigger-time WF snapshot, then enqueues. Interactive parse = the same envelope with auto_confirm=false. The SQS message is the worker's entire window onto PostgreSQL — the worker never queries PG (existence / tenancy are resolved backend-side before send).

Field Type Required Validation Notes
wf2des_id string (uuid) ✓ = the wf2des PG row id; echoed as job_id in the webhook run identity / fencing pair member
attempt integer ✓ monotonic; fences DocDB commits (CAS on (_id, attempt)) always 1 today; nothing bumps it
nonce string ✓ fresh per enqueue; part of the (job_id, attempt, nonce) dedupe key
wireframe_img_url string (s3://) ✓ pre-collected by backend trigger-time WF image snapshot
wireframe_json_url string (s3://) ✓ pre-collected; the trigger-time WF snapshot (frame + enclosing board section, memos included) S3 download source for extraction
snapshot_scope object {frame_node_id, section_node_id} ✓ the frame + its enclosing board section
wf_content_hash string (sha256) ✓ computed at snapshot capture; the parse-cache key short-circuits the LLM on unchanged hash
structure_file_url string (s3://) ✗ for pinning
detail_design_file_url string (s3://) ✗ for pinning
design_file_url string (s3://) ✗ for pinning
screen_id string ✓ generation rows always carry it (from the request)
prompt string | null ✗ selection input, ranked below memo intent
placement_target string (node_id) | null ✗ echoed into the result doc request block
auto_confirm boolean ✓ false normal / true auto → parse+assemble in one invocation
{
  "wf2des_id": "…", "attempt": 1,
  "nonce": "…",
  "wireframe_img_url": "s3://…",
  "wireframe_json_url": "s3://…",
  "snapshot_scope": { "frame_node_id": "…", "section_node_id": "…" },
  "wf_content_hash": "…",
  "structure_file_url": "s3://…",
  "detail_design_file_url": "s3://…",
  "design_file_url": "s3://…",
  "screen_id": "…", "prompt": "…", "placement_target": "…", "auto_confirm": false
}

Reads. (1) The trigger-time WF snapshot from S3 via wireframe_json_url (frame + enclosing board section, memo nodes included) — downloaded ONLY on parse-cache MISS. (2) Parse cache: primary-key lookup in the wireframe collection by composite _id = {project_id}_{figma_file_key}_{node_id}, matched against wf_content_hash. (3) On cache hit: copies the prior run's parse artifacts (no LLM, no S3 download). Memo caveat: service-account Figma REST fallback if memo nodes are missing from the snapshot. (4) The project's LATEST design_rule revision from the design_rule collection (repo.find_latest_design_rule: highest version, ties broken by newest lineage.processed_at) for the rule pin — no revisions ⇒ pin None (the validator's no_rules path). The former design_rule_file_url message field is REMOVED — a backend still sending it is ignored as an unknown field. Optional pre-collected structure/detail_design/design file URLs remain available for pinning. (5) config.json (via project_figma_file.config_url) for the memo-status markers and the frame-name variation separator — absent ⇒ platform defaults; present-but-malformed ⇒ the run fails loudly. Every read filters organization_id + project_id.

Processing Contract

  1. Message received (wf2des_id + attempt + nonce).
  2. PIN inputs into the result doc's inputs block — the FIRST act of the invocation (the pinning moment). The parse invocation pins the parse inputs (wireframe identity / source_hash); the design_rule pin = the project's LATEST design_rule revision (repo.find_latest_design_rule: highest version, ties by newest lineage.processed_at) — no revisions ⇒ pin None → the validator's no_rules path; never an inline rule file.
  3. Parse-cache check: primary-key lookup by composite _id, same wf_content_hash. Cache is SKIPPED on retry-after-reject (a fresh parse is the point).
  4. Cache HIT → copy prior parse artifacts into this job's own parse.json — NO LLM call; jump to step 8. Cache MISS → download the WF snapshot from S3 (memo nodes included, board-section scope).
  5. DETERMINISTIC WFNode extraction: every visible FRAME / GROUP / INSTANCE / TEXT descendant + rectangles with image fills → a WFNode; vector/shape leaves collapse into parent; hidden layers skipped. The tree shape is deterministic.
  6. DETERMINISTIC memo statuses: memos inside a 取込済 ("imported") status frame ⇒ resolved; resolved memos excluded from live intent. screen_id (from request) and variation_label (deterministic frame-name split) are deterministic — NOT LLM.
  7. LLM (fast tier) — assigns node role (from the shared element-type vocabulary — the closed set of WFNode role values, pinned in prompts.py as ROLE_VOCABULARY) + intent (from OPEN memos + variant labels) ONLY. NEVER the tree shape. Low temperature. This is the ONLY LLM step in parse.
  8. Write the wireframe cache doc (LWW fence on (source_hash, processed_at)).
  9. Write the result doc's parse block + the immutable parse.json artifact (CAS on (_id, attempt)).
  10. Webhook: parse done (raises on failure).
  11. Branch on auto_confirm: false → backend flips wf2des.phase = awaiting_confirm, plugin previews via wf2des-api; true → assemble runs back-to-back in the same invocation.
flowchart TD
  A["message received<br/>(_id + attempt + nonce)"] --> P0["PIN inputs into the result doc<br/>(first act of the invocation)"]
  P0 --> C{"parse cache hit?<br/>primary-key lookup by composite _id,<br/>same content hash"}
  C -->|"hit (skipped on retry-after-reject)"| R["copy prior parse artifacts —<br/>no LLM call"]
  C -->|miss| D["download WF snapshot from S3<br/>(memo nodes included: board-section scope)"]
  D --> E["DETERMINISTIC WFNode extraction<br/>visible FRAME / GROUP / INSTANCE / TEXT<br/>+ image-fill rects; vectors collapse; hidden skipped"]
  E --> F["memo statuses — resolved excluded<br/>from live intent (deterministic)"]
  F --> G["LLM (fast tier): roles + intent ONLY<br/>never the tree shape"]
  G --> I["write wireframe cache doc<br/>(LWW fence: source_hash + processed_at)"]
  R --> J
  I --> J["result doc parse block +<br/>immutable parse.json artifact<br/>(CAS on _id + attempt)"]
  J --> K["webhook: parse done<br/>(raises on failure)"]
  K --> L{"auto_confirm?"}
  L -->|no| M["backend flips phase = awaiting_confirm<br/>plugin previews via wf2des-api"]
  L -->|yes| N["assemble runs back-to-back<br/>in the same invocation"]

Emits / Output

DocDB write (A): wireframe cache doc — LWW fence on (source_hash, processed_at). _id = {project_id}_{figma_file_key}_{node_id}.

Field Type Source
_id string composite {project_id}_{figma_file_key}_{node_id}; figma_file_key/node_id derived via split('_', 2) — NOT stored separately; no separate wireframe_id
organization_id, project_id integer tenancy scope
name string full Figma frame name (JP may exceed PG frame_name 100)
type number 0 page · 1 component (platform wireframe-collection shape)
based_on number 0 imported · 1 design · 2 wireframe
screen_id string | null from the request at generation (NULLABLE)
variation_label string deterministic name split
structure object WFNode tree { node_id, name, figma_type, role (LLM), bbox {x,y,w,h}, intent (string\|null, LLM), text?, children[], component_id?, component_variants? (the wireframe instance's OWN componentId + variant values — PRESERVED, the fallback/carry-over source), layout_mode, item_spacing, padding[4], primary_align, counter_align, layout_wrap, counter_gap (auto-layout capture), fill, corner_radius, font_size, font_weight, text_color (styling capture; LINE/thin-RECT divider shapes kept as leaves) } — everything deterministic except role/intent
memos object[] { memo_node_id, text, status } (status deterministic: resolved if inside a resolved-marker container)
board_regions object[] the comprehended board: one entry per region with {node_id, name, figma_type, x, y, features, marker_hit, resolved_marker_hit, is_registered_frame, text_sample[], role, refers_to}. A board carries no fixed layout — wireframe only, or with annotations and memos alongside — so regions are segmented and each is classified. This is the SOURCE of memos: intent comes from what a designer wrote as a NOTE about the screen, not from every string on the board
summaries object { element_type_histogram, node_count } (computed)
lineage object { source_url (RAW REST snapshot), source_hash (≡ inputs.wireframe.source_hash), processor_version (e.g. wf_parse@1.2), index_schema_version, processed_at, job_id }

DocDB write (B): design_generation_result doc — the inputs + parse blocks — CAS on (_id, attempt). _id = the wf2des row id.

Field Type Source
inputs object pinned (first act): wireframe {figma_file_key, node_id, source_hash}; design_rule {design_rule_id, content_hash} (the project's LATEST revision at parse; no revisions ⇒ pin None); components [{platform_design_id, content_hash}]; llm {model_id, prompt_version, screen_review_model_id, screen_review_reasoning_effort, screen_review_enabled}
parse object { confirmed (bool, false interactively), confirmed_by (null), confirmed_at (null — the SINGLE home for the confirmation timestamp), rejected (null; structured {reason_code: wrong_roles|wrong_memos|wrong_sections|other, note} on reject), memo_influences: [{memo_node_id, text}] (TEXT SNAPSHOTTED — survives wireframe reparse) }
attempt integer echo
organization_id, project_id integer tenancy scope
screen_id string | null echo
request object {screen_id, prompt, placement_target, auto_confirm} echo
artifact_urls object {parse} — this run's immutable parse.json pointer (the authoritative parse)
timings object {created_at (run-START anchor, distinct from lineage.processed_at), parse_done_at, <phase>_ms} — parse also stamps its phase durations (snapshot_ms, parse_ms)
lineage object points at the wireframe snapshot

The full WFNode tree THIS run parsed = artifact_urls.parse (immutable), since the wireframe collection doc may be overwritten by later reparses. The design_rule pin always references an existing immutable revision written by rule_process — the generation worker never parses a rule inline (that path is removed).

S3. parse.json — the immutable full parsed WFNode tree + memos as used. Internal wf2des prefix; referenced in artifact_urls.parse as {org}/{proj}/wf2des/{wf2des_id}-{ts}-parse.json (e.g. 1/3/wf2des/8f14e4…-20260706T093000Z-parse.json). This is the AUTHORITATIVE parse a run used — the preview and memo-review surfaces read the result doc + this artifact, never the overwritable wireframe cache doc.

Webhook — parse done. POST /v1/webhooks/ai-status, X-API-Key. Envelope per Shared Contract Primitives, with status: "parse_done":

{
  "type": "wf2des",              // discriminator on the platform ai-status union
  "job_id": "<wf2des_id>",       // = the wf2des PG row id
  "attempt": 1,
  "nonce": "<per-send token>",
  "manifest_schema_version": 1,
  "status": "parse_done"         // phase signal → handler sets wf2des.phase = awaiting_confirm; no manifest at parse-done (no terminal row flip)
}

Raises on failure so SQS redelivers. Effect: the handler parks the wf2des row at phase = awaiting_confirm (attempt-guarded, deduped on (job_id, attempt, nonce); row stays status '0'). On the auto_confirm path there is no parse-done park — assemble runs in the same invocation and only the succeeded webhook fires. A duplicate delivery of an already-terminal attempt re-emits the terminal manifest + webhook from the STORED result doc (never re-running the LLM) — closing the crash window between the result write and the webhook; the backend's (job_id, attempt, nonce) dedupe + attempt guard make a genuinely-duplicate send a no-op. A fail-closed parse failure — an unresolvable pin or a tenancy-scope mismatch, a ContractFailure that can never succeed on redelivery — instead emits the terminal …-failed.json + failed manifest + failed webhook (status: "failed" + error → the row flips status '2', phase cleared), exactly like an assemble failure, rather than churning to the DLQ with the row stuck at status '0'. TRANSIENT parse failures (snapshot GET, the fast-tier LLM, transient I/O) still raise so SQS redelivers, with no terminal surface.

PG effect. wf2des.phase → '1' awaiting_confirm via the ai-status webhook handler (attempt-guarded, one PG txn). The row stays status '0' processing while parked; phase locates the run inside '0'. The plugin then polls GET (row '0', phase=awaiting_confirm) and previews via wf2des-api. Interactive confirm is a SEPARATE backend endpoint (CAS on phase awaiting_confirm + attempt → phase '2' assemble → enqueues the assemble message). Workers NEVER write PG. On auto_confirm there is no phase flip — parse chains directly into assemble.


Generation — Assemble Phase

Selects components per section, stitches the deterministic spec, validates against rules, computes confidence, and produces the terminal result artifact + manifest. One-shot Lambda invocation (pins rehydrated from the result doc). Same platform partial-batch SQS handler recipe.

Consumes / Input

Enqueued by the backend confirm endpoint (interactive: CAS on the wf2des row's phase awaiting_confirm + attempt match → phase '2' assemble → enqueue) OR chained back-to-back in the same parse invocation on the auto_confirm path. The message carries wf2des_id + attempt + a fresh nonce + the confirm decision — everything else is REHYDRATED from the result doc's inputs block.

Field Type Required Validation Notes
wf2des_id string (uuid) ✓ = the wf2des PG row id; echoed as job_id run identity
attempt integer ✓ fences DocDB CAS on (_id, attempt)
nonce string ✓ fresh per enqueue; part of webhook dedupe (job_id, attempt, nonce)
confirm decision attempt integer ✓ CAS-checked by the BACKEND before enqueue (confirm payload {attempt, parse_artifact_hash}); 409 on mismatch, 200-echo on repeat
confirm decision parse_artifact_hash string ✓ CAS-checked by the backend against the parse.json artifact before enqueue
{
  "wf2des_id": "…", "attempt": 1,
  "nonce": "…",
  "confirm": { "attempt": 1, "parse_artifact_hash": "…" }
}

Reads. (1) REHYDRATE pins from the result doc's inputs block — fail closed on ANY hash mismatch: wireframe {figma_file_key, node_id, source_hash}, design_rule {design_rule_id, content_hash}, components [{platform_design_id, content_hash}], llm {model_id, prompt_version, screen_review_model_id, screen_review_reasoning_effort, screen_review_enabled} — and the confirm's own parse_artifact_hash is re-checked against the rehydrated parse.json. (2) The project's component registry — the platform design (type=component) set pinned in inputs.components; the assembly/instancing context read from the design_component collection (keyed by _id = {project_id}_{figma_file_key}_{node_id}): kind, component_key (nullable/local), name, variant_properties, text_slots[{layer_path, default_text, ambiguous}], image_slots[{layer_path, ambiguous}], default_size. (3) The pinned design_rule revision keyed (design_rule_id, content_hash) — _rehydrate_rules returns BOTH the RuleSet (the validator program) AND the revision's guideline board images; a missing pinned doc ⇒ ContractFailure, fail closed. (4) project_figma_file.style_captures (source of the derived style_bindings map) and config.json (via project_figma_file.config_url) to resolve the DESIGN-area rect into placement.design_area. (5) The pinned design_rule revision also supplies the guideline board IMAGES (boards[], loaded from S3), attached to every per-section selection call as vision reference; a missing board image ⇒ ContractFailure, fail closed. (6) The design_resolution decision ledger — prior section-kind → component resolutions keyed by a structural, LLM-free section_signature (sig@1:…); consulted BEFORE selection (see the assemble Emits below). Every read filters organization_id + project_id. Registry hygiene is global (deduped, hygiene-collapsed), then the production-default deterministic role→kind gate builds a reproducible admissible set per section; the selection LLM (K-sample vote) sifts that set by each candidate's full structure. Size is a selection signal, not a gate; there is no similarity or vector scoring.

Processing Contract

  1. Message received.
  2. REHYDRATE pins from the result doc's inputs block; fail closed on any hash mismatch. On redelivery, reuse existing pins — re-run selection only if the block is absent.
  3. Build the registry set: the project registry, filtered by organization_id + project_id, deduped, and hygiene-collapsed. No vector/similarity scoring.
  4. Build per-section candidate sets with the deterministic role→admissible-kind gate (CANDIDATE_KIND_GATE=true by default). Size remains a selection signal, never a gate. Counts are recorded in selection; truncation is flagged. The switch may be disabled for controlled evaluation, not normal production.
  5. DECISION-LEDGER consult (design_resolution, keyed by the structural section_signature): an entry LOCKS its section (no LLM) when provenance = mined or its score ≥ the trust threshold; a weaker entry becomes a FLOOR (the section is re-voted and the higher-scoring decision wins, applied BEFORE the deterministic guards). Instance entries are scope-revalidated at consult time (default-variant entries only); stale/mis-scoped entries fall through to the vote.
  6. LLM — SECTION-PARALLEL selection with SELF-CONSISTENCY VOTING (low temperature, evidence-pinned; K samples per section, majority on (case, platform_design_id)). Per-section decisions ONLY: instance / compose / unmatched; component key; variant props; text-slot fills. Inputs to the LLM: memo intent > the request prompt; rules digest advisory (the validator always overrides); the pinned design_rule revision's guideline board images attached as vision prefix (advisory reference — prefer the component/variant matching the boards; never invent ids/slots). This is the ONLY LLM step in assemble (plus the focused variant-resolution pass below) — the K-sample vote is one call family (strong tier must be vision-capable). Sections are independent → wall-clock = slowest section. Prompt caching shares the static prefix (instructions + rules digest).
  7. DETERMINISTIC guards over the vote (trust boundary — the LLM echo is never trusted): variant props CLAMPED to registry axes before anything reads them; symmetric SCOPE guards (a component grossly larger than the section downgrades to unmatched; a small component claiming a multi-unit section downgrades to compose so its units re-select); duplicate-claim dedupe with wireframe-name evidence; kit-name override (a section that IS one named kit element takes the registry component of that name, with the kit's own variant values carried); a focused variant-resolution pass for under-specified instances; a screen-facts pass (shared variant axes resolved once per screen). Ledger floors apply BEFORE these guards — the guards keep the final say.
  8. DETERMINISTIC stitching: spec tree + spec_nodes_flat + style_bindings map (produced FROM project_figma_file.style_captures), reproducible from the same decisions. Layout-preserving: decisions re-nest into the ORIGINAL wireframe tree; compose children re-select against the registry, with wireframe variant carry-over, differentiation-preserve (siblings the wireframe distinguishes never collapse into identical instances), and look-alike duplicate-claim demotion; unstyled/styled sections wrap in band/card frames with a single padding owner; per-child gap lists align to the EMITTED children.

The bookkeeping doc is now self-registering. Every write to project_figma_file (slot_roles, style_captures, components_synced_at, sweep_error, the sweep marker) is an update_one WITHOUT upsert, because plugin file-registration was meant to own doc creation. Under Arch A no such route exists — the backend exposes only GET …/figma-files — so the doc never appeared and every one of those writes silently matched nothing. Measured on a real sync: enrich_registry re-ran the FULL slot-role classification on all 8 messages, 104 LLM calls and ~91k input / ~69k output tokens, byte-identical every time, because it reads its own prior verdicts back from this doc and an absent doc makes that memory permanently empty. The registry-enrichment pass therefore now calls ensure_project_figma_file before reading slot_roles: $setOnInsert ONLY, role = 1 (library — the file is a component source by construction at that point), so a doc registered later by a real route keeps ownership of role / config_url. After the fix the same sync spent 13 calls once and 0 thereafter, and the capture stage fell from ~6 minutes to ~1 second. Note also that with NO registered file the sweep's own file list is empty and component_sweep logs wf2des.component_sweep.no_files and does nothing — the plugin-capture upload path is what populated the registry in that state. 9. DETERMINISTIC rules validator + rhythm pass: violations auto-fixed where safe, else flagged; the rhythm pass snaps gaps to the hygienic spacing scale (within tolerance only) and binds INTERIOR section joints to the stated section margin; no rules registered → no-op + EVERY node flagged rules_unvalidated (validator_report.status = no_rules). 10. COMPUTED confidence (never model self-report): validator violations + unmatched/composed counts + slot ambiguity + component-fit (role agreement); compose capped below instance lives in the formula. 11. LEDGER write-back: every non-unmatched section (and compose-child) decision upserts into design_resolution with a DETERMINISTIC score — fenced by the RATCHET (a voted write lands only over a non-mined entry with a strictly lower score; mined writes are unfenced; a DuplicateKeyError on the fenced upsert means KEPT, not an error). 12. Write result doc spec / spec_nodes_flat / selection / validator_report / confidence blocks (CAS on (_id, attempt)); worker also writes placement.design_area (resolved from config.json). 13. S3: immutable result artifact + result manifest (schema-versioned). 14. Webhook: succeeded + manifest key → backend flips wf2des to completed + result refs + flag_count.

compose sections are ALWAYS flagged (least-grounded); unmatched nodes become placeholders — never silently dropped.

flowchart TD
  A["message received"] --> B["REHYDRATE pins from result doc inputs<br/>fail closed on any hash mismatch"]
  B --> C["registry set = tenant-scoped + deduped<br/>per-section deterministic role/kind gate<br/>(no similarity; size is a signal, not a gate)"]
  C --> D["per-section candidate set<br/>(counts recorded in the selection block;<br/>truncation flagged)"]
  D --> L0["DECISION LEDGER consult (design_resolution)<br/>mined or score ≥ trust threshold → LOCK (no LLM)<br/>weaker entry → FLOOR (re-vote, best score wins)"]
  L0 --> E["SECTION-PARALLEL selection — LLM<br/>K-sample SELF-CONSISTENCY vote per section:<br/>instance / compose / unmatched,<br/>component key, variant props,<br/>text-slot fills<br/>(inputs: memo intent > prompt; rules digest advisory)"]
  E --> G0["DETERMINISTIC guards (echo never trusted)<br/>variant clamp · symmetric scope guards ·<br/>duplicate-claim dedupe · kit-name override ·<br/>variant resolution · screen facts"]
  G0 --> F["DETERMINISTIC stitching (layout-preserving)<br/>spec tree · spec_nodes_flat · style_bindings map<br/>compose-child re-selection + variant carry-over +<br/>differentiation preserve · band/card wrapping ·<br/>gaps aligned to emitted children"]
  F --> G["RULES VALIDATOR + RHYTHM PASS (deterministic)<br/>violations: auto-fix where safe, else flag<br/>gap snapping (hygienic scale) + interior<br/>section-joint binding<br/>no rules registered: no-op +<br/>every node flagged rules_unvalidated"]
  G --> H["computed confidence<br/>validator violations + unmatched/composed counts<br/>+ slot ambiguity + component-fit (role agreement)<br/>never model self-report"]
  H --> L1["LEDGER write-back (design_resolution)<br/>ratchet fence: voted lands only over a<br/>non-mined, strictly-lower-scoring entry"]
  L1 --> I["result doc spec / validator / confidence<br/>(CAS on _id + attempt)"]
  I --> J["S3: immutable result artifact +<br/>result manifest (schema-versioned)"]
  J --> K["webhook: succeeded + manifest key<br/>backend flips wf2des to completed<br/>+ result refs + flag_count"]

Emits / Output

DocDB write — design_generation_result spec / spec_nodes_flat / selection / validator_report / confidence / placement.design_area blocks. CAS on (_id, attempt). _id = the wf2des row id.

Field Type Source
spec object (DesignSpec) { spec_version, parse_confirmed (mirror), style_bindings (token → Figma style/variable id, derived FROM project_figma_file.style_captures), root } — self-contained; five node types below
spec.root → layout_frame object {node, layer_path, auto_layout, fill, fill_opacity, stroke, stroke_weight, corner_radius, clips_content, bbox, lineage_wf_node_ids, children} — auto_layout = {direction, gap, padding (uniform or [top,right,bottom,left]), sizing, gaps[] (PER-CHILD measured gaps, aligned to the EMITTED children), primary_align, counter_align, wrap, counter_gap}; direction=none + sizing=fixed preserves coordinate-positioned overlapping layers (background/scrim/modal) in document z-order. fill_opacity is the effective paint alpha; stroke / stroke_weight preserve authored boundaries; and clips_content preserves fixed viewport/card clipping. bbox is embedded so the spec is SELF-CONTAINED (materialization needs no snapshot)
spec.root → instance object {node, layer_path, component_key, platform_design_id (= design_component._id, the STABLE validator key), component_node_id, component_name, variant_props, text_slots, bbox, confidence, flagged, source{kind: registry \| wireframe, component_key}, lineage_wf_node_ids} — source.kind = wireframe is the FALLBACK: the wireframe's own component instance (its componentId + variants + text overrides), used when no design match exists or a deterministic demotion preserved the wireframe's stated distinction; always flagged, never worse than the wireframe
spec.root → compose object {node, layer_path, source{kind:composed}, auto_layout (the composed section's OWN rhythm — direction/gaps/padding; nullable), fill, fill_opacity, stroke, stroke_weight, corner_radius, clips_content, bbox, children (inlined), confidence, flagged=ALWAYS true, lineage_wf_node_ids} — compose is still the source container: its modal/card/footer surface contract is preserved even when no single registry component fits
spec.root → text object {node, layer_path, content, style_token, font_size, font_weight, color, bbox, confidence, flagged, source, lineage_wf_node_ids} — wireframe text styling embedded. An empty style_token means use grounded literal typography and per-field style_refs, not an unresolved placeholder. The capture vocabulary does not assign one whole-text style to every node: neither a variable named text nor the first body-style ID may override a screen's measured hierarchy. Explicit style references remain supported; the plugin verifies the resolved style kind and target field before applying them.
spec.root → unmatched object {node, layer_path, placeholder{role,text,bbox}, confidence, flagged=true, source{kind:none}, lineage_wf_node_ids}
spec_nodes_flat object[] [{layer_path, case, component_key, wf_role (joined from parse tree), confidence, flagged, lineage_wf_node_ids}] — one per content node
selection object { sections, candidates_considered (map), composed_count, unmatched_count, truncated }
screen_plan object | null Grounded whole-screen review, accepted/rejected proposals, and evidence retained for replay
spec_hash string | null Canonical SHA-256 identity of the materialized base spec
assembly_state_hash string | null Hash fence for the pinned stitch/validation state used by actual-render review
layout_fidelity object | null {frames_total, frames_preserved, wrap_total, wrap_preserved, units_collapsed, lost[]}
validator_report object { status (ok \| no_rules), violations:[{rule, layer_path, action (auto_fixed\|flagged), detail}], unfixed_flagged }
confidence object { min, avg, flag_count, formula_version } — flag_count mirrors to wf2des.flag_count via the completion webhook
placement.design_area object { x, y, w, h } — the resolved DESIGN-area rect, worker-written from config.json (rest of placement is plugin-written later: placed_node_id, materialized_at, materializer_report)
artifact_urls object assemble writes result, assembly (+ spec when spec > ~1MB spills); the full block is {result, assembly, spec, parse, feedback_diff} — assembly pins stitch inputs for bounded render review
timings object {created_at, parse_done_at, assembled_at, <phase>_ms} — assemble stamps assembled_at + its phase durations (selection_ms, assemble_ms, validate_ms); <phase>_ms = per-phase latencies
lineage.processed_at timestamp terminal write

design_rule writes belong to rule_process alone — no generation phase ever writes the design_rule collection (the inline parse-side path is retired).

DocDB write — design_resolution decision-ledger docs (assemble-only; the AUTONOMOUS QUALITY RATCHET — no human-correction loop exists, so quality must converge machine-side). One doc per resolved section KIND:

Field Type Source
_id string {organization_id}_{project_id}_{section_signature}
organization_id, project_id integer tenancy scope
section_signature string STRUCTURAL section identity, versioned (sig@1:…) — sha256 over the section's kit-component names, normalized name, child-type shape, and text count. Deterministic and LLM-free (never the LLM-assigned roles), so the same section KIND hits the same entry on every screen and rerun
case string enum instance | compose
platform_design_id string | null the resolved component (= design_component._id)
component_name string display convenience
variant_policy object axis → value; CLAMPED to registry axes before write
provenance string enum mined (read from designer-made designs in the file — the only above-vote authority) | voted (K-sample selection vote)
score float DETERMINISTIC decision quality (base + modeled + variant-completeness + scope-fit; mined = 1.0) — the ratchet's currency
prompt_version string provenance of the vote
updated_at timestamp last accepted write

Consult semantics (assemble step 5): mined or score ≥ trust threshold → LOCK (no LLM for that section); below threshold → FLOOR (re-vote; the higher-scoring decision wins, applied BEFORE the deterministic guards so the guards keep the final say). Instance entries are scope-revalidated at consult (default-variant entries only — default_size speaks only for the default variant). Write fencing: see Idempotency & Fencing.

S3. Two outputs. (1) The client-facing immutable result artifact {org}/{proj}/wf2des/{wf2des_id}-{ts}-result.json (…-failed.json on failure) — the durable completion record, independent of webhook delivery; also referenced in artifact_urls.result (and artifact_urls.spec at {org}/{proj}/wf2des/{wf2des_id}-{ts}-spec.json if spec > ~1MB spills). (2) The result manifest (schema-versioned) at {org}/{proj}/wf2des/{wf2des_id}-{ts}-manifest.json (…-failed-manifest.json on failure) — the webhook handler's row-effect input, referenced by the succeeded/failed webhook's result_manifest_url. Its inner result_url points at output (1).

Webhook — succeeded. POST /v1/webhooks/ai-status, X-API-Key, discriminated type, status: "succeeded":

{
  "type": "wf2des",
  "job_id": "<wf2des_id>",
  "attempt": 1,
  "nonce": "<per-send token>",
  "manifest_schema_version": 1,
  "status": "succeeded",                       // maps to wf2des row status '1'
  "result_manifest_url": "{org}/{proj}/wf2des/{wf2des_id}-{ts}-manifest.json"
}

The referenced S3 manifest (the handler's row-effect input; the only manifest the system produces):

{
  "manifest_schema_version": 1,
  "job_id": "…", "attempt": 1,
  "nonce": "…",
  "job_type": "generation",
  "result_url": "{org}/{proj}/wf2des/{wf2des_id}-{ts}-result.json",
  "flag_count": 3,
  "row_effects": { "wf2des": { "status": "1", "result_url": "…", "flag_count": 3 } }
}

Raises on failure so SQS redelivers. Failure variant (same envelope): status: "failed" + error, result_manifest_url points to the failure manifest …-failed-manifest.json (its inner result_url = …-failed.json) → handler records error on the row, clears phase, flips status '2'. (Parse reject is NOT a webhook — the plugin writes parse.rejected to wf2des-api, then the backend confirm endpoint flips status '3'.) A duplicate delivery of an already-terminal attempt re-emits the terminal manifest + webhook from the STORED result doc (never re-running the LLM) — closing the crash window between the result write and the webhook; the backend's (job_id, attempt, nonce) dedupe + attempt guard make a genuinely-duplicate send a no-op.

PG effect. wf2des row → status '1' completed + result refs (result_url) + flag_count (completion-time copy of confidence.flag_count) + phase cleared (NULL). Applied by the ai-status webhook handler — the SOLE completion-time PG writer — deduped on (job_id, attempt, nonce), attempt-guarded, in ONE PostgreSQL transaction, from the manifest's row_effects.wf2des. This is the only wf2des write beyond row creation (excepting placement/feedback plugin flips). The manifest is the durable record and the webhook the fast path, but the replay that would close the gap — the stuck-generation sweep — is not implemented: if the webhook is missed, the terminal manifest sits in S3 and the row is never flipped. A superseded attempt's webhook is a no-op. Workers NEVER write PG.


wf_parse

The plugin registers a WF frame; the worker parses it into the wireframe cache doc. Same parse pipeline as generation-parse steps 4→9 (deterministic extraction, memo statuses, LLM roles/intent) — but no result doc, no preview, no webhook: the wireframe doc IS the record. Triggered by the wf2des-events intake queue (plugin frame-registration event) — NOT EventBridge, NOT a generation queue. Internal runs carry no wf2des row.

Consumes / Input

Thin registration event (refs + pre-collected URL). No generation-message fields (no wf2des_id/attempt/nonce).

Field Type Required Validation Notes
event_type string ✓ plugin frame-registration category routes to wf_parse
figma_file_key string ✓ Figma verbatim; alphanumeric, no underscore the file the frame lives in
node_id string ✓ Figma node id (uses :) the registered frame
snapshot URL (backend-captured) string (S3 URL) ✓ pre-collected by backend the WF snapshot to parse
organization_id, project_id integer ✓ tenancy scope message-carried scope; DEFAULT_ORGANIZATION_ID=1

No JSON example is defined for the wf_parse event; the fields above are the required set.

Reads. The WF snapshot from S3 (the backend-captured registration snapshot URL). Snapshot scope = frame + enclosing board section (memos/status frames are board-level). Reads config.json (the S3 config home {org}/{proj}/wf2des/config.json, via project_figma_file.config_url) for curatable memo-status markers (memo_resolved_markers, default 取込済), the DESIGN-area rect (design_area, resolved at assemble), and the frame-name variation separator (variation_separator, default |). An ABSENT config.json falls back to platform defaults; a present-but-malformed one (unreadable, non-object, or an invalid override value) fails the run loudly — a curated project is never silently served defaults. (Exception: a malformed design_area rect only omits placement.design_area with a flagged warning — the omission is visible in the result, never a silent fallback rect. Multi-file projects resolve the lexicographically first config_url.) Tenancy read filters organization_id + project_id.

Processing Contract

  1. DETERMINISTIC WFNode extraction from the S3 snapshot: every visible FRAME / GROUP / INSTANCE / TEXT descendant + image-fill rectangles → WFNodes; vector/shape leaves collapse into parent; hidden layers skipped.
  2. Memo statuses — DETERMINISTIC: a memo inside a 取込済 frame ⇒ status=resolved; resolved memos excluded as live intent.
  3. screen_id — DETERMINISTIC name grammar [A-Z]{2,3}_[A-Z0-9]+, unanchored, searched in frame name → board name → page name; no match ⇒ null + an ingest issue logged to staging/issues.json.
  4. variation_label — DETERMINISTIC frame-name split (会員登録TOP|案1 → 案1).
  5. LLM (fast tier): roles + intent ONLY — role from the shared element-type vocabulary; intent from OPEN memos + variant labels; NEVER the tree shape. The ONLY LLM step.
  6. Compute derived summaries: element_type_histogram, node_count.
  7. Write the wireframe cache doc directly (LWW fence). NO result doc, NO preview, NO webhook — the wireframe doc IS the record.

Emits / Output

DocDB write — wireframe cache doc (single writer shared with generation-parse; intended non-collision). Same shape as generation-parse write (A):

Field Type Source
_id string composite {project_id}_{figma_file_key}_{node_id}; parts derived via split('_', 2); no separate wireframe_id
organization_id, project_id integer tenancy scope
name string full Figma frame name (JP may exceed PG frame_name 100)
type number 0 page · 1 component (platform wireframe-collection shape)
based_on number 0 imported · 1 design · 2 wireframe
screen_id string | null grammar match; a miss yields null + logs an ingest issue to staging/issues.json
variation_label string deterministic name split
structure object WFNode tree — same shape as the generation-parse write (A), including the deterministic layout/style/component captures (component_id/component_variants, auto-layout fields, text styling, divider leaves) — root node_id EQUALS the doc-identity node_id; a top-level child = a WF section (review scoring unit)
memos object[] { memo_node_id, text, status } (status deterministic: 取込済 containment ⇒ resolved) — NO per-run fields here
summaries object { element_type_histogram, node_count } (computed)
lineage object { source_url (RAW REST snapshot), source_hash (≡ content_hash), processor_version (e.g. wf_parse@1.2), index_schema_version, processed_at, job_id (the wf_parse run id) }

Fencing: LWW guarded by (source_hash, processed_at) — a write lands only with a new source hash or newer processed_at.

S3. None of its own. It consumes snapshots at the content-addressed key wf2design/snapshots/{figma_file_key}/{node_id}.{content_hash}.json; the client-facing parse.json artifact belongs to a generation run's result doc, not to wf_parse.

Webhook. Webhook-free. NO ai-status webhook — the wireframe doc IS the record.

PG effect. PG-free. No wf2des row, no manifest. Workers NEVER write PostgreSQL.


rule_process

The project's design rules are EXTRACTED from the SELECTED Figma guideline boards — one operation ending in one immutable design_rule revision. Triggered by the wf2des-events intake queue (rule-upload category) — NOT EventBridge, NOT a generation queue. The designer selects the guideline board frames in the plugin; the backend emits a thin rule-upload event; a rule_process worker renders and extracts at registration time. This run NEEDS the service-account Figma PAT and is NOT LLM-free: it hosts fence call 4 (per-board vision rule extraction).

Consumes / Input

Thin rule-upload event (event_type rule_upload). The old file_url field (S3 strict-JSON rule file) is GONE — replaced by the board selection.

Field Type Required Validation Notes
event_type string ✓ rule-upload category (rule_upload) routes to rule_process
design_rule_id string (uuid) ✓ platform design_rule row id, by value half of the unique business key
figma_file_key string ✓ Figma verbatim the file the guideline boards live in
board_node_ids string[] ✓ min 1 the guideline nodes the designer selected in the plugin — board FRAMEs, or a CANVAS/SECTION container which the worker EXPANDS to its child board frames (nested sections recurse; other child types are not boards). Resolution of the selection, never curation
organization_id, project_id integer ✓ tenancy scope message-carried scope

The spec gives no JSON example for the rule-upload event.

Reads. Figma REST via the service-account OAuth app grant (Authorization: Bearer, fetched from the Admin internal token provider; figma_pat fallback): (1) get_file_nodes for the selected board nodes (titles) — a MISSING board node is a hard error (raise → redelivery); (2) the file styles, for deterministic style-token names (_style_captures_from_file); (3) the Figma images API (GET /v1/images, PNG scale 2) to render each board — a NULL render is a hard error. No S3 rule file is read — the boards ARE the source. Also reads the current max version for this design_rule_id from the design_rule collection to assign the next ordinal. Tenancy read filters organization_id + project_id.

Processing Contract

Two pipelines exist; the NODE pipeline is live. config.rule_pipeline_nodes (default true) routes ingestion through comprehend → per-role extract → deterministic authority-gated merge, reading the board's NODE TREE rather than a render. It was chosen on a live A/B against the vision path below: more colours (105 vs 93), better token names (91% vs 84%), ~1.8x faster, deterministic, and no fabrication (guarded by a corpus-verbatim check). Prompt pins bc@0.2 (comprehension), sys@0.3 (system/screen-group), cmp@0.3 (component specs + slot rules), tok@0.2 (tokens). The vision path documented in the steps below stays as an opt-out fallback and is otherwise dormant.

  1. Figma node read — get_file_nodes for the selected boards' titles; a MISSING board node is a hard error (raise → SQS redelivery).
  2. DETERMINISTIC style-token names from the file styles (_style_captures_from_file).
  3. Render each board via the Figma images API (GET /v1/images, PNG scale 2, service-account OAuth app grant (Authorization: Bearer, figma_pat fallback)) — a NULL render is a hard error; each render is stored to S3 at wf2design/rule-boards/{figma_file_key}/{node_id}.{sha256}.png.
  4. LLM — FENCE CALL 4: one vision extraction per board (Agent output_type=RuleSet, RULE_EXTRACTION_INSTRUCTIONS, RULE_EXTRACTION_PROMPT_VERSION re@0.10, vision tier / config.vision_model) — extracts ONLY what the board shows; slot_constraints always empty.
  5. DETERMINISTIC merge (merge_rule_fragments): boards processed sorted by node_id; spacing.scale and color allowed UNION sorted; section_gap first-non-null; typography per style_token first-occurrence-wins.
  6. content_hash over the MERGED RuleSet (sha256, canonical JSON, sorted keys) — identity semantics unchanged: identical extractions converge, no new ordinal. Assign version = (current max version for this design_rule_id) + 1.
  7. Write ONE immutable design_rule revision doc DIRECTLY to DocDB (webhook-free) — the merged rules AND boards[] + extraction provenance, draft_source = "llm_extracted"; one immutable doc per (design_rule_id, content_hash). NO webhook, NO PG effect — the doc IS the record.

Review model: extraction lands as draft_source="llm_extracted"; the designer reviews the extracted rules (inspect/preview) and re-registers after guideline fixes — identical merged rules converge on the same revision, changed rules mint the next version (accepted version churn).

flowchart TD
  A["event: rule upload<br/>(design_rule_id + figma_file_key + board_node_ids)"] --> B["Figma REST: resolve selection to board frames<br/>(CANVAS/SECTION expands to child frames)<br/>missing node / empty container = HARD error"]
  B --> C["deterministic style-token names<br/>from the file styles"]
  C --> D["render each board — images API, PNG scale 2<br/>null render = HARD error; store to S3<br/>wf2design/rule-boards/{figma_file_key}/{node_id}.{sha256}.png"]
  D --> E["FENCE CALL 4 — one vision extraction per board<br/>output_type=RuleSet · re@0.10 · vision tier<br/>only what the board shows"]
  E --> F["DETERMINISTIC merge (merge_rule_fragments)<br/>boards sorted by node_id; unions sorted;<br/>section_gap first-non-null;<br/>typography first-occurrence-wins"]
  F --> G["content_hash over the MERGED RuleSet<br/>version = max+1"]
  G --> H["write ONE immutable design_rule revision<br/>(webhook-free) — merged rules + boards[] +<br/>extraction provenance, draft_source=llm_extracted;<br/>the collection IS the revision history"]
  H --> I["done — NO webhook, NO PG effect;<br/>the design_rule doc IS the record"]

Emits / Output

DocDB write — design_rule revision doc (single writer, immutable).

Field Type Source
_id string (uuid) surrogate uuid
design_rule_id string (uuid) event field — half of the immutable unique business key
content_hash string (sha256) canonical JSON over the MERGED RuleSet — the other half of the unique business key
version integer ordinal, assigned per new content_hash
organization_id, project_id integer tenancy scope
draft_source string enum human | llm_draft_adjusted | llm_extracted — this run writes llm_extracted
rules object the MERGED RuleSet — every rule class names the spec field it binds to. spacing {scale[], section_gap}; typography [{style_token, max_lines, usage, size_px, size_min_px, size_max_px, line_height_pct, weight, applies_to[]}]; colors {entries[{token, value, usage}], allowed[]}; usage_rules[]; layout_patterns[]; rich_layouts[]; globals; component_inventory[] (superseded by component_specs, kept for older revisions); component_specs [{component, variants[], states[], seen_on_screens[], slot_rules}]; screen_group_policies [{group, covers, screen_ids[], device, content_width_px, padding_x_px, padding_y_px, section_gap_px, element_gap_px, columns_min, columns_max, section_separator, full_bleed, source_text}]. slot_constraints was removed — no extraction pass ever produced it
boards object[] {node_id, title, image_url, content_hash (sha256 of the PNG image), mime} — one per selected guideline board; image_url → the S3 render, re-read at assemble as vision reference
extraction_model_id, extraction_prompt_version, extracted_at string / string / timestamp extraction provenance. The LIVE path is the node pipeline (comprehend → per-role extract → deterministic merge), pinned bc@0.2 / sys@0.3 / cmp@0.3 / tok@0.2; the vision path (re@0.10) remains as an opt-out fallback
llm_usage object what this ingestion cost, from the provider's own report: {calls, requests, input_tokens, output_tokens, cached_input_tokens, stages[{stage, calls, requests, input_tokens, output_tokens, cached_input_tokens}]} — per stage, costliest first
consolidation object | null the consolidation judge's decisions, when it ran
lineage object { source_url, source_hash (≡ content_hash), processor_version, index_schema_version, processed_at, job_id (the rule_process run id) }

Fencing: idempotent content-addressed insert per (design_rule_id, content_hash) — UNIQUE, with content_hash computed over the MERGED RuleSet (identity semantics unchanged). A replay of unchanged content — identical extractions — maps to the existing doc/version and never mints a new ordinal; changed merged rules mint the next version (accepted version churn). Concurrent uploads of the same rule are serialized by the single upload event. This collection IS the rule's revision history (each revision a new immutable doc).

S3. Board renders at wf2design/rule-boards/{figma_file_key}/{node_id}.{sha256}.png (Figma images API, PNG scale 2) — referenced by boards[].image_url, re-read at assemble as the per-section selection's vision reference. No other S3 artifact — the design_rule doc is the record.

Webhook. Webhook-free. NO ai-status webhook — the design_rule doc IS the record.

PG effect. PG-free. No wf2des row, no manifest. (The platform design_rule REGISTRY row is created client-side through the platform API, not by this worker.)


component_sweep

Keeps the shared component registry true — a DETERMINISTIC Figma REST full-file walk, NO LLM anywhere. This is the ONE internal run with an EventBridge-schedule trigger. Single-flight guarded by a sweep marker on the project_figma_file doc.

Consumes / Input

Two trigger sources: EITHER an EventBridge SCHEDULE (invokes the worker directly — AI-owned; no message body) OR a plugin RESYNC event on the wf2des-events intake queue (the plugin-events category; the backend holds send permission and still emits it) — NOT a generation queue.

Field Type Required Validation Notes
event_type string ✓ (resync path) plugin-events / resync category routes to component_sweep
organization_id, project_id integer ✓ tenancy scope scopes the walk

The worker reads the project's registered files from project_figma_file (via the wf2des-api file list) to scope the walk; it does NOT receive component URLs in the message (it walks Figma REST itself). No sweep/job id exists. No JSON example is given.

Reads. Figma REST via the service-account OAuth app grant (Authorization: Bearer, fetched from the Admin internal token provider; figma_pat fallback). Reads the project's registered files from project_figma_file (working + library roles) to scope the walk. Full-file walk: /components lists PUBLISHED components only; local components found by the full-file-walk fallback. Reads/writes ONLY DocDB + S3 (never PG). Resumable/checkpointed: depth-limited listing + batched node reads, never one full-file GET. Tenancy filters organization_id + project_id.

Processing Contract

  1. Schedule/resync fires; acquire the SINGLE-FLIGHT guard — a sweep_marker {token, acquired_at, expires_at} on the project_figma_file doc (crash-safe stale-lock recovery — a lock past expires_at is reclaimable).
  2. Figma REST full-file walk (/components lists published only; local components are ours, found by the walk) — DETERMINISTIC.
  3. Per COMPONENT / COMPONENT_SET node: write a content-addressed S3 snapshot.
  4. Derive the context doc DETERMINISTICALLY: component_key, node_id, variant_properties, text_slots (layer paths; ambiguity flagged at registration), image_slots, default_size. NO LLM ANYWHERE — straight transforms of Figma JSON.
  5. Capture local styles/variables and library bindings actually used by the selected boards → project_figma_file.style_captures (the source of the derived style_bindings map). Plugin capture uses the board IDs frozen at sync time, walks their descendants and referenced component definitions/variants, and resolves exact variable/style IDs (including mixed text runs and paint-bound variables). It does not import libraries or infer tokens from colors/names. Variables retain bare and collection-qualified aliases; a local token has precedence, while conflicting remote aliases are omitted. Inaccessible individual references are skipped without losing readable bindings. This keeps the existing token → ID contract; existing empty captures require a component-library sync with the updated plugin.
  6. Emit the DISCOVERED-COMPONENT sweep manifest → the backend UPSERTS platform design rows type=component (id · name · type=component · image_url · json_schema_url · status) — the SHARED registry (des2code's catalog too); backend-side PG.
  7. Write the design_component CONTEXT doc DIRECTLY to DocDB, keyed to the platform design row id — no removed_at (removal = the design row's status).
  8. Stamp project_figma_file FIELD-LEVEL: components_synced_at (updated LAST, after the sweep completes), + sweep_error on failure. Release/expire the sweep marker.
flowchart TD
  A["schedule / resync event fires<br/>(single-flight guard: a sweep marker<br/>on the project_figma_file doc)"] --> B["Figma REST: full-file walk<br/>(/components lists published only; ours are local)"]
  B --> C["per COMPONENT / COMPONENT_SET node:<br/>content-addressed S3 snapshot"]
  C --> D["derive context doc: component_key, node_id,<br/>variant_properties, text_slots (layer paths,<br/>ambiguity flagged at registration),<br/>image_slots, default size"]
  D --> E["capture local text styles + color variables<br/>(→ project_figma_file.style_captures;<br/>source of the derived style_bindings map)"]
  E --> F["emit the DISCOVERED-COMPONENT manifest →<br/>backend UPSERTS platform design rows type=component<br/>(id · name · type=component · image_url · json_schema_url · status) —<br/>the SHARED registry (des2code's catalog too); backend-side PG"]
  F --> G["write design_component CONTEXT DIRECTLY to DocDB,<br/>keyed to the platform design row id —<br/>no removed_at (removal = the design row's status)"]
  G --> H["stamp project_figma_file field-level:<br/>components_synced_at (+ sweep_error on failure)"]

Emits / Output

DocDB write (A) — design_component context docs (one per COMPONENT/COMPONENT_SET; single writer, upsert on _id + single-flight guard). INSTANCING CONTEXT ONLY (registry lifecycle lives on the platform design row):

Field Type Source
_id string the platform design row id (type=component) = {project_id}_{figma_file_key}_{node_id} — _id IS the platform design id (no surrogate uuid, no separate platform_design_id field); parts derived via split('_', 2)
organization_id, project_id integer tenancy scope
name string display name
kind string enum published | local (local have no publish key, instanced by node_id)
component_key string | null Figma publish key; null for local
variant_properties object e.g. {type:[default, logged-in]}
text_slots object[] {layer_path, default_text, ambiguous}
image_slots object[] {layer_path, ambiguous}
default_size object {w, h}
variants object[] per-variant profile: {variant_props, size, text_slots[], nested_components[], layout_shape, appearance[], semantics[]} — what each variant actually holds, so selection can tell siblings apart
variant_defaults object axis → default value, as the component set declares it
family object | null {name, node_id, node_type} — the design system's OWN grouping, read from the SECTION the sweep found the component under
semantics object | null {kind, also_kinds[], is_placeholder, function[], appearance[], model_id, prompt_version, tagged_at, facts_hash}. kind is ONE closed-vocabulary category (COMPONENT_KIND_VOCABULARY); is_placeholder marks a component that reserves space rather than providing content. Written by the enrichment pass, pinned cs@0.3
key_provenance, name_provenance string which producer last wrote the publish key / the name (plugin-observed outranks a REST sweep)
text_props string[] non-variant text property names the component exposes
lineage object {source_url, source_hash (≡ the component-subtree content hash — no separate content_hash field), processor_version, index_schema_version, processed_at, job_id}

DocDB write (B) — project_figma_file field-level (disjoint from the plugin's role/config_url writes):

Field Type Source
style_captures object token name → Figma style/variable id; ONE per figma_file_key — source of the derived style_bindings map
components_synced_at timestamp freshness watermark — updated LAST after sweep completes
sweep_error string | null latest sweep failure, else null
sweep_marker object | null {token, acquired_at, expires_at} single-flight lock; NULL when no sweep in flight

Fencing: design_component = upsert on _id + single-flight guard (sweep marker on project_figma_file). project_figma_file = field-level updates (disjoint from plugin fields, no conflict). Component removal is recorded on the platform design row's status, NOT our doc (no removed_at).

S3. Content-addressed component SNAPSHOTS per COMPONENT/COMPONENT_SET node at wf2design/snapshots/{figma_file_key}/{node_id}.{content_hash}.json. PLUS the DISCOVERED-COMPONENT SWEEP MANIFEST (an S3 component manifest).

Webhook. NOT webhook-free, but emits NO ai-status webhook (that is generation-only). Instead it emits a DISCOVERED-COMPONENT sweep manifest to the backend, which is applied backend-side as platform design (type=component) UPSERTS. The manifest is a separate registry path, not the ai-status payload. Manifest content: one entry per Figma component carrying id · name · type=component · image_url · json_schema_url · status.

Not yet finalized: the exact sweep-manifest shape. It follows the generation manifest envelope — same structure, per-type row_effects, here targeting platform design rows instead of the wf2des row — as does the ai-status discriminator type value.

PG effect. The ONE internal run with a backend-side PG effect: the backend UPSERTS platform design rows type=component (the SHARED component registry, also consumed by des2code) from the sweep manifest — rows: id · name · type=component · image_url · json_schema_url · status. This is a registry UPSERT (idempotent), NOT the ai-status webhook and NOT a wf2des-row flip. Workers still NEVER write PG directly — the backend applies the manifest.


Error Handling

All runs use the platform partial-batch SQS handler (ReportBatchItemFailures); a thrown error inside process_record marks only that item as failed and lets SQS redeliver it independently.

Scenario Behavior
Thrown error, any run SQS redelivery via partial-batch ReportBatchItemFailures; the item is added to batchItemFailures
Generation webhook send fails The send RAISES → SQS redelivers and the send retries; the terminal S3 manifest stays the durable record
Provider credits exhausted, credentials/access invalid, or configured model unavailable Terminal, client-visible failure: write …-failed.json + failed manifest, send failed webhook, and consume the message; redelivery cannot repair account/configuration state
Ordinary provider 429 rate limit or 5xx Throw without a terminal surface so SQS redelivers; these conditions are retryable
Worker crash mid-parse (generation) One-shot Lambda dies → redelivery; fencing (wf2des_id, attempt) blocks the zombie worker's DocDB commits and its late webhook
Retries exhausted / row stuck '0' (generation) No recovery path today. The row stays non-terminal, and the partial-unique index keeps blocking new triggers for that wireframe. cancel only applies at awaiting_confirm, so a run stuck in parse or assemble cannot be cleared through the API
Awaiting-confirm forgotten Backend-side sweep over wf2des rows enforces a timeout (config, e.g. 72h), fails closed
Generation fails during parse (fail-closed) An unresolvable pin / scope mismatch (ContractFailure) → …-failed.json + failed webhook → row flips status '2' (phase cleared); a TRANSIENT parse error redelivers instead, no terminal surface
Generation fails after assemble …-failed.json artifact + failed webhook → handler records error, flips status '2' (phase cleared) → designer sees reason + Retry
Pinned input missing / hash mismatch on rehydrate (assemble) Fail closed — never a silent fallback to older rules/components
Missing-row read (generation) A real error (row-first-then-SQS), not an eventual-consistency window
Materializer diagnostics (name_fallback/ordinal_fallback/build_error/font_fallback/prop_rejected/unmatched/preserved) Later PLUGIN-side concern recorded in placement.materializer_report; font_fallback is degraded rendering, not a failed build
screen_id grammar miss (wf_parse) NOT an error: yields null + an ingest issue in staging/issues.json for operator fix
Missing board node / null board render (rule_process) A HARD error — raise → SQS redelivery; never a partial extraction
Thrown error (internal runs) SQS redelivery on wf2des-events; the idempotent content-addressed / LWW write re-converges on redelivery
component_sweep failure Stamps project_figma_file.sweep_error field-level; the sweep marker is crash-safe (a lock past expires_at is reclaimable); a missed manifest apply re-converges on the next sweep
Stuck-run visibility (internal runs) Output-doc freshness (e.g. components_synced_at) + the SQS dead-letter queue (the backend never sees internal runs)

Idempotency & Fencing

Fencing is per collection; there is no PG fencing beyond the wf2des row (generation only).

Collection / surface Fence
design_generation_result CAS on (_id, attempt) — a zombie/superseded attempt's write is rejected (generation only)
wireframe LWW on (source_hash, processed_at) — a write lands only with a new source hash or newer processed_at; generation-parse and wf_parse writing the same doc is intended non-collision (each run reads its own parse.json)
design_rule Idempotent content-addressed insert per (design_rule_id, content_hash) UNIQUE, content_hash over the MERGED RuleSet — identical extractions converge on the existing doc/version, never a new ordinal
design_component Upsert on _id + single-flight sweep marker on project_figma_file
project_figma_file Field-level updates (sweep fields disjoint from plugin role/config_url — no conflict)
design_resolution SCORE-RATCHET upsert on _id (= {org}_{project}_{section_signature}): a voted write lands only where the held entry is NOT mined AND scores STRICTLY lower (server-side filter; a DuplicateKeyError on the fenced upsert means KEPT, returned as such — never an error). mined writes are unfenced. Identical evidence scores identically → reruns never churn; better decisions win → monotonic improvement
Generation webhook Dedupes on (job_id, attempt, nonce); a manifest replay would be safe under the same dedupe (one PG txn, manifest↔payload validation of job_id/job_type/S3-prefix) — but the sweep that would perform it is not implemented, so no replay happens today

Additional invariants:

  • Parse cache (content-level dedupe): wf_content_hash computed at trigger-time snapshot capture rides the message; an unchanged hash short-circuits the LLM by copying the prior job's parse artifacts. SKIPPED on retry-after-reject.
  • Redelivery (generation) reuses existing pins and re-runs selection only if the inputs block is absent — a crash-retry can never silently select different components/rule revisions.
  • COMPLETED generations are NEVER reprocessed — a full replace would overwrite primary human-event data with a different nondeterministic spec. A terminal row is final; re-running a wireframe creates a NEW row.
  • The LLM fence applies throughout: the LLM appears ONLY where a step says so; everything else is deterministic. Parse/wf_parse = roles + intent (fast tier); assemble = section-parallel selection as a K-sample self-consistency vote (strong tier, vision-capable) + whole-screen planning; rule_process = rule ingestion (comprehension/extraction/consolidation, ingestion-time); registry enrichment and render review use their configured models; component_sweep = no LLM. The deterministic guards over the vote, the design_resolution consult/write-back, and the confidence formula (formula_version) are ALL LLM-free — model self-report is banned. Everything the LLM decides is stored WITH its evidence (source, lineage_wf_node_ids, pinned inputs).
  • Internal events carry no wf2des product row, and there is no (_id, attempt) CAS on any internal run. When event_run_id is present, live state is recorded in TTL-backed wf2des_event_status; scheduled resync is the untracked exception, where the output doc's existence + freshness IS the run status. Content writes retain their own idempotency/fencing rules.

Runtime Configuration

Config Value / Source Notes
DocumentDB database guinness_v2 (AWS DocumentDB 5.0) shared platform DB; the 6 wf2des collections unprefixed as peers (incl. design_resolution, env-overridable via design_resolution_table_name); env-overridable collection names
Collection modules packages/models/src/models/documentdb/<name>.py COLLECTION_NAME + get_collection + create_indexes; Pydantic doc schemas in apps/wf2des/src/wf2des/schemas/
DEFAULT_ORGANIZATION_ID 1 single-tenant today; organization_id on every doc
index_schema_version 1.4 for new WF2Des writes bump on document/encoder shape change; additive fields preserve older document readability
Intake queue wf2des-events (AI-owned; backend send-only) discriminated events: frame_registration, rule_upload, resync, component_upload, component_capture, render_review
Generation queues backend-owned generation parse / assemble queues the ONLY two the backend owns
EventBridge schedule AI-owned; invokes the worker directly for component_sweep resync sweeps the exact cron/rate is set at deploy time
ai-status webhook POST /v1/webhooks/ai-status, X-API-Key GENERATION ONLY; no internal event emits it — tracked internal events report through wf2des_event_status instead
Webhook dedupe key (job_id, attempt, nonce); job_id = wf2des row id
Webhook payload {job_id, attempt, nonce, status, error?, result_manifest_url?, manifest_schema_version}
Result manifest schema manifest_schema_version = 1 the only manifest the system produces
S3 result manifest key {org}/{proj}/wf2des/{wf2des_id}-{ts}-manifest.json (…-failed-manifest.json on failure) generation only; the webhook handler's row-effect input, carried by result_manifest_url
S3 client-facing artifact key {org}/{proj}/wf2des/{wf2des_id}-{ts}-{result\|failed}.json generation only; e.g. 1/3/wf2des/8f14e4…-20260706T093000Z-result.json
S3 intermediate artifact keys {org}/{proj}/wf2des/{wf2des_id}-{ts}-{spec\|parse\|feedback}.json internal wf2des prefix
S3 content-addressed snapshot key wf2design/snapshots/{figma_file_key}/{node_id}.{content_hash}.json for ALL worker-captured sources
S3 rule-board render key wf2design/rule-boards/{figma_file_key}/{node_id}.{sha256}.png rule_process per-board PNG render (Figma images API, scale 2); referenced by design_rule.boards[].image_url
S3 config home {org}/{proj}/wf2des/config.json referenced by project_figma_file.config_url
Model tiering FAST_MODEL=openai:gpt-4o-mini (parse roles/intent, mp@0.25); STRONG_MODEL=openai:gpt-5.4 (selection); VISION_MODEL=openai:gpt-5.4-mini (rule_process runs the NODE pipeline, bc@0.2 / sys@0.3 / cmp@0.3 / tok@0.2, with the vision path re@0.10 as an opt-out fallback); SCREEN_REVIEW_MODEL=openai:gpt-5.4; registry enrichment adds slot roles (sr@0.2) and component semantics (cs@0.4) inputs.llm pins matching model, prompt version, review model, reasoning effort, and enabled state; concrete model ids and tier assignments are configurable per deployment. For non-GPT review models use SCREEN_REVIEW_REASONING_EFFORT=none
Candidate/review switches CANDIDATE_KIND_GATE=true; SCREEN_PLANNING_ENABLED=true; SCREEN_REVIEW_REASONING_EFFORT=high Gate off only for controlled evaluation. Reasoning none is provider-neutral
formula_version (confidence) cf@0.6 — pinned per run bump on ANY change to the confidence constants, since a confidence number is only interpretable against the formula that produced it
Section-signature version e.g. sig@1 — rides inside every design_resolution.section_signature a signature-recipe change mints a new version prefix; old entries simply stop hitting
Selection voting K samples per section (self-consistency, majority on (case, platform_design_id)) K configurable; deterministic guards arbitrate ties
config.json curated keys memo_resolved_markers (default 取込済) · annotation_section_markers (board-template annotation headings; shipped defaults) · variation_separator (default |) · design_area per-project overrides for team conventions — the pipeline itself stays design-system agnostic
Service-account Figma auth a PRIVATE OAuth app grant (Authorization: Bearer), with a PAT as fallback The Admin API stores the encrypted grant in PostgreSQL figma_oauth_grant, refreshes it under an organization advisory lock, and exposes only the live access token + expiry through its IAM-protected internal provider. The worker holds no PostgreSQL credentials, refresh token, client secret, or ENCRYPTION_KEY. Figma supports no machine-to-machine flow, so an org admin authorizes ONCE against a dedicated Figma service account. figma_pat remains as the fallback for a deploy without the provider configured
wf2des-api (data plane) route group /internal/wf2des/* in the internal-api app (Lambda Function URL; reads/writes ONLY DocDB + S3, never PG/SQS) file-registration route POST/GET /internal/projects/{project_id}/figma-files (UNIQUE(organization_id, project_id, figma_file_key)) — component_sweep reads this list to scope its walk. Auth: X-AI-Service-Token (read) / plugin session token {org_id, project_id, exp} (read+write)
Awaiting-confirm timeout NOT IMPLEMENTED. Intended: config, e.g. 72h — backend-side sweep, fails closed generation only; a parked run currently waits forever
Stuck-'0' sweep interval NOT IMPLEMENTED. Intended: e.g. ~15 min generation only; a row left at '0' currently blocks its wireframe permanently
Latency soft targets (p50) trigger→parse preview < 60s · confirm→spec < 2min · spec→materialized < 30s soft, measured, not kill criteria
Size limit spec > ~1MB → spills to its S3 key (spec.json); spec_nodes_flat + summaries stay inline generation result doc only; no other byte limits apply
PROMPT_VERSION pinned per the platform worker recipe

Logging

Structured logging is expected from day one; the load-bearing fields:

  • Generation timings live on the result doc's timings block: created_at (the run-START anchor, distinct from lineage.processed_at, the terminal write timestamp — no duplication), parse_done_at, assembled_at, plus per-phase durations <phase>_ms (e.g. snapshot_ms, parse_ms, selection_ms, assemble_ms, validate_ms). Confirmation latency is read from parse.confirmed_at, not a timings copy.
  • Selection record (assemble): the selection block IS the mandated record (per-section candidate counts, composed/unmatched tallies, truncation). validator_report and confidence are computed and reproducible. Everything the LLM decided is stored WITH its evidence for review/feedback attribution.
  • Ingest issues (wf_parse): screen-ID grammar violations, unknown memo markers, and non-auto-layout roots go to staging/issues.json for operator fix (not applicable at generation, where screen_id comes from the request).
  • Internal runs: structured log events are not enumerated here. Recommended practice: timings on each output doc; component_sweep freshness read from project_figma_file.components_synced_at and failures from project_figma_file.sweep_error.
  • Observability from day one: optimize from measured p50/p95, not guesses.

Field Reference (Lookup Table)

Cross-walk of the load-bearing fields to their downstream homes. ✓ = present/echoed; — = absent; a literal name = a rename. Internal runs have no webhook (—).

Field Message in DocDB out Webhook out Notes
wf2des_id ✓ (generation) _id (design_generation_result) job_id run id = the wf2des PG row id
attempt ✓ (generation) fences CAS (_id, attempt) ✓ always 1 today; nothing bumps it
nonce ✓ (generation) — ✓ webhook dedupe key member
organization_id ✓ ✓ — tenancy scope (every read/write filters it)
project_id ✓ ✓ — tenancy scope
screen_id ✓ (generation, from request) wireframe.screen_id (nullable) + result screen_id — wf_parse derives it deterministically (grammar); miss ⇒ null + issue
wf_content_hash ✓ (parse) lineage.source_hash ≡ inputs.wireframe.source_hash — the parse-cache key
snapshot_scope ✓ (parse) drives extraction — frame + enclosing board section
figma_file_key, node_id ✓ (wf_parse) inside composite _id; derived via split('_', 2) — not stored separately (wireframe, design_component)
design_rule_id ✓ (rule_process) design_rule.design_rule_id — with content_hash = unique business key
figma_file_key, board_node_ids[] (rule) ✓ (rule_process) design_rule.boards[] {node_id, title, image_url, content_hash, mime} — the selected guideline boards; renders stored at wf2design/rule-boards/…
flag_count — confidence.flag_count via manifest row_effects.wf2des mirrored to wf2des.flag_count
llm_usage — design_generation_result.llm_usage — what the run cost, per stage, from the provider's own report — the same shape design_rule.llm_usage carries
result_manifest_url — — ✓ (succeeded/failed) webhook-only; points at the terminal S3 manifest object (not stored in the result doc); its inner result_url → the result artifact
status — — parse_done | succeeded | failed generation only

  • Overview — Processing flow diagrams and module breakdown for generation and internal events.
  • Test cases — Test matrix that enforces this contract.
  • apps/wf2des/src/wf2des/schemas/ — Pydantic document schemas, one module per domain; the package re-exports every name, so from wf2des.schemas import X is the contract surface.
  • packages/models/src/models/documentdb/ — collection modules (COLLECTION_NAME + get_collection + create_indexes) for the 6 wf2des collections.
  • Des2Code I/O Definition — shares the platform design (type=component) registry that component_sweep upserts.