{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/dotsy-ai/json-schema/dotsy-ai-site-schema.json",
  "title": "Site",
  "x-generated": "2026-11-06",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/dotsy-ai-openapi.yml#/components/schemas/Site",
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "description": "Stable site id (UUID). Unlike the slug, it never changes."
    },
    "slug": {
      "type": "string"
    },
    "title": {
      "type": [
        "string",
        "null"
      ]
    },
    "keyConvention": {
      "type": "string",
      "enum": [
        "flat",
        "directory"
      ]
    },
    "backend": {
      "type": "string",
      "enum": [
        "kv",
        "do-sqlite"
      ]
    },
    "status": {
      "type": "string",
      "enum": [
        "published",
        "draft"
      ]
    },
    "editingMode": {
      "type": "string",
      "enum": [
        "live",
        "staged"
      ]
    },
    "publishedAt": {
      "type": [
        "number",
        "null"
      ]
    },
    "publicUrl": {
      "type": "string",
      "description": "Canonical public site URL for this slug."
    },
    "liveUrl": {
      "type": "string",
      "description": "Where visitors reach the site: the connected primary custom domain when there is one, else the same as publicUrl."
    },
    "liveReady": {
      "type": "boolean",
      "description": "True after first publish; until then the public URL 404s."
    },
    "build": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "jobId": {
          "type": "string"
        },
        "phase": {
          "type": "string",
          "enum": [
            "pages",
            "assets",
            "done",
            "failed"
          ]
        },
        "pagesDone": {
          "type": "number"
        },
        "pagesTotal": {
          "type": "number"
        },
        "pagesLanded": {
          "type": "number"
        },
        "updatedAt": {
          "type": "number"
        }
      },
      "required": [
        "jobId",
        "phase",
        "pagesDone",
        "pagesTotal",
        "pagesLanded",
        "updatedAt"
      ],
      "description": "Active clone/build progress, or null when no build is running."
    },
    "building": {
      "type": "boolean",
      "description": "build != null && build.phase !== 'done' — clone/build still running."
    },
    "buildPhase": {
      "type": [
        "string",
        "null"
      ],
      "enum": [
        "pages",
        "assets",
        "done",
        "failed"
      ],
      "description": "Current build phase, or null when no build is active."
    },
    "editorReady": {
      "type": "boolean",
      "description": "!building && stats.pages > 0 — draft/editor is renderable. Distinct from liveReady (public URL)."
    },
    "createdAt": {
      "type": "number"
    },
    "updatedAt": {
      "type": "number"
    },
    "stats": {
      "type": "object",
      "properties": {
        "pages": {
          "type": "number"
        },
        "assets": {
          "type": "number",
          "description": "CMS uploads under assets/ only — not mirrored clone CDN bytes (see mirroredAssets)."
        },
        "storageBytes": {
          "type": "number"
        },
        "lastBuild": {
          "type": [
            "number",
            "null"
          ]
        }
      },
      "required": [
        "pages",
        "assets",
        "storageBytes",
        "lastBuild"
      ]
    },
    "pages": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "route": {
            "type": "string"
          },
          "updatedAt": {
            "type": "number"
          }
        },
        "required": [
          "key",
          "name",
          "route",
          "updatedAt"
        ]
      }
    },
    "pagesListTotal": {
      "type": "number",
      "description": "Total HTML pages matching pagesQ (or all pages when pagesQ is omitted)."
    },
    "nextPagesCursor": {
      "type": [
        "string",
        "null"
      ],
      "description": "Present when more HTML pages exist — pass as pagesCursor on the next call."
    },
    "pagesPartial": {
      "type": "boolean",
      "description": "True when `pages` is one window of the page-key space, not all of it (mirrors /pages's own `partial`) — follow nextPagesCursor rather than treating pagesListTotal as exact."
    },
    "statsPartial": {
      "type": "boolean",
      "description": "True when stats.pages/pagesListTotal is a floor from the KV arm's bounded meta-prefix probe, not an exact count. Distinct from pagesPartial, which is about the returned page WINDOW rather than the count."
    },
    "activity": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "published",
              "updated",
              "created"
            ]
          },
          "label": {
            "type": "string"
          },
          "at": {
            "type": "number"
          }
        },
        "required": [
          "kind",
          "label",
          "at"
        ]
      }
    },
    "needsYou": {
      "type": "integer",
      "description": "Only with `?notifications=1` when notifications are enabled: open Needs-you count for this site and the signed-in person (not API keys)."
    },
    "brief": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "purpose": {
          "type": "string",
          "minLength": 5,
          "maxLength": 280
        },
        "audience": {
          "type": "string",
          "maxLength": 200
        },
        "kind": {
          "type": "string",
          "enum": [
            "exploration",
            "production"
          ]
        },
        "priorities": {
          "type": "object",
          "properties": {
            "seo": {
              "type": "string",
              "enum": [
                "must",
                "nice",
                "skip"
              ]
            },
            "analytics": {
              "type": "string",
              "enum": [
                "must",
                "nice",
                "skip"
              ]
            },
            "design": {
              "type": "string",
              "enum": [
                "must",
                "nice",
                "skip"
              ]
            },
            "mobile": {
              "type": "string",
              "enum": [
                "must",
                "nice",
                "skip"
              ]
            }
          }
        },
        "editingModel": {
          "type": "string",
          "enum": [
            "ai",
            "simple-editor",
            "creator"
          ]
        },
        "design": {
          "type": "object",
          "properties": {
            "palette": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "maxItems": 8
            },
            "type": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "maxItems": 6
            },
            "layout": {
              "type": "string",
              "maxLength": 80
            }
          },
          "additionalProperties": false
        },
        "hasBody": {
          "type": "boolean"
        },
        "businessName": {
          "type": "string"
        }
      },
      "required": [
        "purpose",
        "hasBody"
      ],
      "description": "Only with `?brief=1`: the head of the site brief (purpose, audience, priorities…), or null when none is set."
    },
    "guidance": {
      "type": "object",
      "properties": {
        "rules": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "pattern": "^[a-z0-9-]{2,40}$"
              },
              "card": {
                "type": "string",
                "enum": [
                  "intent",
                  "design",
                  "content",
                  "seo"
                ]
              },
              "text": {
                "type": "string"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "rule",
                  "guideline"
                ]
              },
              "level": {
                "type": "string",
                "enum": [
                  "block",
                  "warn"
                ]
              },
              "locked": {
                "type": "boolean"
              },
              "check": {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string"
                  }
                },
                "required": [
                  "type"
                ]
              }
            },
            "required": [
              "id",
              "card",
              "text",
              "kind",
              "locked"
            ]
          }
        },
        "notes": {
          "type": "object",
          "properties": {
            "design": {
              "type": "string",
              "maxLength": 300
            },
            "content": {
              "type": "string",
              "maxLength": 300
            },
            "seo": {
              "type": "string",
              "maxLength": 300
            }
          }
        },
        "missing": {
          "type": "array",
          "items": {
            "type": "string",
            "enum": [
              "design",
              "content",
              "seo"
            ]
          }
        },
        "next": {
          "type": "string",
          "enum": [
            "Before your first change: load get_skill('guidance') and write the missing sections."
          ]
        },
        "aiAccess": {
          "type": "string",
          "enum": [
            "viewer",
            "contributor",
            "editor",
            "owner"
          ]
        },
        "searchLine": {
          "type": "string"
        }
      },
      "required": [
        "rules",
        "notes",
        "missing",
        "aiAccess"
      ],
      "description": "Only with `?brief=1`: the site's rules & guidelines for AI agents — each line (a Rule dotsy checks at publish, or a Guideline), `check` params on Rules (long lists truncated with `more`), short note previews, which sections are still unwritten (`missing`), optional `next: get_skill('guidance')` when `missing` is non-empty, and the AI access level. Present even when the site has no readable brief (everything `missing`)."
    },
    "draftLog": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "tool": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          },
          "context": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500
          },
          "at": {
            "type": "number"
          }
        },
        "required": [
          "tool",
          "context",
          "at"
        ]
      }
    },
    "integrations": {
      "type": "object",
      "properties": {
        "pending": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "vendor": {
                "type": "string"
              },
              "sourceId": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "enum": [
                  "pending"
                ]
              },
              "pageCount": {
                "type": "number"
              },
              "mayAffectUi": {
                "type": "boolean"
              }
            },
            "required": [
              "id",
              "vendor",
              "sourceId",
              "status",
              "pageCount"
            ]
          }
        }
      },
      "required": [
        "pending"
      ],
      "description": "Pending clone integrations stripped at import (0.27.1)."
    },
    "mirroredAssets": {
      "type": "object",
      "properties": {
        "state": {
          "type": "string"
        },
        "warmMode": {
          "type": "string"
        },
        "resolved": {
          "type": "boolean",
          "description": "Warm has settled (not pending/running) — 'stop polling', NOT 'assets are fine'. state:\"aborted\" is also resolved; check assetsReady/mirrorComplete (already exclude aborted) or the `abort` field before reading this as healthy."
        },
        "counts": {
          "type": "object",
          "properties": {
            "total": {
              "type": "number"
            },
            "warmed": {
              "type": "number"
            },
            "failed": {
              "type": "number"
            },
            "capped": {
              "type": "number"
            },
            "skipped": {
              "type": "number"
            }
          },
          "required": [
            "total",
            "warmed",
            "failed",
            "capped",
            "skipped"
          ]
        },
        "externalCapped": {
          "type": "object",
          "properties": {
            "count": {
              "type": "number"
            },
            "total": {
              "type": "number"
            },
            "ratio": {
              "type": "number"
            }
          },
          "required": [
            "count",
            "total",
            "ratio"
          ],
          "description": "Fraction of assets left origin-backed (302 to source, not on dotsy R2) — a fidelity/durability risk even when failed:0. ratio > 0.25 should read as visual PARTIAL minimum per the eval rubric (0.63 §6.4)."
        },
        "failedAssetUrls": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Bounded sample of failed asset source URLs for triage (capped at 50). counts.failed is the true total. Present only when there is something to show."
        },
        "abort": {
          "type": "object",
          "properties": {
            "stage": {
              "type": "string"
            },
            "reason": {
              "type": "string"
            }
          },
          "description": "Present only when state:\"aborted\" — which named phase the run reached and why, so a total abort with 0 failures is never read as a clean success with nothing to do."
        },
        "generations": {
          "type": "number",
          "description": "Manifest generations (import/re-import/segment-recovery attempts) folded into `counts`. Omitted for a single-generation site."
        }
      },
      "required": [
        "state",
        "counts",
        "externalCapped"
      ],
      "description": "Mirrored external CDN warm progress (assetMirror:full), cumulative across every generation this slug has ever warmed. Bytes land in raw/_cdn/ — not counted in stats.assets or list_assets."
    },
    "previewReady": {
      "type": "boolean",
      "description": "Pages staged — preview/publish allowed while asset warm may still run."
    },
    "assetsReady": {
      "type": "boolean",
      "description": "Import complete and external warm settled with zero failures. False when the latest warm aborted, even with 0 failures recorded (see mirroredAssets.abort)."
    },
    "mirrorComplete": {
      "type": "boolean",
      "description": "Alias of assetsReady."
    },
    "failedAssetUrls": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Sample failed external warm URLs from the latest warm job."
    },
    "assetsHint": {
      "type": "string",
      "description": "Present with mirroredAssets — explains CMS assets/ vs mirrored raw/_cdn/ lanes."
    },
    "aiTrain": {
      "type": "boolean",
      "description": "Whether AI training crawlers may use the live site. false asks them not to. true when the site has not turned training off. Omitted on the shell payload."
    },
    "ownRobots": {
      "type": "boolean",
      "description": "True when an uploaded robots.txt is served on the live site (distinct from the generated file). Omitted on the shell payload."
    }
  },
  "required": [
    "id",
    "slug",
    "title",
    "keyConvention",
    "backend",
    "status",
    "editingMode",
    "publishedAt",
    "publicUrl",
    "liveUrl",
    "liveReady",
    "build",
    "building",
    "buildPhase",
    "editorReady",
    "createdAt",
    "updatedAt",
    "stats",
    "pages",
    "pagesListTotal",
    "nextPagesCursor",
    "activity"
  ]
}