AI Code2WF — I/O Definition
This document defines the planned contract for apps/code2wf/. Code2WF has one asynchronous generation attempt and a later client-side Figma materialization step. Page execution and capture belong to Page Import.
Overview
flowchart LR
PI[("Completed Page Import")] --> API["Backend"]
API -->|"resolved immutable result"| Q[("Code2WF SQS")]
Q --> W["Code2WF worker"]
W --> OUT[("Code2WF result artifact in S3")]
W -->|"ai-status"| API
API -->|"validated result artifact"| PL["Figma plugin"]
| Concern | Contract |
|---|---|
| Source selector | HTTP pageImportId |
| Source eligibility | same organization/project, non-deleted, completed |
| PostgreSQL source reference | page_import_id only |
| Worker source reference | exact Page Import result URL/hash/capture hash resolved at dispatch |
| Attempt | fixed at 1 in the MVP |
| Worker database access | none |
| DocumentDB | not used |
| Figma writes | plugin only |
Shared Primitives
Generation status
Code2WF reuses the existing wf2des_status type for Figma generation rows and uses this subset:
| Value | Meaning |
|---|---|
"0" |
processing |
"1" |
completed |
"2" |
failed |
The enum's existing rejected/cancelled values are not used because Code2WF has no confirm, reject, or cancel transition.
There is no phase or materialization status. attempt is the literal integer 1.
Hash and object rules
- Content hashes are lowercase SHA-256 with the
sha256:prefix. - Source references in worker contracts use canonical
s3://bucket/keyURLs; terminal manifest/result pointers use bare keys resolved againstRESULT_BUCKET, matching WF2Des. - Result JSON uses UTF-8 and deterministic key ordering/serialization before hashing.
- Unknown fields are rejected in the Code2WF SQS message and outer result envelope. The shared WF2Des Pydantic models reject unknown node discriminators but currently ignore extra object properties, so the Code2WF producer emits only the documented shared fields and tests that exact emitted shape.
Result keys
{organization_id}/{project_id}/code2wf/{code2wf_id}/1/result/result.json
{organization_id}/{project_id}/code2wf/{code2wf_id}/1/result/manifest.json
{organization_id}/{project_id}/code2wf/{code2wf_id}/1/result/failed.json
{organization_id}/{project_id}/code2wf/{code2wf_id}/1/result/failed-manifest.json
Unlike WF2Des's timestamped multi-phase artifacts, Code2WF has one fixed attempt per row. Its deterministic per-attempt keys let an SQS redelivery validate and reuse the same artifact pair.
On success, the worker writes the client-facing spec first and its terminal manifest last. On failure, it writes the client-facing failed artifact first and its failed terminal manifest last. All writes are create-only. A duplicate delivery validates and reuses the existing pair; it never overwrites either object.
The terminal manifest is callback-only. It follows the WF2Des convention: the webhook carries its URL, and the backend reads row_effects.code2wf and stores the inner client-facing result_url on the PostgreSQL row. PostgreSQL never stores the manifest URL.
{
"manifest_schema_version": 1,
"job_id": "019ffa75-01c0-75a2-8123-456789abcdf0",
"attempt": 1,
"nonce": "8bed02b2-c93d-4854-8f4f-1c91744dc419",
"job_type": "generation",
"result_url": "1/7/code2wf/019ffa75-01c0-75a2-8123-456789abcdf0/1/result/result.json",
"flag_count": 0,
"row_effects": {
"code2wf": {
"status": "1",
"result_url": "1/7/code2wf/019ffa75-01c0-75a2-8123-456789abcdf0/1/result/result.json",
"flag_count": 0
}
}
}
The failure manifest uses result_url for failed.json and row_effects.code2wf.status = "2". The backend validates the job ID, attempt, nonce, job type, allowed S3 prefix, row effect, and status before applying it. On success, flag_count equals the number of validated spec outcomes with flagged=true.
Input 1: Backend Trigger
{
"pageImportId": "0195f16e-6f11-7ea8-b45c-f6712ed9d8a3",
"figmaFileKey": "aBc1DeFg2HiJ3kLmNoPqRs",
"screenId": "AUTORACE_DATABASE",
"placementTarget": "128:9001"
}
The backend generates code2wfId as UUIDv7. Every valid trigger creates a new row and dispatches a new job, including two triggers with identical bodies. The HTTP request does not accept a caller-provided job ID.
The backend stores the Figma destination and page_import_id, then reads and verifies the completed Page Import's immutable normalized manifest once while building the SQS message. Those exact bytes supply one consistent result URL/hash/capture-hash pin set. Page Import URLs and hashes are not copied into the code2wf row, and no PostgreSQL/S3 transaction is claimed.
The exact HTTP contract is defined in Trigger Code2WF.
Input 2: Conversion SQS Message
{
"code2wf_id": "019ffa75-01c0-75a2-8123-456789abcdf0",
"organization_id": 1,
"project_id": 7,
"attempt": 1,
"nonce": "8bed02b2-c93d-4854-8f4f-1c91744dc419",
"page_import_id": "0195f16e-6f11-7ea8-b45c-f6712ed9d8a3",
"page_result_url": "s3://bucket/1/7/page-import/0195f16e-6f11-7ea8-b45c-f6712ed9d8a3/normalized/manifest.json",
"page_result_hash": "sha256:44a7f61a6f90f476c349b264c0cbd824ce45ecad0d9985f0750f8a251aa9592a",
"capture_hash": "sha256:90e54d8b9cf4ff0fdc2b927b4453d59f4c3e219c236bb398e660aca50a62ae27",
"screen_id": "AUTORACE_DATABASE"
}
| Field | Type | Validation |
|---|---|---|
code2wf_id |
UUID | row identity and result prefix |
organization_id |
positive integer | must match Page Import result and S3 prefix |
project_id |
positive integer | must match Page Import result and S3 prefix |
attempt |
integer | must equal 1 |
nonce |
UUID | transport correlation only; not stored and not a PostgreSQL CAS field |
page_import_id |
UUID | must match the Page Import result |
page_result_url |
S3 URL | exact immutable completed Page Import result resolved by backend |
page_result_hash |
SHA-256 | exact UTF-8 bytes at page_result_url |
capture_hash |
SHA-256 | must match the Page Import result's normalized capture hash |
screen_id |
string | 1–255 characters; copied from the trigger row |
Figma destination fields, credentials, source code, HTML, CSS, and presigned URLs do not enter SQS.
Processing Contract
For each SQS record, the worker:
- strictly validates the message and requires
attempt = 1; - reads the exact Page Import result and validates its byte hash;
- validates organization, project, Page Import ID, capture hash, and the allowed S3 prefix;
- loads only the normalized visible evidence declared by Page Import;
- maps grouping, controls, and annotations into the existing
layout_frame/texttree and usesunmatchedonly for irreducible visible placeholders; - builds deterministic annotation wording from captured evidence;
- validates the exact existing WF2Des
DesignSpecModel, node union, and locked Code2WF limits; - writes immutable
result.jsonand then its terminal manifest, or immutablefailed.jsonand then its failed terminal manifest; and - reports terminal success or failure through
ai-status.
The MVP has no Code2WF-only annotation model, node type, asset map, or rendering contract. Retryable processing failures are limited to transient storage, queue, or webhook dependencies.
Output 1: Code2WF result artifact and existing DesignSpecModel
{
"job_id": "019ffa75-01c0-75a2-8123-456789abcdf0",
"attempt": 1,
"organization_id": 1,
"project_id": 7,
"screen_id": "AUTORACE_DATABASE",
"status": "success",
"generated_at": "2026-08-13T09:31:00Z",
"inputs": {
"page_import_id": "0195f16e-6f11-7ea8-b45c-f6712ed9d8a3",
"page_result_url": "s3://bucket/1/7/page-import/0195f16e-6f11-7ea8-b45c-f6712ed9d8a3/normalized/manifest.json",
"page_result_hash": "sha256:44a7f61a6f90f476c349b264c0cbd824ce45ecad0d9985f0750f8a251aa9592a",
"capture_hash": "sha256:90e54d8b9cf4ff0fdc2b927b4453d59f4c3e219c236bb398e660aca50a62ae27"
},
"spec": {
"spec_version": "1.0",
"parse_confirmed": true,
"style_bindings": {},
"root": {
"node": "layout_frame",
"layer_path": "root",
"auto_layout": {
"direction": "horizontal",
"gap": 32.0,
"padding": 0.0,
"sizing": "fixed",
"gaps": [],
"primary_align": "",
"counter_align": "",
"wrap": "",
"counter_gap": 0.0
},
"fill": "#ffffff",
"corner_radius": 0.0,
"bbox": { "x": 0.0, "y": 0.0, "w": 1824.0, "h": 1860.0 },
"lineage_wf_node_ids": [],
"children": [
{
"node": "layout_frame",
"layer_path": "root/screen",
"auto_layout": {
"direction": "vertical",
"gap": 16.0,
"padding": 24.0,
"sizing": "fixed",
"gaps": [],
"primary_align": "",
"counter_align": "",
"wrap": "",
"counter_gap": 0.0
},
"fill": "#ffffff",
"corner_radius": 0.0,
"bbox": { "x": 0.0, "y": 0.0, "w": 1440.0, "h": 1860.0 },
"lineage_wf_node_ids": [],
"children": [
{
"node": "layout_frame",
"layer_path": "root/screen/details-button",
"auto_layout": {
"direction": "horizontal",
"gap": 8.0,
"padding": [12.0, 16.0, 12.0, 16.0],
"sizing": "fixed",
"gaps": [],
"primary_align": "",
"counter_align": "CENTER",
"wrap": "",
"counter_gap": 0.0
},
"fill": "#e5e7eb",
"corner_radius": 4.0,
"bbox": { "x": 24.0, "y": 24.0, "w": 240.0, "h": 48.0 },
"lineage_wf_node_ids": [],
"children": [
{
"node": "text",
"layer_path": "root/screen/details-button/label",
"content": "View race details",
"style_token": "",
"style_refs": {},
"font_size": 16.0,
"font_weight": 600,
"color": "#111111",
"bbox": { "x": 16.0, "y": 12.0, "w": 160.0, "h": 24.0 },
"confidence": 1.0,
"flagged": false,
"source": { "kind": "none", "component_key": null },
"lineage_wf_node_ids": []
},
{
"node": "text",
"layer_path": "root/screen/details-button/A001",
"content": "[A001]",
"style_token": "",
"style_refs": {},
"font_size": 12.0,
"font_weight": 600,
"color": "#111111",
"bbox": { "x": 184.0, "y": 12.0, "w": 40.0, "h": 24.0 },
"confidence": 1.0,
"flagged": false,
"source": { "kind": "none", "component_key": null },
"lineage_wf_node_ids": []
}
]
}
]
},
{
"node": "layout_frame",
"layer_path": "root/annotations",
"auto_layout": {
"direction": "vertical",
"gap": 12.0,
"padding": 16.0,
"sizing": "fixed",
"gaps": [],
"primary_align": "",
"counter_align": "",
"wrap": "",
"counter_gap": 0.0
},
"fill": "#f3f4f6",
"corner_radius": 4.0,
"bbox": { "x": 1472.0, "y": 0.0, "w": 352.0, "h": 120.0 },
"lineage_wf_node_ids": [],
"children": [
{
"node": "text",
"layer_path": "root/annotations/A001",
"content": "[A001] Navigates to /proto-pages/auto-race/at_db_rslt03",
"style_token": "",
"style_refs": {},
"font_size": 14.0,
"font_weight": 400,
"color": "#111111",
"bbox": { "x": 16.0, "y": 16.0, "w": 320.0, "h": 48.0 },
"confidence": 1.0,
"flagged": false,
"source": { "kind": "none", "component_key": null },
"lineage_wf_node_ids": []
}
]
}
]
}
}
}
Result envelope contract
| Field | Contract |
|---|---|
job_id |
Code2WF row UUID, following the WF2Des result artifact convention |
attempt |
literal 1 |
organization_id, project_id |
positive integers matching input |
screen_id |
1–255 characters; exact trigger value |
status |
literal success, following the WF2Des result artifact convention |
generated_at |
UTC ISO 8601 terminal-write time |
inputs |
exact Page Import ID, immutable result pointer/hash, and capture hash from the SQS message |
spec |
exact existing WF2Des DesignSpecModel object |
The worker persists the nested spec with the same DesignSpecModel.model_dump(mode="json") convention used by WF2Des. Default-valued shared fields are therefore present in the artifact, including lineage_wf_node_ids, style_refs, source.component_key, and every AutoLayout default shown above. Code2WF does not add a second serializer or omit defaults selectively.
Existing DesignSpecModel contract
| Field | Contract |
|---|---|
spec_version |
Code2WF requires the current shared version 1.0; the shared model itself stores this as a string |
parse_confirmed |
literal true; compatibility field because Code2WF has no confirm phase |
style_bindings |
empty object in the low-fi MVP; existing text fallback fields carry neutral typography |
root |
one existing SpecNode; Code2WF emits an existing layout_frame root |
Node contract
Code2WF does not define a second Figma schema. The nested spec is validated by the same AI DesignSpecModel and is assignable to the plugin's existing AssemblySpec. The full existing node union is layout_frame, instance, compose, text, and unmatched; Code2WF emits only layout_frame, text, and unmatched in the MVP.
| Field | Contract |
|---|---|
node |
existing layout_frame, text, or unmatched |
layer_path |
unique slash-delimited path, 1–1024 characters |
bbox |
finite x/y in -1,000,000…1,000,000 and w/h in 0…32,768 |
layout_frame uses the same auto_layout, fill, corner_radius, bbox, lineage_wf_node_ids, and ordered children field names as WF2Des. Direction is horizontal or vertical; padding is one number or [top,right,bottom,left]; sizing is the existing hug or fixed value; gaps is empty when uniform gap applies; and no frame has more than 1,000 direct children.
text uses the existing required fields content, style_token, confidence, and source, plus existing fallback fields such as font_size, font_weight, color, and bbox. Code2WF-generated text uses source.kind="none"; the existing wireframe source kind remains reserved for WF2Des wireframe fallback. Low-fi controls are nested layout_frame / text nodes, not a new primitive. Unsupported media or shapes become visible unmatched nodes with the existing placeholder {role,text,bbox}, flagged=true, and source.kind="none" contract.
Annotations are also ordinary existing nodes. The root contains a screen frame and an annotation-panel frame; a stable marker such as [A001] is a sibling of the original control label and appears again on its annotation text row, so the captured copy is unchanged. Numbering follows normalized DOM order. Wording uses fixed evidence-only templates such as Navigates to {href}, Submits {METHOD} to {action}, Input: {type}; required, and Capture warning: {code}. It never infers uncaptured JavaScript behavior.
Geometry conversion
- The screen frame uses the Page Import
document_widthand fulldocument_height, including below-the-fold content; the viewport height is not used as a crop. - Normal flow, flex, and grid evidence is converted into ordered nested
layout_framenodes using the existing direction, padding, gap, alignment, wrap, and bounding-box fields. - A child
bbox.x/yis source evidence and a validation aid, not a new absolute-positioning instruction. Placement inside a frame is determined by the existing auto-layout tree. Code2WF does not add an absolute-position field or node case. - If overlap, transform, fixed/sticky positioning, or another relationship cannot be represented without hiding or reordering visible content, the worker emits an existing visible
unmatchedplaceholder at that location and setsflagged=trueinstead of claiming unsupported fidelity.
Unknown node discriminator values fail shared DesignSpecModel validation. Extra properties are currently ignored by the shared Pydantic models, so Code2WF does not rely on them and emits only the documented shared fields.
The existing planner, node cases, materializer, wf2des stamp/index, and rebuild-and-swap path are reused; no Code2WF renderer or node schema is added. The current builder already creates these node cases, fills frames, applies padding/gaps/alignment, pins widths, writes text content, and renders unmatched placeholders. Before Code2WF release, the same shared builder must finish support for fields that already exist in the shared schema: sizing="fixed" pins both bbox.w and bbox.h; sizing="hug" keeps the current bbox.w pin with auto height; wrap="WRAP" is authoritative, while an empty wrap keeps only the current overflow-safety wrap; and counter_gap maps to Figma counter-axis spacing whenever wrapping is active. Text fallback applies font_size, font_weight, color, and bbox.w before style bindings, allows text height to grow rather than clip, and lets a successfully resolved binding override its field. An unavailable requested weight uses the existing shared font fallback and diagnostic. The shared plugin type must also accept source.component_key: null, matching the existing Pydantic serialization. These are shared-contract/materializer alignments, not a parallel Code2WF path.
The planned plugin wiring resolves the row's optional placementTarget and passes the target's absolute current-page rectangle to the existing placeRoot. The shared function places the generated root at that rectangle's x/y; it does not replace, resize, or delete the target. An absent, stale, or other-page target uses the existing viewport-center fallback. The MVP accepts the existing root label wf2des · 1.0 and existing plugin-data namespace.
Locked V1 limits
| Limit | Value |
|---|---|
UTF-8 result.json size |
10 MiB |
| Total nodes | 5,000 |
| Tree depth | 64 |
| Visible annotation rows | 500 |
Exceeding a structural limit is a terminal result_limit_exceeded failure; the worker does not silently drop interactive content. The existing spec has no Code2WF-only assets, annotations, or warnings collection.
Output 2: Terminal Webhook
Success reports the immutable terminal-manifest pointer:
{
"type": "code2wf",
"job_id": "019ffa75-01c0-75a2-8123-456789abcdf0",
"attempt": 1,
"nonce": "8bed02b2-c93d-4854-8f4f-1c91744dc419",
"manifest_schema_version": 1,
"status": "succeeded",
"result_manifest_url": "1/7/code2wf/019ffa75-01c0-75a2-8123-456789abcdf0/1/result/manifest.json"
}
A stable worker failure uses the same envelope with status: "failed", result_manifest_url pointing to failed-manifest.json, and safe error: {message}, matching the WF2Des producer contract. The backend validates the manifest, stores its inner failed-artifact result_url and safe error, and leaves row flag_count null while (id, attempt, status="0") matches. Immutable artifacts plus the row CAS make a duplicate event after the row is terminal an accepted no-op.
Error Handling
| Code | Retryable | Meaning |
|---|---|---|
invalid_message |
No | SQS body violates the closed contract |
source_not_found |
No | resolved Page Import result/object is absent |
source_hash_mismatch |
No | source bytes do not match the dispatch hash |
source_scope_mismatch |
No | tenant, import ID, capture hash, or prefix differs |
unsupported_source_schema |
No | Page Import result version is unsupported |
invalid_result |
No | generated spec violates the closed schema |
result_limit_exceeded |
No | generated spec exceeds a locked V1 limit |
storage_unavailable |
Yes | transient S3 dependency failure |
webhook_unavailable |
Yes | terminal event was not accepted |
Annotation model errors are absent because the MVP creates annotation text deterministically and calls no annotation-only model.
Idempotency & Fencing
- Every accepted trigger receives a new backend-generated UUIDv7. Identical request bodies create separate rows and jobs; the caller cannot supply or reuse a job ID.
- The MVP fixes
attemptat1. Backend terminal updates use(id, attempt=1, status="0")as the PostgreSQL compare-and-set fence. - The worker writes the client-facing result or failure artifact first and its terminal manifest last. Both writes are create-only. A duplicate delivery validates and reuses the exact existing pair; different identity or Page Import pins fail closed and are never overwritten.
- The backend validates the terminal manifest and applies
row_effects.code2wfonly while the processing-row fence matches. A duplicate or conflicting callback after a terminal transition is an accepted no-op. nonceis webhook transport correlation only. It is not stored in PostgreSQL and is not a row fence.- Placement is idempotent through the existing row-level
materialized_atconvention. The MVP intentionally has no cross-session materialization claim or lease.
Runtime Configuration
| Config | Value / Source | Notes |
|---|---|---|
RESULT_BUCKET |
shared result bucket | Resolves allowed Page Import s3:// inputs and owns immutable Code2WF result/manifest keys |
WEBHOOK_BASE_URL |
private backend origin | Code2WF posts terminal status to the existing ai-status endpoint |
WEBHOOK_API_KEY |
deployment secret | Sent only as the existing X-API-Key webhook header |
The Code2WF queue is attached through the Lambda event-source mapping; the worker does not need a queue URL. The worker has no model, DocumentDB, PostgreSQL, Figma token, or browser configuration.
Logging
Structured logs include code2wf_id, organization_id, project_id, attempt, screen_id, terminal outcome, safe error code, source/result hashes, node and flag counts, and read/convert/validate/write/webhook durations. Logs and failure payloads never include raw source code, DOM or page copy, object bytes, cookies, credentials, authorization headers, browser storage, or presigned URLs.
Field Reference (Lookup Table)
— means the field is absent from that surface.
| HTTP field | PostgreSQL code2wf |
SQS message | Result artifact | Notes |
|---|---|---|---|---|
code2wfId |
id |
code2wf_id |
job_id |
backend-generated UUIDv7 |
pageImportId |
page_import_id |
page_import_id |
inputs.page_import_id |
references the completed Page Import row |
| path organization/project IDs | organization_id, project_id |
organization_id, project_id |
organization_id, project_id |
every access is tenant-scoped |
| — | attempt |
attempt |
attempt |
literal 1 in the MVP |
| — | — | nonce |
— | webhook transport correlation only |
| — | — | page_result_url, page_result_hash, capture_hash |
matching fields under inputs |
one verified immutable Page Import pin set; not copied into PostgreSQL |
figmaFileKey |
figma_file_key |
— | — | used by backend/plugin discovery only |
screenId |
screen_id |
screen_id |
screen_id |
exact trigger value |
placementTarget |
placement_target |
— | — | optional plugin placement reference only |
| — | result_url |
— | written at the deterministic result key | backend stores the manifest row effect's inner client-facing artifact URL |
| — | flag_count |
— | derived from spec |
count of validated shared-spec outcomes with flagged=true |
| placement response | materialized_at |
— | — | set only after successful plugin placement |