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
- Message received (
wf2des_id+attempt+nonce). - PIN inputs into the result doc's
inputsblock — the FIRST act of the invocation (the pinning moment). The parse invocation pins the parse inputs (wireframe identity /source_hash); thedesign_rulepin = the project's LATESTdesign_rulerevision (repo.find_latest_design_rule: highestversion, ties by newestlineage.processed_at) — no revisions ⇒ pinNone→ the validator'sno_rulespath; never an inline rule file. - Parse-cache check: primary-key lookup by composite
_id, samewf_content_hash. Cache is SKIPPED on retry-after-reject (a fresh parse is the point). - 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). - 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.
- DETERMINISTIC memo statuses: memos inside a 取込済 ("imported") status frame ⇒ resolved; resolved memos excluded from live intent.
screen_id(from request) andvariation_label(deterministic frame-name split) are deterministic — NOT LLM. - LLM (fast tier) — assigns node
role(from the shared element-type vocabulary — the closed set of WFNode role values, pinned inprompts.pyasROLE_VOCABULARY) +intent(from OPEN memos + variant labels) ONLY. NEVER the tree shape. Low temperature. This is the ONLY LLM step in parse. - Write the
wireframecache doc (LWW fence on(source_hash, processed_at)). - Write the result doc's
parseblock + the immutableparse.jsonartifact (CAS on(_id, attempt)). - Webhook: parse done (raises on failure).
- Branch on
auto_confirm:false→ backend flipswf2des.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
- Message received.
- REHYDRATE pins from the result doc's
inputsblock; fail closed on any hash mismatch. On redelivery, reuse existing pins — re-run selection only if the block is absent. - Build the registry set: the project registry, filtered by
organization_id+project_id, deduped, and hygiene-collapsed. No vector/similarity scoring. - Build per-section candidate sets with the deterministic role→admissible-kind gate (
CANDIDATE_KIND_GATE=trueby default). Size remains a selection signal, never a gate. Counts are recorded inselection; truncation is flagged. The switch may be disabled for controlled evaluation, not normal production. - DECISION-LEDGER consult (
design_resolution, keyed by the structuralsection_signature): an entry LOCKS its section (no LLM) whenprovenance = minedor itsscore ≥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. - 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 pinneddesign_rulerevision'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). - 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 tocomposeso 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. - DETERMINISTIC stitching: spec tree +
spec_nodes_flat+style_bindingsmap (produced FROMproject_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 anupdate_oneWITHOUT upsert, because plugin file-registration was meant to own doc creation. Under Arch A no such route exists — the backend exposes onlyGET …/figma-files— so the doc never appeared and every one of those writes silently matched nothing. Measured on a real sync:enrich_registryre-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 callsensure_project_figma_filebefore readingslot_roles:$setOnInsertONLY,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 ofrole/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 andcomponent_sweeplogswf2des.component_sweep.no_filesand 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 flaggedrules_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-unmatchedsection (and compose-child) decision upserts intodesign_resolutionwith a DETERMINISTIC score — fenced by the RATCHET (avotedwrite lands only over a non-minedentry with a strictly lower score;minedwrites are unfenced; aDuplicateKeyErroron the fenced upsert means KEPT, not an error). 12. Write result docspec/spec_nodes_flat/selection/validator_report/confidenceblocks (CAS on(_id, attempt)); worker also writesplacement.design_area(resolved fromconfig.json). 13. S3: immutable result artifact + result manifest (schema-versioned). 14. Webhook: succeeded + manifest key → backend flipswf2desto 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_parseevent; 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
- 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.
- Memo statuses — DETERMINISTIC: a memo inside a 取込済 frame ⇒
status=resolved; resolved memos excluded as live intent. 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 tostaging/issues.json.variation_label— DETERMINISTIC frame-name split (会員登録TOP|案1 → 案1).- 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.
- Compute derived summaries:
element_type_histogram,node_count. - Write the
wireframecache doc directly (LWW fence). NO result doc, NO preview, NO webhook — thewireframedoc 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 throughcomprehend → 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 pinsbc@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.
- Figma node read —
get_file_nodesfor the selected boards' titles; a MISSING board node is a hard error (raise → SQS redelivery). - DETERMINISTIC style-token names from the file styles (
_style_captures_from_file). - Render each board via the Figma images API (
GET /v1/images, PNG scale 2, service-account OAuth app grant (Authorization: Bearer,figma_patfallback)) — a NULL render is a hard error; each render is stored to S3 atwf2design/rule-boards/{figma_file_key}/{node_id}.{sha256}.png. - LLM — FENCE CALL 4: one vision extraction per board (Agent
output_type=RuleSet,RULE_EXTRACTION_INSTRUCTIONS,RULE_EXTRACTION_PROMPT_VERSIONre@0.10, vision tier /config.vision_model) — extracts ONLY what the board shows;slot_constraintsalways empty. - DETERMINISTIC merge (
merge_rule_fragments): boards processed sorted bynode_id;spacing.scaleand colorallowedUNION sorted;section_gapfirst-non-null; typography perstyle_tokenfirst-occurrence-wins. content_hashover the MERGED RuleSet (sha256, canonical JSON, sorted keys) — identity semantics unchanged: identical extractions converge, no new ordinal. Assignversion= (current max version for thisdesign_rule_id) + 1.- Write ONE immutable
design_rulerevision doc DIRECTLY to DocDB (webhook-free) — the merged rules ANDboards[]+ 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
- Schedule/resync fires; acquire the SINGLE-FLIGHT guard — a
sweep_marker {token, acquired_at, expires_at}on theproject_figma_filedoc (crash-safe stale-lock recovery — a lock pastexpires_atis reclaimable). - Figma REST full-file walk (
/componentslists published only; local components are ours, found by the walk) — DETERMINISTIC. - Per COMPONENT / COMPONENT_SET node: write a content-addressed S3 snapshot.
- 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. - Capture local styles/variables and library bindings actually used by the selected boards →
project_figma_file.style_captures(the source of the derivedstyle_bindingsmap). 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. - Emit the DISCOVERED-COMPONENT sweep manifest → the backend UPSERTS platform
designrowstype=component(id · name · type=component · image_url · json_schema_url · status) — the SHARED registry (des2code's catalog too); backend-side PG. - Write the
design_componentCONTEXT doc DIRECTLY to DocDB, keyed to the platform design row id — noremoved_at(removal = the design row's status). - Stamp
project_figma_fileFIELD-LEVEL:components_synced_at(updated LAST, after the sweep completes), +sweep_erroron 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 platformdesignrows instead of thewf2desrow — as does the ai-status discriminatortypevalue.
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_hashcomputed 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
inputsblock 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, thedesign_resolutionconsult/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
wf2desproduct row, and there is no(_id, attempt)CAS on any internal run. Whenevent_run_idis present, live state is recorded in TTL-backedwf2des_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
timingsblock:created_at(the run-START anchor, distinct fromlineage.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 fromparse.confirmed_at, not atimingscopy. - Selection record (assemble): the
selectionblock IS the mandated record (per-section candidate counts, composed/unmatched tallies, truncation).validator_reportandconfidenceare 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 tostaging/issues.jsonfor operator fix (not applicable at generation, wherescreen_idcomes from the request). - Internal runs: structured log events are not enumerated here. Recommended practice:
timingson each output doc;component_sweepfreshness read fromproject_figma_file.components_synced_atand failures fromproject_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 |
Related Links
- 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, sofrom wf2des.schemas import Xis 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 thatcomponent_sweepupserts.