{
  "openapi": "3.1.0",
  "info": {
    "version": "1.0.0",
    "title": "@flam/api",
    "description": "FLAM — the AI house for fashion brands. Session-cookie auth unless a route says otherwise."
  },
  "components": {
    "schemas": {
      "ChatSession": {
        "type": "object",
        "required": [
          "id",
          "title",
          "createdAt",
          "lastTurnAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "lastTurnAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "BrandMark": {
        "type": "object",
        "description": "One rendition of the house's mark — the logo as it really appears on a material. The bytes live in `assets` (uploaded with `kind=mark`); this is the structured half. `collectionId` null means the house's own set; set means that drop's override, which answers whole rather than merging.\n",
        "properties": {
          "id": {
            "type": "string"
          },
          "assetId": {
            "type": "string"
          },
          "collectionId": {
            "type": "string",
            "nullable": true
          },
          "material": {
            "type": "string",
            "description": "What it is on. Rides the generation prompt verbatim."
          },
          "placement": {
            "type": "string",
            "nullable": true
          },
          "note": {
            "type": "string",
            "nullable": true
          },
          "sortOrder": {
            "type": "integer",
            "description": "Riding order, lowest first. At most two travel with a frame."
          },
          "filename": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "BrandProfile": {
        "type": "object",
        "description": "The house's visual DNA, read off its own imagery. `clause` is exactly what the generators inject — built by the same builder the worker calls.\n",
        "properties": {
          "visionEn": {
            "type": "string"
          },
          "vibe": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "palette": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "signatureKeywords": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "category": {
                  "type": "string"
                },
                "phrase": {
                  "type": "string"
                }
              }
            }
          },
          "injectEnabled": {
            "type": "boolean"
          },
          "dnaRuns": {
            "type": "integer"
          },
          "clause": {
            "type": "string",
            "description": "Empty when injectEnabled is false."
          }
        }
      },
      "VerdictFinding": {
        "type": "object",
        "description": "One mark on a frame. The panel renders `Where — label.` — \"Left hand — drawn wrong.\" — so `label` is a PREDICATE that follows the location, never a definition that stands alone.\n",
        "properties": {
          "code": {
            "type": "string",
            "description": "One of the twelve codes in libs/api-types/src/judge.ts."
          },
          "where": {
            "type": "string",
            "maxLength": 60,
            "description": "The model's own place on the frame. The ONLY model-authored string she reads."
          },
          "label": {
            "type": "string",
            "description": "OUR sentence for the code, resolved server-side from DEFECT_LABELS."
          },
          "confidence": {
            "type": "string",
            "enum": [
              "sure",
              "not-sure"
            ]
          }
        }
      },
      "Verdict": {
        "type": "object",
        "description": "What the judge thought, in the director's language. `note`, `costUsd`, `judgeModel` and `latencyMs` are on the row and are deliberately NOT on this wire.\n",
        "properties": {
          "jobId": {
            "type": "string"
          },
          "score": {
            "type": "integer",
            "nullable": true,
            "description": "0-100. NULL exactly when unreadable — never 0, which would read as \"we looked\"."
          },
          "unreadable": {
            "type": "boolean",
            "description": "WE could not look. Nothing is shown to her."
          },
          "flagged": {
            "type": "boolean",
            "description": "Computed against the threshold when the row was written, not at read time."
          },
          "showScore": {
            "type": "boolean",
            "description": "Is there a number worth showing? False for a clean frame (the word is \"checked\") and for a frame whose only mark weighs 0.\n"
          },
          "retryFutile": {
            "type": "boolean",
            "description": "TRUE means offer no paid retry. A fresh draw reproduces our dropped brief exactly, and charging her to redo our bug is the real failure.\n"
          },
          "findings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/VerdictFinding"
            }
          },
          "rubricVersion": {
            "type": "integer",
            "description": "Two scores from different rubrics are not comparable."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "answer": {
            "type": "object",
            "description": "HER OWN RULING ON THIS VERDICT, from POST /judge/verdicts/{jobId}/label. ABSENT when she has not answered — never `answer: null`, and never `{agrees: false}` for silence: `agrees_with_judge IS NULL` on the row means \"she never said\" and is not \"we were wrong\". Present so a client renders an answered frame as answered instead of asking her the same question after every reload.\n",
            "required": [
              "agrees"
            ],
            "properties": {
              "agrees": {
                "type": "boolean",
                "description": "TRUE = we flagged it and we were right. FALSE = we were wrong."
              },
              "note": {
                "type": "string",
                "description": "HER sentence, if she wrote one — not the model's. Absent otherwise, and never empty.\n"
              }
            }
          }
        }
      },
      "Folder": {
        "type": "object",
        "description": "A collection. Flat on the wire — the client builds the tree from parentId.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "parentId": {
            "type": "string",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SearchResult": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "piece",
              "collection",
              "look",
              "workflow"
            ]
          },
          "title": {
            "type": "string"
          },
          "meta": {
            "type": "string",
            "nullable": true,
            "description": "The lane's caption — a SKU, a piece count, a status. Never invented."
          },
          "thumbUrl": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "DevelopRun": {
        "type": "object",
        "description": "A run is DERIVED from what generation_jobs already carries — there is no run_id column.\n",
        "properties": {
          "id": {
            "type": "string",
            "description": "run.<runId|oldestJobId> or look.<lookId>.<stage>"
          },
          "kind": {
            "type": "string",
            "enum": [
              "lookbook",
              "batch",
              "single"
            ]
          },
          "tool": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "stage": {
            "type": "string",
            "nullable": true,
            "enum": [
              "lock",
              "variation"
            ]
          },
          "lookId": {
            "type": "string",
            "nullable": true
          },
          "state": {
            "type": "string",
            "enum": [
              "queued",
              "developing",
              "done",
              "failed",
              "cancelled"
            ]
          },
          "counts": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer"
              },
              "done": {
                "type": "integer"
              },
              "failed": {
                "type": "integer"
              },
              "cancelled": {
                "type": "integer"
              },
              "active": {
                "type": "integer"
              }
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "finishedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "thumbAssetId": {
            "type": "string",
            "nullable": true
          },
          "jobIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "againCost": {
            "type": "integer",
            "nullable": true,
            "description": "Tokens to develop this whole run again. NULL when the price is unknown — the UI then does not offer the verb at all, because the house never puts a spend behind a button without its price.\n"
          }
        }
      },
      "Look": {
        "type": "object",
        "description": "The lookbook_looks row as stored — jsonb columns come back as-is.",
        "properties": {
          "id": {
            "type": "string"
          },
          "organizationId": {
            "type": "string"
          },
          "createdByUserId": {
            "type": "string",
            "nullable": true
          },
          "brandId": {
            "type": "string",
            "nullable": true
          },
          "name": {
            "type": "string"
          },
          "modelAssetId": {
            "type": "string"
          },
          "backgroundAssetId": {
            "type": "string",
            "nullable": true
          },
          "itemAssetIds": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "assetId": {
                  "type": "string"
                },
                "role": {
                  "type": "string"
                },
                "label": {
                  "type": "string",
                  "description": "The piece's own name, as stored (spec 017). Absent on every look written before the wave, and on any piece whose name was not usable prose."
                },
                "hero": {
                  "type": "boolean",
                  "description": "True on the ONE piece this lookbook is selling (spec 019), as stored. At most one per look; absent on every look written before the wave and on every look that highlights nothing."
                }
              }
            }
          },
          "preset": {
            "type": "object",
            "properties": {
              "aesthetic": {
                "type": "string"
              },
              "background": {
                "type": "object"
              }
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "developing",
              "locked",
              "complete"
            ]
          },
          "lookBaseAssetId": {
            "type": "string",
            "nullable": true
          },
          "lookBaseJobId": {
            "type": "string",
            "nullable": true
          },
          "variations": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Model": {
        "type": "object",
        "description": "A face on the casting board.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "tier": {
            "type": "string",
            "enum": [
              "free",
              "sale",
              "exclusive"
            ]
          },
          "priceCents": {
            "type": "integer"
          },
          "currency": {
            "type": "string"
          },
          "coverUrl": {
            "type": "string",
            "nullable": true
          },
          "previewUrls": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "owned": {
            "type": "boolean",
            "description": "Granted to this house, or its own"
          },
          "mine": {
            "type": "boolean",
            "description": "This house registered it"
          },
          "usable": {
            "type": "boolean",
            "description": "free OR owned — what actually gates casting"
          },
          "exclusiveYours": {
            "type": "boolean"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PipelineRun": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "developId": {
            "type": "string",
            "description": "The Darkroom's id for the same run"
          },
          "status": {
            "type": "string",
            "enum": [
              "developing",
              "done",
              "failed",
              "cancelled"
            ]
          },
          "framesPlanned": {
            "type": "integer"
          },
          "framesSkipped": {
            "type": "integer"
          },
          "tokensPlanned": {
            "type": "integer"
          },
          "tokensSpent": {
            "type": "integer",
            "description": "SETTLED tokens, never an estimate"
          },
          "steps": {
            "type": "object",
            "properties": {
              "done": {
                "type": "integer"
              },
              "failed": {
                "type": "integer"
              },
              "cancelled": {
                "type": "integer"
              },
              "active": {
                "type": "integer"
              },
              "total": {
                "type": "integer"
              }
            }
          },
          "line": {
            "type": "string",
            "description": "The honest sentence the screen shows"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PipelinePreset": {
        "type": "object",
        "description": "A plan with no items bound.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "nodes": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "sinkFolderId": {
            "type": "string",
            "nullable": true
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "HousePreset": {
        "type": "object",
        "description": "The house's standing choices. Always answers — a director who never opened brand settings still gets a working one.\n",
        "properties": {
          "modelAssetId": {
            "type": "string",
            "nullable": true
          },
          "background": {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "preset",
                  "asset"
                ]
              },
              "value": {
                "type": "string"
              },
              "assetId": {
                "type": "string"
              }
            }
          },
          "aesthetic": {
            "type": "string"
          },
          "variationKeys": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "quality": {
            "type": "string",
            "enum": [
              "1k",
              "2k",
              "4k"
            ]
          },
          "sizes": {
            "type": "array",
            "description": "SPEC 022 — the house's OWN slots, named in her words. She picks \"Product page\", not \"1600 x 2000\", and she picks it once: set here, inherited by every develop and every export after. Pixels are validated by @flam/ai's own resolveOutput, so a slot can never carry a size the develop routes would refuse later. An ABSENT key leaves the stored list alone; an empty array clears it. Empty is the honest default and a house that has named nothing behaves exactly as it did before this field existed.\n",
            "maxItems": 12,
            "items": {
              "type": "object",
              "required": [
                "name",
                "width",
                "height"
              ],
              "properties": {
                "name": {
                  "type": "string",
                  "maxLength": 40
                },
                "width": {
                  "type": "integer"
                },
                "height": {
                  "type": "integer"
                }
              }
            }
          }
        }
      },
      "PostureCard": {
        "type": "object",
        "description": "A named posture as a director's screen sees it. FOUR ORTHOGONAL FIELDS, never flattened: a \"walking shot\" conflates a direction (motion), a crop, a family and a register, and here they are four properties. The geometry phrases the model receives are deliberately absent — see the route.\n",
        "properties": {
          "id": {
            "type": "string",
            "description": "The slug",
            "and the id a saved variation names as its base.": null
          },
          "tier": {
            "type": "integer",
            "description": "1 = the seeded twelve."
          },
          "name": {
            "type": "string"
          },
          "provenance": {
            "type": "string",
            "description": "Where the name and the geometry come from. No canonical industry pose dictionary exists, so this is what keeps the library honest.\n"
          },
          "definition": {
            "type": "string",
            "description": "The geometry",
            "in one line": null,
            "camera-relative.": null
          },
          "whenUsed": {
            "type": "string",
            "description": "When a director reaches for this one."
          },
          "register": {
            "type": "string",
            "enum": [
              "editorial",
              "commercial",
              "compliance"
            ]
          },
          "crop": {
            "type": "string",
            "enum": [
              "packshot",
              "front-crop",
              "three-quarter",
              "full-body",
              "back-view",
              "side-view",
              "detail"
            ],
            "description": "Zalando-anchored. `three-quarter` is MID-THIGH UP — the modelling convention, not the film one, where a three-quarter cuts at the knees.\n"
          },
          "motion": {
            "type": "string",
            "enum": [
              "held",
              "settling",
              "walking",
              "mid-stride",
              "airborne"
            ]
          },
          "family": {
            "type": "string",
            "description": "The structural kin — standing, walking, seated, leaning."
          },
          "frameAssetIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The reference frames the generating call receives with a role sentence. EMPTY on every seeded entry until the measured diagram-versus-photograph verdict lands.\n"
          },
          "controls": {
            "type": "object",
            "description": "Which axes this posture exposes and where each starts. `defaults` is what makes the card castable with nothing touched; an axis ABSENT from it contributes nothing, because the entry's own geometry already places it. `only` narrows an axis a posture cannot honestly offer every stop of.\n",
            "properties": {
              "defaults": {
                "type": "object",
                "additionalProperties": true
              },
              "only": {
                "type": "object",
                "additionalProperties": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          },
          "house": {
            "type": "boolean",
            "description": "true = FLAM's own, everyone's. false = this house minted it."
          }
        }
      },
      "PostureAxis": {
        "type": "object",
        "description": "A control axis with its NAMED stops. Sliders land on stops; they do not interpolate.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "slider",
              "pick",
              "multi",
              "toggle"
            ]
          },
          "stops": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "CustomPosture": {
        "type": "object",
        "description": "A director's saved variation — BASE + DELTAS, never a copy. Correct the house entry and hers inherits the correction; deleting hers never touches the house set.\n",
        "required": [
          "id",
          "name",
          "baseId"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "maxLength": 120
          },
          "baseId": {
            "type": "string",
            "description": "A posture she can actually cast."
          },
          "deltas": {
            "type": "array",
            "maxItems": 12,
            "items": {
              "type": "string",
              "maxLength": 600
            },
            "description": "Her sentences, ADDED to the base's geometry. Never a rewrite of it."
          },
          "controlPresets": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "axisId → stopId. An unknown axis or stop is dropped, not stored."
          },
          "frameAssetId": {
            "type": "string"
          },
          "savedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PosturePrefs": {
        "type": "object",
        "description": "One row per house. Every list holds ids of postures the house can actually cast.",
        "properties": {
          "favorites": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "defaults": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What the automatic lookbook casts when nobody chooses."
          },
          "exploreAdded": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "custom": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CustomPosture"
            }
          }
        }
      },
      "DevelopRequest": {
        "type": "object",
        "description": "What a develop is told. Every field optional unless noted.",
        "properties": {
          "quality": {
            "type": "string",
            "enum": [
              "1k",
              "2k",
              "4k"
            ],
            "default": "2k",
            "description": "The tier the frame develops at — 1,024 / 2,048 / 4,096 px on the long edge."
          },
          "output": {
            "$ref": "#/components/schemas/OutputDestination"
          },
          "idempotencyKey": {
            "type": "string",
            "description": "Makes one gesture safe to retry. Generated when omitted."
          }
        }
      },
      "OutputDestination": {
        "type": "object",
        "description": "WHERE the frame is going. Omit and the frame simply develops at the tier. A named place carries its own exact pixels; \"custom\" carries yours. The frame is COMPOSED for that shape and developed at the tier's pixels — the response is not resampled to width × height, so cut it yourself (a pure downscale for every place except the two A-series print sizes, which are composed at 2:3 and cost a 5.7% trim). Never upscale a frame that came back smaller than the place asked for: that is invented detail, and the house sells it only as a priced restoration pass on the 4K tier.\n",
        "required": [
          "destination"
        ],
        "properties": {
          "destination": {
            "type": "string",
            "enum": [
              "web-tile",
              "web-hero",
              "shopify-product",
              "ecom-packshot",
              "marketplace-listing",
              "ig-square",
              "ig-portrait",
              "ig-story",
              "pinterest-pin",
              "print-page",
              "print-spread",
              "custom"
            ]
          },
          "width": {
            "type": "integer",
            "minimum": 64,
            "maximum": 8192,
            "description": "Required with \"custom\", ignored otherwise."
          },
          "height": {
            "type": "integer",
            "minimum": 64,
            "maximum": 8192,
            "description": "Required with \"custom\", ignored otherwise."
          }
        }
      },
      "OutputError": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "detail": {
            "type": "string",
            "description": "The refusal in plain language, in pixels the caller can check."
          }
        }
      },
      "Job": {
        "type": "object",
        "description": "The client-facing job shape (WS push + reconnect backfill).",
        "properties": {
          "id": {
            "type": "string"
          },
          "tool": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "done",
              "failed",
              "canceled"
            ]
          },
          "resultUrl": {
            "type": "string",
            "nullable": true
          },
          "resultAssetId": {
            "type": "string",
            "nullable": true
          },
          "error": {
            "type": "string",
            "nullable": true
          },
          "brandId": {
            "type": "string",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "eventSeq": {
            "type": "integer"
          }
        }
      },
      "Asset": {
        "type": "object",
        "description": "One piece in the house library. `sku`, `kind`, `groupId`, `fit` and `wearer` are projected out of the row's `tags` array (`sku:` / `kind:` / `group:` / `fit:` / `wearer:`), which is where they are stored — there are no columns for them.\n",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "description": "The filename, or the label a director gave it"
          },
          "sku": {
            "type": "string",
            "nullable": true,
            "description": "The brand's own code — never invented"
          },
          "kind": {
            "type": "string",
            "nullable": true,
            "enum": [
              "clothes",
              "accessory",
              "jewelry",
              "model",
              "background"
            ]
          },
          "url": {
            "type": "string",
            "description": "Owner-gated streaming route (/api/toolkit/assets/{id}/file)"
          },
          "folderId": {
            "type": "string",
            "nullable": true
          },
          "groupId": {
            "type": "string",
            "nullable": true,
            "description": "Shared by pieces grouped together"
          },
          "fit": {
            "type": "string",
            "nullable": true,
            "enum": [
              "auto",
              "fitted",
              "regular",
              "loose",
              "oversized"
            ],
            "description": "How the garment is cut to be worn. Read off the piece for free at upload, or set by the director. Rides every lookbook frame this piece appears in."
          },
          "wearer": {
            "type": "string",
            "nullable": true,
            "enum": [
              "women",
              "men",
              "unisex"
            ],
            "description": "The body the garment is cut for. Guidance for the model, never a refusal."
          },
          "fitGuessed": {
            "type": "boolean",
            "description": "The house read the fit rather than being told it."
          },
          "wearerGuessed": {
            "type": "boolean",
            "description": "The house read the wearer rather than being told it."
          },
          "carriesMark": {
            "type": "boolean",
            "nullable": true,
            "description": "Whether this piece carries the house's brand mark, read off the piece for free at upload or set by the director. While false, the brand-mark rendition is NOT staged as a reference into frames this piece is in. Null means nobody has looked, and not-looked still rides."
          },
          "markGuessed": {
            "type": "boolean",
            "description": "The house read it rather than being told it. True for every false it read, because a mark it could not see may simply be on the other side of the piece."
          },
          "mime": {
            "type": "string"
          },
          "tool": {
            "type": "string",
            "nullable": true,
            "description": "The tool that produced it, if any"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ToolItem": {
        "type": "object",
        "description": "Snake_case on the wire — matches the raw-SQL rows + the client ToolItem type.",
        "properties": {
          "id": {
            "type": "string"
          },
          "tool_id": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "title_he": {
            "type": "string",
            "nullable": true
          },
          "label_he": {
            "type": "string",
            "nullable": true
          },
          "content_en": {
            "type": "string",
            "nullable": true
          },
          "image_url": {
            "type": "string",
            "nullable": true
          },
          "notes_he": {
            "type": "string",
            "nullable": true
          },
          "sort_order": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OnboardingProgress": {
        "type": "object",
        "properties": {
          "steps": {
            "type": "object",
            "description": "step id → ISO completion timestamp or null",
            "additionalProperties": {
              "type": "string",
              "nullable": true
            }
          },
          "completed": {
            "type": "integer"
          },
          "total": {
            "type": "integer"
          },
          "reward": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer"
              },
              "granted": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "AccountMemory": {
        "type": "object",
        "required": [
          "id",
          "scope",
          "memoryClass",
          "kind",
          "statement",
          "confidence",
          "status",
          "evidenceCount",
          "projected",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "scope": {
            "type": "string",
            "enum": [
              "house",
              "person"
            ]
          },
          "memoryClass": {
            "type": "string",
            "enum": [
              "fact",
              "preference",
              "lesson",
              "playbook",
              "outcome_pattern",
              "image_reading"
            ]
          },
          "kind": {
            "type": "string",
            "enum": [
              "told",
              "learned",
              "acted"
            ]
          },
          "statement": {
            "type": "string"
          },
          "confidence": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "status": {
            "type": "string",
            "enum": [
              "candidate",
              "active",
              "corrected",
              "retired",
              "rejected"
            ]
          },
          "evidenceCount": {
            "type": "integer",
            "minimum": 0
          },
          "supersedesId": {
            "type": "string",
            "nullable": true
          },
          "projected": {
            "type": "boolean"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Account": {
        "type": "object",
        "description": "Everything the settings room needs that nothing else can answer. Money is stated only where we recorded it — Dodo is the merchant of record and issues the tax document, so `reference` is what a director quotes.\n",
        "properties": {
          "profile": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string"
              },
              "emailVerified": {
                "type": "boolean"
              },
              "image": {
                "type": "string",
                "nullable": true
              },
              "joinedAt": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              }
            }
          },
          "plan": {
            "type": "object",
            "nullable": true,
            "description": "null = no plan. `status` 'canceled' means paid to `renewsAt`, then it stops.",
            "properties": {
              "tier": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "enum": [
                  "trialing",
                  "active",
                  "past_due",
                  "canceled",
                  "revoked"
                ]
              },
              "priceUsd": {
                "type": "number"
              },
              "monthlyTokens": {
                "type": "integer"
              },
              "rolloverMultiple": {
                "type": "integer"
              },
              "renewsAt": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              }
            }
          },
          "tokens": {
            "type": "object",
            "properties": {
              "balance": {
                "type": "integer",
                "description": "Spendable — derived, never stored"
              },
              "grant": {
                "type": "integer"
              },
              "topup": {
                "type": "integer"
              },
              "held": {
                "type": "integer",
                "description": "Reserved by develops in flight"
              },
              "days": {
                "type": "integer",
                "enum": [
                  7,
                  30,
                  90
                ]
              },
              "byWorkflow": {
                "type": "array",
                "description": "Where the tokens actually went, heaviest first.",
                "items": {
                  "type": "object",
                  "properties": {
                    "workflow": {
                      "type": "string"
                    },
                    "tokens": {
                      "type": "integer"
                    },
                    "runs": {
                      "type": "integer"
                    },
                    "lastAt": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "invoices": {
            "type": "array",
            "description": "Money that landed — newest first, capped at 100.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "at": {
                  "type": "string",
                  "format": "date-time",
                  "nullable": true
                },
                "kind": {
                  "type": "string",
                  "enum": [
                    "subscription_grant",
                    "topup",
                    "promo",
                    "refund",
                    "purchase"
                  ]
                },
                "item": {
                  "type": "string"
                },
                "tokens": {
                  "type": "integer",
                  "nullable": true
                },
                "amountUsd": {
                  "type": "number",
                  "nullable": true,
                  "description": "Null where no amount was recorded — never an invented one."
                },
                "reference": {
                  "type": "string",
                  "nullable": true
                }
              }
            }
          },
          "billing": {
            "type": "object",
            "properties": {
              "cardOnFile": {
                "type": "boolean"
              },
              "email": {
                "type": "string",
                "format": "email",
                "description": "House-scoped invoice recipient; separate from sign-in identity."
              },
              "canManage": {
                "type": "boolean",
                "description": "Whether this actor may change billing for the house."
              }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      },
      "Post": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "p_abc123"
          },
          "title": {
            "type": "string",
            "example": "Hello world"
          },
          "body": {
            "type": "string",
            "example": "My first post."
          },
          "authorId": {
            "type": "string",
            "example": "u_abc123"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "example": "2026-07-01T12:00:00.000Z"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "example": "2026-07-01T12:00:00.000Z"
          }
        },
        "required": [
          "id",
          "title",
          "body",
          "authorId",
          "createdAt",
          "updatedAt"
        ]
      },
      "NewPost": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255
          },
          "body": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100000
          }
        },
        "required": [
          "title",
          "body"
        ]
      },
      "PatchPost": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255
          },
          "body": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100000
          }
        }
      },
      "StandingNote": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "scopeTools": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "scopePhrase": {
            "type": "string"
          },
          "evidenceJobIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "activeFrom": {
            "type": "string"
          },
          "retiredAt": {
            "type": "string",
            "nullable": true
          }
        },
        "required": [
          "id",
          "text",
          "scopeTools",
          "scopePhrase",
          "evidenceJobIds",
          "activeFrom",
          "retiredAt"
        ]
      },
      "StandingNoteError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "reason": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      },
      "StandingNoteAccept": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string",
            "minLength": 1,
            "maxLength": 600
          },
          "scopeTools": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 16
          },
          "evidenceJobIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 64
          }
        },
        "required": [
          "text"
        ]
      },
      "StandingNoteUpdate": {
        "type": "object",
        "properties": {
          "retire": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "scopeTools": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 16
          }
        }
      },
      "InviteRequestResult": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "received",
              "already_requested"
            ]
          }
        },
        "required": [
          "status"
        ]
      },
      "InviteRequestError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      },
      "InviteRequest": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 2,
            "maxLength": 80,
            "example": "Noa Berger"
          },
          "house": {
            "type": "string",
            "maxLength": 120,
            "example": "Atelier Berger"
          },
          "email": {
            "type": "string",
            "maxLength": 160,
            "format": "email",
            "example": "noa@atelierberger.com"
          },
          "collection": {
            "type": "string",
            "minLength": 10,
            "maxLength": 400,
            "example": "Resort 26 — 40 pieces of knitwear, shooting the week of the 12th."
          },
          "intake": {
            "$ref": "#/components/schemas/IntakeAnswers"
          },
          "link": {
            "type": "string",
            "maxLength": 200
          },
          "links": {
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 3,
              "maxLength": 300
            },
            "maxItems": 6
          },
          "logoUrl": {
            "type": "string",
            "maxLength": 500,
            "format": "uri"
          },
          "website": {
            "type": "string",
            "maxLength": 200
          }
        },
        "required": [
          "name",
          "email"
        ]
      },
      "IntakeAnswers": {
        "type": "object",
        "properties": {
          "audience": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "label",
                "studio",
                "creator",
                "other"
              ]
            },
            "maxItems": 4
          },
          "volume": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "under-20",
                "20-100",
                "100-plus",
                "looking"
              ]
            },
            "maxItems": 4
          },
          "draws": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "packshots",
                "lookbooks",
                "campaign",
                "onmodel",
                "looking"
              ]
            },
            "maxItems": 5
          },
          "aiToday": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "packshots",
                "lookbooks",
                "campaign",
                "tryon",
                "nothing"
              ]
            },
            "maxItems": 5
          },
          "pain": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "details",
                "identity",
                "consistency",
                "slow",
                "cost",
                "other"
              ]
            },
            "maxItems": 6
          },
          "painText": {
            "type": "string",
            "maxLength": 400
          },
          "cost": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "reshoots",
                "lead-time",
                "studio-time",
                "budget"
              ]
            },
            "maxItems": 4
          }
        }
      },
      "LinkPreviewResult": {
        "type": "object",
        "properties": {
          "cards": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LinkPreviewCard"
            }
          }
        },
        "required": [
          "cards"
        ]
      },
      "LinkPreviewCard": {
        "type": "object",
        "properties": {
          "link": {
            "type": "string"
          },
          "ok": {
            "type": "boolean"
          },
          "host": {
            "type": "string",
            "nullable": true
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "imageUrl": {
            "type": "string",
            "nullable": true
          },
          "iconUrl": {
            "type": "string",
            "nullable": true
          }
        },
        "required": [
          "link",
          "ok",
          "host",
          "title",
          "description",
          "imageUrl",
          "iconUrl"
        ]
      },
      "LinkPreviewError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      },
      "LinkPreviewRequest": {
        "type": "object",
        "properties": {
          "links": {
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 3,
              "maxLength": 300
            },
            "minItems": 1,
            "maxItems": 6,
            "example": [
              "https://atelier-berger.com",
              "https://instagram.com/atelierberger"
            ]
          }
        },
        "required": [
          "links"
        ]
      },
      "InviteRequestRow": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "house": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "collection": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string"
          },
          "createdAt": {
            "type": "string"
          },
          "answers": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "question": {
                  "type": "string"
                },
                "answer": {
                  "type": "string"
                }
              },
              "required": [
                "question",
                "answer"
              ]
            },
            "description": "Her answers, question by question, from INTAKE_QUESTIONS."
          }
        },
        "required": [
          "id",
          "name",
          "house",
          "email",
          "collection",
          "status",
          "createdAt",
          "answers"
        ]
      },
      "AdminOnboardingRow": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "house": {
            "type": "string",
            "nullable": true
          },
          "completed": {
            "type": "number"
          },
          "total": {
            "type": "number"
          },
          "steps": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "startedAt": {
            "type": "string"
          },
          "lastStepAt": {
            "type": "string"
          }
        },
        "required": [
          "userId",
          "name",
          "email",
          "house",
          "completed",
          "total",
          "steps",
          "startedAt",
          "lastStepAt"
        ]
      },
      "IntakeResult": {
        "type": "object",
        "properties": {
          "recorded": {
            "type": "boolean"
          }
        },
        "required": [
          "recorded"
        ]
      },
      "IntakeError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      },
      "IntakeRecord": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "maxLength": 160,
            "format": "email",
            "example": "noa@atelierberger.com"
          },
          "source": {
            "type": "string",
            "enum": [
              "card",
              "code"
            ],
            "example": "code"
          },
          "answers": {
            "$ref": "#/components/schemas/IntakeAnswers"
          },
          "link": {
            "type": "string",
            "maxLength": 200
          },
          "code": {
            "type": "string",
            "maxLength": 60
          }
        },
        "required": [
          "email",
          "source"
        ]
      },
      "ShareManifest": {
        "type": "object",
        "properties": {
          "house": {
            "type": "string"
          },
          "recipientLabel": {
            "type": "string",
            "nullable": true
          },
          "expiresAt": {
            "type": "string"
          },
          "finishedAt": {
            "type": "string",
            "nullable": true
          },
          "frames": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ShareFrame"
            }
          }
        },
        "required": [
          "house",
          "recipientLabel",
          "expiresAt",
          "finishedAt",
          "frames"
        ]
      },
      "ShareFrame": {
        "type": "object",
        "properties": {
          "i": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "note": {
            "type": "string",
            "nullable": true
          },
          "w": {
            "type": "number",
            "nullable": true
          },
          "h": {
            "type": "number",
            "nullable": true
          },
          "mark": {
            "type": "string",
            "nullable": true,
            "enum": [
              "keep",
              "pass",
              null
            ]
          },
          "markNote": {
            "type": "string",
            "nullable": true
          },
          "drawings": {
            "$ref": "#/components/schemas/ShareDrawings"
          }
        },
        "required": [
          "i",
          "name",
          "note",
          "w",
          "h",
          "mark",
          "markNote",
          "drawings"
        ]
      },
      "ShareDrawings": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "kind": {
              "type": "string",
              "enum": [
                "circle",
                "free"
              ]
            },
            "points": {
              "type": "array",
              "items": {
                "type": "array",
                "items": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 1
                },
                "minItems": 2,
                "maxItems": 2
              },
              "minItems": 1,
              "maxItems": 120
            },
            "note": {
              "type": "string",
              "maxLength": 120
            }
          },
          "required": [
            "kind",
            "points"
          ]
        },
        "maxItems": 20
      },
      "ShareError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      },
      "ShareMarkBody": {
        "type": "object",
        "properties": {
          "i": {
            "type": "integer",
            "minimum": 0
          },
          "verdict": {
            "type": "string",
            "enum": [
              "keep",
              "pass"
            ]
          },
          "note": {
            "type": "string",
            "maxLength": 600
          },
          "drawings": {
            "$ref": "#/components/schemas/ShareDrawings"
          }
        },
        "required": [
          "i"
        ]
      },
      "ShareCreateBody": {
        "type": "object",
        "properties": {
          "assetIds": {
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1
            },
            "minItems": 1,
            "maxItems": 60
          },
          "recipientLabel": {
            "type": "string",
            "maxLength": 120
          },
          "email": {
            "type": "string",
            "maxLength": 160,
            "format": "email"
          },
          "message": {
            "type": "string",
            "maxLength": 600
          }
        },
        "required": [
          "assetIds"
        ]
      },
      "ShareSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "token": {
            "type": "string"
          },
          "recipientLabel": {
            "type": "string",
            "nullable": true
          },
          "frames": {
            "type": "number"
          },
          "answered": {
            "type": "number"
          },
          "kept": {
            "type": "number"
          },
          "expiresAt": {
            "type": "string"
          },
          "revokedAt": {
            "type": "string",
            "nullable": true
          },
          "createdAt": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "token",
          "recipientLabel",
          "frames",
          "answered",
          "kept",
          "expiresAt",
          "revokedAt",
          "createdAt"
        ]
      },
      "ShareMarkView": {
        "type": "object",
        "properties": {
          "assetId": {
            "type": "string"
          },
          "recipientLabel": {
            "type": "string",
            "nullable": true
          },
          "verdict": {
            "type": "string",
            "nullable": true,
            "enum": [
              "keep",
              "pass",
              null
            ]
          },
          "note": {
            "type": "string",
            "nullable": true
          },
          "drawings": {
            "$ref": "#/components/schemas/ShareDrawings"
          },
          "at": {
            "type": "string"
          }
        },
        "required": [
          "assetId",
          "recipientLabel",
          "verdict",
          "note",
          "drawings",
          "at"
        ]
      },
      "InviteIssueResult": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string"
          },
          "token": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "issued",
              "already_invited",
              "already_accepted"
            ]
          },
          "toolkitsOpened": {
            "type": "number"
          }
        },
        "required": [
          "email",
          "token",
          "url",
          "status",
          "toolkitsOpened"
        ]
      },
      "InviteIssue": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "maxLength": 160,
            "format": "email",
            "example": "noa@atelierberger.com"
          }
        },
        "required": [
          "email"
        ]
      },
      "SeatInviteResult": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "admin",
              "member",
              "viewer"
            ]
          },
          "organizationId": {
            "type": "string"
          },
          "organizationName": {
            "type": "string"
          },
          "expiresAt": {
            "type": "string"
          },
          "url": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "email",
          "role",
          "organizationId",
          "organizationName",
          "expiresAt",
          "url"
        ]
      },
      "SeatInvite": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "maxLength": 160,
            "format": "email",
            "example": "eli@maison.com"
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "admin",
              "member",
              "viewer"
            ],
            "default": "member"
          }
        },
        "required": [
          "email"
        ]
      },
      "SeatAcceptResult": {
        "type": "object",
        "properties": {
          "organizationId": {
            "type": "string"
          },
          "organizationName": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "admin",
              "member",
              "viewer"
            ]
          }
        },
        "required": [
          "organizationId",
          "organizationName",
          "role"
        ]
      },
      "SeatAccept": {
        "type": "object",
        "properties": {
          "invitationId": {
            "type": "string",
            "minLength": 1
          }
        },
        "required": [
          "invitationId"
        ]
      },
      "InviteCodeRow": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "example": "FLAM-INNER-7QK4M2"
          },
          "label": {
            "type": "string"
          },
          "grantTokens": {
            "type": "number"
          },
          "maxUses": {
            "type": "number"
          },
          "usedCount": {
            "type": "number"
          },
          "joinedCount": {
            "type": "number"
          },
          "expiresAt": {
            "type": "string",
            "nullable": true
          },
          "revokedAt": {
            "type": "string",
            "nullable": true
          },
          "createdAt": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "code",
          "label",
          "grantTokens",
          "maxUses",
          "usedCount",
          "joinedCount",
          "expiresAt",
          "revokedAt",
          "createdAt"
        ]
      },
      "InviteCodeError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      },
      "InviteCodeCreate": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "minLength": 2,
            "maxLength": 40,
            "example": "FLAM_INNER"
          },
          "maxUses": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000,
            "example": 25
          },
          "grantTokens": {
            "type": "integer",
            "minimum": 0,
            "maximum": 10000,
            "default": 0,
            "example": 2000
          },
          "expiresInDays": {
            "type": "integer",
            "minimum": 1,
            "maximum": 365,
            "example": 30
          }
        },
        "required": [
          "label",
          "maxUses"
        ]
      },
      "InviteCodeRevoke": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1
          }
        },
        "required": [
          "id"
        ]
      },
      "InviteRedeemResult": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "opened",
              "already_open"
            ]
          },
          "label": {
            "type": "string"
          },
          "grantTokens": {
            "type": "number"
          },
          "frames": {
            "type": "number",
            "example": 153
          },
          "looks": {
            "type": "number",
            "example": 30
          },
          "days": {
            "type": "number",
            "example": 7
          }
        },
        "required": [
          "status",
          "label",
          "grantTokens",
          "frames",
          "looks",
          "days"
        ]
      },
      "InviteRedeem": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "minLength": 4,
            "maxLength": 60,
            "example": "FLAM-INNER-7QK4M2"
          },
          "email": {
            "type": "string",
            "maxLength": 160,
            "format": "email",
            "example": "noa@atelierberger.com"
          }
        },
        "required": [
          "code",
          "email"
        ]
      },
      "PromotionPanel": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "example": "ATELIER-BERGER-7QK4M2"
          },
          "url": {
            "type": "string",
            "example": "https://flam.fashion/i/ATELIER-BERGER-7QK4M2"
          },
          "maxUses": {
            "type": "number"
          },
          "baseMaxUses": {
            "type": "number",
            "example": 10
          },
          "usedCount": {
            "type": "number"
          },
          "invitesLeft": {
            "type": "number"
          },
          "guestLooks": {
            "type": "number",
            "example": 7
          },
          "guestTokens": {
            "type": "number",
            "example": 500
          },
          "hostTokens": {
            "type": "number",
            "example": 250
          },
          "guestValueUsd": {
            "type": "string",
            "example": "$20"
          },
          "guestArrivalUsd": {
            "type": "string",
            "example": "$10"
          },
          "guestBonusTokens": {
            "type": "number",
            "example": 250
          },
          "hostValueUsd": {
            "type": "string",
            "example": "$20"
          },
          "holdDays": {
            "type": "number",
            "example": 14
          },
          "invitees": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PromotionInvitee"
            }
          }
        },
        "required": [
          "code",
          "url",
          "maxUses",
          "baseMaxUses",
          "usedCount",
          "invitesLeft",
          "guestLooks",
          "guestTokens",
          "hostTokens",
          "guestValueUsd",
          "guestArrivalUsd",
          "guestBonusTokens",
          "hostValueUsd",
          "holdDays",
          "invitees"
        ]
      },
      "PromotionInvitee": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "example": "noa@atelierberger.com"
          },
          "invitedAt": {
            "type": "string"
          },
          "signedUp": {
            "type": "boolean"
          },
          "subscribed": {
            "type": "boolean"
          },
          "subscribedAt": {
            "type": "string",
            "nullable": true
          }
        },
        "required": [
          "email",
          "invitedAt",
          "signedUp",
          "subscribed",
          "subscribedAt"
        ]
      },
      "PromotionError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      },
      "SupportTicketRaised": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SupportTicket"
          },
          {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "nullable": true
              },
              "status": {
                "type": "string",
                "example": "noted"
              }
            }
          }
        ]
      },
      "SupportTicket": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "example": "open"
          },
          "summary": {
            "type": "string"
          },
          "messageCount": {
            "type": "integer"
          },
          "unreadCount": {
            "type": "integer"
          },
          "createdAt": {
            "type": "string"
          },
          "lastMessageAt": {
            "type": "string",
            "nullable": true
          }
        },
        "required": [
          "id",
          "status",
          "summary",
          "messageCount",
          "unreadCount",
          "createdAt",
          "lastMessageAt"
        ]
      },
      "SupportTicketError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      },
      "SupportTicketCreate": {
        "type": "object",
        "properties": {
          "topic": {
            "type": "string",
            "enum": [
              "develop",
              "billing",
              "identity",
              "quality",
              "access",
              "api",
              "other"
            ],
            "default": "other",
            "example": "develop"
          },
          "subject": {
            "type": "string",
            "minLength": 4,
            "maxLength": 160,
            "example": "The lookbook stopped at frame three"
          },
          "body": {
            "type": "string",
            "minLength": 10,
            "maxLength": 4000,
            "example": "Ran a five-frame look at 2K. Three came back, the last two never did, and the balance looks the same as before I started."
          },
          "contactEmail": {
            "type": "string",
            "maxLength": 160,
            "format": "email"
          },
          "currentUrl": {
            "type": "string",
            "maxLength": 2000
          },
          "screenshots": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 600,
              "format": "uri"
            },
            "maxItems": 4
          },
          "ticketId": {
            "type": "string",
            "maxLength": 64
          },
          "rating": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5,
            "example": 4
          },
          "screen": {
            "type": "string",
            "maxLength": 80,
            "example": "settings"
          },
          "needsReply": {
            "type": "boolean",
            "default": true
          }
        },
        "required": [
          "subject",
          "body"
        ]
      },
      "SupportTicketMessage": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "content": {
            "type": "string"
          },
          "author": {
            "type": "string",
            "enum": [
              "customer",
              "human",
              "AI"
            ]
          },
          "authorName": {
            "type": "string",
            "nullable": true
          },
          "createdAt": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "content",
          "author",
          "authorName",
          "createdAt"
        ]
      },
      "SupportEscalationCreate": {
        "type": "object",
        "properties": {
          "reason": {
            "type": "string",
            "enum": [
              "handoff",
              "turn_limit",
              "ai_down",
              "leak_guard",
              "requested"
            ],
            "example": "handoff"
          },
          "sessionId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128,
            "example": "k_8f3a12c0"
          },
          "organizationId": {
            "type": "string",
            "format": "uuid"
          },
          "contactEmail": {
            "type": "string",
            "maxLength": 160,
            "format": "email"
          },
          "transcript": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "author": {
                  "type": "string",
                  "enum": [
                    "visitor",
                    "bot",
                    "operator"
                  ]
                },
                "text": {
                  "type": "string",
                  "maxLength": 4000
                }
              },
              "required": [
                "author",
                "text"
              ]
            },
            "maxItems": 40,
            "default": []
          }
        },
        "required": [
          "reason",
          "sessionId"
        ]
      },
      "SeatRoleResult": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "admin",
              "member",
              "viewer"
            ]
          },
          "email": {
            "type": "string"
          }
        },
        "required": [
          "userId",
          "role",
          "email"
        ]
      },
      "HouseError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      },
      "SeatRole": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "minLength": 1,
            "example": "usr_eli"
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "admin",
              "member",
              "viewer"
            ],
            "example": "admin"
          }
        },
        "required": [
          "userId",
          "role"
        ]
      },
      "SeatRemoveResult": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "workStaysWithHouse": {
            "type": "boolean",
            "enum": [
              true
            ]
          }
        },
        "required": [
          "userId",
          "email",
          "workStaysWithHouse"
        ]
      },
      "HouseTransferResult": {
        "type": "object",
        "properties": {
          "organizationId": {
            "type": "string"
          },
          "userId": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "previousOwnerRole": {
            "type": "string",
            "enum": [
              "admin"
            ]
          }
        },
        "required": [
          "organizationId",
          "userId",
          "email",
          "previousOwnerRole"
        ]
      },
      "HouseTransfer": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "minLength": 1,
            "example": "usr_eli"
          }
        },
        "required": [
          "userId"
        ]
      },
      "McpResponse": {
        "type": "object",
        "properties": {
          "jsonrpc": {
            "type": "string",
            "enum": [
              "2.0"
            ]
          },
          "id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "number"
              },
              {
                "nullable": true
              }
            ]
          },
          "result": {
            "nullable": true
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "number"
              },
              "message": {
                "type": "string"
              }
            },
            "required": [
              "code",
              "message"
            ]
          }
        },
        "required": [
          "jsonrpc"
        ]
      },
      "McpRequest": {
        "type": "object",
        "properties": {
          "jsonrpc": {
            "type": "string",
            "enum": [
              "2.0"
            ]
          },
          "id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "number"
              },
              {
                "nullable": true
              }
            ]
          },
          "method": {
            "type": "string"
          },
          "params": {
            "type": "object",
            "additionalProperties": {
              "nullable": true
            }
          }
        },
        "required": [
          "jsonrpc",
          "method"
        ]
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "No valid session",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Session email not in the admin allowlist",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "BadRequest": {
        "description": "Invalid input",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "parameters": {}
  },
  "paths": {
    "/openapi.json": {
      "get": {
        "summary": "The merged OpenAPI document this API serves",
        "description": "The zod-openapi-generated half plus the hand-maintained half compiled from this file (bun run gen:openapi). /docs renders it. A route missing here is invisible to every generated client, typed SDK and MCP tool list.\n",
        "tags": [
          "meta"
        ],
        "responses": {
          "200": {
            "description": "An OpenAPI 3.1 document",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/my-access": {
      "get": {
        "tags": [
          "toolkit"
        ],
        "responses": {
          "200": {
            "description": "The active tool + toolkit registry, per-user access",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "toolkits": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": {
                          "nullable": true
                        }
                      }
                    },
                    "tools": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "slug": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "sortOrder": {
                            "type": "number"
                          },
                          "accessible": {
                            "type": "boolean"
                          },
                          "settings": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "additionalProperties": {
                                "nullable": true
                              }
                            }
                          }
                        },
                        "required": [
                          "id",
                          "slug",
                          "name",
                          "sortOrder",
                          "accessible"
                        ],
                        "additionalProperties": {
                          "nullable": true
                        }
                      }
                    },
                    "trialExpired": {
                      "type": "boolean"
                    },
                    "trialActive": {
                      "type": "boolean"
                    },
                    "trialDaysLeft": {
                      "type": "number",
                      "nullable": true
                    }
                  },
                  "required": [
                    "toolkits",
                    "tools",
                    "trialExpired",
                    "trialActive",
                    "trialDaysLeft"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/tools/{slug}/items": {
      "get": {
        "summary": "Curated content items for a tool",
        "tags": [
          "toolkit"
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "tool_items rows, ordered per category by sort_order",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ToolItem"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/ai/remove-background": {
      "post": {
        "summary": "Remove background (sync, fal birefnet)",
        "tags": [
          "toolkit"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "image"
                ],
                "properties": {
                  "image": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The cutout, in the library",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "url",
                    "spent",
                    "assetId"
                  ],
                  "properties": {
                    "url": {
                      "type": "string",
                      "description": "Absolute URL of the transparent PNG, served from THIS origin. The result is captured into the library before you see it, so the URL does not expire — it used to be a fal.media URL with a 60-day clock on it."
                    },
                    "spent": {
                      "type": "number",
                      "description": "Tokens actually charged for this cutout."
                    },
                    "assetId": {
                      "type": "string",
                      "format": "uuid",
                      "description": "The library asset the cutout was captured into — it appears in GET /api/toolkit/assets and its bytes are at GET /api/toolkit/assets/{id}."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "NO_IMAGE | EMPTY_IMAGE | BAD_FORM"
          },
          "402": {
            "description": "INSUFFICIENT_TOKENS — body includes need + spendable"
          },
          "403": {
            "description": "A viewer may look but not spend"
          },
          "413": {
            "description": "IMAGE_TOO_LARGE (12 MB cap) — body includes maxMb"
          },
          "502": {
            "description": "REMOVE_BG_FAILED — the engine failed, OR the house could not keep the result. Nothing is charged either way."
          },
          "503": {
            "description": "FAL_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/ai/prompt-enhancer": {
      "post": {
        "summary": "Improve a rough prompt (sync text lane)",
        "tags": [
          "text-tools"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "prompt"
                ],
                "properties": {
                  "prompt": {
                    "type": "string"
                  },
                  "intent": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Improved prompt + notes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "improved": {
                      "type": "string"
                    },
                    "notes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "PROMPT_REQUIRED"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "AI_NEEDS_BILLING"
          },
          "502": {
            "description": "AI_BAD_OUTPUT | AI_FAILED"
          },
          "503": {
            "description": "AI_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/ai/prompt-builder": {
      "post": {
        "summary": "Build one photographic prompt from a subject",
        "tags": [
          "text-tools"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "subject"
                ],
                "properties": {
                  "subject": {
                    "type": "string"
                  },
                  "vibe": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The built prompt",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "prompt": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "SUBJECT_REQUIRED"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "AI_NEEDS_BILLING"
          },
          "502": {
            "description": "AI_BAD_OUTPUT | AI_FAILED"
          },
          "503": {
            "description": "AI_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/ai/midjourney-prompts": {
      "post": {
        "summary": "Generate N varied shot prompts from a reference description",
        "tags": [
          "text-tools"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "description"
                ],
                "properties": {
                  "description": {
                    "type": "string"
                  },
                  "direction": {
                    "type": "string"
                  },
                  "count": {
                    "type": "integer",
                    "minimum": 1
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Labeled shot prompts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "shots": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "label": {
                            "type": "string"
                          },
                          "prompt": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "DESCRIPTION_REQUIRED"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "AI_NEEDS_BILLING"
          },
          "502": {
            "description": "AI_BAD_OUTPUT | AI_FAILED"
          },
          "503": {
            "description": "AI_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/ai/inspiration-keywords": {
      "post": {
        "summary": "Categorized keyword sets for a vibe",
        "tags": [
          "text-tools"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "vibe"
                ],
                "properties": {
                  "vibe": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Keyword categories",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "categories": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "category": {
                            "type": "string"
                          },
                          "keywords": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "VIBE_REQUIRED"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "AI_NEEDS_BILLING"
          },
          "502": {
            "description": "AI_BAD_OUTPUT | AI_FAILED"
          },
          "503": {
            "description": "AI_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/ai/face-features": {
      "post": {
        "summary": "20 facial-feature keywords from a portrait (vision lane)",
        "tags": [
          "text-tools"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "imageUrl": {
                    "type": "string"
                  }
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "image": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Keywords (empty + faceDetected=false when no face)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "faceDetected": {
                      "type": "boolean"
                    },
                    "keywords": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "IMAGE_REQUIRED | EMPTY_IMAGE | BAD_FORM"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "AI_NEEDS_BILLING"
          },
          "413": {
            "description": "IMAGE_TOO_LARGE"
          },
          "502": {
            "description": "AI_BAD_OUTPUT | AI_FAILED"
          },
          "503": {
            "description": "AI_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/jobs/submit": {
      "post": {
        "summary": "Submit an async generation job",
        "description": "Generic over tools and N named images: `tool` must be a known async slug; every file field stages as a named input (field name = role, e.g. try-on sends `model` + `garment`); other string fields become the tool's params. Reserves a token hold before enqueue; idempotent on (user, idempotencyKey). Progress is pushed over the jobs WebSocket.\n",
        "tags": [
          "jobs"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "tool"
                ],
                "properties": {
                  "tool": {
                    "type": "string",
                    "description": "One of ASYNC_TOOL_SLUGS (upscale, reframe, generate-character, packshot, camera-lens, harmonize, tryon, iron, cleanup, color-change, fit-adjust, lookbook-video)"
                  },
                  "idempotencyKey": {
                    "type": "string"
                  },
                  "brandId": {
                    "type": "string"
                  },
                  "image": {
                    "type": "string",
                    "format": "binary",
                    "description": "Single-image tools; multi-input tools use per-role field names instead"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Job queued (or the existing job on an idempotent replay)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "UNKNOWN_TOOL | NO_IMAGE | EMPTY_IMAGE | BAD_FORM"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "INSUFFICIENT_TOKENS — body includes need + spendable"
          },
          "413": {
            "description": "IMAGE_TOO_LARGE (12 MB per image)"
          },
          "500": {
            "description": "SUBMIT_FAILED"
          }
        }
      }
    },
    "/api/toolkit/jobs": {
      "get": {
        "summary": "Reconnect backfill — jobs newer than an eventSeq cursor",
        "description": "The session user's generation_jobs with event_seq > since, ascending, in the same Job shape the WebSocket pushes. The web client calls this on every WS (re)open with its max seen eventSeq so events missed while the socket was down replay. since=0 returns everything.\n",
        "tags": [
          "jobs"
        ],
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Missed job events, ascending by eventSeq",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobs": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Job"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_SINCE"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/jobs/ws": {
      "get": {
        "summary": "Job event stream (WebSocket upgrade)",
        "description": "Upgrades to a WebSocket forwarded to the caller's per-user JobStreamDO. Every job status change is pushed as {\"type\":\"job\",\"job\":Job}. Requires the session cookie on the handshake.\n",
        "tags": [
          "jobs"
        ],
        "responses": {
          "101": {
            "description": "Switching Protocols — socket attached"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "426": {
            "description": "Upgrade header missing — not a WebSocket request"
          }
        }
      }
    },
    "/api/toolkit/ai/billing/balance": {
      "get": {
        "summary": "Spendable token balance + open holds",
        "tags": [
          "billing"
        ],
        "responses": {
          "200": {
            "description": "Derived from the append-only credit_ledger",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "balance": {
                      "type": "integer"
                    },
                    "grant": {
                      "type": "integer"
                    },
                    "topup": {
                      "type": "integer"
                    },
                    "held": {
                      "type": "integer"
                    },
                    "holds": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "amount": {
                            "type": "integer"
                          },
                          "generationId": {
                            "type": "string",
                            "nullable": true
                          },
                          "createdAt": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/ai/billing/catalog": {
      "get": {
        "summary": "Public pricing catalog (tiers + packs)",
        "tags": [
          "billing"
        ],
        "responses": {
          "200": {
            "description": "Active subscription tiers and top-up packs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "provider": {
                      "type": "string",
                      "enum": [
                        "dodo",
                        "none"
                      ]
                    },
                    "tiers": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "slug": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "priceUsd": {
                            "type": "number"
                          },
                          "monthlyTokens": {
                            "type": "integer"
                          },
                          "rolloverMultiple": {
                            "type": "number"
                          },
                          "featured": {
                            "type": "boolean"
                          }
                        }
                      }
                    },
                    "packs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "slug": {
                            "type": "string"
                          },
                          "tokens": {
                            "type": "integer"
                          },
                          "priceUsd": {
                            "type": "number"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/ai/billing/checkout/subscription": {
      "post": {
        "summary": "Hosted Dodo checkout for a subscription tier",
        "tags": [
          "billing"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "tier"
                ],
                "properties": {
                  "tier": {
                    "type": "string",
                    "enum": [
                      "studio",
                      "editorial",
                      "couture"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Hosted checkout URL",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_TIER"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "502": {
            "description": "CHECKOUT_FAILED"
          },
          "503": {
            "description": "BILLING_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/ai/billing/checkout/topup": {
      "post": {
        "summary": "Hosted Dodo checkout for a token pack",
        "tags": [
          "billing"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "pack"
                ],
                "properties": {
                  "pack": {
                    "type": "string",
                    "enum": [
                      "pack_250",
                      "pack_1000",
                      "pack_3000"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Hosted checkout URL",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_PACK"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "502": {
            "description": "CHECKOUT_FAILED"
          },
          "503": {
            "description": "BILLING_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/ai/billing/checkout/custom": {
      "post": {
        "summary": "Hosted Dodo checkout for a pay-what-you-want top-up",
        "description": "The director names a dollar amount; it becomes tokens at the top-up rate — the highest price per token any active row in `topup_packs` sells at, so a custom amount is never cheaper per token than a pack, and packs are already pricier than every plan (check:pricing rule 4). The amount is quantised DOWN to whole tokens, so $10.30 buys 257 tokens and charges $10.28. No processing-fee gross-up, so no fee line: the fee is the same one the fixed packs already carry. Owner only — it charges the house.\n",
        "tags": [
          "billing"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "amountUsd"
                ],
                "properties": {
                  "amountUsd": {
                    "type": "number",
                    "description": "Dollars. Minimum is the cheapest active pack's price ($10 today); maximum is $5,000, a typo guard rather than a policy.\n",
                    "example": 25
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Hosted checkout URL, plus what will be charged and granted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string"
                    },
                    "tokens": {
                      "type": "integer",
                      "description": "Whole tokens the webhook will grant when the money lands."
                    },
                    "amountCents": {
                      "type": "integer",
                      "description": "What the card is actually charged — never more than was named."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_AMOUNT or AMOUNT_OUT_OF_RANGE (with minCents/maxCents)"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "FORBIDDEN — only the owner spends the house's money"
          },
          "502": {
            "description": "CHECKOUT_FAILED"
          },
          "503": {
            "description": "BILLING_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/ai/billing/autocharge": {
      "get": {
        "summary": "The house's auto top-up rule, ceiling, card and audit trail",
        "description": "The standing order — \"when the balance falls below X, buy pack Y\" — plus what it has already spent this calendar month, whether a card is saved, and every automatic charge the provider has confirmed. `available` is false unless AUTOCHARGE_ENABLED=1 AND a Dodo token is configured; `canArm` mirrors the server's canSpendMoney (owner only).\n",
        "tags": [
          "billing"
        ],
        "responses": {
          "200": {
            "description": "Settings + ceiling + packs + charges",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "available": {
                      "type": "boolean"
                    },
                    "canArm": {
                      "type": "boolean"
                    },
                    "hasCard": {
                      "type": "boolean"
                    },
                    "hasCustomer": {
                      "type": "boolean"
                    },
                    "settings": {
                      "type": "object",
                      "properties": {
                        "enabled": {
                          "type": "boolean"
                        },
                        "thresholdTokens": {
                          "type": "integer"
                        },
                        "refillCents": {
                          "type": "integer"
                        },
                        "refillPack": {
                          "type": "string",
                          "nullable": true
                        },
                        "monthlyCapCents": {
                          "type": "integer"
                        }
                      }
                    },
                    "thisMonth": {
                      "type": "object",
                      "properties": {
                        "period": {
                          "type": "string",
                          "example": "2026-07"
                        },
                        "spentCents": {
                          "type": "integer"
                        }
                      }
                    },
                    "lastChargeAt": {
                      "type": "string",
                      "nullable": true,
                      "format": "date-time"
                    },
                    "packs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "slug": {
                            "type": "string"
                          },
                          "tokens": {
                            "type": "integer"
                          },
                          "priceCents": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "limits": {
                      "type": "object"
                    },
                    "charges": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "amountCents": {
                            "type": "integer"
                          },
                          "currency": {
                            "type": "string"
                          },
                          "reference": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "put": {
        "summary": "Arm, retune or disarm the auto top-up rule (OWNER ONLY)",
        "description": "Real money on a standing order, so this is gated by canSpendMoney — owner only, 403 for every other seat. The refill is always a real catalogue pack; the monthly ceiling is a hard stop and may never be below one refill.\n",
        "tags": [
          "billing"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "enabled",
                  "thresholdTokens",
                  "pack",
                  "monthlyCapUsd"
                ],
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  },
                  "thresholdTokens": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100000
                  },
                  "pack": {
                    "type": "string",
                    "enum": [
                      "pack_250",
                      "pack_1000",
                      "pack_3000"
                    ]
                  },
                  "monthlyCapUsd": {
                    "type": "number",
                    "minimum": 10,
                    "maximum": 2000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The saved rule, read back",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "settings": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_THRESHOLD · BAD_CAP · BAD_PACK · CAP_BELOW_REFILL"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "forbidden — role_cannot_spend_money"
          },
          "409": {
            "description": "AMBIGUOUS_PACK_PRICE"
          }
        }
      }
    },
    "/api/toolkit/ai/billing/autocharge/run": {
      "post": {
        "summary": "Fire the standing order if the house is genuinely short",
        "description": "A HINT endpoint — every condition is re-validated server-side, so calling it is safe and it answers 200 either way with `{ charged, reason }`. The reservation (enabled · 10-minute debounce · monthly ceiling · live spendable balance) is one atomic UPDATE, so two concurrent calls can never both charge. Nothing here credits a token: payment.succeeded is the sole grant authority. Gated by canSpendTokens (viewers excluded) — it executes an instruction the owner already gave.\n",
        "tags": [
          "billing"
        ],
        "responses": {
          "200": {
            "description": "Charged, or the reason nothing happened",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "charged": {
                      "type": "boolean"
                    },
                    "reason": {
                      "type": "string",
                      "enum": [
                        "flag_off",
                        "not_configured",
                        "disabled",
                        "bad_refill",
                        "no_customer",
                        "no_payment_method",
                        "above_threshold",
                        "monthly_cap_reached",
                        "debounced",
                        "raced"
                      ]
                    },
                    "pack": {
                      "type": "string"
                    },
                    "tokens": {
                      "type": "integer"
                    },
                    "amountCents": {
                      "type": "integer"
                    },
                    "paymentId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "forbidden — role_cannot_spend_tokens"
          },
          "502": {
            "description": "charge_failed — the provider refused; the reservation is rolled back"
          }
        }
      }
    },
    "/api/toolkit/ai/billing/portal": {
      "post": {
        "summary": "Open the merchant of record's hosted portal (OWNER ONLY)",
        "description": "Dodo owns the card, the subscription controls and the tax documents, so this hands off rather than rebuilding them. Owner only (canSpendMoney).\n",
        "tags": [
          "billing"
        ],
        "responses": {
          "200": {
            "description": "The portal session URL",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "NO_CUSTOMER — nothing has ever been bought"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "forbidden — role_cannot_spend_money"
          },
          "502": {
            "description": "PORTAL_FAILED · PORTAL_NO_URL"
          },
          "503": {
            "description": "BILLING_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/ai/billing/webhook/dodo": {
      "post": {
        "summary": "Dodo payment webhook (standard-webhooks signature)",
        "description": "The only unauthenticated billing write — authenticated by the webhook-id / webhook-timestamp / webhook-signature headers, not a session. Duplicate deliveries dedupe via mor_webhook_events.\n",
        "tags": [
          "billing"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Applied ({ ok, ... }) or deduped ({ ok, duplicate })"
          },
          "400": {
            "description": "UNPARSEABLE"
          },
          "401": {
            "description": "BAD_SIGNATURE"
          },
          "500": {
            "description": "APPLY_FAILED"
          }
        }
      }
    },
    "/api/toolkit/email/webhook/resend": {
      "post": {
        "summary": "Resend delivery webhook (standard-webhooks signature)",
        "description": "Delivery truth. email_log.status is written once at send time — 'sent' means Resend accepted it, not that a director read it — so without this a hard bounce is indistinguishable from a delivered mail. Public and unauthenticated, authenticated by the webhook-id / webhook-timestamp / webhook-signature headers (the same verifier the Dodo webhook uses), and it fails CLOSED when RESEND_WEBHOOK_SECRET is unset. Correlates on provider_id (Resend's message id, echoed back as data.email_id). email.bounced and email.complained also raise an alert; events that say nothing about arrival answer 200 and write nothing.\n",
        "tags": [
          "email"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recorded ({ ok, status }), unmatched ({ ok, matched:false }) or ignored ({ ok, ignored })"
          },
          "400": {
            "description": "UNPARSEABLE"
          },
          "401": {
            "description": "BAD_SIGNATURE"
          }
        }
      }
    },
    "/api/toolkit/email/drip": {
      "post": {
        "summary": "PostHog triggers a drip, FLAM sends it",
        "description": "PostHog decides WHEN a lifecycle email goes out; this renders and sends WHAT. Before this, PostHog's own email step sent copies of the templates that had been retyped into its drag-and-drop builder — measured 3 Aug 2026, all three carried zero <img> tags, so every one arrived with no FLAM masthead. Now there is one rendering pipeline: the send lands in email_log with the exact HTML and the Resend delivery webhook updates that row, so a bounce is visible.\n\nPUBLIC, authenticated by `Authorization: Bearer <DRIP_WEBHOOK_SECRET>` — a PostHog workflow HTTP destination cannot sign a body the way Dodo and Resend do, so the shared secret is what the caller can actually produce. Compared in constant time, and it fails CLOSED (503) when the secret is unset.\n\n`dedupeKey` is REQUIRED and the caller names the unit, because only drip-day3 is once-per-person — develop-interrupted and look-waiting should send again the next time a develop dies. Use `drip-day3:{{ person.id }}` and `develop-interrupted:{{ event.uuid }}`. A repeat inserts nothing and answers 200 `{ sent: false, reason: 'ALREADY_SENT' }` rather than 409, because PostHog retries non-2xx and a retry storm against a correctly-behaving endpoint is the failure mode.\n\n`template` is an allowlist of the three lifecycle letters, never all of @flam/email: whoever holds this secret must not be able to post somebody a sign-in link.\n",
        "tags": [
          "email"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "template",
                  "email",
                  "dedupeKey"
                ],
                "properties": {
                  "template": {
                    "type": "string",
                    "enum": [
                      "drip-day3",
                      "develop-interrupted",
                      "look-waiting"
                    ]
                  },
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "dedupeKey": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 60,
                    "description": "drip-day3 opens with it; absent falls back to 'there'."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ sent: true, template, status } or { sent: false, reason: 'ALREADY_SENT' }"
          },
          "400": {
            "description": "BAD_REQUEST — unknown template, bad address, or no dedupeKey"
          },
          "401": {
            "description": "UNAUTHORIZED — wrong or missing bearer secret"
          },
          "503": {
            "description": "DRIP_NOT_CONFIGURED — DRIP_WEBHOOK_SECRET is unset"
          }
        }
      }
    },
    "/api/toolkit/assets": {
      "get": {
        "summary": "The house's captured assets, newest first",
        "description": "The body is `{ assets: [...] }`, not a bare array — it was documented as a bare array until task #40 and never was one.\n",
        "tags": [
          "assets"
        ],
        "parameters": [
          {
            "name": "folderId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Narrow to one collection"
          }
        ],
        "responses": {
          "200": {
            "description": "Asset list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "assets": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Asset"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/account/avatar": {
      "post": {
        "summary": "Set your own picture (multipart)",
        "description": "The bytes arrive ALREADY SQUARE, downscaled and webp-encoded: `ImageCropper` (@flam/ui) does the crop and the encode in the browser, because the API is a Cloudflare Worker and workerd has no image library. So this validates rather than transforms.\nAccepts image/webp, image/png, image/jpeg up to 512KB — roughly 17x what an honest client sends, and the bound on a hostile one.\nAnswers the URL now on your row, so a caller can render it immediately instead of re-reading the account. That URL carries a `?v=` stamp: the serving route sends `immutable, max-age=1y`, which is only safe because each replacement is a new URL.\n",
        "tags": [
          "account"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stored. `image` is the URL now on your user row.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "image": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No file, an empty file, or an unsupported type."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "description": "Larger than 512KB."
          }
        }
      },
      "delete": {
        "summary": "Remove your picture",
        "description": "Back to your initial. The bytes go too — 'remove my picture' means remove it, not hide it. Answers `{ image: null }`.\n",
        "tags": [
          "account"
        ],
        "responses": {
          "200": {
            "description": "Removed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "image": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/account/avatar/{userId}": {
      "get": {
        "summary": "A user's picture (bytes)",
        "description": "A SESSION IS REQUIRED and that is the whole access rule: a colleague's face is directory-level information inside a house, and gating it further would mean a membership query on every <img> on every screen. It is NOT public — an unauthenticated request gets 401, so a portrait never leaks to a stranger who guesses a user id.\n",
        "tags": [
          "account"
        ],
        "parameters": [
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The image bytes."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "That user has no picture."
          }
        }
      }
    },
    "/api/toolkit/assets/upload": {
      "post": {
        "summary": "Hang one image in the house library (multipart)",
        "description": "Bytes → R2, row → assets. The upload itself is FREE.\nSMART UPLOAD — `classify=1` is an OPT-IN read priced at 1 token per image (the describe-item classifier, run in-process after the row is written). It files the piece into its wardrobe bucket (`kind`) and gives it a human name — \"IMG_2489.jpeg\" becomes \"white ribbed lace trim henley\", a model collage comes back kind=model. Anything you state yourself wins: a supplied `name`/`kind` is never overwritten, and `sku` is your own code — it is never invented. Omit `classify` and nothing is read and nothing is charged.\nA failed read never loses the upload and never charges: 201 with the asset unclassified and `classify.ok=false` carrying the reason (INSUFFICIENT_TOKENS, AI_NOT_CONFIGURED, AI_FAILED, …).\n",
        "tags": [
          "assets"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  },
                  "folderId": {
                    "type": "string",
                    "description": "Target collection; omit for the root"
                  },
                  "name": {
                    "type": "string",
                    "description": "Your own label; wins over the smart read"
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "clothes",
                      "accessory",
                      "jewelry",
                      "model",
                      "background"
                    ],
                    "description": "Your own wardrobe bucket; wins over the smart read"
                  },
                  "sku": {
                    "type": "string",
                    "maxLength": 64,
                    "description": "The brand's own code"
                  },
                  "classify": {
                    "type": "string",
                    "enum": [
                      "1",
                      "true",
                      "on",
                      "0"
                    ],
                    "description": "Opt in to the 1-token smart read"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The hung asset (+ `classify` when the read was requested)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "asset": {
                      "$ref": "#/components/schemas/Asset"
                    },
                    "classify": {
                      "type": "object",
                      "properties": {
                        "ok": {
                          "type": "boolean"
                        },
                        "nameEn": {
                          "type": "string"
                        },
                        "kind": {
                          "type": "string"
                        },
                        "error": {
                          "type": "string"
                        },
                        "tokens": {
                          "type": "integer",
                          "description": "Charged — 1 on a read, 0 on a failure"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "NO_FILE / UNSUPPORTED_TYPE / EMPTY_FILE / BAD_KIND / BAD_SKU / BAD_FORM"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Unknown or foreign folderId"
          },
          "413": {
            "description": "IMAGE_TOO_LARGE (12 MB cap)"
          }
        }
      }
    },
    "/api/toolkit/assets/{id}": {
      "get": {
        "summary": "Stream one asset's bytes from R2 (owner-gated)",
        "tags": [
          "assets"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The image bytes (content-type from the asset row)"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not the owner"
          },
          "404": {
            "description": "Unknown or deleted"
          }
        }
      },
      "patch": {
        "summary": "Rename, classify, tag or move one piece",
        "description": "An explicit `folderId: null` moves the piece back to the root. `kind` is stored as the reserved `kind:` tag and `sku` as the `sku:` tag, so both can be cleared with null. An empty patch is a 200 no-op, not an error.\n",
        "tags": [
          "assets"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "sku": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 64
                  },
                  "kind": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "clothes",
                      "accessory",
                      "jewelry",
                      "model",
                      "background"
                    ]
                  },
                  "folderId": {
                    "type": "string",
                    "nullable": true
                  },
                  "fit": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "auto",
                      "fitted",
                      "regular",
                      "loose",
                      "oversized"
                    ],
                    "description": "How the garment is cut to be worn. Stored as the reserved `fit:` tag; null clears it back to unread."
                  },
                  "wearer": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "women",
                      "men",
                      "unisex"
                    ],
                    "description": "The body the garment is CUT FOR — not the body a packshot's ghost form implies. Stored as the reserved `wearer:` tag; null clears it. `unisex` is a merchandising fact only a human can state, so the house never guesses it."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated asset",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "asset": {
                      "$ref": "#/components/schemas/Asset"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / BAD_KIND / BAD_SKU / BAD_FOLDER"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Unknown or foreign asset, or unknown target folder"
          }
        }
      }
    },
    "/api/toolkit/onboarding": {
      "get": {
        "summary": "First-workflow checklist progress",
        "tags": [
          "onboarding"
        ],
        "responses": {
          "200": {
            "description": "Progress + reward state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OnboardingProgress"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "summary": "Complete a checklist step (idempotent)",
        "description": "Completing the last step mints a one-time 30-token promo grant (rewardJustGranted true exactly once).\n",
        "tags": [
          "onboarding"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "step"
                ],
                "properties": {
                  "step": {
                    "type": "string",
                    "enum": [
                      "brand",
                      "upload",
                      "cleanup",
                      "model",
                      "tryon",
                      "organize",
                      "tokens"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated progress",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/OnboardingProgress"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "rewardJustGranted": {
                          "type": "boolean"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY | UNKNOWN_STEP"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/admin/users": {
      "get": {
        "summary": "All users with balance + access (admin)",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "User list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "users": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/access": {
      "post": {
        "summary": "Grant / revoke toolkit or tool access (admin)",
        "tags": [
          "admin"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "action"
                ],
                "properties": {
                  "action": {
                    "type": "string",
                    "enum": [
                      "grant_toolkit",
                      "revoke_toolkit",
                      "grant_tool",
                      "revoke_tool"
                    ]
                  },
                  "userId": {
                    "type": "string"
                  },
                  "email": {
                    "type": "string"
                  },
                  "toolkitId": {
                    "type": "string"
                  },
                  "toolId": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ ok, count }"
          },
          "400": {
            "description": "Missing action / item id / target"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/grant-tokens": {
      "post": {
        "summary": "Comp a token grant to a user (admin)",
        "tags": [
          "admin"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "userId",
                  "tokens"
                ],
                "properties": {
                  "userId": {
                    "type": "string"
                  },
                  "tokens": {
                    "type": "integer",
                    "minimum": 1
                  },
                  "expiryDays": {
                    "type": "integer",
                    "nullable": true
                  },
                  "reason": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ ok, granted, tokens, expiresAt }"
          },
          "400": {
            "description": "USER_REQUIRED | BAD_TOKENS (body includes max)"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "USER_NOT_FOUND"
          }
        }
      }
    },
    "/api/toolkit/admin/tool-items": {
      "get": {
        "summary": "Curated items for one tool (admin)",
        "tags": [
          "admin"
        ],
        "parameters": [
          {
            "name": "toolId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ items }"
          },
          "400": {
            "description": "toolId required"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "post": {
        "summary": "Append a curated item (admin)",
        "tags": [
          "admin"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "toolId",
                  "category"
                ],
                "properties": {
                  "toolId": {
                    "type": "string"
                  },
                  "category": {
                    "type": "string"
                  },
                  "contentEn": {
                    "type": "string"
                  },
                  "titleHe": {
                    "type": "string"
                  },
                  "labelHe": {
                    "type": "string"
                  },
                  "imageUrl": {
                    "type": "string"
                  },
                  "notesHe": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ ok, id, sortOrder }"
          },
          "400": {
            "description": "toolId and category required"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "delete": {
        "summary": "Delete a curated item by id (admin)",
        "tags": [
          "admin"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ ok, deleted }"
          },
          "400": {
            "description": "id required"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/ai-costs": {
      "get": {
        "summary": "Token/cost usage summary from the ledger (admin)",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "Settled tokens + real USD per tool, per day, and totals",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "byTool": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "tool": {
                            "type": "string"
                          },
                          "tokens": {
                            "type": "integer"
                          },
                          "cost_usd": {
                            "type": "number"
                          },
                          "n": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "daily": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "day": {
                            "type": "string"
                          },
                          "tokens": {
                            "type": "integer"
                          },
                          "cost_usd": {
                            "type": "number"
                          }
                        }
                      }
                    },
                    "totalTokensSettled": {
                      "type": "integer"
                    },
                    "totalCostUsd": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/explore": {
      "get": {
        "summary": "Every offering this director may see, across all kinds",
        "tags": [
          "explore"
        ],
        "responses": {
          "200": {
            "description": "The board",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "offerings"
                  ],
                  "properties": {
                    "offerings": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "kind",
                          "id",
                          "name"
                        ],
                        "properties": {
                          "kind": {
                            "type": "string",
                            "enum": [
                              "model",
                              "background",
                              "variation_set",
                              "workflow"
                            ]
                          },
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "note": {
                            "type": "string",
                            "nullable": true
                          },
                          "previewUrl": {
                            "type": "string",
                            "nullable": true
                          },
                          "priceCents": {
                            "type": "integer",
                            "description": "Absent or 0 = free"
                          },
                          "currency": {
                            "type": "string"
                          },
                          "owned": {
                            "type": "boolean"
                          },
                          "exclusive": {
                            "type": "boolean"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/explore/{kind}/{id}/take": {
      "post": {
        "summary": "Take an offering — free grants now, priced opens checkout",
        "tags": [
          "explore"
        ],
        "parameters": [
          {
            "name": "kind",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Free — granted; or priced — a hosted checkout url",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "url": {
                      "type": "string",
                      "description": "Hosted checkout (priced offerings)"
                    },
                    "pending": {
                      "type": "boolean",
                      "description": "A live claim already had a checkout open"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "TAKEN (an exclusive is claimed) or CHECKOUT_PENDING",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "NOT_FOR_SALE or BILLING_NOT_CONFIGURED",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/entitlements": {
      "get": {
        "summary": "What this director holds, and any checkout still open",
        "description": "`holdings` is granted only. `open` is pending-and-unexpired and is returned SEPARATELY so a standing charge is never dressed as a possession.\n",
        "tags": [
          "explore"
        ],
        "responses": {
          "200": {
            "description": "Holdings and open checkouts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "holdings",
                    "open"
                  ],
                  "properties": {
                    "holdings": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "kind": {
                            "type": "string"
                          },
                          "offeringId": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string",
                            "nullable": true
                          },
                          "source": {
                            "type": "string",
                            "enum": [
                              "purchase",
                              "own"
                            ]
                          },
                          "priceCents": {
                            "type": "integer"
                          },
                          "currency": {
                            "type": "string"
                          },
                          "paymentRef": {
                            "type": "string",
                            "nullable": true
                          },
                          "exclusive": {
                            "type": "boolean"
                          },
                          "acquiredAt": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "open": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "kind": {
                            "type": "string"
                          },
                          "offeringId": {
                            "type": "string"
                          },
                          "checkoutUrl": {
                            "type": "string",
                            "nullable": true
                          },
                          "expiresAt": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/admin/offerings": {
      "get": {
        "summary": "The catalogue, as the house sees it",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "Offerings"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "post": {
        "summary": "Create or update an offering (price, tier, previews)",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "The saved offering"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/offerings/retire": {
      "post": {
        "summary": "Retire an offering from the board",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "Retired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/entitlements": {
      "get": {
        "summary": "Real-money purchases across the house",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "Entitlements"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/entitlements/revoke": {
      "post": {
        "summary": "Revoke a granted entitlement (a refund's other half)",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "Revoked"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/develops": {
      "get": {
        "summary": "Develop runs across the house, newest first",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "Runs"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/develops/action": {
      "post": {
        "summary": "Retry or cancel a develop",
        "description": "Cancel carries the Darkroom's honest semantics — queued frames release their hold, a frame already rendering finishes and still costs.\n",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "The action's honest tally"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/overview": {
      "get": {
        "summary": "The house at a glance — the first screen",
        "description": "Directors and houses, how many worked this week, the outstanding token float, what the engine is doing right now, what moved today against yesterday, and a fourteen-day strip. Money and tokens are separate fields and are never summed — there is deliberately no field holding the two added together.\n",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "The house at a glance"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/frames": {
      "get": {
        "summary": "Every house's outputs, browsable",
        "description": "What is actually being MADE on FLAM. Narrows server-side by house (organization id), tool (source_tool), age in days, and origin — `made` is anything the house developed, `brought` is what a director uploaded. The filter lists ride along in the same answer so the panel never needs a second round trip.\n",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "Frames, plus the houses and tools that exist"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/frames/{id}/file": {
      "get": {
        "summary": "The bytes of any house's frame (admin only)",
        "description": "assets.ts's streamAsset owner-gates on organization_id and answers 404 to everyone else — correct for a director, and fatal for an admin who has to look at the frame a customer is complaining about. Widening that route would put \"or an admin\" inside the DIRECTOR path, where a bug in the allowlist becomes a cross-house read for real users. This one is admin-only from its first line.\n",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "The image bytes"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such frame"
          }
        }
      }
    },
    "/api/toolkit/admin/tokenomics": {
      "get": {
        "summary": "What a token costs against what we charge, and the funnel",
        "description": "Three layers, never blended — the price book per tool AND per quality (tokens charged against credit_price_book.loaded_cost_usd), what actually ran from the jobs spine, and the conversion funnel. While `costUnmeasured` is true the worker still settles with a measured cost of zero, so every margin is MODELLED from the price book and the panel says so rather than printing a 100% margin.\n",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "Unit economics, actuals and the funnel"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/kept-cost": {
      "get": {
        "summary": "Cost per KEPT frame, per house and per tool, over a window",
        "description": "Cost per generation flatters us — if she develops six and keeps one, that frame cost six renders. Money out of credit_ledger (joined on generation_jobs.hold_id, never metadata->>'tool') divided by the frames that SURVIVED. The denominator basis is `spoke` (kept + regenerated + deleted), never `runs`: a frame nobody has downloaded has not spoken, and silence is not rejection. Below `minKept` kept frames every rate on the row is null and `why` says so; below `minCoverage` the MEASURED rate is null while `credit_ledger.real_cost_usd` is still mostly zero, and only the MODELLED rate answers. `deletedWired` is false because no delete verb exists, so \"fraction thrown away\" is NOT YET MEASURABLE.\n",
        "tags": [
          "admin"
        ],
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 30
            },
            "description": "Window in days"
          },
          {
            "name": "house",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "One organization_id"
          },
          {
            "name": "tool",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "One tool slug"
          }
        ],
        "responses": {
          "200": {
            "description": "Cost per kept frame with its denominator, or a stated refusal"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/emails": {
      "get": {
        "summary": "Every letter the house sent (email_log)",
        "description": "The list omits `html` on purpose — a hundred rendered emails is megabytes of body for a table that shows none of it. Pass ?id= for ONE row WITH its byte-exact archived HTML, exactly as the recipient received it. `skipped` is the dev-safe lane, not a fault.\n",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "Letters, or one letter with its archive"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such letter"
          }
        }
      }
    },
    "/api/toolkit/admin/support": {
      "get": {
        "summary": "Tickets and feedback, with the screenshots directors sent",
        "description": "The one admin book that is not read out of Postgres. Tickets come from PostHog Support (the same inbox the studio's report sheet writes to) and feedback from REPORT_SURVEY's `survey sent` events. Every ticket's FULL first message is read per ticket rather than taken from the list, because the list truncates at ~500 characters and cuts the signed screenshot links mid-signature — a truncated signed link is a broken image, and the images are the point. `screenshots` are signed, expiring links on FLAM's own R2 that need no session, so they render as pictures. `available: false` means POSTHOG_PERSONAL_API_KEY is absent in this environment; it is a deployment fact and never an empty inbox.\n",
        "tags": [
          "admin"
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "maximum": 100
            },
            "description": "How many tickets and how many feedback rows to return"
          }
        ],
        "responses": {
          "200": {
            "description": "Tickets with their pictures, and every rating given"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/failures": {
      "get": {
        "summary": "Failures grouped by what the provider actually said",
        "description": "Twelve rows of \"the provider returned no frame\" is one problem wearing twelve uuids, so failures group on the error text with ids and digits knocked out. Each group carries how many HOUSES it reached — one house is probably their input, several is ours — and the whole real message from the most recent occurrence. Refused emails ride along.\n",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "Grouped failures, plus refused emails"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/judge/verdicts": {
      "get": {
        "summary": "Every judged frame, across every house (admin only)",
        "description": "The back office's half of the judge — the same rows /api/toolkit/judge/verdicts serves a director, plus the four things that route hides on purpose: the judge's own `note`, the `model` id, `costUsd` and `latencyMs`. Narrows server-side by house, tool, defect code, verdict band, director response, recipe arm and age. `?flagged=true` is a synonym for `?band=flagged`. `unreadable` comes back as its own count and is never folded into `flagged` — a wall of unreadable is US broken (a dead model id, a 404 on the frame, a timeout), not a wall of bad frames. Every query writes an admin_access_log row.\n",
        "tags": [
          "admin"
        ],
        "parameters": [
          {
            "name": "band",
            "in": "query",
            "required": false,
            "description": "flagged · clean · unreadable · unjudged. Omit for every judged frame.",
            "schema": {
              "type": "string",
              "enum": [
                "flagged",
                "clean",
                "unreadable",
                "unjudged"
              ]
            }
          },
          {
            "name": "tool",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "house",
            "in": "query",
            "required": false,
            "description": "An organization id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "defect",
            "in": "query",
            "required": false,
            "description": "One of the twelve defect codes.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "Days back. 0 or absent is all time.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 60,
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Judged frames, the three counts, the judge's spend, and the filter lists"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/judge/verdicts/{jobId}": {
      "get": {
        "summary": "One judged frame, whole (admin only)",
        "description": "What was asked for (her own params, scalars only — `inputs` is dropped because it holds base64 data URIs), what was fed in, what came out, what the judge said including its own sentence, and what the director did. `feedback` is all-null and `recipe` is the baseline arm until generation_feedback and prompt_recipes exist. Opening a frame writes an admin_access_log row: this read puts another house's unreleased imagery on a screen.\n",
        "tags": [
          "admin"
        ],
        "parameters": [
          {
            "name": "jobId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The frame, its inputs, the verdict and the response"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such job"
          }
        }
      }
    },
    "/api/toolkit/admin/judge/rollup": {
      "get": {
        "summary": "The judge's verdicts, counted (admin only)",
        "description": "L1, the measurement layer — the same rows the quality browser lists, aggregated. Defect code × tool × rubric version × judge model, over time, for one house (`?house=`) or across the whole platform (omit it). No new table: this is a GROUP BY, and nothing is written. `rubricVersion` and `judgeModel` are grouping keys on every row including the trend, because two scores from different rubrics are not comparable and a line drawn through a rubric change is a lie — `rubrics` names the boundaries so the caller can break the series. Every rate ships with its denominator (`frames` of `of`). `unreadable` is excluded from every defect and flag rate and reported on its own: it is US failing to look, not a clean frame. `outcomes` joins generation_feedback so `flagged` can be checked against what the director then did, and `caveats.deleted` is `NOT_YET_MEASURABLE` because nothing in this API deletes an asset. Every query writes an admin_access_log row. Aggregates only — no imagery, no prompt, no email.\n",
        "tags": [
          "admin"
        ],
        "parameters": [
          {
            "name": "house",
            "in": "query",
            "required": false,
            "description": "An organization id. Omit for the whole platform.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tool",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "rubric",
            "in": "query",
            "required": false,
            "description": "Pin one rubric version. Omit to see every version, grouped.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "model",
            "in": "query",
            "required": false,
            "description": "Pin one judge model id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "bucket",
            "in": "query",
            "required": false,
            "description": "Trend granularity.",
            "schema": {
              "type": "string",
              "enum": [
                "day",
                "week",
                "month"
              ],
              "default": "day"
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "Days back. 0 or absent is all time.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum cells per block.",
            "schema": {
              "type": "integer",
              "default": 200,
              "maximum": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Totals, defect cells with denominators, per tool, per house, the trend, the rubric boundaries and the outcome join"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/analyst": {
      "get": {
        "summary": "The platform analyst's latest 12-hour digest (admin only)",
        "description": "The latest thing the platform analyst wrote, plus the headlines behind it. The analyst itself runs in services/ai-worker on a 12-hour cron (`0 */12 * * *`, src/analyst.ts); this route only reads, and there is deliberately no POST — a digest that can be triggered from a browser moves the baseline the next delta is measured against. It is a DELTA, not a dashboard: `/api/toolkit/admin/judge/rollup` already prints the totals, and the product here is the sentence the rollup cannot say. `thin: true` means the analyst REFUSED to narrate — below the floor no model is called at all, so `model` is null, `actions` is empty and the headline states the count and the floor. `movements` are computed in TypeScript from two counted snapshots before any model exists in the story, each carrying both denominators, and `rubricVersion` plus `judgeModel` are part of every cell key so a rubric bump breaks the comparison instead of being reported as a quality change. Every `actions` entry is a PROPOSAL — nothing in FLAM applies one. Every read writes an admin_access_log row; counts and one sentence, no imagery, no prompt, no email, no house name.\n",
        "tags": [
          "admin"
        ],
        "parameters": [
          {
            "name": "history",
            "in": "query",
            "required": false,
            "description": "How many previous headlines to return.",
            "schema": {
              "type": "integer",
              "default": 10,
              "maximum": 60
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The latest digest with its movements and proposed actions, the previous headlines, and the caveats that say what a thin digest means"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/analyst/run": {
      "post": {
        "summary": "Run the analyst now (admin only)",
        "description": "D12: *\"it is not a schedule.\"* The 12-hour cron is gone from services/ai-worker/wrangler.toml — the analyst runs when a person presses this, and never otherwise. An analysis nobody asked for is a row nobody reads and a baseline the next delta is silently measured against.\nA SECOND PRESS INSIDE THE WINDOW COSTS NOTHING. The claim SQL in src/analyst.ts already refuses to start a second run inside `window_hours`, so this returns the digest that already exists rather than spending the model call again. That is what makes the button safe to put in front of a person.\nThe floor still holds: below the readable-verdict minimum the digest is `thin`, `actions` is empty and no model is called at all. Writes an admin_access_log row.\n",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "The digest — freshly written, or the existing one when a run is already inside the window"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/analyst/cadence": {
      "patch": {
        "summary": "Set the analyst's window (admin only)",
        "description": "D12's actual complaint, in one route — *\"cannot change its window without a deploy.\"* The window was a `WINDOW_HOURS` constant marked `/** Not negotiable */`; it is now a row in `analyst_config`, and the next digest carries the `window_hours` it was written under so an old digest still says what it was measuring.\nPersists. Writes an admin_access_log row.\n",
        "tags": [
          "admin"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "hours"
                ],
                "properties": {
                  "hours": {
                    "type": "integer",
                    "description": "The lookback window in hours. 168 is a week — the founder's own override of the 12-hour default."
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "Off means the run verb refuses. The analyst has no other trigger, so this is the whole switch."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The cadence as stored"
          },
          "400": {
            "description": "A window outside the accepted range"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/payments": {
      "get": {
        "summary": "Real transactions — subscriptions, top-ups, offerings, comps",
        "description": "Money reaches FLAM through three doors and the Money book only showed one. Comps are included and read as zero: hiding them makes the token float unexplainable, pricing them at anything else makes them look like revenue. `amountUsd` is currency and `tokens` is what that payment minted — separate fields, no total, because a top-up is money that bought tokens, not money and tokens.\n",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "Transactions and standing subscriptions"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/account": {
      "get": {
        "summary": "The director's own account, in one read",
        "description": "Profile, current plan, token balance WITH where those tokens went by workflow, and the book of money that actually landed. Deliberately does NOT re-serve what already has a door: name/avatar, password, social linking and sessions are Better Auth (/api/auth/*); plan and pack prices are /api/toolkit/ai/billing/catalog; upgrades and top-ups are the checkout routes; export/erase are /me/export and /me/delete.\n",
        "tags": [
          "account"
        ],
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "required": false,
            "description": "Spend window — 7, 30 or 90. Anything else falls back to 30.",
            "schema": {
              "type": "integer",
              "enum": [
                7,
                30,
                90
              ],
              "default": 30
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The account",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/account/plan/cancel": {
      "post": {
        "summary": "Stop the renewal (cancel at period end)",
        "description": "Cancel means \"do not renew\", never \"take it back now\" — every token already granted stays, and the plan runs to the end of the period it is paid for. The provider is told first and our row is mirrored only on its yes, so a failed vendor call can never leave a director believing they cancelled while the card is still charged. Grants nothing; the webhook remains the only thing that grants.\n",
        "tags": [
          "account"
        ],
        "responses": {
          "200": {
            "description": "The plan will not renew",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "canceled"
                      ]
                    },
                    "endsAt": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "NO_ACTIVE_PLAN — nothing is renewing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "CANCEL_FAILED — the provider refused; nothing was changed here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "BILLING_NOT_CONFIGURED — no provider credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/account/billing-email": {
      "patch": {
        "summary": "Set the house's invoice email",
        "description": "Owner-only. Keeps the house-scoped invoice recipient separate from sign-in identity and updates the payment provider when a customer already exists.",
        "tags": [
          "account"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Invoice email saved"
          },
          "400": {
            "description": "Invalid email"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "502": {
            "description": "Payment provider update failed"
          }
        }
      }
    },
    "/api/account/memory": {
      "get": {
        "summary": "Read the current house and actor-scoped memory book",
        "tags": [
          "account",
          "memory"
        ],
        "description": "Returns shared memories for the active organization plus only the current actor's personal memories inside that organization. Corrected and retired history remains visible for provenance but is never injected into chat.\n",
        "responses": {
          "200": {
            "description": "The caller's scoped living-memory record",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "memories"
                  ],
                  "properties": {
                    "memories": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AccountMemory"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "summary": "Add or append a correction to a scoped memory",
        "tags": [
          "account",
          "memory"
        ],
        "description": "A correction creates a successor and preserves its predecessor. The canonical Neon write and Core projection event are committed in one transaction.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "statement",
                  "scope",
                  "memoryClass"
                ],
                "properties": {
                  "statement": {
                    "type": "string",
                    "minLength": 8,
                    "maxLength": 2000
                  },
                  "scope": {
                    "type": "string",
                    "enum": [
                      "house",
                      "person"
                    ]
                  },
                  "memoryClass": {
                    "type": "string",
                    "enum": [
                      "fact",
                      "preference",
                      "lesson",
                      "playbook",
                      "outcome_pattern",
                      "image_reading"
                    ]
                  },
                  "replacesMemoryId": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A correction was appended"
          },
          "201": {
            "description": "A new memory was appended"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "The predecessor is not active in the caller's scope"
          }
        }
      }
    },
    "/api/account/memory/{id}": {
      "delete": {
        "summary": "Retire a scoped active memory",
        "tags": [
          "account",
          "memory"
        ],
        "description": "Stops future retrieval without deleting evidence or history. The current organization is always the outer boundary and personal memories must belong to the current actor.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The memory was retired"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No active memory is available in this scope"
          }
        }
      }
    },
    "/api/toolkit/lookbook/looks/{id}/develop-lock": {
      "post": {
        "summary": "Develop the LOOK's lock frame",
        "description": "Composes the lock frame server-side (the prompt never enters the jobs DB) and runs it through the spine as ONE job. Draft looks only. Reserves the whole cost — tier plus any restoration pass the destination forces — before anything is enqueued. The finished `resultUrl` is the frame as DEVELOPED: the tier's pixels at the destination's shape. It is not resampled to `output.width` × `output.height` — cut it yourself, which is a pure downscale for every place except the two A-series print sizes.\n",
        "tags": [
          "lookbook"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DevelopRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Job queued (or the existing job on an idempotent replay)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY | UNKNOWN_DESTINATION | BAD_DIMENSIONS | OUTPUT_TOO_LARGE | QUALITY_TOO_LOW_FOR_OUTPUT — the body carries a plain-language `detail` naming the pixels (\"1K develops at 1,024 px — an Instagram post needs 1,080.\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OutputError"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "INSUFFICIENT_TOKENS — body includes need + spendable"
          },
          "404": {
            "description": "NOT_FOUND"
          },
          "409": {
            "description": "DEVELOP_IN_FLIGHT | LOOK_LOCKED"
          }
        }
      }
    },
    "/api/toolkit/lookbook/looks/{id}/variations": {
      "post": {
        "summary": "Fan the LOOK out into its variations",
        "description": "One job per chosen variation, with smart refs per the library (a medium shot drops the shoes; a back shot keeps the model). The look must be locked. All-or-nothing reserve across the whole batch: the first shortfall releases every hold already taken. An `output` destination applies to EVERY frame, so its cost is reserved per frame up front — and every `resultUrl` comes back as developed (the tier's pixels at that destination's shape), not resampled to its exact width × height.\n\nA key may name a POSTURE instead — `posture:<id>`, from GET /api/toolkit/postures (spec 015). A posture carries its own geometry, its own framing derived from the entry's crop (so a detail entry is never told head-to-toe) and its own reference photograph, which rides as a pose controller reference. The photograph yields its slot only after every rendition of the house mark has yielded, and never before one of her pieces.\n",
        "tags": [
          "lookbook"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/DevelopRequest"
                  },
                  {
                    "type": "object",
                    "required": [
                      "keys"
                    ],
                    "properties": {
                      "keys": {
                        "type": "array",
                        "minItems": 1,
                        "maxItems": 12,
                        "items": {
                          "type": "string"
                        },
                        "description": "Variation keys from the library, or `posture:<id>` for an entry in the direction language"
                      },
                      "controls": {
                        "type": "object",
                        "description": "Optional, per key: axis id → the stop id she chose, using the ids GET /api/toolkit/postures returns. Absent means the entry's own defaults, which are already a complete direction. Only the ids travel — the physical phrase each stop compiles to is house IP and is attached server-side.",
                        "additionalProperties": {
                          "type": "object",
                          "additionalProperties": {
                            "oneOf": [
                              {
                                "type": "string"
                              },
                              {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                }
                              }
                            ]
                          }
                        }
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "One queued job per requested key",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "key": {
                            "type": "string"
                          },
                          "jobId": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY | KEYS_REQUIRED | TOO_MANY_FRAMES | UNKNOWN_VARIATION | UNKNOWN_DESTINATION | BAD_DIMENSIONS | OUTPUT_TOO_LARGE | QUALITY_TOO_LOW_FOR_OUTPUT\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OutputError"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "INSUFFICIENT_TOKENS — body includes need + spendable"
          },
          "404": {
            "description": "NOT_FOUND"
          },
          "409": {
            "description": "DEVELOP_IN_FLIGHT | LOCK_REQUIRED"
          }
        }
      }
    },
    "/api/toolkit/ai/extract-character": {
      "post": {
        "summary": "Read a person out of a reference image (or a four-view collage)",
        "description": "Returns the five character-studio fields — age, skin, eyes, hair, extras — so a director can review WHO the house just cast before any frame is developed. A FLAM house model is a collage of several views of one person; the engine reads across all the panels and answers once. No-face guard: a landscape, an object or a garment shot returns faceDetected=false with every field empty. It never invents a person, and `extras` stays empty unless a real identifying mark is visible. Flat 1 token, refunded on any failure.\n",
        "tags": [
          "ai"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "image"
                ],
                "properties": {
                  "image": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "imageUrl"
                ],
                "properties": {
                  "imageUrl": {
                    "type": "string",
                    "description": "URL or data URI"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The five fields",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "faceDetected": {
                      "type": "boolean"
                    },
                    "fields": {
                      "type": "object",
                      "properties": {
                        "age": {
                          "type": "string"
                        },
                        "skin": {
                          "type": "string"
                        },
                        "eyes": {
                          "type": "string"
                        },
                        "hair": {
                          "type": "string"
                        },
                        "extras": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "IMAGE_REQUIRED / EMPTY_IMAGE / BAD_FORM"
          },
          "401": {
            "description": "Unauthorized"
          },
          "402": {
            "description": "INSUFFICIENT_TOKENS / AI_NEEDS_BILLING"
          },
          "403": {
            "description": "A viewer may not spend the house's tokens"
          },
          "413": {
            "description": "IMAGE_TOO_LARGE"
          },
          "502": {
            "description": "AI_BAD_OUTPUT / AI_FAILED"
          },
          "503": {
            "description": "AI_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/ai/extract-build": {
      "post": {
        "summary": "Read a body out of a full-length photograph, in casting vocabulary",
        "description": "The dossier's second analyst pass. extract-character reads the FACE off a director's collage and is instructed to ignore the body; this reads the BODY off one full-length photograph and returns the five words a casting call sheet carries — height, build, presence, posture, hands — so a full-length reference sheet can be rendered from her proportions rather than the image model's default ones. Every field is hers to correct before a sheet is developed.\nNEVER INVENTS. A field it cannot see comes back empty, height is a casting word (\"tall\", \"petite\") and never a measurement a photograph cannot take, and the no-figure guard returns figureDetected=false with every field empty for a landscape, an object, a flat-lay garment or a head-and-shoulders portrait — a face crop is not a full-length photo and there is no proportion in one. Flat 1 token, refunded on any failure.\n",
        "tags": [
          "ai"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "image"
                ],
                "properties": {
                  "image": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "imageUrl"
                ],
                "properties": {
                  "imageUrl": {
                    "type": "string",
                    "description": "URL or data URI"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The five casting fields",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "figureDetected": {
                      "type": "boolean"
                    },
                    "fields": {
                      "type": "object",
                      "properties": {
                        "height": {
                          "type": "string"
                        },
                        "build": {
                          "type": "string"
                        },
                        "presence": {
                          "type": "string"
                        },
                        "posture": {
                          "type": "string"
                        },
                        "hands": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "IMAGE_REQUIRED / EMPTY_IMAGE / BAD_FORM"
          },
          "401": {
            "description": "Unauthorized"
          },
          "402": {
            "description": "INSUFFICIENT_TOKENS / AI_NEEDS_BILLING"
          },
          "403": {
            "description": "A viewer may not spend the house's tokens"
          },
          "413": {
            "description": "IMAGE_TOO_LARGE"
          },
          "502": {
            "description": "AI_BAD_OUTPUT / AI_FAILED"
          },
          "503": {
            "description": "AI_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/ai/detect-items": {
      "post": {
        "summary": "Find every garment and accessory in a frame",
        "description": "Step 1 of 2 (detect then segment). Exhaustively detects each distinct garment, accessory and editable hair/face feature so a UI can draw tap targets. A rail of hanging clothes, a flat lay or a shop shelf is a normal input. Boxes are [ymin, xmin, ymax, xmax] normalized 0-1000 and are APPROXIMATE tap targets, not cutouts.\nORIENTATION: boxes are in the RAW stored pixel matrix; this lane does not apply a JPEG's EXIF Orientation flag and fal (behind /segment) does. Pass the returned `label` to /segment, or bake orientation before both calls. Flat 1 token, refunded on any failure.\n",
        "tags": [
          "ai"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "image"
                ],
                "properties": {
                  "image": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "imageUrl"
                ],
                "properties": {
                  "imageUrl": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Detected items",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "label": {
                            "type": "string"
                          },
                          "box": {
                            "type": "array",
                            "items": {
                              "type": "integer",
                              "minimum": 0,
                              "maximum": 1000
                            },
                            "minItems": 4,
                            "maxItems": 4
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "IMAGE_REQUIRED / EMPTY_IMAGE / BAD_FORM"
          },
          "401": {
            "description": "Unauthorized"
          },
          "402": {
            "description": "INSUFFICIENT_TOKENS / AI_NEEDS_BILLING"
          },
          "403": {
            "description": "A viewer may not spend the house's tokens"
          },
          "413": {
            "description": "IMAGE_TOO_LARGE"
          },
          "502": {
            "description": "AI_BAD_OUTPUT / AI_FAILED"
          },
          "503": {
            "description": "AI_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/ai/segment": {
      "post": {
        "summary": "A clean cutout of the ONE item that was tapped",
        "description": "Step 2 of 2 (detect then segment). SAM 3 on fal, prompted by the detect-items `label` (orientation-immune — prefer it), a `box` [x0,y0,x1,y1] in PIXELS of the EXIF-APPLIED frame, or a `point` [x,y]. At least one target is required. What comes back is an RGBA CUTOUT, not a white-on-black binary mask. It is an internal compositing input and is never captured as a library asset. Flat 1 token, refunded on failure.\n",
        "tags": [
          "ai"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "imageUrl"
                ],
                "properties": {
                  "imageUrl": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string"
                  },
                  "box": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    },
                    "minItems": 4,
                    "maxItems": 4
                  },
                  "point": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    },
                    "minItems": 2,
                    "maxItems": 2
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The cutout",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string"
                    },
                    "bbox": {
                      "type": "array",
                      "nullable": true,
                      "items": {
                        "type": "number"
                      },
                      "description": "[x_min, y_min, x_max, y_max] NORMALIZED 0-1. Note the request's `box` is in PIXELS — same axis order, different units. Multiply by the frame's width/height to compare.\n"
                    },
                    "spent": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_JSON / IMAGE_REQUIRED / TARGET_REQUIRED"
          },
          "401": {
            "description": "Unauthorized"
          },
          "402": {
            "description": "INSUFFICIENT_TOKENS"
          },
          "403": {
            "description": "A viewer may not spend the house's tokens"
          },
          "502": {
            "description": "SEGMENT_FAILED"
          },
          "503": {
            "description": "FAL_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/brand": {
      "get": {
        "summary": "The house's extracted visual DNA",
        "description": "Returns the profile plus `clause` — the EXACT text the generators will inject, built by the same @flam/ai builder the worker calls, so this review surface can never disagree with the renders. A house that has never run a read gets empty defaults, never a 404.\n",
        "tags": [
          "brand"
        ],
        "responses": {
          "200": {
            "description": "The profile",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "profile": {
                      "$ref": "#/components/schemas/BrandProfile"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      },
      "patch": {
        "summary": "Correct the extracted DNA, or switch the injection off",
        "description": "The house gets the last word. Every field is optional and merges onto what is stored; the merged result is then clamped exactly as an extraction would be, so a partial edit cannot smuggle past a bound the extractor enforces. `injectEnabled: false` is the kill switch — the profile stays, the clause goes empty, every generator renders unbranded. dna_runs is never touched here, so editing is free and unlimited.\n",
        "tags": [
          "brand"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "visionEn": {
                    "type": "string",
                    "maxLength": 600
                  },
                  "vibe": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "palette": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "pattern": "^#[0-9a-fA-F]{6}$"
                    }
                  },
                  "signatureKeywords": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "category": {
                          "type": "string"
                        },
                        "phrase": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "injectEnabled": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated profile",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "profile": {
                      "$ref": "#/components/schemas/BrandProfile"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_JSON"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "A viewer may not edit the clause every render pays for"
          }
        }
      }
    },
    "/api/toolkit/brand/marks": {
      "get": {
        "summary": "The house's logo renditions, and which set rides a frame",
        "description": "Brand DNA is prose; this is the pixels. A rendition is the house's mark as it really appears on a material — embossed on calf, woven into a label, foil on a swing tag — and one or two of them ride a lookbook frame as reference images.\n\nWHY IT MATTERS: the judge has carried `mark-wrong` (weight 20) since the rubric was written, with a predicate ending \"or is not the mark in the reference\". There was no mark in the reference. A comparison against nothing is not a lenient check, it is a guaranteed pass — so until a house registers a rendition, that code is now withdrawn rather than asked unanswerably (rubric v6).\n\nPass `collectionId` to see a drop's own set beside the house's. The override answers WHOLE, never merged: a capsule that foil-stamps where the main line embosses must not be shown both finishes. `riding` says which of the two a frame developed there will actually carry.\n",
        "tags": [
          "brand"
        ],
        "parameters": [
          {
            "name": "collectionId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "A collection (asset folder) id. Unknown or another house's is 404."
          }
        ],
        "responses": {
          "200": {
            "description": "The house's renditions, the collection's override, and which rides",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "house": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BrandMark"
                      }
                    },
                    "collection": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BrandMark"
                      }
                    },
                    "riding": {
                      "type": "string",
                      "enum": [
                        "house",
                        "collection"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "No such collection in this house"
          }
        }
      },
      "post": {
        "summary": "Register an uploaded image as a rendition of the mark",
        "description": "THERE IS NO UPLOAD HERE. The bytes go through `POST /api/toolkit/assets/upload` with `kind=mark` like every other image, and this route registers the resulting asset id. A second bytes-door would be the third implementation of \"store bytes then insert an assets row\" in this codebase.\n\n`material` is REQUIRED and it is not decoration — it rides the generation prompt verbatim (\"embossed on black calf\"), and it is the question that catches a wrong upload: a house about to register a screenshot of its website header discovers, while trying to name the material, that it has not got what we asked for.\n\nOmit `collectionId` for the house's own set; send one to create that drop's override. Nothing is charged.\n",
        "tags": [
          "brand"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "assetId",
                  "material"
                ],
                "properties": {
                  "assetId": {
                    "type": "string",
                    "description": "An asset owned by THIS house. Re-read under the actor's org."
                  },
                  "material": {
                    "type": "string",
                    "maxLength": 120,
                    "description": "What it is on — \"embossed on black calf\". Rides the prompt verbatim."
                  },
                  "placement": {
                    "type": "string",
                    "maxLength": 120,
                    "description": "Where on the piece — \"left chest\". Rides the sentence; not yet verified by the judge."
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 280
                  },
                  "sortOrder": {
                    "type": "integer",
                    "description": "Which renditions ride. At most two travel with a frame, lowest first."
                  },
                  "collectionId": {
                    "type": "string",
                    "description": "Omit for the house's set. Set to override that collection whole."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Registered",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "mark": {
                      "$ref": "#/components/schemas/BrandMark"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ASSET_REQUIRED",
            "BAD_MATERIAL": null,
            "BAD_TEXT": null,
            "BAD_SORT_ORDER": null,
            "BAD_BODY": null
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "role_cannot_edit_marks"
          },
          "404": {
            "description": "No such asset or collection in this house"
          },
          "409": {
            "description": "ALREADY_REGISTERED at this level",
            "or TOO_MANY_MARKS": null
          }
        }
      }
    },
    "/api/toolkit/brand/marks/{id}": {
      "patch": {
        "summary": "Correct a rendition's material, placement, note or riding order",
        "description": "Partial — an absent key keeps what is stored. `material` cannot be cleared; `placement` and `note` can be, by sending an empty string or null. Nothing is charged.\n",
        "tags": [
          "brand"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "material": {
                    "type": "string",
                    "maxLength": 120
                  },
                  "placement": {
                    "type": "string",
                    "maxLength": 120,
                    "nullable": true
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 280,
                    "nullable": true
                  },
                  "sortOrder": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Corrected",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "mark": {
                      "$ref": "#/components/schemas/BrandMark"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_MATERIAL",
            "BAD_TEXT": null,
            "BAD_SORT_ORDER": null,
            "NOTHING_TO_PATCH": null
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "role_cannot_edit_marks"
          },
          "404": {
            "description": "No such rendition in this house"
          }
        }
      },
      "delete": {
        "summary": "Take a rendition out of the rotation",
        "description": "THE IMAGE SURVIVES. The bytes are hers, in her library, and un-registering a rendition is not a request to delete a picture she uploaded — deleting the asset itself is the library's own door.\n",
        "tags": [
          "brand"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Out of the rotation; the asset is untouched",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "role_cannot_edit_marks"
          },
          "404": {
            "description": "No such rendition in this house"
          }
        }
      }
    },
    "/api/toolkit/brand/dna": {
      "post": {
        "summary": "Read the house's visual DNA off its own imagery",
        "description": "The cross-tool magic. Upload 1-10 of the house's own previous frames and one multi-image vision call finds the through-line: vibe, palette, signature cues and a visionEn paragraph that is injected into EVERY packshot, character and lookbook frame the house develops afterwards. The read describes the house faithfully — the 8-category fashion vocabulary is a descriptive lens, never a style to impose.\nBILLING is server-decided from brand_profiles.dna_runs and never from the client: the FIRST read per house is free (charged 0), every re-read costs 2 tokens. The counter is bumped on every successful read whether or not the house keeps the answer. A failed read charges nothing and does not bump it.\n",
        "tags": [
          "brand"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "image"
                ],
                "properties": {
                  "image": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "maxItems": 10
                  },
                  "vibe": {
                    "type": "string",
                    "description": "JSON string[] of vibe hints"
                  },
                  "products": {
                    "type": "string",
                    "description": "what the house sells"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The extracted (and stored) DNA",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "profile": {
                      "$ref": "#/components/schemas/BrandProfile"
                    },
                    "charged": {
                      "type": "integer",
                      "description": "0 on the first read, 2 after"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "NO_IMAGES / BAD_FORM"
          },
          "401": {
            "description": "Unauthorized"
          },
          "402": {
            "description": "INSUFFICIENT_TOKENS / AI_NEEDS_BILLING"
          },
          "403": {
            "description": "A viewer may not spend the house's tokens"
          },
          "413": {
            "description": "IMAGE_TOO_LARGE / SET_TOO_LARGE (32MB across the set)"
          },
          "502": {
            "description": "AI_BAD_OUTPUT / AI_FAILED"
          },
          "503": {
            "description": "AI_NOT_CONFIGURED"
          }
        }
      }
    },
    "/health": {
      "get": {
        "summary": "Liveness",
        "description": "Unauthenticated. The readiness probe Tilt polls.",
        "tags": [
          "meta"
        ],
        "responses": {
          "200": {
            "description": "The process is up",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "ok"
                      ]
                    },
                    "service": {
                      "type": "string",
                      "enum": [
                        "api"
                      ]
                    },
                    "uptime": {
                      "type": "number",
                      "description": "Seconds since the process started"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/docs": {
      "get": {
        "summary": "Swagger UI rendered over /openapi.json",
        "tags": [
          "meta"
        ],
        "responses": {
          "200": {
            "description": "An HTML page"
          }
        }
      }
    },
    "/me": {
      "get": {
        "summary": "The signed-in user",
        "description": "The Better Auth `user` row for the session cookie. Identity only — no house scope.",
        "tags": [
          "me"
        ],
        "responses": {
          "200": {
            "description": "The Better Auth user",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/me/export": {
      "get": {
        "summary": "GDPR access — export the caller's own user row",
        "description": "Sent as an attachment (Content-Disposition: attachment; filename=\"my-data.json\"). STARTER SCOPE — the Better Auth `user` row only; widen it before relying on this for a real DSAR.\n",
        "tags": [
          "me"
        ],
        "responses": {
          "200": {
            "description": "The export document",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "exportedAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "user": {
                      "type": "object",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/me/delete": {
      "post": {
        "summary": "GDPR erasure — delete the caller's account",
        "description": "ONE transaction. Every organization the caller is the SOLE member of is deleted FIRST (that CASCADEs its imagery, wallet and ledger), then the user row. A SHARED house survives its departing seat — the house is the brand, not the person. No body.\n",
        "tags": [
          "me"
        ],
        "responses": {
          "200": {
            "description": "Erased",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean"
                    },
                    "userId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/folders": {
      "get": {
        "summary": "Every collection in the house, flat",
        "description": "Oldest first. The client builds the tree from `parentId`.",
        "tags": [
          "assets"
        ],
        "responses": {
          "200": {
            "description": "The folder list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "folders": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Folder"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "summary": "Create a collection",
        "tags": [
          "assets"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "parentId": {
                    "type": "string",
                    "nullable": true,
                    "description": "Nest under an owned folder"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created folder",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "folder": {
                      "$ref": "#/components/schemas/Folder"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / NAME_REQUIRED"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Unknown or foreign parentId"
          }
        }
      }
    },
    "/api/toolkit/folders/{id}": {
      "patch": {
        "summary": "Rename a collection",
        "description": "A no-op rename (same name, or none supplied) still answers 200 with the folder.",
        "tags": [
          "assets"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The folder",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "folder": {
                      "$ref": "#/components/schemas/Folder"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Unknown or foreign id"
          }
        }
      }
    },
    "/api/toolkit/assets/{id}/file": {
      "get": {
        "summary": "Stream one asset's bytes (the contract serving route)",
        "description": "Where every `url` in a list response points. Owner-gated, and a FOREIGN id answers 404, never 403 — this route never confirms that someone else's asset exists. (The legacy /api/toolkit/assets/{id} answers 403 for a foreign id; it is kept only for old links.)\n",
        "tags": [
          "assets"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The image bytes (Cache-Control private, max-age=3600; ETag from R2)"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Unknown, deleted, foreign, or missing in R2"
          }
        }
      }
    },
    "/api/toolkit/packs/{id}/file": {
      "get": {
        "summary": "Stream a batch download's archive",
        "description": "The ONE download URL a pack answers with, on both tiers — see POST /api/toolkit/assets/pack, which is where an id comes from.\n\nUnder the instant threshold nothing was ever stored: the zip is assembled from R2 inside this request and streamed, chunked, with no Content-Length. Above it, this hands back the archive the background job built, and honours `Range` so a dropped 3 GB download resumes instead of starting over.\n\nA foreign or unknown id answers 404 and never 403 — the same precedent as the single-asset byte route. `409` means the pack is still building (the bar will say when — nothing here is meant to be polled) or it failed. `410` means the seven-day link has expired.\n",
        "tags": [
          "assets"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Range",
            "in": "header",
            "required": false,
            "description": "bytes=N- / bytes=N-M. Honoured on the background tier only.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "application/zip, Content-Disposition: attachment"
          },
          "206": {
            "description": "A byte range of the archive"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Unknown, foreign, or the object is gone from R2"
          },
          "409": {
            "description": "Still packing, or the pack failed"
          },
          "410": {
            "description": "The link has expired"
          }
        }
      }
    },
    "/api/toolkit/assets/group": {
      "post": {
        "summary": "Stamp a fresh shared group onto a set of pieces",
        "description": "Writes `group:<uuid>` onto every named asset, REPLACING any prior group (re-grouping moves a piece). All-or-nothing: every id must be one of the caller's live assets or nothing is written.\n",
        "tags": [
          "assets"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "assetIds"
                ],
                "properties": {
                  "assetIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Unique, non-empty. Duplicates are rejected."
                  },
                  "name": {
                    "type": "string",
                    "description": "Accepted and currently unused"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new group id",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "groupId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / BAD_ASSET_IDS"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "One of the ids is unknown, deleted or foreign — nothing was written"
          }
        }
      }
    },
    "/api/toolkit/backgrounds": {
      "get": {
        "summary": "The whole library a look can stand on",
        "description": "Two sources, one call — the house's curated `grounds` (static, shipped with the code) and this director's own `scenes` (assets tagged `kind:background`, uploaded through /api/toolkit/assets/upload). Only the PUBLIC projection of a ground crosses the wire; the prompt clause it contributes stays server-side.\n",
        "tags": [
          "assets"
        ],
        "responses": {
          "200": {
            "description": "Grounds + the house's own sets",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "grounds": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "kind": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "note": {
                            "type": "string"
                          },
                          "tone": {
                            "type": "string"
                          },
                          "previewUrl": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "scenes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string"
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/search": {
      "get": {
        "summary": "One field over the whole house",
        "description": "Four lanes in ONE statement — piece (assets, matched on filename AND the `sku:` tag), collection (folders), look, workflow (lookbook templates). Under 2 characters is not an error: the same query runs wide open and the answer arrives as a flat `recent` list with `groups: []`.\n",
        "tags": [
          "search"
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 64
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Grouped hits, or `recent` for a short query",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "query": {
                      "type": "string"
                    },
                    "groups": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "kind": {
                            "type": "string",
                            "enum": [
                              "piece",
                              "collection",
                              "look",
                              "workflow"
                            ]
                          },
                          "items": {
                            "type": "array",
                            "items": {
                              "$ref": "#/components/schemas/SearchResult"
                            }
                          },
                          "total": {
                            "type": "integer",
                            "description": "Hits BEFORE the per-lane cap"
                          }
                        }
                      }
                    },
                    "recent": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SearchResult"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/develops": {
      "get": {
        "summary": "The house's develop RUNS, newest first",
        "description": "A run is DERIVED, not stored — params.runId, else the lookbook batch (lookId + stage), else same tool within a time gap. `state=active` keeps only runs with a frame still queued or rendering; the counts stay WHOLE (a 2-of-5 run still reports 5).\n",
        "tags": [
          "jobs"
        ],
        "parameters": [
          {
            "name": "state",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "all"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The runs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "runs": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DevelopRun"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_STATE / BAD_LIMIT"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/judge/verdicts": {
      "get": {
        "summary": "The judge's verdicts for a batch of frames",
        "description": "BATCH, because the Darkroom renders a GRID and must not fire N requests. Up to 50 comma-separated job ids; extras are ignored rather than refused. Scoped to the caller's organisation — a foreign job id has no verdict here. A frame with no verdict yet is ABSENT from the array, never `verdict: null`.\n`label` is OUR predicate for the code, resolved server-side (\"not your model\"), because the browser has no vocabulary table. The judge's own `note` is NOT on this wire (CANON C-2: no model-authored sentence reaches a director), and neither is cost, model id or latency.\n`answer` carries the caller's OWN ruling back when one exists, so a client renders an answered frame as answered instead of asking the same question after every reload.\n",
        "tags": [
          "judge"
        ],
        "parameters": [
          {
            "name": "jobIds",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated generation_jobs ids, max 50."
          }
        ],
        "responses": {
          "200": {
            "description": "The verdicts that exist, in no guaranteed order",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "verdicts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Verdict"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "JOB_IDS_REQUIRED"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/judge/verdicts/{jobId}": {
      "get": {
        "summary": "One frame's verdict",
        "description": "Same scoping as the batch. 404 both when the verdict does not exist and when the job belongs to another house — a foreign frame must not be distinguishable from an unjudged one.\n",
        "tags": [
          "judge"
        ],
        "parameters": [
          {
            "name": "jobId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The verdict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Verdict"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No verdict for this frame, or not this house's frame"
          }
        }
      }
    },
    "/api/toolkit/judge/redraw": {
      "post": {
        "summary": "Draw it again, with the fix carried",
        "description": "The one verb that answers a verdict. Re-submits the SAME develop as a new job through the normal priced path, with the verdict's findings preloaded as a corrective clause, and stamps `retry_of_job_id` so the attempt strip has a chain instead of two unrelated frames.\nPAID AT THE NORMAL PRICE — no free-retry cap, no discount, no separate price-book row. A correction changes the words, never the megapixels, so the reserve is the source frame's own cost including any pass its destination forces.\nThe clause is built from OUR labels (\"the left hand — drawn wrong\"), never from the judge's `note`: no model-authored prose reaches a prompt any more than it reaches her screen. It lands on `params.correction` always, and after her own words in `params.prompt` for the tools that read one.\nTWO VERDICTS ARE REFUSED with 409 REDRAW_REFUSED and both are OURS: `retryFutile` (our compiler dropped her note, so a fresh draw reproduces it) and `unreadable` (our judge could not read the frame — her develop may be perfect). Charging for our own failure is the one thing a redraw must not do.\n",
        "tags": [
          "judge"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "jobId"
                ],
                "properties": {
                  "jobId": {
                    "type": "string",
                    "description": "The flagged frame, this house's own. Foreign ids are 404, never 403."
                  },
                  "idempotencyKey": {
                    "type": "string",
                    "description": "Send one: without it a double-click reserves twice and queues two paid draws."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "A new attempt is queued and charged",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "queued"
                      ]
                    },
                    "tokens": {
                      "type": "integer",
                      "description": "Held at the normal price."
                    },
                    "retryOfJobId": {
                      "type": "string",
                      "description": "The frame this is an attempt at."
                    },
                    "correction": {
                      "type": "string",
                      "description": "What the next draw was asked to fix. Empty when the frame was clean."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "JOB_ID_REQUIRED"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "INSUFFICIENT_TOKENS { need, spendable }"
          },
          "403": {
            "description": "A viewer may not spend the house's tokens"
          },
          "404": {
            "description": "No such frame, or not this house's frame"
          },
          "409": {
            "description": "NOT_DEVELOPED (no pixels yet), or REDRAW_REFUSED with reason retryFutile | unreadable"
          },
          "422": {
            "description": "UNKNOWN_TOOL / NO_INPUTS — never charged for work that cannot run"
          }
        }
      }
    },
    "/api/toolkit/judge/verdicts/{jobId}/label": {
      "post": {
        "summary": "She tells us the judge was wrong (L2)",
        "description": "The only direct measure of the judge, and the reason this route exists at all: `/api/toolkit/admin/judge/rollup` can say \"the judge flags 34% of packshots\" and cannot say whether it is RIGHT about them. `agrees` is that answer, one boolean per frame.\nONE GESTURE. `agrees` is the whole required body — a rating and a note are there for the director who reaches for them, and a form nobody opens collects nothing. `rating` is an integer 1-5 and a value outside that range is REFUSED (400), never clamped: a 5 stored for a 7 is a number nobody typed, sitting in a rollup.\nTHE ROW IS SHARED WITH THE KEEP SIGNAL and the columns are not. This upserts onto the same `generation_feedback` row an export writes, setting `agrees_with_judge`, `rating` and `note` and nothing else. `outcome` stays first-wins, because a re-export is not a second opinion. The label itself is last-wins — changing your mind about a verdict is a real thing a person does, and the newer answer is the true one.\nA LABEL MAY ARRIVE WITH NO EXPORT BEHIND IT. She opens a flagged frame, says the judge is wrong, closes the tab. That row has a label and a null `outcome`; inventing a `kept` for her would land in the same rollup as the real ones.\nScoped to the caller's organisation from the session and nothing else. No verdict, or another house's frame, is 404 — a foreign frame must not be distinguishable from an unjudged one. A `viewer` seat is 403.\n",
        "tags": [
          "judge"
        ],
        "parameters": [
          {
            "name": "jobId",
            "in": "path",
            "required": true,
            "description": "The judged frame, this house's own.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "agrees"
                ],
                "properties": {
                  "agrees": {
                    "type": "boolean",
                    "description": "true = we flagged it and we were right. false = we were wrong — the false-positive counter. Absent is NOT false; it is 'she never said'."
                  },
                  "rating": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 5,
                    "nullable": true,
                    "description": "Her 1-5 on the frame itself. A different fact from whether the judge was right; folding the two loses the false-positive rate."
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 500,
                    "nullable": true,
                    "description": "Her sentence — the one thing no rollup can synthesise. Trimmed; empty stores null."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The label as stored",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobId": {
                      "type": "string"
                    },
                    "agrees": {
                      "type": "boolean"
                    },
                    "rating": {
                      "type": "integer",
                      "nullable": true
                    },
                    "note": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "AGREES_REQUIRED (no gesture, no row) or RATING_OUT_OF_RANGE (1-5, refused rather than clamped)"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "A viewer seat may read a verdict and may not answer it"
          },
          "404": {
            "description": "No verdict for this frame, or not this house's frame"
          }
        }
      }
    },
    "/api/toolkit/develops/{id}/cancel": {
      "post": {
        "summary": "Stop a whole run — honestly",
        "description": "A frame still QUEUED is cancelled and its hold RELEASED. A frame already RENDERING finishes and still costs: the model call is paid for and cannot be aborted. The response says both numbers out loud rather than implying a refund that is not happening.\n",
        "tags": [
          "jobs"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "description": "A develop-run id (run.<uuid> / look.<uuid>.<stage>)"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "What actually stopped",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "cancelled": {
                      "type": "integer"
                    },
                    "cancelledJobIds": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "stillRendering": {
                      "type": "integer",
                      "description": "Frames that will still land and still cost"
                    },
                    "tokensReleased": {
                      "type": "integer"
                    },
                    "run": {
                      "$ref": "#/components/schemas/DevelopRun"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Unknown, foreign, or aged out of the recent window"
          }
        }
      }
    },
    "/api/toolkit/jobs/{id}/cancel": {
      "post": {
        "summary": "Stop ONE frame",
        "description": "Same honesty as the run-level stop. `cancelled: false` carries a `reason` — ALREADY_RENDERING (paid for, still coming) or ALREADY_SETTLED.\n",
        "tags": [
          "jobs"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The frame's true state",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "cancelled": {
                      "type": "boolean"
                    },
                    "state": {
                      "type": "string",
                      "enum": [
                        "queued",
                        "running",
                        "done",
                        "failed",
                        "canceled"
                      ]
                    },
                    "tokensReleased": {
                      "type": "integer"
                    },
                    "reason": {
                      "type": "string",
                      "nullable": true,
                      "enum": [
                        "ALREADY_RENDERING",
                        "ALREADY_SETTLED"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "NOT_FOUND — unknown or foreign (never a 403 that leaks existence)"
          }
        }
      }
    },
    "/api/toolkit/jobs/{id}/duplicate": {
      "post": {
        "summary": "Run the same frame again as a NEW job",
        "description": "A fresh reserve, and that is correct — it is a new model call. Params are copied VERBATIM including the destination, so the quote covers any pass that destination forces. The source job is never re-billed or mutated.\n",
        "tags": [
          "jobs"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "idempotencyKey": {
                    "type": "string",
                    "description": "Makes one gesture safe to retry. Without it a double-click reserves twice and enqueues two paid model calls.\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "queued"
                      ]
                    },
                    "cost": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "INSUFFICIENT_TOKENS { need, spendable } — nothing created, nothing charged"
          },
          "403": {
            "description": "A viewer may not spend the house's tokens"
          },
          "404": {
            "description": "NOT_FOUND"
          },
          "422": {
            "description": "UNKNOWN_TOOL / NO_INPUTS — never charged for a job that cannot run"
          }
        }
      }
    },
    "/api/toolkit/check-email": {
      "post": {
        "summary": "Route an entered email to the right sign-in step",
        "description": "PUBLIC by design. Reveals only whether an address exists and whether it has a password, for an address the caller already typed — the login page uses it to choose sign-in / Google-only / sign-up. Never 4xx: an unknown or missing email answers 200 with both flags false.\n",
        "tags": [
          "auth"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Existence + method",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "exists": {
                      "type": "boolean"
                    },
                    "hasPassword": {
                      "type": "boolean",
                      "description": "A Better Auth `credential` account with a password"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/invite/accept": {
      "get": {
        "summary": "Look up an invitation token before signing in",
        "description": "Answers the invited email so the sign-up screen can pre-fill it. No session needed.",
        "tags": [
          "invite"
        ],
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The invited address",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "email": {
                      "type": "string",
                      "format": "email"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Token required"
          },
          "404": {
            "description": "Invalid token"
          },
          "410": {
            "description": "Already accepted"
          }
        }
      },
      "post": {
        "summary": "Accept an invitation as the signed-in user",
        "description": "The email on the invite must match the session's, otherwise 403 — an invite is not transferable. Accepting attaches the pre-created email-keyed grants to the caller's HOUSE and repairs a missing personal org (ensurePersonalOrg is idempotent). Replaying an accepted token is a no-op, not an error.\n",
        "tags": [
          "invite"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token"
                ],
                "properties": {
                  "token": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted (or already was)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "alreadyAccepted": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Token required"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "This invite is for a different email"
          },
          "404": {
            "description": "Invalid token"
          }
        }
      }
    },
    "/api/toolkit/ai/depth": {
      "post": {
        "summary": "Depth map for the Lens Blur tool (1 token)",
        "description": "Depth Anything V2 on fal → a grayscale depth PNG url. The map is an INTERNAL input to the client-side depth-of-field; it is NOT captured as a library asset. Reserve → settle on 200 / release on any failure, so a failed call charges nothing.\n",
        "tags": [
          "ai"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "image": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "imageUrl": {
                    "type": "string",
                    "description": "An http(s) URL or a data URI"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The depth map",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "description": "A fal.media URL"
                    },
                    "spent": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "IMAGE_REQUIRED / EMPTY_IMAGE / BAD_FORM"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "INSUFFICIENT_TOKENS { need, spendable }"
          },
          "403": {
            "description": "A viewer may not spend the house's tokens"
          },
          "413": {
            "description": "IMAGE_TOO_LARGE"
          },
          "502": {
            "description": "DEPTH_FAILED"
          },
          "503": {
            "description": "FAL_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/ai/describe-item": {
      "post": {
        "summary": "Classify one piece — auto-name + wardrobe bucket (1 token)",
        "description": "The smart-upload classifier as a standalone lane. The same read runs in-process for /api/toolkit/assets/upload?classify=1. `nameEn` is capped at 40 characters and an unknown `kind` is clamped to \"clothes\" — the model can drift, the contract cannot.\n",
        "tags": [
          "ai"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "image": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "imageUrl": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The label",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "nameEn": {
                      "type": "string",
                      "maxLength": 40
                    },
                    "kind": {
                      "type": "string",
                      "enum": [
                        "clothes",
                        "accessory",
                        "jewelry",
                        "model",
                        "background"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "IMAGE_REQUIRED / EMPTY_IMAGE / BAD_FORM"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "INSUFFICIENT_TOKENS / AI_NEEDS_BILLING"
          },
          "403": {
            "description": "A viewer may not spend the house's tokens"
          },
          "413": {
            "description": "IMAGE_TOO_LARGE"
          },
          "502": {
            "description": "AI_BAD_OUTPUT / AI_FAILED"
          },
          "503": {
            "description": "AI_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/ai/preflight": {
      "post": {
        "summary": "Pre-flight — what will go wrong, before you spend (free)",
        "description": "The judge, pointed backwards. Every other quality check in this API runs after the money is gone; this one runs on the references you are about to spend on and predicts what will go wrong in the generated result — three photos lit differently mean the frames will not match each other, and you find that out before the develop rather than after it.\nWHAT IT ASKS DEPENDS ON WHAT YOU SENT. With no `lens` it asks the GARMENT questions only — light, colour, crop, shadow, background, size, hidden parts. It never asks whether a set is one person, because a rail of clothes has no answer to that and a finding you cannot act on is the same as a wrong one. Send `lens` to ask a narrower or a different question; a casting look is where the sentences about a face come from.\nFREE, on purpose and by construction: a check that saves you money must not cost you money. Nothing in this path reserves against the ledger.\nThe vocabulary is CLOSED and the sentences are ours. The model returns a code and 1-based image indices; `says` (the consequence) and `fix` (what we will do about it) are looked up server-side, so an unknown code is dropped rather than surfaced with no sentence.\nAn empty `findings` array is the correct and common answer — a pre-flight that always finds something is one you learn to click past. `clear` is exactly `findings.length === 0`.\nCapped at six images: a collage is three to six references, and past that the answer stops being actionable.\n",
        "tags": [
          "ai"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "images": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    }
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "imageUrls": {
                    "type": "array",
                    "maxItems": 6,
                    "items": {
                      "type": "string"
                    }
                  },
                  "lens": {
                    "type": "array",
                    "description": "WHICH QUESTIONS TO ASK, as codes. Omit it and you get the garment questions — the default, and the only one that suits photographs of clothes. Name codes here to look at something else: a casting set (is this one person? is the face readable?) or a single logo. A code outside the lens is dropped rather than answered, so a narrower lens is a narrower answer and never a louder one.\n",
                    "items": {
                      "type": "string"
                    }
                  },
                  "clearing": {
                    "type": "array",
                    "description": "THE VERIFICATION PASS. Send the FIXED bytes back with the codes you were trying to clear, and the response separates `cleared` from `remaining`. A fix is not finished when it is applied — it is finished when the same inspection no longer finds the problem. This call is free, so verifying costs nothing, and the verdict is computed server-side from a fresh look: a client cannot report a success it did not achieve.\n",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What will go wrong, or nothing",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "clear": {
                      "type": "boolean",
                      "description": "True when findings is empty — the common answer"
                    },
                    "cleared": {
                      "type": "array",
                      "description": "Present only when `clearing` was sent. The codes you asked about that we can no longer see.\n",
                      "items": {
                        "type": "string"
                      }
                    },
                    "remaining": {
                      "type": "array",
                      "description": "Present only when `clearing` was sent, and the honest half: the codes that SURVIVED the fix. A grade that did not even out the light must say so and offer the paid pass or the swap. Each code is judged on its own — a fix that trades one problem for another is not a success just because the count held steady.\n",
                      "items": {
                        "type": "string"
                      }
                    },
                    "findings": {
                      "type": "array",
                      "maxItems": 3,
                      "items": {
                        "type": "object",
                        "properties": {
                          "code": {
                            "type": "string",
                            "enum": [
                              "light-mismatch",
                              "colour-cast",
                              "mixed-subject",
                              "too-small",
                              "hard-shadow",
                              "busy-ground",
                              "part-hidden",
                              "tight-crop"
                            ]
                          },
                          "images": {
                            "type": "array",
                            "description": "1-based, in the order you sent them",
                            "items": {
                              "type": "integer",
                              "minimum": 1,
                              "maximum": 6
                            }
                          },
                          "says": {
                            "type": "string",
                            "description": "The consequence, in the director's language"
                          },
                          "fix": {
                            "type": "string",
                            "description": "What happens next"
                          },
                          "by": {
                            "type": "string",
                            "enum": [
                              "house",
                              "paid",
                              "swap"
                            ],
                            "description": "WHO performs it. \"house\" is free and runs in the browser on files not yet uploaded — there is no image processing on the server at all. \"paid\" needs a provider pass and is always offered with its price. \"swap\" means we CANNOT fix it (you cannot invent resolution that was never captured) and must not offer to — the fix is phrased as your action.\n"
                          },
                          "sure": {
                            "type": "boolean",
                            "description": "False when the model hedged — shown quieter, never blocking"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "IMAGES_REQUIRED / EMPTY_IMAGE / BAD_FORM"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "description": "IMAGE_TOO_LARGE"
          },
          "502": {
            "description": "AI_BAD_OUTPUT / AI_FAILED"
          },
          "503": {
            "description": "AI_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/lookbook/templates": {
      "get": {
        "summary": "Shot templates — the house's built-ins plus the caller's own",
        "description": "Built-ins are the rows with `organization_id IS NULL`; they list first.",
        "tags": [
          "lookbook"
        ],
        "responses": {
          "200": {
            "description": "The templates, each with its shot count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "templates": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "slots": {
                            "type": "object",
                            "nullable": true
                          },
                          "shot_count": {
                            "type": "integer"
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/lookbook/templates/{id}": {
      "get": {
        "summary": "One template and its shots, in order",
        "tags": [
          "lookbook"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The template + its shots",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "template": {
                      "type": "object"
                    },
                    "shots": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "type": {
                            "type": "string"
                          },
                          "framing": {
                            "type": "string",
                            "nullable": true
                          },
                          "pose": {
                            "type": "string",
                            "nullable": true
                          },
                          "expression": {
                            "type": "string",
                            "nullable": true
                          },
                          "sort_order": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "id required"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "NOT_FOUND — unknown, or another house's template"
          }
        }
      }
    },
    "/api/toolkit/lookbook/looks": {
      "get": {
        "summary": "The house's looks, newest first (50)",
        "tags": [
          "lookbook"
        ],
        "responses": {
          "200": {
            "description": "The looks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "looks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Look"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "summary": "Create a look from the director's three choices",
        "description": "Model + items + background, dressed by an aesthetic preset. SELECTIONS ONLY — nothing develops and nothing is charged here. Every referenced asset is owner-gated up front, so a foreign id writes nothing.\n",
        "tags": [
          "lookbook"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "modelAssetId",
                  "itemAssetIds",
                  "background",
                  "aesthetic"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 120
                  },
                  "modelAssetId": {
                    "type": "string"
                  },
                  "itemAssetIds": {
                    "type": "array",
                    "maxItems": 6,
                    "items": {
                      "type": "object",
                      "required": [
                        "assetId",
                        "role"
                      ],
                      "properties": {
                        "assetId": {
                          "type": "string"
                        },
                        "role": {
                          "type": "string",
                          "description": "One of the composer's ITEM_ROLES. `set` (spec 018) is the one role nobody wears — the house's own furniture, which rides the frame as an object in the scene rather than on the model."
                        },
                        "label": {
                          "type": "string",
                          "maxLength": 48,
                          "description": "The piece's own name, and it becomes the NOUN in the compiled frame — 'the gold chain shown in reference image 2' rather than 'the jewelry shown in reference image 2', twice, in a look carrying two. Optional: omitted, the role is the noun and nothing changes. Normalised server-side (newlines stripped, length-capped) and DROPPED rather than rejected when it is not usable prose — a leftover 'IMG_4471.jpg' falls back to the role, because a filename in a prompt is worse than the noun it replaced."
                        },
                        "hero": {
                          "type": "boolean",
                          "description": "The ONE piece this lookbook is selling (spec 019). The starred piece buys a close-up of its own — the tightest framing that can actually show its role, so a garment gets the fabric detail, shoes get the knee-down crop, and a bag or a necklace gets the waist-up that brings a carried piece forward — and it is named as the SUBJECT of that frame instead of the model choosing which piece a detail crop is about. At most one per look: a second star is normalised away (the first wins) rather than refused, and a `set` piece is never the hero because the hero is what the house SELLS. The extra frame is proposed and quoted in the composer before the director confirms; this route spends nothing and the develop route never appends it."
                        }
                      }
                    }
                  },
                  "background": {
                    "type": "object",
                    "description": "{ kind: \"preset\", value } or { kind: \"asset\", assetId }",
                    "properties": {
                      "kind": {
                        "type": "string",
                        "enum": [
                          "preset",
                          "asset"
                        ]
                      },
                      "value": {
                        "type": "string"
                      },
                      "assetId": {
                        "type": "string"
                      }
                    }
                  },
                  "aesthetic": {
                    "type": "string",
                    "description": "A key of the composer's AESTHETIC_PRESETS"
                  },
                  "brandId": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The draft look",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "look": {
                      "$ref": "#/components/schemas/Look"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / NAME_REQUIRED / MODEL_REQUIRED / ITEMS_REQUIRED / TOO_MANY_ITEMS / BAD_ITEM / BAD_BACKGROUND / BAD_AESTHETIC — the rejection carries the legal vocabulary (roles / presets)\n"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "ASSET_NOT_FOUND — a referenced asset is unknown, deleted or foreign"
          }
        }
      }
    },
    "/api/toolkit/lookbook/looks/{id}": {
      "get": {
        "summary": "One look, whole — selections, lock state, the variation batch",
        "description": "Each variation is joined with its generation_job, so status / resultAssetId / error come back per frame. `lockDevelop` is the newest lock-stage job, which is what lets a reloaded wizard rehydrate the loop.\n",
        "tags": [
          "lookbook"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The look + its frames",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "look": {
                      "$ref": "#/components/schemas/Look"
                    },
                    "variations": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "key": {
                            "type": "string"
                          },
                          "framing": {
                            "type": "string"
                          },
                          "jobId": {
                            "type": "string"
                          },
                          "assetId": {
                            "type": "string",
                            "nullable": true
                          },
                          "locked": {
                            "type": "boolean"
                          },
                          "status": {
                            "type": "string"
                          },
                          "error": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "lockDevelop": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string"
                        },
                        "resultAssetId": {
                          "type": "string",
                          "nullable": true
                        },
                        "error": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "NOT_FOUND"
          }
        }
      }
    },
    "/api/toolkit/lookbook/looks/{id}/lock": {
      "post": {
        "summary": "Freeze the Look Base",
        "description": "PROVENANCE IS REQUIRED — the asset must be the captured result of one of THIS look's develop jobs, otherwise 422. Locking also records lineage: the Look Base's parent becomes the model collage.\n",
        "tags": [
          "lookbook"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "assetId"
                ],
                "properties": {
                  "assetId": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The locked look",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "look": {
                      "$ref": "#/components/schemas/Look"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / ASSET_REQUIRED"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "NOT_FOUND"
          },
          "409": {
            "description": "DEVELOP_IN_FLIGHT — the look is mid-batch"
          },
          "422": {
            "description": "NOT_A_LOOK_FRAME — that asset did not develop from this look"
          }
        }
      }
    },
    "/api/toolkit/lookbook/looks/{id}/unlock": {
      "post": {
        "summary": "The explicit intent required before re-developing a locked look",
        "description": "Nothing is destroyed — the look-base fields stay and the next lock supersedes them. Already a draft ⇒ 200, unchanged. No body.\n",
        "tags": [
          "lookbook"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The draft look",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "look": {
                      "$ref": "#/components/schemas/Look"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "NOT_FOUND"
          },
          "409": {
            "description": "DEVELOP_IN_FLIGHT"
          }
        }
      }
    },
    "/api/toolkit/models": {
      "get": {
        "summary": "The casting board",
        "description": "House models plus this director's own. An exclusive face under a LIVE claim by SOMEONE ELSE is absent entirely — it can be neither bought nor cast, so showing it would be noise. Order: yours, then free, sale, exclusive; newest first inside each band.\n",
        "tags": [
          "models"
        ],
        "responses": {
          "200": {
            "description": "The board",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "models": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Model"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/models/{id}": {
      "get": {
        "summary": "One face",
        "description": "Same visibility rule as the board — out of scope is 404, never a 403 that leaks.",
        "tags": [
          "models"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The model",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "model": {
                      "$ref": "#/components/schemas/Model"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Unknown, foreign, or claimed by another director"
          }
        }
      },
      "patch": {
        "summary": "Add to your own model's dossier — her range, her build, her marks",
        "description": "The optional step after a model's sheet holds. `sheetAssetIds` are the dossier sheets she just developed (the expressions sheet, the full-length build sheet); they are inserted AFTER the collage and BEFORE her original photographs, so identity_asset_ids always reads [collage, ...sheets, ...originals]. `traits` is MERGED, never replaced — a PATCH carrying only the five build fields cannot erase the face the casting wizard read an hour earlier, and a field she blanks to \"\" is a real correction that is honoured.\nHER OWN MODELS ONLY. A house model is the house's and a bought face is a licence to cast it, never to edit the row every other director reads, so a model this organization does not OWN is 403. Both keys are optional: a body with neither is a no-op that returns the row unchanged.\nFree — no tokens, no money. The sheets were paid for when they were developed.\n",
        "tags": [
          "models"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "sheetAssetIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Dossier sheets — must be the caller's own live assets"
                  },
                  "traits": {
                    "type": "object",
                    "description": "Merged into models.traits. height / build / presence / posture / hands / marks[]"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated model",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "model": {
                      "$ref": "#/components/schemas/Model"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / BAD_ASSET_IDS"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "FORBIDDEN — not your model"
          },
          "404": {
            "description": "Unknown model, or a sheet asset is unknown, deleted or foreign — nothing was written"
          }
        }
      }
    },
    "/api/toolkit/models/own": {
      "post": {
        "summary": "Bring your own — register held assets as a personal model",
        "description": "Free by definition: the imagery is already the house's, so nothing is billed and the entitlement records source `own`. All-or-nothing — every assetId must be one of the caller's live assets.\n",
        "tags": [
          "models"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "assetIds"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "assetIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Unique",
                    "non-empty": null
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The registered model",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "model": {
                      "$ref": "#/components/schemas/Model"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / NAME_REQUIRED / BAD_ASSET_IDS"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "An assetId is unknown, deleted or foreign — nothing was written"
          }
        }
      }
    },
    "/api/toolkit/models/{id}/checkout": {
      "post": {
        "summary": "Open the hosted checkout for a face",
        "description": "REAL MONEY — owner only. This route NEVER grants; the payment webhook does. Order is claim (the cross-director gate) → reserve (the per-director gate) → create the checkout, and every failure after the claim releases what it took. A double-click gets the SAME url back (`pending: true`); an already-owned face answers `already: true`.\n",
        "tags": [
          "models"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The hosted page, the same live one, or \"you already own this\"",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string"
                    },
                    "pending": {
                      "type": "boolean"
                    },
                    "ok": {
                      "type": "boolean"
                    },
                    "already": {
                      "type": "boolean"
                    },
                    "model": {
                      "$ref": "#/components/schemas/Model"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ALREADY_YOURS / FREE_MODEL / NOT_FOR_SALE"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Only an owner may spend the house's money"
          },
          "404": {
            "description": "Unknown or another director's personal model"
          },
          "409": {
            "description": "TAKEN (the exclusive went to someone else) / CHECKOUT_PENDING"
          },
          "502": {
            "description": "CHECKOUT_FAILED"
          },
          "503": {
            "description": "BILLING_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/pipelines/preview": {
      "post": {
        "summary": "The number before the click",
        "description": "Compiles the plan against the chosen items and prices every frame, then states the live spendable balance beside it. Reads only — nothing is reserved, nothing is queued.\n",
        "tags": [
          "pipelines"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "plan",
                  "items"
                ],
                "properties": {
                  "plan": {
                    "type": "object",
                    "description": "A pipeline plan { id?, name, nodes }"
                  },
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The compiled line + affordability",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "compiled": {
                      "type": "object",
                      "description": "{ steps, frames, tokensTotal, skippedTotal }"
                    },
                    "spendable": {
                      "type": "integer"
                    },
                    "affordable": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / BAD_PLAN / BAD_ITEMS"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "ASSET_NOT_FOUND"
          },
          "422": {
            "description": "LINE_TOO_WIDE { frames, max }"
          }
        }
      }
    },
    "/api/toolkit/pipelines/runs": {
      "get": {
        "summary": "The house's pipeline runs, newest first",
        "tags": [
          "pipelines"
        ],
        "responses": {
          "200": {
            "description": "The runs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "runs": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PipelineRun"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "summary": "Start the line",
        "description": "Drives either an inline `plan` or a saved `planId`. Reserve BEFORE the job row exists, one hold per step — money first, always. If a concurrent run takes the tokens mid-fan-out, everything this request started is retired and the run is cancelled rather than left half-running.\n",
        "tags": [
          "pipelines"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "items"
                ],
                "properties": {
                  "plan": {
                    "type": "object"
                  },
                  "planId": {
                    "type": "string",
                    "description": "A saved preset; used when `plan` is absent"
                  },
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    }
                  },
                  "brandId": {
                    "type": "string",
                    "nullable": true
                  },
                  "idempotencyKey": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The line is running",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "runId": {
                      "type": "string"
                    },
                    "developId": {
                      "type": "string",
                      "description": "The Darkroom's id — run.<runId>"
                    },
                    "started": {
                      "type": "integer"
                    },
                    "compiled": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / BAD_PLAN / BAD_ITEMS"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "INSUFFICIENT_TOKENS { need, spendable }"
          },
          "403": {
            "description": "A viewer may not spend the house's tokens"
          },
          "404": {
            "description": "PRESET_NOT_FOUND / ASSET_NOT_FOUND"
          },
          "422": {
            "description": "LINE_TOO_WIDE"
          }
        }
      }
    },
    "/api/toolkit/pipelines/runs/{id}": {
      "get": {
        "summary": "One run, its compiled steps and its plan",
        "tags": [
          "pipelines"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The run",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "run": {
                      "$ref": "#/components/schemas/PipelineRun"
                    },
                    "steps": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "plan": {
                      "type": "object",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "NOT_FOUND"
          }
        }
      }
    },
    "/api/toolkit/pipelines/runs/{id}/cancel": {
      "post": {
        "summary": "Stop the line",
        "description": "The run is flagged FIRST so the scheduler stops enqueueing, then every queued frame is claimed and released. A frame already rendering finishes and still costs. Never implies a refund that is not happening.\n",
        "tags": [
          "pipelines"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "What actually stopped",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "cancelled": {
                      "type": "integer"
                    },
                    "cancelledJobIds": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "stillRendering": {
                      "type": "integer"
                    },
                    "tokensReleased": {
                      "type": "integer"
                    },
                    "run": {
                      "$ref": "#/components/schemas/PipelineRun"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "NOT_FOUND"
          }
        }
      }
    },
    "/api/toolkit/pipelines/presets": {
      "get": {
        "summary": "Saved plans — a line with no items bound",
        "tags": [
          "pipelines"
        ],
        "responses": {
          "200": {
            "description": "The presets, newest first (100)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "presets": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PipelinePreset"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "summary": "Save a plan as a preset",
        "description": "The body may be `{ plan }` or the plan object itself.",
        "tags": [
          "pipelines"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "plan": {
                    "type": "object"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The saved preset",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "preset": {
                      "$ref": "#/components/schemas/PipelinePreset"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / BAD_PLAN"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/pipelines/presets/{id}": {
      "patch": {
        "summary": "Replace a saved plan",
        "tags": [
          "pipelines"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "plan": {
                    "type": "object"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated preset",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "preset": {
                      "$ref": "#/components/schemas/PipelinePreset"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / BAD_PLAN"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "NOT_FOUND"
          }
        }
      },
      "delete": {
        "summary": "Delete a saved plan",
        "tags": [
          "pipelines"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "deleted": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "NOT_FOUND"
          }
        }
      }
    },
    "/api/toolkit/presets": {
      "get": {
        "summary": "The house's standing preset",
        "description": "NEVER 404s. A director who has never opened brand settings still gets a working preset, so the factory is one drop away.\n",
        "tags": [
          "factory"
        ],
        "responses": {
          "200": {
            "description": "The preset",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "preset": {
                      "$ref": "#/components/schemas/HousePreset"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "summary": "Set the house's standing preset",
        "description": "Every field is validated against the COMPOSER's own vocabulary, so a stored preset can never carry a key the develop routes would reject later — a preset that fails at develop time is a trap.\n",
        "tags": [
          "factory"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "aesthetic",
                  "background",
                  "variationKeys"
                ],
                "properties": {
                  "aesthetic": {
                    "type": "string"
                  },
                  "background": {
                    "type": "object",
                    "properties": {
                      "kind": {
                        "type": "string",
                        "enum": [
                          "preset",
                          "asset"
                        ]
                      },
                      "value": {
                        "type": "string"
                      },
                      "assetId": {
                        "type": "string"
                      }
                    }
                  },
                  "variationKeys": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "modelAssetId": {
                    "type": "string",
                    "nullable": true
                  },
                  "quality": {
                    "type": "string",
                    "enum": [
                      "1k",
                      "2k",
                      "4k"
                    ]
                  },
                  "sizes": {
                    "type": "array",
                    "description": "SPEC 022 — the house's OWN slots, named in her words. She picks \"Product page\", not \"1600 x 2000\", and she picks it once: set here, inherited by every develop and every export after. Pixels are validated by @flam/ai's own resolveOutput, so a slot can never carry a size the develop routes would refuse later. An ABSENT key leaves the stored list alone; an empty array clears it. Empty is the honest default and a house that has named nothing behaves exactly as it did before this field existed.\n",
                    "maxItems": 12,
                    "items": {
                      "type": "object",
                      "required": [
                        "name",
                        "width",
                        "height"
                      ],
                      "properties": {
                        "name": {
                          "type": "string",
                          "maxLength": 40
                        },
                        "width": {
                          "type": "integer"
                        },
                        "height": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The stored preset",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "preset": {
                      "$ref": "#/components/schemas/HousePreset"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / BAD_AESTHETIC / BAD_BACKGROUND / KEYS_REQUIRED / TOO_MANY_FRAMES"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "ASSET_NOT_FOUND"
          }
        }
      }
    },
    "/api/toolkit/postures": {
      "get": {
        "summary": "The posture library, the control axes, and this house's preferences",
        "description": "A variation stops being an adjective and becomes a NAMED posture. Returns FLAM's own entries (the seeded twelve) plus anything this house minted, the control axes with their named stops, and the house's favourites, defaults, Explore adds and saved variations — one call, because a card cannot be drawn without all three.\n\nEVERY ENTRY IS CASTABLE WITH NOTHING TOUCHED: `controls.defaults` already names a stop on every axis the entry pre-sets. The axes are there for a director who wants them; there is no state in which she must use them.\n\nTHE GEOMETRY PHRASES ARE NOT IN THIS RESPONSE, by design. The prompt fragments each posture and each stop compiles to are house IP and are attached server-side by the composer. What a screen needs is names and definitions, and that is what it gets.\n",
        "tags": [
          "postures"
        ],
        "responses": {
          "200": {
            "description": "The library, the axes and the house's preferences",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "library": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PostureCard"
                      }
                    },
                    "axes": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PostureAxis"
                      }
                    },
                    "registerConstraints": {
                      "type": "object",
                      "description": "register → what it demands of the REST of the frame. A `compliance` posture developed on studio white passes every check and silently loses the catalogue slot it exists to fill, so the ground rule ships beside the postures.\n",
                      "additionalProperties": {
                        "type": "object",
                        "properties": {
                          "ground": {
                            "type": "string"
                          },
                          "why": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "prefs": {
                      "$ref": "#/components/schemas/PosturePrefs"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/postures/prefs": {
      "put": {
        "summary": "Star, default, add to Explore, and save her own variations",
        "description": "A PARTIAL merge — an absent key keeps what is stored, so starring one card never wipes her saved variations. `defaults` is what the automatic lookbook casts when nobody chooses, exactly where `variationKey` reads today.\n\nA saved variation is BASE + DELTAS, never a copy: correct a house entry and her variation inherits the correction, and deleting hers never touches the house set. Her sentences are ADDED to the base's geometry and never rewrite it.\n\nAn id she cannot cast is DROPPED rather than rejected — a star on a retired entry must not lose the eleven that are still good.\n\nA `viewer` is refused: a read-only seat may read the house's record and does not set what its automatic lookbook casts.\n",
        "tags": [
          "postures"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "favorites": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "defaults": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "exploreAdded": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "custom": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/CustomPosture"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The stored preferences",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PosturePrefs"
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / BAD_IDS / BAD_CUSTOM"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "role_cannot_edit_postures"
          }
        }
      }
    },
    "/api/toolkit/factory/plan": {
      "post": {
        "summary": "What the house INTENDS, before a token moves",
        "description": "Groups the dropped pieces into candidate looks using the stamps the smart upload already wrote (a hand-made group beats a shared SKU; a piece with neither is its own look), itemises the whole cost and states the live balance beside it. `modelReady` says up front whether the preset's model still exists — the one thing the house cannot invent. Reads only.\n",
        "tags": [
          "factory"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "assetIds"
                ],
                "properties": {
                  "assetIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "quality": {
                    "type": "string",
                    "enum": [
                      "1k",
                      "2k",
                      "4k"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The proposal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "preset": {
                      "$ref": "#/components/schemas/HousePreset"
                    },
                    "looks": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "truncated": {
                      "type": "boolean"
                    },
                    "modelReady": {
                      "type": "boolean"
                    },
                    "cost": {
                      "type": "object",
                      "properties": {
                        "perFrame": {
                          "type": "integer"
                        },
                        "framesPerLook": {
                          "type": "integer"
                        },
                        "frames": {
                          "type": "integer"
                        },
                        "total": {
                          "type": "integer"
                        },
                        "quality": {
                          "type": "string"
                        }
                      }
                    },
                    "balance": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / ASSETS_REQUIRED"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "ASSET_NOT_FOUND"
          }
        }
      }
    },
    "/api/toolkit/factory/run": {
      "post": {
        "summary": "The one confirm — create the batch of draft looks",
        "description": "SPENDS NOTHING. It creates one DRAFT look per proposed look, stamped with a shared batch marker, and answers with their ids; the caller then walks those ids through the crown's own develop routes, which own every reserve. The body is the EDITED proposal — a dropped look is simply absent, and a look may override the preset's ground or variation set. One bad look rejects the whole confirm rather than leaving half a batch.\n",
        "tags": [
          "factory"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "looks"
                ],
                "properties": {
                  "looks": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "name",
                        "itemAssetIds"
                      ],
                      "properties": {
                        "name": {
                          "type": "string",
                          "maxLength": 120
                        },
                        "itemAssetIds": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        },
                        "background": {
                          "type": "object"
                        },
                        "variationKeys": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  },
                  "modelAssetId": {
                    "type": "string"
                  },
                  "aesthetic": {
                    "type": "string"
                  },
                  "quality": {
                    "type": "string",
                    "enum": [
                      "1k",
                      "2k",
                      "4k"
                    ]
                  },
                  "brandId": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created batch",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "batchId": {
                      "type": "string"
                    },
                    "quality": {
                      "type": "string"
                    },
                    "looks": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "variationKeys": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / LOOKS_REQUIRED / TOO_MANY_LOOKS / MODEL_REQUIRED / BAD_AESTHETIC / NAME_REQUIRED / ITEMS_REQUIRED / TOO_MANY_ITEMS / BAD_ITEM / KEYS_REQUIRED / TOO_MANY_FRAMES / UNKNOWN_VARIATION\n"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "ASSET_NOT_FOUND"
          }
        }
      }
    },
    "/api/chat": {
      "post": {
        "summary": "One turn of the loop",
        "description": "The house reads the message against the current draft and answers with what it said plus the mutations it applied. Every applied mutation is ALSO pushed over the job socket the moment it lands (`turnId` is echoed so the tab that asked does not render it twice, and `sessionId` keeps another open topic from receiving it). Free — a turn never spends tokens.\n",
        "tags": [
          "chat"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "message"
                ],
                "properties": {
                  "message": {
                    "type": "string"
                  },
                  "draft": {
                    "type": "object"
                  },
                  "history": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "role": {
                          "type": "string",
                          "enum": [
                            "director",
                            "house"
                          ]
                        },
                        "text": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "screen": {
                    "type": "string",
                    "maxLength": 60
                  },
                  "turnId": {
                    "type": "string",
                    "maxLength": 64
                  },
                  "sessionId": {
                    "type": "string",
                    "description": "Existing conversation. Omit to create one atomically with the turn."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The turn",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "text": {
                      "type": "string"
                    },
                    "applied": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "mirror": {
                      "type": "object"
                    },
                    "turnId": {
                      "type": "string"
                    },
                    "sessionId": {
                      "type": "string"
                    },
                    "persisted": {
                      "type": "boolean",
                      "description": "False when the answer succeeded but transcript persistence degraded to the local fallback."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / BAD_MESSAGE / BAD_DRAFT"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "502": {
            "description": "AI_UNAVAILABLE"
          },
          "503": {
            "description": "AI_NOT_CONFIGURED / AI_GATEWAY_NEEDS_BILLING"
          }
        }
      }
    },
    "/api/chat/apply": {
      "post": {
        "summary": "Undo, redo, and any direct edit",
        "description": "ONE door for both directions of the walk — undo posts the inverse, redo posts the mutation. Free by definition: a mutation never spends. A selection can only ever name pieces this house holds.\n",
        "tags": [
          "chat"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "mutation"
                ],
                "properties": {
                  "draft": {
                    "type": "object"
                  },
                  "mutation": {
                    "type": "object"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new draft",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "draft": {
                      "type": "object"
                    },
                    "applied": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / BAD_DRAFT / BAD_MUTATION"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "ASSET_NOT_FOUND"
          },
          "422": {
            "description": "CANNOT_APPLY { reason }"
          }
        }
      }
    },
    "/api/chat/sessions": {
      "get": {
        "summary": "List this director's recent conversations",
        "description": "Newest first, scoped to both the active house and actor.",
        "tags": [
          "chat"
        ],
        "responses": {
          "200": {
            "description": "Recent open conversations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sessions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ChatSession"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "summary": "Start a new conversation",
        "description": "Creates an empty actor-visible working context in the active house.",
        "tags": [
          "chat"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 120
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new conversation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "session": {
                      "$ref": "#/components/schemas/ChatSession"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/chat/sessions/{id}/turns": {
      "get": {
        "summary": "Restore one conversation",
        "description": "Returns at most 200 exact UI turns, oldest first.",
        "tags": [
          "chat"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The persisted transcript",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "turns": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "CHAT_SESSION_NOT_FOUND"
          }
        }
      }
    },
    "/posts": {
      "get": {
        "tags": [
          "posts"
        ],
        "responses": {
          "200": {
            "description": "All posts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Post"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "posts"
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewPost"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created post",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Post"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/posts/{id}": {
      "get": {
        "tags": [
          "posts"
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The post",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Post"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "posts"
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PatchPost"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated post",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Post"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — not the author",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "posts"
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — not the author",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/takes": {
      "post": {
        "tags": [
          "toolkit"
        ],
        "summary": "Another take — N frames back in, her sentence on each",
        "description": "Reserves tokens for every named frame before enqueueing any and releases every fresh hold on any single failure — a 402 leaves zero holds. Each new job carries her sentence verbatim in params.direction, inherits its source's runId, and is chained to the frame it retakes by retryOfJobId. Idempotent on idempotencyKey: a replayed submit answers with the same jobs and charges nothing.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "jobIds": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "minLength": 1
                    },
                    "minItems": 1,
                    "maxItems": 24
                  },
                  "direction": {
                    "type": "string",
                    "maxLength": 600
                  },
                  "idempotencyKey": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 120
                  }
                },
                "required": [
                  "jobIds"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Replay of an identical submit — the same jobs, nothing charged twice.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "jobId": {
                            "type": "string"
                          },
                          "sourceJobId": {
                            "type": "string"
                          },
                          "tokens": {
                            "type": "number"
                          }
                        },
                        "required": [
                          "jobId",
                          "sourceJobId",
                          "tokens"
                        ]
                      }
                    },
                    "offer": {
                      "type": "object",
                      "properties": {
                        "text": {
                          "type": "string"
                        },
                        "scopeTools": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "evidence": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "jobId": {
                                "type": "string"
                              },
                              "tool": {
                                "type": "string"
                              },
                              "said": {
                                "type": "string"
                              },
                              "at": {
                                "type": "string"
                              },
                              "shoot": {
                                "type": "string"
                              }
                            },
                            "required": [
                              "jobId",
                              "tool",
                              "said",
                              "at",
                              "shoot"
                            ]
                          }
                        },
                        "alreadyStanding": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string"
                              },
                              "text": {
                                "type": "string"
                              },
                              "scopePhrase": {
                                "type": "string"
                              }
                            },
                            "required": [
                              "id",
                              "text",
                              "scopePhrase"
                            ]
                          }
                        }
                      },
                      "required": [
                        "text",
                        "scopeTools",
                        "evidence"
                      ]
                    },
                    "replayed": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "jobs"
                  ]
                }
              }
            }
          },
          "202": {
            "description": "Every frame reserved and queued. `offer` rides along when the promotion detector has one.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "jobId": {
                            "type": "string"
                          },
                          "sourceJobId": {
                            "type": "string"
                          },
                          "tokens": {
                            "type": "number"
                          }
                        },
                        "required": [
                          "jobId",
                          "sourceJobId",
                          "tokens"
                        ]
                      }
                    },
                    "offer": {
                      "type": "object",
                      "properties": {
                        "text": {
                          "type": "string"
                        },
                        "scopeTools": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "evidence": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "jobId": {
                                "type": "string"
                              },
                              "tool": {
                                "type": "string"
                              },
                              "said": {
                                "type": "string"
                              },
                              "at": {
                                "type": "string"
                              },
                              "shoot": {
                                "type": "string"
                              }
                            },
                            "required": [
                              "jobId",
                              "tool",
                              "said",
                              "at",
                              "shoot"
                            ]
                          }
                        },
                        "alreadyStanding": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string"
                              },
                              "text": {
                                "type": "string"
                              },
                              "scopePhrase": {
                                "type": "string"
                              }
                            },
                            "required": [
                              "id",
                              "text",
                              "scopePhrase"
                            ]
                          }
                        }
                      },
                      "required": [
                        "text",
                        "scopeTools",
                        "evidence"
                      ]
                    },
                    "replayed": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "jobs"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "No session",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    },
                    "state": {
                      "type": "string"
                    },
                    "jobId": {
                      "type": "string"
                    },
                    "missing": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "need": {
                      "type": "number"
                    },
                    "spendable": {
                      "type": "number"
                    },
                    "jobs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "jobId": {
                            "type": "string"
                          },
                          "sourceJobId": {
                            "type": "string"
                          },
                          "tokens": {
                            "type": "number"
                          }
                        },
                        "required": [
                          "jobId",
                          "sourceJobId",
                          "tokens"
                        ]
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_TOKENS — need/spendable are the honest cost line. Zero holds remain.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    },
                    "state": {
                      "type": "string"
                    },
                    "jobId": {
                      "type": "string"
                    },
                    "missing": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "need": {
                      "type": "number"
                    },
                    "spendable": {
                      "type": "number"
                    },
                    "jobs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "jobId": {
                            "type": "string"
                          },
                          "sourceJobId": {
                            "type": "string"
                          },
                          "tokens": {
                            "type": "number"
                          }
                        },
                        "required": [
                          "jobId",
                          "sourceJobId",
                          "tokens"
                        ]
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "A viewer seat may not spend the house's tokens",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    },
                    "state": {
                      "type": "string"
                    },
                    "jobId": {
                      "type": "string"
                    },
                    "missing": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "need": {
                      "type": "number"
                    },
                    "spendable": {
                      "type": "number"
                    },
                    "jobs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "jobId": {
                            "type": "string"
                          },
                          "sourceJobId": {
                            "type": "string"
                          },
                          "tokens": {
                            "type": "number"
                          }
                        },
                        "required": [
                          "jobId",
                          "sourceJobId",
                          "tokens"
                        ]
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "A named frame this house does not own",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    },
                    "state": {
                      "type": "string"
                    },
                    "jobId": {
                      "type": "string"
                    },
                    "missing": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "need": {
                      "type": "number"
                    },
                    "spendable": {
                      "type": "number"
                    },
                    "jobs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "jobId": {
                            "type": "string"
                          },
                          "sourceJobId": {
                            "type": "string"
                          },
                          "tokens": {
                            "type": "number"
                          }
                        },
                        "required": [
                          "jobId",
                          "sourceJobId",
                          "tokens"
                        ]
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "409": {
            "description": "A named frame is not developed yet, or the idempotency key was reused for a different set",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    },
                    "state": {
                      "type": "string"
                    },
                    "jobId": {
                      "type": "string"
                    },
                    "missing": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "need": {
                      "type": "number"
                    },
                    "spendable": {
                      "type": "number"
                    },
                    "jobs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "jobId": {
                            "type": "string"
                          },
                          "sourceJobId": {
                            "type": "string"
                          },
                          "tokens": {
                            "type": "number"
                          }
                        },
                        "required": [
                          "jobId",
                          "sourceJobId",
                          "tokens"
                        ]
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "422": {
            "description": "A named frame cannot run again (retired tool, no inputs)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    },
                    "state": {
                      "type": "string"
                    },
                    "jobId": {
                      "type": "string"
                    },
                    "missing": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "need": {
                      "type": "number"
                    },
                    "spendable": {
                      "type": "number"
                    },
                    "jobs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "jobId": {
                            "type": "string"
                          },
                          "sourceJobId": {
                            "type": "string"
                          },
                          "tokens": {
                            "type": "number"
                          }
                        },
                        "required": [
                          "jobId",
                          "sourceJobId",
                          "tokens"
                        ]
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "500": {
            "description": "Submit failed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    },
                    "state": {
                      "type": "string"
                    },
                    "jobId": {
                      "type": "string"
                    },
                    "missing": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "need": {
                      "type": "number"
                    },
                    "spendable": {
                      "type": "number"
                    },
                    "jobs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "jobId": {
                            "type": "string"
                          },
                          "sourceJobId": {
                            "type": "string"
                          },
                          "tokens": {
                            "type": "number"
                          }
                        },
                        "required": [
                          "jobId",
                          "sourceJobId",
                          "tokens"
                        ]
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "503": {
            "description": "The studio queue is unreachable — released holds for anything not queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    },
                    "state": {
                      "type": "string"
                    },
                    "jobId": {
                      "type": "string"
                    },
                    "missing": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "need": {
                      "type": "number"
                    },
                    "spendable": {
                      "type": "number"
                    },
                    "jobs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "jobId": {
                            "type": "string"
                          },
                          "sourceJobId": {
                            "type": "string"
                          },
                          "tokens": {
                            "type": "number"
                          }
                        },
                        "required": [
                          "jobId",
                          "sourceJobId",
                          "tokens"
                        ]
                      }
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/standing-notes": {
      "get": {
        "tags": [
          "takes"
        ],
        "summary": "Every standing note this house holds, in force and retired",
        "description": "The call sheet's read: every standing note with its scope (as tool slugs and as a phrase in the director's language), the evidence job ids it was learned from, when it started applying, and when it was switched off. Retired rows stay listed — a preference that vanishes is a preference she cannot audit. Scoped to the acting house; a viewer seat may read it.",
        "responses": {
          "200": {
            "description": "Newest first, retired rows included with their retiredAt.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "notes": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/StandingNote"
                      }
                    }
                  },
                  "required": [
                    "notes"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Signed out",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StandingNoteError"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "takes"
        ],
        "summary": "Promote a direction to a standing note — her tap, never inference",
        "description": "The accept door (C-T3: a direction never silently becomes standing). Accepting an offer sends the text plus evidenceJobIds, and the scope is RE-DERIVED server-side from those jobs' tools — a client cannot widen it. Writing from the call sheet sends text plus scopeTools with no evidence; an empty scopeTools means global and is reachable only through this door, with the call sheet's own confirm. The same sentence already standing on the same scope is returned, not duplicated.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StandingNoteAccept"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Already standing — the existing note, untouched.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "note": {
                      "$ref": "#/components/schemas/StandingNote"
                    }
                  },
                  "required": [
                    "note"
                  ]
                }
              }
            }
          },
          "201": {
            "description": "Standing from now on. Visible on the call sheet.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "note": {
                      "$ref": "#/components/schemas/StandingNote"
                    }
                  },
                  "required": [
                    "note"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Empty text, unknown evidence, or a scope the evidence does not support.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StandingNoteError"
                }
              }
            }
          },
          "401": {
            "description": "Signed out",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StandingNoteError"
                }
              }
            }
          },
          "403": {
            "description": "A viewer seat reads the house's preferences; it does not set them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StandingNoteError"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/standing-notes/{id}": {
      "patch": {
        "tags": [
          "takes"
        ],
        "summary": "Retire a standing note, or narrow its scope",
        "description": "Retire sets retiredAt and keeps the row — the call sheet greys it in place with its evidence, because a preference that vanishes is a preference she cannot audit. Narrowing replaces scopeTools with a subset of the current set. Widening — including any PATCH to an empty scope — is refused: out of scope, first ship.",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StandingNoteUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The note as it now stands.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "note": {
                      "$ref": "#/components/schemas/StandingNote"
                    }
                  },
                  "required": [
                    "note"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Nothing to change, or a scope that widens.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StandingNoteError"
                }
              }
            }
          },
          "401": {
            "description": "Signed out",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StandingNoteError"
                }
              }
            }
          },
          "403": {
            "description": "A viewer seat cannot change the house's preferences.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StandingNoteError"
                }
              }
            }
          },
          "404": {
            "description": "Not this house's note. Indistinguishable from absent, on purpose.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StandingNoteError"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/admin/memory": {
      "get": {
        "tags": [
          "admin"
        ],
        "summary": "Observe living memory across every house",
        "description": "Admin-only, audited observatory for typed memories, Core projection state, outbox delivery, digestion, and retrieval. Statements and evidence ids are sensitive cross-tenant data, so every read is allowlist-gated and logged.",
        "responses": {
          "200": {
            "description": "The memory system's current state.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "totals": {
                      "type": "object",
                      "properties": {
                        "memories": {
                          "type": "number"
                        },
                        "active": {
                          "type": "number"
                        },
                        "coreProjected": {
                          "type": "number"
                        },
                        "pendingOutbox": {
                          "type": "number"
                        },
                        "failedOutbox": {
                          "type": "number"
                        },
                        "digestCostUsd": {
                          "type": "number"
                        },
                        "retrievals24h": {
                          "type": "number"
                        },
                        "injected24h": {
                          "type": "number"
                        },
                        "digests24h": {
                          "type": "number"
                        },
                        "failedDigests24h": {
                          "type": "number"
                        },
                        "averageContextMemories24h": {
                          "type": "number"
                        },
                        "latestDigestAt": {
                          "type": "string",
                          "nullable": true
                        },
                        "latestDigestModel": {
                          "type": "string",
                          "nullable": true
                        },
                        "populationCandidates": {
                          "type": "number"
                        },
                        "observations24h": {
                          "type": "number"
                        },
                        "observationCostUsd24h": {
                          "type": "number"
                        }
                      },
                      "required": [
                        "memories",
                        "active",
                        "coreProjected",
                        "pendingOutbox",
                        "failedOutbox",
                        "digestCostUsd",
                        "retrievals24h",
                        "injected24h",
                        "digests24h",
                        "failedDigests24h",
                        "averageContextMemories24h",
                        "latestDigestAt",
                        "latestDigestModel",
                        "populationCandidates",
                        "observations24h",
                        "observationCostUsd24h"
                      ]
                    },
                    "digests": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "organizationName": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string"
                          },
                          "model": {
                            "type": "string"
                          },
                          "candidateCount": {
                            "type": "number"
                          },
                          "contextMemoryCount": {
                            "type": "number"
                          },
                          "inputTokens": {
                            "type": "number"
                          },
                          "outputTokens": {
                            "type": "number"
                          },
                          "costUsd": {
                            "type": "number"
                          },
                          "error": {
                            "type": "string",
                            "nullable": true
                          },
                          "finishedAt": {
                            "type": "string",
                            "nullable": true
                          },
                          "createdAt": {
                            "type": "string"
                          }
                        },
                        "required": [
                          "id",
                          "organizationName",
                          "status",
                          "model",
                          "candidateCount",
                          "contextMemoryCount",
                          "inputTokens",
                          "outputTokens",
                          "costUsd",
                          "error",
                          "finishedAt",
                          "createdAt"
                        ]
                      }
                    },
                    "memories": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "organizationId": {
                            "type": "string"
                          },
                          "organizationName": {
                            "type": "string"
                          },
                          "subjectType": {
                            "type": "string",
                            "enum": [
                              "house",
                              "person"
                            ]
                          },
                          "memoryClass": {
                            "type": "string",
                            "enum": [
                              "fact",
                              "preference",
                              "lesson",
                              "playbook",
                              "outcome_pattern",
                              "image_reading"
                            ]
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "told",
                              "learned",
                              "acted"
                            ]
                          },
                          "statement": {
                            "type": "string"
                          },
                          "confidence": {
                            "type": "number"
                          },
                          "status": {
                            "type": "string"
                          },
                          "evidence": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "coreMemoryId": {
                            "type": "string",
                            "nullable": true
                          },
                          "updatedAt": {
                            "type": "string"
                          },
                          "replayEvidence": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "string"
                                },
                                "startMs": {
                                  "type": "number"
                                },
                                "focusMs": {
                                  "type": "number"
                                },
                                "endMs": {
                                  "type": "number"
                                },
                                "reason": {
                                  "type": "string"
                                },
                                "archiveState": {
                                  "type": "string",
                                  "enum": [
                                    "pending",
                                    "ready",
                                    "failed",
                                    "deleted"
                                  ]
                                },
                                "providerAvailableUntil": {
                                  "type": "string"
                                },
                                "openUrl": {
                                  "type": "string"
                                },
                                "renders": {
                                  "type": "array",
                                  "items": {
                                    "type": "object",
                                    "properties": {
                                      "id": {
                                        "type": "string"
                                      },
                                      "format": {
                                        "type": "string",
                                        "enum": [
                                          "mp4",
                                          "webm"
                                        ]
                                      },
                                      "state": {
                                        "type": "string",
                                        "enum": [
                                          "requested",
                                          "rendering",
                                          "ready",
                                          "failed",
                                          "deleted"
                                        ]
                                      },
                                      "failureCode": {
                                        "type": "string",
                                        "nullable": true
                                      },
                                      "requestedAt": {
                                        "type": "string"
                                      },
                                      "completedAt": {
                                        "type": "string",
                                        "nullable": true
                                      },
                                      "expiresAt": {
                                        "type": "string",
                                        "nullable": true
                                      },
                                      "downloadUrl": {
                                        "type": "string",
                                        "nullable": true
                                      }
                                    },
                                    "required": [
                                      "id",
                                      "format",
                                      "state",
                                      "failureCode",
                                      "requestedAt",
                                      "completedAt",
                                      "expiresAt",
                                      "downloadUrl"
                                    ]
                                  }
                                }
                              },
                              "required": [
                                "id",
                                "startMs",
                                "focusMs",
                                "endMs",
                                "reason",
                                "archiveState",
                                "providerAvailableUntil",
                                "openUrl",
                                "renders"
                              ]
                            }
                          }
                        },
                        "required": [
                          "id",
                          "organizationId",
                          "organizationName",
                          "subjectType",
                          "memoryClass",
                          "kind",
                          "statement",
                          "confidence",
                          "status",
                          "evidence",
                          "coreMemoryId",
                          "updatedAt",
                          "replayEvidence"
                        ]
                      }
                    },
                    "reflectionFindings": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "reflectionRunId": {
                            "type": "string"
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "population_lesson",
                              "product_friction",
                              "instruction_gap",
                              "tool_gap",
                              "measurement_gap"
                            ]
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "proposed",
                              "accepted",
                              "rejected",
                              "superseded",
                              "implemented"
                            ]
                          },
                          "title": {
                            "type": "string"
                          },
                          "claim": {
                            "type": "string"
                          },
                          "recommendation": {
                            "type": "string"
                          },
                          "evidenceRefs": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "replayEvidence": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "string"
                                },
                                "startMs": {
                                  "type": "number"
                                },
                                "focusMs": {
                                  "type": "number"
                                },
                                "endMs": {
                                  "type": "number"
                                },
                                "reason": {
                                  "type": "string"
                                },
                                "archiveState": {
                                  "type": "string",
                                  "enum": [
                                    "pending",
                                    "ready",
                                    "failed",
                                    "deleted"
                                  ]
                                },
                                "providerAvailableUntil": {
                                  "type": "string"
                                },
                                "openUrl": {
                                  "type": "string"
                                },
                                "renders": {
                                  "type": "array",
                                  "items": {
                                    "type": "object",
                                    "properties": {
                                      "id": {
                                        "type": "string"
                                      },
                                      "format": {
                                        "type": "string",
                                        "enum": [
                                          "mp4",
                                          "webm"
                                        ]
                                      },
                                      "state": {
                                        "type": "string",
                                        "enum": [
                                          "requested",
                                          "rendering",
                                          "ready",
                                          "failed",
                                          "deleted"
                                        ]
                                      },
                                      "failureCode": {
                                        "type": "string",
                                        "nullable": true
                                      },
                                      "requestedAt": {
                                        "type": "string"
                                      },
                                      "completedAt": {
                                        "type": "string",
                                        "nullable": true
                                      },
                                      "expiresAt": {
                                        "type": "string",
                                        "nullable": true
                                      },
                                      "downloadUrl": {
                                        "type": "string",
                                        "nullable": true
                                      }
                                    },
                                    "required": [
                                      "id",
                                      "format",
                                      "state",
                                      "failureCode",
                                      "requestedAt",
                                      "completedAt",
                                      "expiresAt",
                                      "downloadUrl"
                                    ]
                                  }
                                }
                              },
                              "required": [
                                "id",
                                "startMs",
                                "focusMs",
                                "endMs",
                                "reason",
                                "archiveState",
                                "providerAvailableUntil",
                                "openUrl",
                                "renders"
                              ]
                            }
                          },
                          "confidence": {
                            "type": "number"
                          },
                          "risk": {
                            "type": "string",
                            "enum": [
                              "low",
                              "medium",
                              "high",
                              "critical"
                            ]
                          },
                          "proposedReplay": {
                            "type": "object",
                            "nullable": true,
                            "additionalProperties": {
                              "nullable": true
                            }
                          },
                          "proposedTicket": {
                            "type": "object",
                            "nullable": true,
                            "additionalProperties": {
                              "nullable": true
                            }
                          },
                          "interventionId": {
                            "type": "string",
                            "nullable": true
                          },
                          "reviewedAt": {
                            "type": "string",
                            "nullable": true
                          },
                          "createdAt": {
                            "type": "string"
                          }
                        },
                        "required": [
                          "id",
                          "reflectionRunId",
                          "kind",
                          "status",
                          "title",
                          "claim",
                          "recommendation",
                          "evidenceRefs",
                          "replayEvidence",
                          "confidence",
                          "risk",
                          "proposedReplay",
                          "proposedTicket",
                          "interventionId",
                          "reviewedAt",
                          "createdAt"
                        ]
                      }
                    },
                    "populationCandidates": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "signalCode": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string"
                          },
                          "observations": {
                            "type": "number"
                          },
                          "distinctOrganizations": {
                            "type": "number"
                          },
                          "confidence": {
                            "type": "number"
                          },
                          "lastObservedAt": {
                            "type": "string"
                          },
                          "expiresAt": {
                            "type": "string"
                          }
                        },
                        "required": [
                          "id",
                          "signalCode",
                          "status",
                          "observations",
                          "distinctOrganizations",
                          "confidence",
                          "lastObservedAt",
                          "expiresAt"
                        ]
                      }
                    },
                    "behaviorObservations": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "organizationName": {
                            "type": "string"
                          },
                          "provider": {
                            "type": "string"
                          },
                          "method": {
                            "type": "string"
                          },
                          "contextVersion": {
                            "type": "string"
                          },
                          "contextDigest": {
                            "type": "string"
                          },
                          "release": {
                            "type": "object",
                            "additionalProperties": {
                              "nullable": true
                            }
                          },
                          "status": {
                            "type": "string"
                          },
                          "summary": {
                            "type": "string"
                          },
                          "outcome": {
                            "type": "string"
                          },
                          "confidence": {
                            "type": "number"
                          },
                          "findings": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "additionalProperties": {
                                "nullable": true
                              }
                            }
                          },
                          "segments": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "additionalProperties": {
                                "nullable": true
                              }
                            }
                          },
                          "costUsd": {
                            "type": "number"
                          },
                          "observedAt": {
                            "type": "string"
                          }
                        },
                        "required": [
                          "id",
                          "organizationName",
                          "provider",
                          "method",
                          "contextVersion",
                          "contextDigest",
                          "release",
                          "status",
                          "summary",
                          "outcome",
                          "confidence",
                          "findings",
                          "segments",
                          "costUsd",
                          "observedAt"
                        ]
                      }
                    },
                    "proofs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "lesson": {
                            "type": "string"
                          },
                          "release": {
                            "type": "object",
                            "properties": {
                              "name": {
                                "type": "string"
                              },
                              "version": {
                                "type": "string"
                              },
                              "exposurePercent": {
                                "type": "number"
                              },
                              "exposedSubjects": {
                                "type": "number"
                              },
                              "eligibleSubjects": {
                                "type": "number"
                              },
                              "startedAt": {
                                "type": "string"
                              }
                            },
                            "required": [
                              "name",
                              "version",
                              "exposurePercent",
                              "exposedSubjects",
                              "eligibleSubjects",
                              "startedAt"
                            ]
                          },
                          "conclusion": {
                            "type": "string",
                            "enum": [
                              "helpful",
                              "neutral",
                              "harmful",
                              "inconclusive"
                            ]
                          },
                          "interventionStatus": {
                            "type": "string",
                            "enum": [
                              "proposed",
                              "shadow",
                              "canary",
                              "stable",
                              "paused",
                              "rolled_back"
                            ]
                          },
                          "metrics": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "label": {
                                  "type": "string"
                                },
                                "baseline": {
                                  "type": "number"
                                },
                                "treatment": {
                                  "type": "number"
                                },
                                "unit": {
                                  "type": "string",
                                  "enum": [
                                    "count",
                                    "ratio",
                                    "milliseconds",
                                    "usd",
                                    "tokens",
                                    "score"
                                  ]
                                },
                                "betterWhen": {
                                  "type": "string",
                                  "enum": [
                                    "higher",
                                    "lower"
                                  ]
                                }
                              },
                              "required": [
                                "label",
                                "baseline",
                                "treatment",
                                "unit"
                              ]
                            }
                          },
                          "evidence": {
                            "type": "object",
                            "properties": {
                              "strength": {
                                "type": "string",
                                "enum": [
                                  "weak",
                                  "moderate",
                                  "strong"
                                ]
                              },
                              "observations": {
                                "type": "number"
                              },
                              "distinctOrganizations": {
                                "type": "number"
                              },
                              "confounders": {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                }
                              }
                            },
                            "required": [
                              "strength",
                              "observations",
                              "distinctOrganizations",
                              "confounders"
                            ]
                          },
                          "replay": {
                            "type": "object",
                            "properties": {
                              "status": {
                                "type": "string",
                                "enum": [
                                  "not_ready",
                                  "queued",
                                  "running",
                                  "passed",
                                  "failed"
                                ]
                              },
                              "passed": {
                                "type": "number"
                              },
                              "total": {
                                "type": "number"
                              },
                              "lastRunAt": {
                                "type": "string",
                                "nullable": true
                              }
                            },
                            "required": [
                              "status",
                              "passed",
                              "total",
                              "lastRunAt"
                            ]
                          },
                          "cost": {
                            "type": "object",
                            "properties": {
                              "measurementStatus": {
                                "type": "string",
                                "enum": [
                                  "complete",
                                  "estimated",
                                  "incomplete"
                                ]
                              },
                              "unmeasuredComponents": {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                }
                              },
                              "notes": {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                }
                              },
                              "totalUsd": {
                                "type": "number"
                              },
                              "items": {
                                "type": "array",
                                "items": {
                                  "type": "object",
                                  "properties": {
                                    "label": {
                                      "type": "string"
                                    },
                                    "costUsd": {
                                      "type": "number"
                                    }
                                  },
                                  "required": [
                                    "label",
                                    "costUsd"
                                  ]
                                }
                              }
                            },
                            "required": [
                              "measurementStatus",
                              "unmeasuredComponents",
                              "notes",
                              "totalUsd",
                              "items"
                            ]
                          },
                          "updatedAt": {
                            "type": "string"
                          }
                        },
                        "required": [
                          "id",
                          "lesson",
                          "release",
                          "conclusion",
                          "interventionStatus",
                          "metrics",
                          "evidence",
                          "replay",
                          "cost",
                          "updatedAt"
                        ]
                      }
                    },
                    "chatScenarios": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "replayKey": {
                            "type": "string"
                          },
                          "version": {
                            "type": "number"
                          },
                          "title": {
                            "type": "string"
                          },
                          "risk": {
                            "type": "string",
                            "enum": [
                              "low",
                              "medium",
                              "high",
                              "critical"
                            ]
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "pending",
                              "running",
                              "passed",
                              "failed",
                              "error"
                            ]
                          },
                          "candidateRelease": {
                            "type": "object",
                            "properties": {
                              "name": {
                                "type": "string"
                              },
                              "version": {
                                "type": "string"
                              }
                            },
                            "required": [
                              "name",
                              "version"
                            ]
                          },
                          "passed": {
                            "type": "number"
                          },
                          "repetitions": {
                            "type": "number"
                          },
                          "passRate": {
                            "type": "number",
                            "nullable": true
                          },
                          "failures": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "repetition": {
                                  "type": "number"
                                },
                                "reasons": {
                                  "type": "array",
                                  "items": {
                                    "type": "string"
                                  }
                                }
                              },
                              "required": [
                                "repetition",
                                "reasons"
                              ]
                            }
                          },
                          "durationMs": {
                            "type": "number",
                            "nullable": true
                          },
                          "firstActivityMs": {
                            "type": "number",
                            "nullable": true
                          },
                          "firstResponseMs": {
                            "type": "number",
                            "nullable": true
                          },
                          "costUsd": {
                            "type": "number"
                          },
                          "finishedAt": {
                            "type": "string",
                            "nullable": true
                          }
                        },
                        "required": [
                          "id",
                          "replayKey",
                          "version",
                          "title",
                          "risk",
                          "status",
                          "candidateRelease",
                          "passed",
                          "repetitions",
                          "passRate",
                          "failures",
                          "durationMs",
                          "firstActivityMs",
                          "firstResponseMs",
                          "costUsd",
                          "finishedAt"
                        ]
                      }
                    },
                    "scenarioEvidence": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "suiteId": {
                            "type": "string"
                          },
                          "suiteVersion": {
                            "type": "string"
                          },
                          "replayKey": {
                            "type": "string"
                          },
                          "replayVersion": {
                            "type": "integer",
                            "minimum": 0,
                            "exclusiveMinimum": true
                          },
                          "title": {
                            "type": "string",
                            "nullable": true
                          },
                          "layer": {
                            "type": "string",
                            "enum": [
                              "deterministic_contract",
                              "repeated_model_comparison",
                              "production_smoke"
                            ]
                          },
                          "passed": {
                            "type": "boolean"
                          },
                          "passRate": {
                            "type": "number",
                            "minimum": 0,
                            "maximum": 1
                          },
                          "minimumPassRate": {
                            "type": "number",
                            "minimum": 0,
                            "maximum": 1
                          },
                          "executions": {
                            "type": "integer",
                            "minimum": 0
                          },
                          "totalDurationMs": {
                            "type": "number",
                            "minimum": 0
                          },
                          "totalCostUsd": {
                            "type": "number",
                            "minimum": 0
                          },
                          "userCharge": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "unit": {
                                  "type": "string"
                                },
                                "amount": {
                                  "type": "number"
                                }
                              },
                              "required": [
                                "unit",
                                "amount"
                              ]
                            }
                          },
                          "agentReleases": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "name": {
                                  "type": "string"
                                },
                                "version": {
                                  "type": "string"
                                },
                                "digest": {
                                  "type": "string"
                                }
                              },
                              "required": [
                                "name",
                                "version"
                              ]
                            }
                          },
                          "models": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "featureFlags": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "createdAt": {
                            "type": "string"
                          },
                          "recordedAt": {
                            "type": "string"
                          }
                        },
                        "required": [
                          "id",
                          "suiteId",
                          "suiteVersion",
                          "replayKey",
                          "replayVersion",
                          "title",
                          "layer",
                          "passed",
                          "passRate",
                          "minimumPassRate",
                          "executions",
                          "totalDurationMs",
                          "totalCostUsd",
                          "userCharge",
                          "agentReleases",
                          "models",
                          "featureFlags",
                          "createdAt",
                          "recordedAt"
                        ]
                      }
                    },
                    "valuePlans": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "interventionStatus": {
                            "type": "string",
                            "enum": [
                              "proposed",
                              "shadow",
                              "canary",
                              "stable",
                              "paused",
                              "rolled_back"
                            ]
                          },
                          "headline": {
                            "type": "string"
                          },
                          "userOutcome": {
                            "type": "string"
                          },
                          "productOutcome": {
                            "type": "string"
                          },
                          "primaryMetric": {
                            "type": "object",
                            "properties": {
                              "label": {
                                "type": "string"
                              },
                              "minimumEffect": {
                                "type": "number"
                              },
                              "unit": {
                                "type": "string",
                                "enum": [
                                  "count",
                                  "ratio",
                                  "milliseconds",
                                  "usd",
                                  "tokens",
                                  "score"
                                ]
                              },
                              "betterWhen": {
                                "type": "string",
                                "enum": [
                                  "higher",
                                  "lower"
                                ]
                              }
                            },
                            "required": [
                              "label",
                              "minimumEffect",
                              "unit",
                              "betterWhen"
                            ]
                          },
                          "guardrails": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "label": {
                                  "type": "string"
                                },
                                "maximumRegression": {
                                  "type": "number"
                                },
                                "unit": {
                                  "type": "string",
                                  "enum": [
                                    "count",
                                    "ratio",
                                    "milliseconds",
                                    "usd",
                                    "tokens",
                                    "score"
                                  ]
                                }
                              },
                              "required": [
                                "label",
                                "maximumRegression",
                                "unit"
                              ]
                            }
                          },
                          "evidence": {
                            "type": "object",
                            "properties": {
                              "minimumControl": {
                                "type": "number"
                              },
                              "minimumTreatment": {
                                "type": "number"
                              },
                              "minimumOrganizations": {
                                "type": "number"
                              },
                              "confidenceLevel": {
                                "type": "number"
                              }
                            },
                            "required": [
                              "minimumControl",
                              "minimumTreatment",
                              "minimumOrganizations",
                              "confidenceLevel"
                            ]
                          },
                          "budget": {
                            "type": "object",
                            "properties": {
                              "maximumTotalUsd": {
                                "type": "number"
                              },
                              "maximumCostPerSuccessfulOutcomeUsd": {
                                "type": "number"
                              }
                            },
                            "required": [
                              "maximumTotalUsd"
                            ]
                          }
                        },
                        "required": [
                          "id",
                          "interventionStatus",
                          "headline",
                          "userOutcome",
                          "productOutcome",
                          "primaryMetric",
                          "guardrails",
                          "evidence",
                          "budget"
                        ]
                      }
                    },
                    "retrievalReceipts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "organizationName": {
                            "type": "string"
                          },
                          "mode": {
                            "type": "string",
                            "enum": [
                              "shadow",
                              "injected",
                              "skipped",
                              "failed"
                            ]
                          },
                          "considered": {
                            "type": "number"
                          },
                          "selected": {
                            "type": "number"
                          },
                          "injected": {
                            "type": "number"
                          },
                          "applied": {
                            "type": "number"
                          },
                          "applicationEvidence": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "source": {
                                  "type": "string"
                                },
                                "count": {
                                  "type": "number"
                                },
                                "averageConfidence": {
                                  "type": "number"
                                }
                              },
                              "required": [
                                "source",
                                "count",
                                "averageConfidence"
                              ]
                            }
                          },
                          "releaseName": {
                            "type": "string"
                          },
                          "releaseVersion": {
                            "type": "string"
                          },
                          "outcomeStatus": {
                            "type": "string",
                            "enum": [
                              "succeeded",
                              "failed",
                              "partial",
                              "unknown"
                            ]
                          },
                          "repeatedCorrection": {
                            "type": "boolean"
                          },
                          "error": {
                            "type": "string",
                            "nullable": true
                          },
                          "createdAt": {
                            "type": "string"
                          }
                        },
                        "required": [
                          "id",
                          "organizationName",
                          "mode",
                          "considered",
                          "selected",
                          "injected",
                          "applied",
                          "applicationEvidence",
                          "releaseName",
                          "releaseVersion",
                          "outcomeStatus",
                          "repeatedCorrection",
                          "error",
                          "createdAt"
                        ]
                      }
                    },
                    "storyGraph": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "schema_version": {
                          "type": "string",
                          "enum": [
                            "1.0.0"
                          ]
                        },
                        "id": {
                          "type": "string"
                        },
                        "scope": {
                          "type": "object",
                          "additionalProperties": {
                            "nullable": true
                          }
                        },
                        "view": {
                          "type": "string",
                          "enum": [
                            "lesson_neighborhood",
                            "causal_trace",
                            "subject_story",
                            "release_impact",
                            "asset_lineage"
                          ]
                        },
                        "query": {
                          "type": "object",
                          "additionalProperties": {
                            "nullable": true
                          }
                        },
                        "nodes": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "additionalProperties": {
                              "nullable": true
                            }
                          }
                        },
                        "edges": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "additionalProperties": {
                              "nullable": true
                            }
                          }
                        },
                        "list_equivalent": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "additionalProperties": {
                              "nullable": true
                            }
                          }
                        },
                        "layout": {
                          "type": "object",
                          "additionalProperties": {
                            "nullable": true
                          }
                        },
                        "truncated": {
                          "type": "boolean"
                        },
                        "next_cursor": {
                          "type": "string"
                        },
                        "access_audit_id": {
                          "type": "string"
                        },
                        "generated_at": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "schema_version",
                        "id",
                        "scope",
                        "view",
                        "query",
                        "nodes",
                        "edges",
                        "list_equivalent",
                        "truncated",
                        "generated_at"
                      ]
                    }
                  },
                  "required": [
                    "totals",
                    "digests",
                    "memories",
                    "reflectionFindings",
                    "populationCandidates",
                    "behaviorObservations",
                    "proofs",
                    "chatScenarios",
                    "scenarioEvidence",
                    "valuePlans",
                    "retrievalReceipts",
                    "storyGraph"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Signed out"
          },
          "403": {
            "description": "Not a platform admin"
          }
        }
      }
    },
    "/api/toolkit/admin/memory/{id}/review": {
      "post": {
        "tags": [
          "admin"
        ],
        "summary": "Approve or reject one learned memory",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "decision": {
                    "type": "string",
                    "enum": [
                      "approve",
                      "reject"
                    ]
                  }
                },
                "required": [
                  "decision"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The review was recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "active",
                        "rejected"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "id",
                    "status"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "Not a platform admin"
          },
          "404": {
            "description": "Memory not found"
          },
          "409": {
            "description": "Memory was already reviewed or its predecessor changed"
          }
        }
      }
    },
    "/api/toolkit/admin/memory/reflection-findings/{id}/decision": {
      "post": {
        "tags": [
          "admin"
        ],
        "summary": "Accept or reject one evidence-backed reflection proposal",
        "description": "Accepting creates a review-only Longstory intervention. It does not expose a change, edit instructions, or grant serving authority.",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "decision": {
                    "type": "string",
                    "enum": [
                      "accept",
                      "reject"
                    ]
                  }
                },
                "required": [
                  "decision"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The decision was recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "accepted",
                        "rejected"
                      ]
                    },
                    "interventionId": {
                      "type": "string",
                      "nullable": true
                    }
                  },
                  "required": [
                    "ok",
                    "id",
                    "status",
                    "interventionId"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "Not a platform admin"
          },
          "404": {
            "description": "Reflection finding not found"
          },
          "409": {
            "description": "Reflection finding was already reviewed"
          }
        }
      }
    },
    "/api/toolkit/admin/memory/replay-evidence/{id}": {
      "get": {
        "tags": [
          "admin"
        ],
        "summary": "Open one audited session replay evidence moment",
        "description": "Admin-only capability-gated read. Every allowed or denied attempt is written to the immutable Longstory access ledger. Live PostHog is preferred while retained; verified R2 archives are the fallback.",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "auto",
                "archive"
              ],
              "default": "auto"
            },
            "required": false,
            "name": "mode",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "The verified raw replay archive when the live provider has expired"
          },
          "302": {
            "description": "Redirect to the exact retained provider moment"
          },
          "403": {
            "description": "Not a platform admin"
          },
          "404": {
            "description": "Evidence not found"
          },
          "410": {
            "description": "Neither the provider nor a verified archive is available"
          }
        }
      }
    },
    "/api/toolkit/admin/memory/replay-evidence/{id}/renders": {
      "post": {
        "tags": [
          "admin"
        ],
        "summary": "Request an optional video rendering of replay evidence",
        "description": "Queues a derived MP4 or WebM from the canonical rrweb archive. Rendering is optional, audited, and inherits the evidence scope, expiry, and deletion policy.",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "format": {
                    "type": "string",
                    "enum": [
                      "mp4",
                      "webm"
                    ],
                    "default": "mp4"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Existing or newly queued render",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "format": {
                      "type": "string",
                      "enum": [
                        "mp4",
                        "webm"
                      ]
                    },
                    "state": {
                      "type": "string",
                      "enum": [
                        "requested",
                        "rendering",
                        "ready",
                        "failed",
                        "deleted"
                      ]
                    },
                    "failureCode": {
                      "type": "string",
                      "nullable": true
                    },
                    "requestedAt": {
                      "type": "string"
                    },
                    "completedAt": {
                      "type": "string",
                      "nullable": true
                    },
                    "expiresAt": {
                      "type": "string",
                      "nullable": true
                    },
                    "downloadUrl": {
                      "type": "string",
                      "nullable": true
                    }
                  },
                  "required": [
                    "id",
                    "format",
                    "state",
                    "failureCode",
                    "requestedAt",
                    "completedAt",
                    "expiresAt",
                    "downloadUrl"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "Not a platform admin"
          },
          "404": {
            "description": "Evidence archive not found"
          },
          "409": {
            "description": "Canonical replay archive is not ready"
          }
        }
      }
    },
    "/api/toolkit/admin/memory/replay-evidence/{id}/renders/{renderId}": {
      "get": {
        "tags": [
          "admin"
        ],
        "summary": "Download one audited replay evidence video",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "required": true,
            "name": "renderId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Verified MP4 or WebM bytes"
          },
          "403": {
            "description": "Not a platform admin"
          },
          "404": {
            "description": "Ready render not found"
          }
        }
      }
    },
    "/api/toolkit/admin/longstory/autonomy": {
      "get": {
        "tags": [
          "admin",
          "longstory"
        ],
        "summary": "Read Longstory's delegated authority",
        "description": "Admin-only, audited view of the active Progressive Autonomy policy and its append-only history. No saved policy means the safe recommend-only default.",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 128
            },
            "required": false,
            "name": "organizationId",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "The effective Longstory autonomy boundary.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "source": {
                      "type": "string",
                      "enum": [
                        "default",
                        "saved"
                      ]
                    },
                    "revision": {
                      "type": "integer",
                      "minimum": 0,
                      "exclusiveMinimum": true
                    },
                    "selection": {
                      "type": "object",
                      "properties": {
                        "scopeType": {
                          "type": "string",
                          "enum": [
                            "platform",
                            "organization"
                          ],
                          "default": "platform"
                        },
                        "organizationId": {
                          "type": "string",
                          "nullable": true,
                          "minLength": 1,
                          "maxLength": 128,
                          "default": null
                        },
                        "level": {
                          "type": "string",
                          "enum": [
                            "observe",
                            "recommend",
                            "shadow",
                            "experiment",
                            "progressive",
                            "autonomous"
                          ]
                        },
                        "maximumAutomaticRisk": {
                          "type": "string",
                          "enum": [
                            "low",
                            "medium"
                          ]
                        },
                        "maximumCanaryPercentage": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 100
                        },
                        "maximumProgressivePercentage": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 100
                        },
                        "maximumAutomaticChangesPerDay": {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 100
                        },
                        "maximumAutomaticCostUsdPerDay": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 10000
                        },
                        "minimumReplayPassRate": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 1
                        },
                        "minimumDistinctSubjects": {
                          "type": "integer",
                          "minimum": 1,
                          "maximum": 1000000
                        },
                        "minimumDistinctOrganizations": {
                          "type": "integer",
                          "minimum": 1,
                          "maximum": 100000
                        }
                      },
                      "required": [
                        "level",
                        "maximumAutomaticRisk",
                        "maximumCanaryPercentage",
                        "maximumProgressivePercentage",
                        "maximumAutomaticChangesPerDay",
                        "maximumAutomaticCostUsdPerDay",
                        "minimumReplayPassRate",
                        "minimumDistinctSubjects",
                        "minimumDistinctOrganizations"
                      ]
                    },
                    "policy": {
                      "type": "object",
                      "additionalProperties": {
                        "nullable": true
                      }
                    },
                    "updatedAt": {
                      "type": "string"
                    },
                    "history": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "revision": {
                            "type": "number"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "active",
                              "superseded",
                              "revoked"
                            ]
                          },
                          "level": {
                            "type": "string",
                            "enum": [
                              "observe",
                              "recommend",
                              "shadow",
                              "experiment",
                              "progressive",
                              "autonomous"
                            ]
                          },
                          "createdAt": {
                            "type": "string"
                          },
                          "supersededAt": {
                            "type": "string",
                            "nullable": true
                          }
                        },
                        "required": [
                          "id",
                          "revision",
                          "status",
                          "level",
                          "createdAt",
                          "supersededAt"
                        ]
                      }
                    }
                  },
                  "required": [
                    "source",
                    "revision",
                    "selection",
                    "policy",
                    "updatedAt",
                    "history"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "Not a platform admin"
          }
        }
      },
      "post": {
        "tags": [
          "admin",
          "longstory"
        ],
        "summary": "Create a new Longstory authority revision",
        "description": "Supersedes the current policy atomically. Frozen surfaces and evidence requirements cannot be weakened through this FLAM control plane.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "scopeType": {
                    "type": "string",
                    "enum": [
                      "platform",
                      "organization"
                    ],
                    "default": "platform"
                  },
                  "organizationId": {
                    "type": "string",
                    "nullable": true,
                    "minLength": 1,
                    "maxLength": 128,
                    "default": null
                  },
                  "level": {
                    "type": "string",
                    "enum": [
                      "observe",
                      "recommend",
                      "shadow",
                      "experiment",
                      "progressive",
                      "autonomous"
                    ]
                  },
                  "maximumAutomaticRisk": {
                    "type": "string",
                    "enum": [
                      "low",
                      "medium"
                    ]
                  },
                  "maximumCanaryPercentage": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100
                  },
                  "maximumProgressivePercentage": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100
                  },
                  "maximumAutomaticChangesPerDay": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100
                  },
                  "maximumAutomaticCostUsdPerDay": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000
                  },
                  "minimumReplayPassRate": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1
                  },
                  "minimumDistinctSubjects": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 1000000
                  },
                  "minimumDistinctOrganizations": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100000
                  }
                },
                "required": [
                  "level",
                  "maximumAutomaticRisk",
                  "maximumCanaryPercentage",
                  "maximumProgressivePercentage",
                  "maximumAutomaticChangesPerDay",
                  "maximumAutomaticCostUsdPerDay",
                  "minimumReplayPassRate",
                  "minimumDistinctSubjects",
                  "minimumDistinctOrganizations"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new active policy revision.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "source": {
                      "type": "string",
                      "enum": [
                        "default",
                        "saved"
                      ]
                    },
                    "revision": {
                      "type": "integer",
                      "minimum": 0,
                      "exclusiveMinimum": true
                    },
                    "selection": {
                      "type": "object",
                      "properties": {
                        "scopeType": {
                          "type": "string",
                          "enum": [
                            "platform",
                            "organization"
                          ],
                          "default": "platform"
                        },
                        "organizationId": {
                          "type": "string",
                          "nullable": true,
                          "minLength": 1,
                          "maxLength": 128,
                          "default": null
                        },
                        "level": {
                          "type": "string",
                          "enum": [
                            "observe",
                            "recommend",
                            "shadow",
                            "experiment",
                            "progressive",
                            "autonomous"
                          ]
                        },
                        "maximumAutomaticRisk": {
                          "type": "string",
                          "enum": [
                            "low",
                            "medium"
                          ]
                        },
                        "maximumCanaryPercentage": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 100
                        },
                        "maximumProgressivePercentage": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 100
                        },
                        "maximumAutomaticChangesPerDay": {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 100
                        },
                        "maximumAutomaticCostUsdPerDay": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 10000
                        },
                        "minimumReplayPassRate": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 1
                        },
                        "minimumDistinctSubjects": {
                          "type": "integer",
                          "minimum": 1,
                          "maximum": 1000000
                        },
                        "minimumDistinctOrganizations": {
                          "type": "integer",
                          "minimum": 1,
                          "maximum": 100000
                        }
                      },
                      "required": [
                        "level",
                        "maximumAutomaticRisk",
                        "maximumCanaryPercentage",
                        "maximumProgressivePercentage",
                        "maximumAutomaticChangesPerDay",
                        "maximumAutomaticCostUsdPerDay",
                        "minimumReplayPassRate",
                        "minimumDistinctSubjects",
                        "minimumDistinctOrganizations"
                      ]
                    },
                    "policy": {
                      "type": "object",
                      "additionalProperties": {
                        "nullable": true
                      }
                    },
                    "updatedAt": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "source",
                    "revision",
                    "selection",
                    "policy",
                    "updatedAt"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid policy selection"
          },
          "403": {
            "description": "Not a platform admin"
          }
        }
      }
    },
    "/api/internal/replay-renders/claim": {
      "post": {
        "tags": [
          "internal"
        ],
        "summary": "Claim one replay render job",
        "description": "Machine-only endpoint used by the scoped Hetzner renderer.",
        "responses": {
          "200": {
            "description": "A claimed job, or null when the queue is empty",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "job": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "evidenceId": {
                          "type": "string"
                        },
                        "format": {
                          "type": "string",
                          "enum": [
                            "mp4",
                            "webm"
                          ]
                        },
                        "archive": {
                          "nullable": true
                        }
                      },
                      "required": [
                        "id",
                        "evidenceId",
                        "format"
                      ]
                    }
                  },
                  "required": [
                    "job"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Invalid renderer credential"
          }
        }
      }
    },
    "/api/internal/replay-renders/{id}/complete": {
      "post": {
        "tags": [
          "internal"
        ],
        "summary": "Store a completed replay video render",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Render stored",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid render bytes or checksum"
          },
          "401": {
            "description": "Invalid renderer credential"
          },
          "404": {
            "description": "Claimed render not found"
          }
        }
      }
    },
    "/api/internal/replay-renders/{id}/fail": {
      "post": {
        "tags": [
          "internal"
        ],
        "summary": "Record a failed replay video render",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "code": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 120
                  }
                },
                "required": [
                  "code"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Failure recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Invalid renderer credential"
          },
          "404": {
            "description": "Claimed render not found"
          }
        }
      }
    },
    "/api/internal/analyst/snapshot": {
      "get": {
        "tags": [
          "internal"
        ],
        "summary": "Read a bounded platform snapshot for the FLAM analyst",
        "description": "Read-only, bearer-gated aggregate operational, commercial, memory, and learning health. It returns no prompts, images, emails, person memories, or mutation capability.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 720,
              "default": 24
            },
            "required": false,
            "name": "hours",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Aggregate platform snapshot.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "nullable": true
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid analyst token"
          }
        }
      }
    },
    "/api/internal/analyst/learnings": {
      "get": {
        "tags": [
          "internal"
        ],
        "summary": "Read recent cross-house learnings for the FLAM analyst",
        "description": "Returns active house-level lessons, playbooks, outcome patterns, and image readings. Person memories and evidence identifiers are intentionally excluded.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 720,
              "default": 24
            },
            "required": false,
            "name": "hours",
            "in": "query"
          },
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            },
            "required": false,
            "name": "limit",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Recent house-level learnings.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "nullable": true
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid analyst token"
          }
        }
      }
    },
    "/api/internal/analyst/longstory": {
      "get": {
        "tags": [
          "internal"
        ],
        "summary": "Read Longstory lessons, interventions, evidence, and cost",
        "description": "Returns governed Longstory evidence for the platform or one explicitly selected house. Platform results suppress one-house themes; person memories, transcripts, prompts, and raw evidence are excluded.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 720,
              "default": 24
            },
            "required": false,
            "name": "hours",
            "in": "query"
          },
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            },
            "required": false,
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 128
            },
            "required": false,
            "name": "organizationId",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Bounded Longstory proof and lessons.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "nullable": true
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid analyst token"
          }
        }
      }
    },
    "/api/internal/analyst/failures": {
      "get": {
        "tags": [
          "internal"
        ],
        "summary": "Read grouped recent failures for the FLAM analyst",
        "description": "Groups generation failures into stable signatures without returning job ids, user identities, prompts, or images.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 720,
              "default": 24
            },
            "required": false,
            "name": "hours",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Grouped generation failure patterns.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "nullable": true
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid analyst token"
          }
        }
      }
    },
    "/api/internal/analyst/deliverability": {
      "get": {
        "tags": [
          "internal"
        ],
        "summary": "Read email delivery outcomes for the FLAM analyst",
        "description": "Counts by delivery status and by template — never an address, a subject or a body. Only rows the delivery webhook has actually touched are counted: before its first write nothing updated a row, so older letters are frozen at 'sent' and would read as a perfect delivery rate.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 720,
              "default": 24
            },
            "required": false,
            "name": "hours",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Delivery outcomes, aggregate only.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "nullable": true
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid analyst token"
          }
        }
      }
    },
    "/api/internal/analyst/chat-quality": {
      "get": {
        "tags": [
          "internal"
        ],
        "summary": "How the chat is doing — self-found lapses, and what it wished it could do",
        "description": "Aggregated chat shortfalls by code, with counts and a few example sentences. No identities, no transcripts.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 720,
              "default": 24
            },
            "required": false,
            "name": "hours",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Chat lapses and wishes, grouped.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "nullable": true
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid analyst token"
          }
        }
      }
    },
    "/api/internal/analyst/product-usage": {
      "get": {
        "tags": [
          "internal"
        ],
        "summary": "Read bounded PostHog product usage for the FLAM analyst",
        "description": "Runs fixed aggregate HogQL queries. The caller cannot supply SQL, and the response contains event names and counts rather than event properties or identities.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 720,
              "default": 24
            },
            "required": false,
            "name": "hours",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Aggregate product analytics, or an explicit unavailable state.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "nullable": true
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid analyst token"
          }
        }
      }
    },
    "/api/internal/analyst/funnel": {
      "get": {
        "tags": [
          "internal"
        ],
        "summary": "Read FLAM's fixed acquisition and activation funnel",
        "description": "Runs a fixed PostHog query over FLAM's typed event catalog. No caller-supplied SQL, event properties, or person identities are returned.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 720,
              "default": 24
            },
            "required": false,
            "name": "hours",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Ordered funnel stages with event and actor counts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "nullable": true
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid analyst token"
          }
        }
      }
    },
    "/api/internal/analyst/feature-flags": {
      "get": {
        "tags": [
          "internal"
        ],
        "summary": "Read bounded PostHog feature flag state",
        "description": "Returns flag status, rollout configuration, variants, and experiment links without mutation capability or person evaluations.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            },
            "required": false,
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false",
                "all"
              ],
              "default": "all"
            },
            "required": false,
            "name": "active",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Feature flag state, or an explicit unavailable state.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "nullable": true
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid analyst token"
          }
        }
      }
    },
    "/api/internal/analyst/experiments": {
      "get": {
        "tags": [
          "internal"
        ],
        "summary": "Read bounded PostHog experiment state and aggregate results",
        "description": "Lists experiments or loads one experiment and runs its fixed aggregate metric queries. It cannot launch, pause, end, or ship a variant.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            },
            "required": false,
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "complete",
                "draft",
                "exposure_frozen",
                "paused",
                "running",
                "stopped"
              ],
              "default": "all"
            },
            "required": false,
            "name": "status",
            "in": "query"
          },
          {
            "schema": {
              "type": "integer",
              "minimum": 0,
              "exclusiveMinimum": true
            },
            "required": false,
            "name": "experimentId",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Experiment state and, for a selected experiment, aggregate results.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "nullable": true
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid analyst token"
          }
        }
      }
    },
    "/api/internal/analyst/recordings": {
      "get": {
        "tags": [
          "internal"
        ],
        "summary": "Read bounded PostHog session replay metadata",
        "description": "Returns replay activity, error, URL, and summary metadata plus links for human review. It excludes person identity and raw recording payloads.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 720,
              "default": 24
            },
            "required": false,
            "name": "hours",
            "in": "query"
          },
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            },
            "required": false,
            "name": "limit",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Recent session replay metadata, or an explicit unavailable state.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "nullable": true
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid analyst token"
          }
        }
      }
    },
    "/api/internal/analyst/recordings/{id}/analysis": {
      "get": {
        "tags": [
          "internal"
        ],
        "summary": "Read PostHog's stored AI analysis of one session replay",
        "description": "Returns a bounded, identity-minimized PostHog replay summary and an authenticated human review link. It never returns raw snapshots or creates a public sharing link.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 100
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Stored analysis, or an explicit not-yet-generated state.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "nullable": true
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid analyst token"
          }
        }
      }
    },
    "/api/internal/analyst/support": {
      "get": {
        "tags": [
          "internal"
        ],
        "summary": "Read bounded PostHog support tickets",
        "description": "Returns recent support ticket content for the operating analyst. Ticket access is bounded and bearer-gated; replies and status changes are not exposed here.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 720,
              "default": 24
            },
            "required": false,
            "name": "hours",
            "in": "query"
          },
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            },
            "required": false,
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "maxLength": 80
            },
            "required": false,
            "name": "ticketId",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "new",
                "open",
                "pending",
                "on_hold",
                "resolved"
              ]
            },
            "required": false,
            "name": "status",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Recent tickets or one ticket thread.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "nullable": true
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid analyst token"
          }
        }
      }
    },
    "/api/internal/analyst/invitations": {
      "get": {
        "tags": [
          "internal"
        ],
        "summary": "Read the bounded invitation applicant queue",
        "description": "Returns applicants, their intake answers, and the optional brand link for review. It cannot approve, decline, or modify an application.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 720,
              "default": 24
            },
            "required": false,
            "name": "hours",
            "in": "query"
          },
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            },
            "required": false,
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "invited",
                "declined",
                "all"
              ],
              "default": "pending"
            },
            "required": false,
            "name": "status",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Applicant queue.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "nullable": true
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid analyst token"
          }
        }
      }
    },
    "/api/internal/analyst/rejections": {
      "get": {
        "tags": [
          "internal"
        ],
        "summary": "Read explicit generation rejection evidence",
        "description": "Returns explicit regenerate/delete gestures and low ratings with their judge context. Silent frames are never counted as rejection.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 720,
              "default": 24
            },
            "required": false,
            "name": "hours",
            "in": "query"
          },
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            },
            "required": false,
            "name": "limit",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Recent explicit rejection evidence.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "nullable": true
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid analyst token"
          }
        }
      }
    },
    "/api/internal/analyst/assets/{id}/preview": {
      "get": {
        "tags": [
          "internal"
        ],
        "summary": "Read one bounded asset thumbnail for visual analysis",
        "description": "Streams only an existing generated thumbnail after bearer authentication. It never lists R2 keys, serves originals, or grants bucket access.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Thumbnail bytes"
          },
          "401": {
            "description": "Missing or invalid analyst token"
          },
          "404": {
            "description": "Unknown asset, deleted asset, missing thumbnail, or missing object"
          }
        }
      }
    },
    "/api/internal/analyst/actions/invitations/{id}/approve": {
      "post": {
        "tags": [
          "internal"
        ],
        "summary": "Approve one invitation applicant after human confirmation",
        "description": "Uses a separate action credential, resolves the email from the pending application row, issues the existing idempotent platform invitation, and appends an Eve audit record.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "minLength": 3,
                    "maxLength": 500
                  }
                },
                "required": [
                  "reason"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The applicant was invited, or already held the same invitation.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "nullable": true
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid action token"
          },
          "404": {
            "description": "Unknown application"
          },
          "409": {
            "description": "Application is no longer pending"
          }
        }
      }
    },
    "/api/internal/analyst/actions/invitations/{id}/research": {
      "post": {
        "tags": [
          "internal"
        ],
        "summary": "Research one applicant's safe public storefront",
        "description": "Resolves the stored applicant email and link, rejects social/private hosts, fetches only the public homepage and Shopify products endpoint, and persists bounded findings.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Bounded storefront research.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "nullable": true
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid action token"
          },
          "404": {
            "description": "Unknown application"
          }
        }
      }
    },
    "/api/internal/analyst/actions/recordings/{id}/summarize": {
      "post": {
        "tags": [
          "internal"
        ],
        "summary": "Ask PostHog to analyze one session replay",
        "description": "Triggers PostHog's asynchronous single-session summary workflow with a bounded focus area. It does not return snapshots or enable public sharing.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 100
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "focusArea": {
                    "type": "string",
                    "minLength": 3,
                    "maxLength": 500
                  }
                },
                "required": [
                  "focusArea"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "PostHog accepted the replay for asynchronous analysis.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "nullable": true
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid action token"
          },
          "503": {
            "description": "PostHog analysis is not configured"
          }
        }
      }
    },
    "/api/toolkit/assets/batch": {
      "patch": {
        "tags": [
          "assets"
        ],
        "summary": "Move or re-file many pieces at once",
        "description": "`{ assetIds, folderId?, kind? }` — the batch twin of `PATCH /api/toolkit/assets/:id`, minus `name` and `sku` (those are one piece's own words and have no batch meaning).\n\n`folderId: null` moves the whole selection back to the root. `kind: null` un-files them.\n\n`updated` is the number the server actually touched — ids this house does not own, or that were already taken out, are skipped rather than failing the batch.\n\n`approved` (spec 021) is THE DIRECTOR'S DECISION on a selection: `true` stamps her name and the time on every frame, `false` writes both back to null. This is the door the one-tap adopt offer rides — a returned share's keeps become a PRE-FILLED decision she confirms, never an automation. The house never sets this by itself.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "assetIds": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "minLength": 1
                    },
                    "minItems": 1,
                    "maxItems": 500
                  },
                  "folderId": {
                    "type": "string",
                    "nullable": true
                  },
                  "kind": {
                    "type": "string",
                    "nullable": true
                  },
                  "approved": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "assetIds"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "How many pieces were re-filed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "updated": {
                      "type": "number"
                    }
                  },
                  "required": [
                    "updated"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "BAD_KIND, or none of `folderId` / `kind` / `approved` was given",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "The target collection is not this house's",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/assets/batch/delete": {
      "post": {
        "tags": [
          "assets"
        ],
        "summary": "Take many pieces out of the library",
        "description": "`{ assetIds }` — a SOFT take-out. `assets.deleted_at` is stamped, which every read path in the service already filters on; the row stays, the R2 object stays, nothing is destroyed.\n\n`deleted` is counted SERVER-SIDE from the ids this house actually owns and had not already taken out. The client's own count is never used for anything.\n\nA `viewer` may look at the library and may not empty it.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "assetIds": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "minLength": 1
                    },
                    "minItems": 1,
                    "maxItems": 500
                  }
                },
                "required": [
                  "assetIds"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "How many pieces were taken out",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "number"
                    }
                  },
                  "required": [
                    "deleted"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "A viewer cannot take pieces out",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/assets/batch/quote": {
      "post": {
        "tags": [
          "assets"
        ],
        "summary": "What would this cost, for all of them",
        "description": "`{ assetIds, operation }` → one summed quote for the whole selection, spending nothing and reserving nothing. `items` is how many of the ids this house really holds; `tokens` is the price book's per-piece price times that number; `affordable` compares it to the house's spendable balance.\n\n`operation` is `remove-background` or `packshot`.\n\n`quality` is optional and only read for `packshot` — it prices the batch at that resolution, so a size control on the calling surface can move the number it charges. Omit it and the batch prices at the house's default.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "assetIds": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "minLength": 1
                    },
                    "minItems": 1,
                    "maxItems": 500
                  },
                  "operation": {
                    "type": "string",
                    "enum": [
                      "remove-background",
                      "packshot"
                    ]
                  },
                  "quality": {
                    "type": "string",
                    "enum": [
                      "1k",
                      "2k",
                      "4k"
                    ]
                  },
                  "alsoRemoveBackground": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "assetIds",
                  "operation"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The whole selection's price, before anything runs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "operation": {
                      "type": "string"
                    },
                    "items": {
                      "type": "number"
                    },
                    "perItem": {
                      "type": "number"
                    },
                    "tokens": {
                      "type": "number"
                    },
                    "spendable": {
                      "type": "number"
                    },
                    "affordable": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "operation",
                    "items",
                    "perItem",
                    "tokens",
                    "spendable",
                    "affordable"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/assets/pack": {
      "post": {
        "tags": [
          "assets"
        ],
        "summary": "Take a whole selection as one file",
        "description": "`{ assetIds }` → one archive of the frames this house really owns, at any size.\n\nThe server decides the tier and SAYS which, so the caller never has to. Under 250 frames AND 256 MiB the archive is assembled inside the request and `status` comes back `ready` — fetch `href` and the download starts at once. Above either, `mode` is `pack`, `status` is `queued`, `seconds` is roughly how long it will take, and a `pack.ready` notification arrives on the existing socket when the archive is up. Either way `href` is the same URL and the link lives 7 days.\n\nRefused above 700 frames or 4 GiB — the ceiling a single background invocation fits. Split the selection.\n\nFREE. Nothing here reads the price book and nothing here reserves tokens: these are her own files leaving her own library.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "assetIds": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "minLength": 1
                    },
                    "minItems": 1,
                    "maxItems": 700
                  },
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80
                  },
                  "sizes": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 60
                        },
                        "width": {
                          "type": "integer",
                          "minimum": 16,
                          "maximum": 12000
                        },
                        "height": {
                          "type": "integer",
                          "minimum": 16,
                          "maximum": 12000
                        }
                      },
                      "required": [
                        "name",
                        "width",
                        "height"
                      ]
                    },
                    "maxItems": 6
                  }
                },
                "required": [
                  "assetIds"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Ready now — fetch `href`",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "packId": {
                      "type": "string"
                    },
                    "mode": {
                      "type": "string",
                      "enum": [
                        "instant",
                        "pack"
                      ]
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "queued",
                        "running",
                        "ready",
                        "failed",
                        "expired"
                      ]
                    },
                    "items": {
                      "type": "number"
                    },
                    "bytes": {
                      "type": "number"
                    },
                    "href": {
                      "type": "string"
                    },
                    "filename": {
                      "type": "string"
                    },
                    "seconds": {
                      "type": "number",
                      "nullable": true
                    },
                    "error": {
                      "type": "string",
                      "nullable": true
                    }
                  },
                  "required": [
                    "packId",
                    "mode",
                    "status",
                    "items",
                    "bytes",
                    "href",
                    "filename",
                    "seconds",
                    "error"
                  ]
                }
              }
            }
          },
          "202": {
            "description": "Packing in the background; the bar will say when",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "packId": {
                      "type": "string"
                    },
                    "mode": {
                      "type": "string",
                      "enum": [
                        "instant",
                        "pack"
                      ]
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "queued",
                        "running",
                        "ready",
                        "failed",
                        "expired"
                      ]
                    },
                    "items": {
                      "type": "number"
                    },
                    "bytes": {
                      "type": "number"
                    },
                    "href": {
                      "type": "string"
                    },
                    "filename": {
                      "type": "string"
                    },
                    "seconds": {
                      "type": "number",
                      "nullable": true
                    },
                    "error": {
                      "type": "string",
                      "nullable": true
                    }
                  },
                  "required": [
                    "packId",
                    "mode",
                    "status",
                    "items",
                    "bytes",
                    "href",
                    "filename",
                    "seconds",
                    "error"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "None of those ids are this house's",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "413": {
            "description": "Past what one pack can carry — split the selection",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "503": {
            "description": "Sizes were asked for and this tier cannot cut them",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/packs/{id}": {
      "get": {
        "tags": [
          "assets"
        ],
        "summary": "Where a pack got to",
        "description": "The refresh/reconnect read. The LIVE path is the socket — a `pack.ready` notification arrives the moment the archive is up, and nothing in this house polls this route on a timer (CLAUDE.md: push, don't poll). It exists for the tab that was closed while the pack was building and the bar line that is opened tomorrow.",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The pack",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "packId": {
                      "type": "string"
                    },
                    "mode": {
                      "type": "string",
                      "enum": [
                        "instant",
                        "pack"
                      ]
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "queued",
                        "running",
                        "ready",
                        "failed",
                        "expired"
                      ]
                    },
                    "items": {
                      "type": "number"
                    },
                    "bytes": {
                      "type": "number"
                    },
                    "href": {
                      "type": "string"
                    },
                    "filename": {
                      "type": "string"
                    },
                    "seconds": {
                      "type": "number",
                      "nullable": true
                    },
                    "error": {
                      "type": "string",
                      "nullable": true
                    }
                  },
                  "required": [
                    "packId",
                    "mode",
                    "status",
                    "items",
                    "bytes",
                    "href",
                    "filename",
                    "seconds",
                    "error"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "No such pack in this house",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/chat/context/warm": {
      "post": {
        "tags": [
          "chat"
        ],
        "summary": "Prepare private chat context",
        "description": "Starts a background refresh of the signed-in person's house and personal memory. It returns immediately and never exposes the cached memory to the browser.",
        "responses": {
          "202": {
            "description": "The background refresh started.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "warming": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  },
                  "required": [
                    "warming"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "No signed-in person.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/invite/request": {
      "post": {
        "tags": [
          "invite"
        ],
        "summary": "Request an invitation to the house",
        "description": "Public, unauthenticated. Rate-limited per IP. A duplicate address answers 200 with status `already_requested` rather than an error — asking twice is not a fault.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InviteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The request was recorded (or already was).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteRequestResult"
                }
              }
            }
          },
          "400": {
            "description": "Invalid body, or a disposable email domain.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteRequestError"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests from this address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteRequestError"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/invite/preview": {
      "post": {
        "tags": [
          "invite"
        ],
        "summary": "Read what her links say about her",
        "description": "Public, unauthenticated, rate-limited per IP. Reads the OpenGraph tags and site icon of up to 6 public pages so the invitation wizard can ask 'is this you?'. A link that cannot be read comes back with `ok: false` rather than failing the call — one bad row must never cost her the others.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LinkPreviewRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One card per link, in the order they were sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LinkPreviewResult"
                }
              }
            }
          },
          "400": {
            "description": "No links, or more than six.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LinkPreviewError"
                }
              }
            }
          },
          "429": {
            "description": "Too many fresh reads from this address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LinkPreviewError"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/admin/invite-requests": {
      "get": {
        "tags": [
          "admin",
          "invite"
        ],
        "summary": "List invitation requests (admin)",
        "responses": {
          "200": {
            "description": "Every request, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "requests": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/InviteRequestRow"
                      }
                    }
                  },
                  "required": [
                    "requests"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "Not an admin",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteRequestError"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/admin/onboarding": {
      "get": {
        "tags": [
          "admin",
          "onboarding"
        ],
        "summary": "Who is setting up, and how far they got (admin)",
        "responses": {
          "200": {
            "description": "Everyone who has started setting up, latest movement first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "onboardings": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AdminOnboardingRow"
                      }
                    }
                  },
                  "required": [
                    "onboardings"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "Not an admin",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/intake": {
      "post": {
        "tags": [
          "intake"
        ],
        "summary": "Record the answers given at the door",
        "description": "Public, unauthenticated, rate-limited per IP. ASKED ONCE, FIRST ANSWER WINS — the staging table's primary key is the normalised email and the write is ON CONFLICT DO NOTHING, so a second call for the same identity answers 200 with `recorded: false` and changes nothing. Run it twice with different answers to see it. A `code` write is only stored when that address already holds an invitation; when it does not, the answer is the same 200 `recorded: false`, so this door cannot be used to find out who was invited. The answers are adopted onto the house at signup — nothing here creates an account, an organisation or a session.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/IntakeRecord"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The answers were taken (or an answer was already on file).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IntakeResult"
                }
              }
            }
          },
          "400": {
            "description": "Invalid body — a bad address, or an answer id we do not know.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IntakeError"
                }
              }
            }
          },
          "429": {
            "description": "Too many writes from this address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IntakeError"
                }
              }
            }
          }
        }
      }
    },
    "/api/share/{token}": {
      "get": {
        "tags": [
          "share"
        ],
        "summary": "Open a send (no account, no session)",
        "description": "The manifest a brand's decision-maker's browser reads. **No `requireActor`** — authority is the 256-bit token, which lives in the page's URL FRAGMENT and therefore reaches no server log, no `Referer` and no proxy.\n\nIt answers with LABELS AND POSITIONS ONLY: no asset ids, job ids, user ids, org id, R2 keys, judge verdicts, scores, SKUs, lineage or prices. Frames are addressed by `i` — an index into this share's own ordered array — so there is nothing to enumerate.\n\nExpired, revoked and never-existed are BYTE-IDENTICAL 404s, on purpose.",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "token",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The frames she sent, in her order",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareManifest"
                }
              }
            }
          },
          "404": {
            "description": "Expired, revoked, or never existed — one body for all three",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareError"
                }
              }
            }
          }
        }
      }
    },
    "/api/share/{token}/f/{i}": {
      "get": {
        "tags": [
          "share"
        ],
        "summary": "The bytes of one frame in a send",
        "description": "Full quality, byte-for-byte the original R2 object — no re-encode, no watermark, no downscale. `?thumb=1` serves the small one when the house holds one and falls back to the original when it does not. `?export=1` means the recipient is TAKING the bytes, and writes the same first-party keep signal the library writes (never on a thumbnail — a wall is not a keep).\n\n`Cache-Control: private, max-age=3600` and NOT the library's year-long immutable: revocation cannot reach into a cache, so an hour bounds how long a revoked link's bytes survive in the recipient's browser. Revoking stops new reads; it cannot un-download what was already taken.\n\n`i` is a position in this share's array. Past its end is a 404, like everything else here.",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "token",
            "in": "path"
          },
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "i",
            "in": "path"
          },
          {
            "schema": {
              "type": "string"
            },
            "required": false,
            "name": "thumb",
            "in": "query"
          },
          {
            "schema": {
              "type": "string"
            },
            "required": false,
            "name": "export",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "The image."
          },
          "404": {
            "description": "Expired, revoked, out of range, or never existed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareError"
                }
              }
            }
          }
        }
      }
    },
    "/api/share/{token}/mark": {
      "post": {
        "tags": [
          "share"
        ],
        "summary": "Say keep or pass on one frame",
        "description": "`{ i, verdict, note?, drawings? }` — an UPSERT keyed on (share, frame), so a reviewer may change their mind and the last word stands. One mark per frame per LINK, which is one mark per recipient per frame: two decision-makers are two links.\n\n**THE HANDLER TAKES NO ASSET ID FROM THE WIRE.** It resolves `share.asset_ids[i]` server-side, so a mark can only ever touch a frame inside this share — narrow by construction, not by a check.\n\n`drawings` is COORDINATES: `[{kind:'circle'|'free', points:[[x,y],…]}]`, every coordinate a finite float in [0,1], at most 3 strokes of at most 120 points. SVG, HTML, a `d` string, an object, `NaN` or a coordinate of 1.5 are each a 400 — the recipient sends numbers, and the house draws them in its own ink.\n\nThis writes ONE notification per share, on the FIRST mark. The count she reads (\"12 of 14\") is joined at READ time, so one row stays true as the reviewer keeps going.",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "token",
            "in": "path"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ShareMarkBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The mark stands",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "recorded": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  },
                  "required": [
                    "recorded"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "A note over 600 characters, or a drawing that is not numbers",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareError"
                }
              }
            }
          },
          "404": {
            "description": "Expired, revoked, out of range, or never existed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareError"
                }
              }
            }
          },
          "429": {
            "description": "Too many marks from one address in an hour",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareError"
                }
              }
            }
          }
        }
      }
    },
    "/api/share/{token}/finish": {
      "post": {
        "tags": [
          "share"
        ],
        "summary": "Say you are done",
        "description": "The end of the walk. A reviewer never really signs off — they just stop — so until this existed there was no moment the house could tell the sender about. This is them SAYING so, which is a different fact from the marks.\n\nIDEMPOTENT. Finishing twice keeps the FIRST stamp, writes no second notification and sends no second email, because \"when did they tell me they were done\" has one honest answer. It does NOT close the link: they may reopen it, change a mark and finish again.\n\nIT REACHES HER TWICE OVER, and both are the same fact: an in-app notification (`share.finished`) AND an email to the member who minted the link, carrying the counts and a way back to the frames. The email replies to the address she mailed the link to, when she used one, so answering the approver is one tap. A director who is not in the app still learns her frames came back.\n\nAnswering with nothing marked is refused — there is no news in it.\n\nNo account, no session, no tokens, rate-limited like the mark door.",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "token",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Their answers are with her",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "finished": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "kept": {
                      "type": "number"
                    },
                    "passed": {
                      "type": "number"
                    }
                  },
                  "required": [
                    "finished",
                    "kept",
                    "passed"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Nothing was marked, so there is nothing to send",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareError"
                }
              }
            }
          },
          "404": {
            "description": "Expired, revoked, or never existed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareError"
                }
              }
            }
          }
        }
      }
    },
    "/api/share": {
      "post": {
        "tags": [
          "share"
        ],
        "summary": "Mint a link a brand can open with no account",
        "description": "`{ assetIds, recipientLabel? }` — freezes the selection into an ORDERED SNAPSHOT and returns `{ url }`. The link is `{WEB_ORIGIN}/s#<token>`: the token is in the FRAGMENT, which is never sent to any server, so it appears in no access log, no API log, no `Referer` and no proxy.\n\nA share is a snapshot, never a live view. Adding a frame to the lookbook afterwards does not change an open link — an approval is an approval of what the approver SAW. She wants the brand to see two more frames? She sends a second link.\n\n`recipientLabel` is free text SHE types (\"Maya at Ganni\"), or nothing, in which case every surface says \"the link holder\". FLAM never emails the recipient and never cookies them.\n\nCOSTS NOTHING. No job, no tool slug, no reserve, no price-book row.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ShareCreateBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The link, ready to copy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string"
                    },
                    "token": {
                      "type": "string"
                    },
                    "frames": {
                      "type": "number"
                    },
                    "emailed": {
                      "type": "string",
                      "nullable": true,
                      "enum": [
                        "sent",
                        "skipped",
                        "failed",
                        null
                      ]
                    }
                  },
                  "required": [
                    "id",
                    "url",
                    "token",
                    "frames",
                    "emailed"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "No frames, more than 60 of them, or a bad recipient address",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareError"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareError"
                }
              }
            }
          },
          "404": {
            "description": "None of those frames are this house's",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareError"
                }
              }
            }
          },
          "429": {
            "description": "Too many links minted by this house in an hour",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareError"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "share"
        ],
        "summary": "What this house has sent",
        "description": "Newest first. `answered` and `kept` are COUNTED AT READ TIME from `share_mark` — there is no stored rollup anywhere in this feature, because a stored rollup drifts the first time a mark changes and then the board lies, which is worse than no board.\n\nA revoked or expired link still appears here WITH ITS MARKS. Expiry closes a door; it does not retract an opinion, and the marks are hers forever.",
        "responses": {
          "200": {
            "description": "Her sends",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "shares": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ShareSummary"
                      }
                    }
                  },
                  "required": [
                    "shares"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareError"
                }
              }
            }
          }
        }
      }
    },
    "/api/share/{id}/revoke": {
      "post": {
        "tags": [
          "share"
        ],
        "summary": "Close a link now",
        "description": "Stamps `revoked_at`. Enforced on EVERY request to every public door from the next one onward — never by hiding a row from a list, and never by the client.\n\nTHE HONEST SENTENCE, and the docs page says it too: revoking stops new reads; it cannot un-download what was already downloaded. The share bytes carry `max-age=3600`, so an hour bounds how long they survive in a browser that already has them.\n\nThe marks stay. They are hers.",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Closed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "revoked": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "revoked"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareError"
                }
              }
            }
          },
          "404": {
            "description": "Not this house's link",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareError"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/share-marks": {
      "get": {
        "tags": [
          "share"
        ],
        "summary": "What came back, on these frames",
        "description": "`?assetIds=a,b,c` — the recipients' marks on the named frames, for her library tiles and her lightbox. Org-scoped; a frame she does not own returns nothing rather than an error.\n\nWHEN TWO RECIPIENTS DISAGREE, BOTH ROWS COME BACK AND NO RULE PICKS A WINNER. Maya keeping frame 7 while Tom passes it is not a data problem — it is the most important thing on the screen, and a house that silently computes a winner has hidden the one fact she needed. The tie is broken by her, in the decision layer.",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "assetIds",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Evidence, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "marks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ShareMarkView"
                      }
                    }
                  },
                  "required": [
                    "marks"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareError"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/folders/{id}/coverage": {
      "get": {
        "tags": [
          "share"
        ],
        "summary": "What is done in this collection, and what is still missing",
        "description": "DERIVED AT READ TIME. There is no status column, no stored rollup and no cached count anywhere in this feature — a stored rollup drifts the first time a mark, an approval or a look's cast changes, and then the board lies, which is worse than no board. The source rows ARE the state.\n\n`missing` counts pieces with **no director-approved frame**. `approved` there means `assets.approved_at IS NOT NULL` — HER decision, never a recipient's keep. A recipient's enthusiasm is not completion.\n\nPer piece, four states: `covered` (≥1 approved frame, or the piece's own packshot approved), `changes-asked` (an open `pass` came back and nothing of this piece is approved — an unresolved disagreement is unfinished work), `awaiting` (in a live share, nothing back yet), `not-sent`.",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The collection's coverage",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "pieces": {
                      "type": "number"
                    },
                    "covered": {
                      "type": "number"
                    },
                    "missing": {
                      "type": "number"
                    },
                    "states": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string",
                        "enum": [
                          "covered",
                          "changes-asked",
                          "awaiting",
                          "not-sent"
                        ]
                      }
                    }
                  },
                  "required": [
                    "pieces",
                    "covered",
                    "missing",
                    "states"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareError"
                }
              }
            }
          },
          "404": {
            "description": "Not this house's collection",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareError"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/admin/invite": {
      "post": {
        "tags": [
          "admin",
          "invite"
        ],
        "summary": "Open the door for one email (admin)",
        "description": "Issues the PLATFORM-gate invitation: an `invitations` row plus permanent email-keyed studio access, so the invitee lands usable on signup. Idempotent — re-issuing an open door returns the same token and opens nothing twice. This is NOT the seat invitation (`/api/toolkit/org/seats/invite`).",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InviteIssue"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The door is open for this address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteIssueResult"
                }
              }
            }
          },
          "400": {
            "description": "Bad address",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteRequestError"
                }
              }
            }
          },
          "403": {
            "description": "Not an admin",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteRequestError"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/org/seats/invite": {
      "post": {
        "tags": [
          "organization",
          "invite"
        ],
        "summary": "Invite a teammate into this house",
        "description": "Organization-level. Owner or admin only, and only an owner may offer an owner's seat. Re-inviting the same address supersedes the pending invitation rather than stacking a second one. NOT the platform gate (`/api/toolkit/admin/invite`).",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SeatInvite"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The seat is offered.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SeatInviteResult"
                }
              }
            }
          },
          "400": {
            "description": "Already a member, or a bad address",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteRequestError"
                }
              }
            }
          },
          "401": {
            "description": "No session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteRequestError"
                }
              }
            }
          },
          "403": {
            "description": "This role may not invite",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteRequestError"
                }
              }
            }
          },
          "429": {
            "description": "This house has offered its weekly limit of seats",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteRequestError"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/org/seats/accept": {
      "post": {
        "tags": [
          "organization",
          "invite"
        ],
        "summary": "Take an offered seat",
        "description": "Auth-gated, and the session's email must match the invited one. Writes the `member` row and makes that house the caller's ACTIVE one, so their very next request resolves to it rather than to their personal house.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SeatAccept"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The seat is taken.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SeatAcceptResult"
                }
              }
            }
          },
          "401": {
            "description": "No session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteRequestError"
                }
              }
            }
          },
          "403": {
            "description": "This seat is for a different email",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteRequestError"
                }
              }
            }
          },
          "404": {
            "description": "No such seat, or it lapsed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteRequestError"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/admin/invite-codes": {
      "post": {
        "tags": [
          "admin",
          "invite"
        ],
        "summary": "Mint a shareable invitation code (admin)",
        "description": "Mints `LABEL-XXXXXX` — a vanity prefix plus six characters of Crockford base32. Redeeming it writes an ordinary invitation for the redeemer's address, so the invite-only gate is unchanged. `maxUses` is mandatory: a code that travels without a cap is an uncapped spend.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InviteCodeCreate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The code, in full. Copy it now or read it back from the list later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteCodeRow"
                }
              }
            }
          },
          "400": {
            "description": "Missing a label, a cap, or a sane number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteCodeError"
                }
              }
            }
          },
          "403": {
            "description": "Not an admin",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteCodeError"
                }
              }
            }
          },
          "409": {
            "description": "The minted code collided. Ask again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteCodeError"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "admin",
          "invite"
        ],
        "summary": "List invitation codes (admin)",
        "description": "Newest first, live and dead together. `usedCount` is doors opened; `joinedCount` is how many of those addresses became an account — the conversion of one channel.",
        "responses": {
          "200": {
            "description": "Every code the house has minted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "codes": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/InviteCodeRow"
                      }
                    }
                  },
                  "required": [
                    "codes"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "Not an admin",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteCodeError"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/admin/invite-codes/revoke": {
      "post": {
        "tags": [
          "admin",
          "invite"
        ],
        "summary": "Kill an invitation code (admin)",
        "description": "Immediate and final — the code stops redeeming on the next request. Nobody who already came through it loses anything: their invitation and their tokens are theirs. POST, not DELETE: this house registers its destructive verbs as POST (see /admin/offerings/retire, whose DELETE answered 404 for a week).",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InviteCodeRevoke"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The code is dead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteCodeRow"
                }
              }
            }
          },
          "400": {
            "description": "No id given",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteCodeError"
                }
              }
            }
          },
          "403": {
            "description": "Not an admin",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteCodeError"
                }
              }
            }
          },
          "404": {
            "description": "No such code",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteCodeError"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/invite/redeem": {
      "post": {
        "tags": [
          "invite"
        ],
        "summary": "Redeem an invitation code",
        "description": "Public, unauthenticated, rate-limited per IP. Opens the platform door for ONE address by writing its invitation — it does not create an account and does not sign anybody in. The caller signs up normally afterwards, through the unchanged invite-only gate. Redeeming for an address that is already invited consumes no use of the code.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InviteRedeem"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The door is open for this address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteRedeemResult"
                }
              }
            }
          },
          "400": {
            "description": "Not a code, a throwaway address, or the code is spent / expired / revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteCodeError"
                }
              }
            }
          },
          "404": {
            "description": "No such code",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteCodeError"
                }
              }
            }
          },
          "429": {
            "description": "Too many attempts from this address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteCodeError"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/invites/mine": {
      "get": {
        "tags": [
          "invite"
        ],
        "summary": "This house's invitation link and who has used it",
        "description": "The promotion panel: one shareable link, how many invites are left, and every address the link has let in with whether they signed up and whether they subscribed. Scoped to the acting HOUSE. Self-healing — a house that predates the promotion (or whose signup hook did not run) gets its code minted on the first read, so nobody has to be backfilled twice.",
        "responses": {
          "200": {
            "description": "The link, the remaining invites, and the guest list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PromotionPanel"
                }
              }
            }
          },
          "401": {
            "description": "Signed out",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PromotionError"
                }
              }
            }
          },
          "503": {
            "description": "The code could not be minted. Try again in a moment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PromotionError"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/support/tickets": {
      "post": {
        "tags": [
          "support"
        ],
        "summary": "Raise a support ticket, or reply on one",
        "description": "Opens a ticket in FLAM's help desk (PostHog Support) on behalf of the acting house. The plan, the spendable balance and the last develop are read off the database HERE and attached to the message — the director never describes their own account, and what reaches us is measured rather than remembered. Pass `ticketId` to reply on an existing thread instead. Any role may raise one, `viewer` included: asking for help is not spending.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SupportTicketCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The ticket, as the screen shows it — or a noted report with a null id when she was only telling us how it is going and asked for no reply.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SupportTicketRaised"
                }
              }
            }
          },
          "400": {
            "description": "Say a little more — a subject and a few sentences.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SupportTicketError"
                }
              }
            }
          },
          "401": {
            "description": "Signed out",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SupportTicketError"
                }
              }
            }
          },
          "503": {
            "description": "The desk is unreachable. The sentence tells the director where else to write.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SupportTicketError"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "support"
        ],
        "summary": "The tickets this director has raised",
        "description": "Newest first, open and closed together, read from FLAM's help desk under a verified identity — so a director's threads follow their account rather than the browser they opened them in.",
        "responses": {
          "200": {
            "description": "Every ticket this director has raised.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tickets": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SupportTicket"
                      }
                    }
                  },
                  "required": [
                    "tickets"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Signed out",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SupportTicketError"
                }
              }
            }
          },
          "503": {
            "description": "The desk is unreachable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SupportTicketError"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/support/screenshots": {
      "post": {
        "tags": [
          "support"
        ],
        "summary": "Attach a screenshot to a support ticket",
        "description": "Stores one screenshot and returns a signed, expiring link to put in a ticket. PostHog Support takes attachments on email and Slack but not on the widget API FLAM posts through, so the image is hosted here and the ticket carries the link. The file is deleted after SUPPORT_SHOT_RETENTION_DAYS and the link expires with it.",
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The signed link, ready to paste into a ticket.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string"
                    },
                    "expiresAt": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "id",
                    "url",
                    "expiresAt"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Signed out"
          },
          "413": {
            "description": "Too large"
          },
          "415": {
            "description": "Not an image we accept"
          },
          "503": {
            "description": "Screenshots are not configured on this deployment."
          }
        }
      }
    },
    "/api/toolkit/support/screenshots/{id}": {
      "get": {
        "tags": [
          "support"
        ],
        "summary": "Open a screenshot from a support ticket",
        "description": "Deliberately NOT session-gated: the reader is a support engineer in PostHog's inbox, not a signed-in director. Authority comes from the signature, which is scoped to this one object and expires with the file.",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "o",
            "in": "query"
          },
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "e",
            "in": "query"
          },
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "s",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "The image."
          },
          "403": {
            "description": "Bad or expired signature."
          },
          "404": {
            "description": "Gone — screenshots are deleted on a retention clock."
          }
        }
      }
    },
    "/api/toolkit/support/tickets/{id}": {
      "get": {
        "tags": [
          "support"
        ],
        "summary": "The messages on one ticket",
        "description": "The whole thread, oldest first. Internal notes are excluded by the help desk itself. Answers are written by a person — FLAM runs no bot on this path.",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "01927f3c-0000-7000-8000-000000000000"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The conversation.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string"
                    },
                    "messages": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SupportTicketMessage"
                      }
                    }
                  },
                  "required": [
                    "status",
                    "messages"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Signed out",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SupportTicketError"
                }
              }
            }
          },
          "503": {
            "description": "The desk is unreachable, or this thread is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SupportTicketError"
                }
              }
            }
          }
        }
      }
    },
    "/api/support/escalations": {
      "post": {
        "tags": [
          "support"
        ],
        "summary": "Record a support chat that needed a human",
        "description": "Machine door for FLAM's self-hosted Krispy support Worker. Opens a ticket in the same help desk the studio's ticket form writes to, carrying the transcript and — when the caller knows the house — the plan, the spendable balance and the last develop, read off the database HERE rather than taken from the body. Authenticated by a shared secret in `x-krispy-bridge-secret`, not by a session: the caller is a Worker.",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "…"
            },
            "required": true,
            "name": "x-krispy-bridge-secret",
            "in": "header"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SupportEscalationCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The ticket the conversation became.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SupportTicket"
                }
              }
            }
          },
          "401": {
            "description": "Wrong or missing shared secret.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SupportTicketError"
                }
              }
            }
          },
          "503": {
            "description": "The bridge is not configured in this environment, or the desk is unreachable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SupportTicketError"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/org/seats/role": {
      "post": {
        "tags": [
          "organization"
        ],
        "summary": "Change what a seat may do",
        "description": "Owner or admin. An admin may only re-seat a `member` or a `viewer` — never an owner, never another admin. `owner` is NOT settable here: handing the house over is `POST /api/toolkit/org/transfer`, which demotes the outgoing owner in the same write so a house never has two.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SeatRole"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The seat is changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SeatRoleResult"
                }
              }
            }
          },
          "401": {
            "description": "No session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseError"
                }
              }
            }
          },
          "403": {
            "description": "This seat may not do that",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseError"
                }
              }
            }
          },
          "404": {
            "description": "Nobody by that id sits in this house",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseError"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/org/seats/{userId}": {
      "delete": {
        "tags": [
          "organization"
        ],
        "summary": "Empty a seat (or leave the house yourself)",
        "description": "Owner or admin — and anyone may remove THEMSELVES, which is how a person leaves. Deletes exactly one `member` row. NOTHING ELSE MOVES: every frame, model, look and collection keys to `organization_id`, and `created_by_user_id` is attribution only, so the work stays with the house and keeps their name on it. The founder of a personal house cannot be removed from it — the house's id is derived from theirs and a lazy repair would silently re-seat them as owner.",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "required": true,
            "name": "userId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The seat is empty. The work stayed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SeatRemoveResult"
                }
              }
            }
          },
          "401": {
            "description": "No session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseError"
                }
              }
            }
          },
          "403": {
            "description": "This seat may not do that",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseError"
                }
              }
            }
          },
          "404": {
            "description": "Nobody by that id sits in this house",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseError"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/org/transfer": {
      "post": {
        "tags": [
          "organization"
        ],
        "summary": "Hand this house to somebody else",
        "description": "OWNER ONLY, and the one route that can create an `owner`. The outgoing owner is demoted to `admin` in the SAME statement that promotes the new one, so the house never has two owners and never has none. Money follows the seat: `canSpendMoney` is `owner` only, so the plan, top-ups and purchases move with it immediately.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/HouseTransfer"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The house has a new owner.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseTransferResult"
                }
              }
            }
          },
          "401": {
            "description": "No session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseError"
                }
              }
            }
          },
          "403": {
            "description": "Only an owner may hand the house over",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseError"
                }
              }
            }
          },
          "404": {
            "description": "Nobody by that id sits in this house",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseError"
                }
              }
            }
          }
        }
      }
    },
    "/api/keys": {
      "post": {
        "tags": [
          "keys"
        ],
        "summary": "Mint an organisation API key",
        "description": "Returns the raw key ONCE — it is stored only as a SHA-256 hash and cannot be shown again. Session auth only: an API key may not mint another API key.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80
                  },
                  "role": {
                    "type": "string",
                    "enum": [
                      "viewer",
                      "member"
                    ],
                    "default": "member"
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new key — `key` is shown only here",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "role": {
                      "type": "string",
                      "enum": [
                        "viewer",
                        "member"
                      ]
                    },
                    "prefix": {
                      "type": "string"
                    },
                    "lastUsedAt": {
                      "type": "string",
                      "nullable": true
                    },
                    "revokedAt": {
                      "type": "string",
                      "nullable": true
                    },
                    "createdAt": {
                      "type": "string"
                    },
                    "key": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "id",
                    "name",
                    "role",
                    "prefix",
                    "lastUsedAt",
                    "revokedAt",
                    "createdAt",
                    "key"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated, or authenticated with an API key",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "A viewer seat may not mint a token-spending key",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "keys"
        ],
        "summary": "List the house's API keys",
        "description": "Revoked keys are included (with `revokedAt` set) so the audit trail is visible.",
        "responses": {
          "200": {
            "description": "Every key this organisation has minted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "keys": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "role": {
                            "type": "string",
                            "enum": [
                              "viewer",
                              "member"
                            ]
                          },
                          "prefix": {
                            "type": "string"
                          },
                          "lastUsedAt": {
                            "type": "string",
                            "nullable": true
                          },
                          "revokedAt": {
                            "type": "string",
                            "nullable": true
                          },
                          "createdAt": {
                            "type": "string"
                          }
                        },
                        "required": [
                          "id",
                          "name",
                          "role",
                          "prefix",
                          "lastUsedAt",
                          "revokedAt",
                          "createdAt"
                        ]
                      }
                    }
                  },
                  "required": [
                    "keys"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated, or authenticated with an API key",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/keys/{id}": {
      "delete": {
        "tags": [
          "keys"
        ],
        "summary": "Revoke an API key",
        "description": "Takes effect on the next request — the key row is re-read from Postgres on every call, so there is no cache to wait out. The row is kept, not deleted.",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked (idempotent)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "revoked": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "revoked",
                    "id"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated, or authenticated with an API key",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "No such key in this organisation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/mcp": {
      "post": {
        "tags": [
          "mcp"
        ],
        "summary": "MCP server (Streamable HTTP, JSON responses)",
        "description": "Model Context Protocol endpoint. Send JSON-RPC 2.0: `initialize`, `tools/list`, `tools/call`, `ping`. Authenticate with an organisation API key (`Authorization: Bearer flam_sk_…`) or a session cookie — scope, role and 403s are identical either way. Notifications (no `id`) are answered 202 with an empty body. No SSE: a develop is started here and watched with the `get_develop` tool or the existing jobs WebSocket.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/McpRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The JSON-RPC response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/McpResponse"
                }
              }
            }
          },
          "202": {
            "description": "A notification was accepted (no body)"
          },
          "401": {
            "description": "No valid API key or session",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/webhooks": {
      "post": {
        "tags": [
          "webhooks"
        ],
        "summary": "Register an endpoint FLAM should call",
        "description": "Returns `secret` ONCE — it is the HMAC key for every delivery to this endpoint and is never returned again. Deliveries are signed to the standard-webhooks spec (`webhook-id`, `webhook-timestamp`, `webhook-signature: v1,<base64>`), so any standard-webhooks client verifies them unchanged. Owner or admin only.\n\nThe URL must be `https://` and must not name a private address. Refused: loopback (127.0.0.0/8, ::1, 0.0.0.0), RFC1918 (10/8, 172.16/12, 192.168/16), link-local (169.254.0.0/16 — cloud metadata — and fe80::/10), IPv6 unique-local (fc00::/7), IPv4-mapped forms of any of those, hostnames ending .internal/.local/.svc, and single-label hostnames. The same check runs again at delivery time, so re-pointing the name after registration does not get past it, and redirects are never followed.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "develop.done",
                        "verdict.written"
                      ]
                    },
                    "minItems": 1
                  }
                },
                "required": [
                  "url",
                  "events"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new endpoint — `secret` is shown only here",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string"
                    },
                    "events": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "active": {
                      "type": "boolean"
                    },
                    "createdAt": {
                      "type": "string"
                    },
                    "secret": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "id",
                    "url",
                    "events",
                    "active",
                    "createdAt",
                    "secret"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "BAD_URL — `reason` says which rule the URL broke",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "Only an owner or admin may register an endpoint",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "webhooks"
        ],
        "summary": "List the house's endpoints",
        "description": "Never returns `secret` — it exists only in the create response. Owner or admin only.",
        "responses": {
          "200": {
            "description": "Every endpoint this house has",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "endpoints": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string"
                          },
                          "events": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "active": {
                            "type": "boolean"
                          },
                          "createdAt": {
                            "type": "string"
                          }
                        },
                        "required": [
                          "id",
                          "url",
                          "events",
                          "active",
                          "createdAt"
                        ]
                      }
                    }
                  },
                  "required": [
                    "endpoints"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "Only an owner or admin may see the house's integrations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/webhooks/{id}": {
      "delete": {
        "tags": [
          "webhooks"
        ],
        "summary": "Remove an endpoint",
        "description": "Deletes the endpoint AND drains its queued deliveries (ON DELETE CASCADE) — nothing is left to POST to a URL you just removed. Org-scoped in the WHERE, so one house can never remove another's endpoint; an id that is not yours is a 404, not a 403. Owner or admin only.",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Removed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "deleted",
                    "id"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "Only an owner or admin may remove an endpoint",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint in this organisation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/notifications": {
      "get": {
        "tags": [
          "notifications"
        ],
        "summary": "The board — what happened while she was gone",
        "description": "ONE handler, three jobs. No params: the panel's newest page. `before=<cursor>`: the next page, using the opaque cursor from the previous response's `nextCursor`. `since=<eventSeq>`: the reconnect backfill, ascending, for a client that already holds rows.\n\n`unread` is ALWAYS returned and is always the whole truth, however the list is truncated — it is the badge, and it is bounded to the last 90 days so its cost stays O(recent) rather than O(the house's history) once she has pressed Mark all read.\n\n`unread` counts only MATURED rows (older than the grace window); the list deliberately does not filter by maturity, because she opened the panel on purpose. Each pending row carries `maturesInMs`, the server's own remainder — the client never reads its own clock.",
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "nullable": true,
              "minimum": 0
            },
            "required": false,
            "name": "since",
            "in": "query"
          },
          {
            "schema": {
              "type": "string"
            },
            "required": false,
            "name": "before",
            "in": "query"
          },
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200
            },
            "required": false,
            "name": "limit",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "The house's board, plus the badge",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "notifications": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "kind": {
                            "type": "string"
                          },
                          "subjectType": {
                            "type": "string"
                          },
                          "subjectId": {
                            "type": "string",
                            "nullable": true
                          },
                          "payload": {
                            "type": "object",
                            "additionalProperties": {
                              "nullable": true
                            }
                          },
                          "createdAt": {
                            "type": "string"
                          },
                          "maturesInMs": {
                            "type": "number"
                          },
                          "eventSeq": {
                            "type": "number"
                          },
                          "read": {
                            "type": "boolean"
                          },
                          "needsYou": {
                            "type": "number"
                          },
                          "answered": {
                            "type": "number"
                          }
                        },
                        "required": [
                          "id",
                          "kind",
                          "subjectType",
                          "subjectId",
                          "payload",
                          "createdAt",
                          "maturesInMs",
                          "eventSeq",
                          "read",
                          "needsYou",
                          "answered"
                        ]
                      }
                    },
                    "unread": {
                      "type": "number"
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true
                    }
                  },
                  "required": [
                    "notifications",
                    "unread",
                    "nextCursor"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "A query parameter did not read as a number",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/notifications/read": {
      "post": {
        "tags": [
          "notifications"
        ],
        "summary": "She saw it",
        "description": "`{ ids: [...] }` marks those rows; `{ all: true }` marks everything this house has that this person has not read. Idempotent — a second POST from a second tab inserts nothing and returns the ids it found already marked as `marked: 0`.\n\nNOTHING IS EVER DELETED. `Mark all read` writes receipts; the rows stay in the board, dated.\n\n`source` records HOW she came to know — `open` (she clicked the row), `presence` (she was looking at the thing when it landed, so the house stayed quiet), `all`, `chat`. It is the only way to audit whether presence is OVER-suppressing, which is a failure nobody files a ticket about.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "maxItems": 500
                  },
                  "all": {
                    "type": "boolean"
                  },
                  "source": {
                    "type": "string",
                    "enum": [
                      "open",
                      "presence",
                      "all",
                      "chat"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The ids that were newly marked read — never ids from another house",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "marked": {
                      "type": "number"
                    },
                    "ids": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  },
                  "required": [
                    "marked",
                    "ids"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Neither `ids` nor `all` was given",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/notifications/{id}/opinion": {
      "get": {
        "tags": [
          "notifications"
        ],
        "summary": "The facts behind the house's opinion of one board row",
        "description": "Resolves a matured notification to its develop's frames, their verdicts, the previous pass of each frame, and two 14-day aggregates over the house (the cross-run pattern and the learning floor) — then asks route() which surface may carry it. FACTS ONLY: an enum, ids, counts and vocabulary terms. The sentence is composed on the client, which is what keeps the hedge gate and the tool-noun gate greps instead of hopes.\n\n404 for a notification belonging to another house, indistinguishable from an id that does not exist. No model is called on this path, ever, and the judge's own `note` is never selected (ADMIN/L1 only).",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The opinion shape — facts, never a sentence",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "surface": {
                      "type": "string",
                      "enum": [
                        "bar",
                        "chat",
                        "email",
                        "none"
                      ]
                    },
                    "moment": {
                      "type": "string",
                      "enum": [
                        "ranking",
                        "one-sure",
                        "not-sure",
                        "better",
                        "broke",
                        "floor",
                        "pattern",
                        "stop",
                        "none"
                      ]
                    },
                    "subject": {
                      "type": "string",
                      "nullable": true
                    },
                    "href": {
                      "type": "string",
                      "nullable": true
                    },
                    "frames": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "jobId": {
                            "type": "string"
                          },
                          "ordinal": {
                            "type": "integer"
                          },
                          "code": {
                            "type": "string",
                            "nullable": true
                          },
                          "where": {
                            "type": "string",
                            "nullable": true
                          },
                          "confidence": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "sure",
                              "not-sure",
                              null
                            ]
                          },
                          "unreadable": {
                            "type": "boolean"
                          }
                        },
                        "required": [
                          "jobId",
                          "ordinal",
                          "code",
                          "where",
                          "confidence",
                          "unreadable"
                        ]
                      }
                    },
                    "tokensReleased": {
                      "type": "number",
                      "nullable": true
                    },
                    "patternCode": {
                      "type": "string",
                      "nullable": true
                    },
                    "patternWhere": {
                      "type": "string",
                      "nullable": true
                    },
                    "patternRuns": {
                      "type": "number",
                      "nullable": true
                    },
                    "floorCode": {
                      "type": "string",
                      "nullable": true
                    },
                    "floorWhere": {
                      "type": "string",
                      "nullable": true
                    },
                    "cost": {
                      "type": "number",
                      "nullable": true
                    }
                  },
                  "required": [
                    "surface",
                    "moment",
                    "subject",
                    "href",
                    "frames",
                    "tokensReleased",
                    "patternCode",
                    "patternWhere",
                    "patternRuns",
                    "floorCode",
                    "floorWhere",
                    "cost"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "No such notification in this house",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    }
  }
}
