{
  "openapi": "3.1.0",
  "info": {
    "title": "Skylens API",
    "version": "2026-08-12",
    "summary": "The project-scoped HTTP API of the Skylens Spatial OS.",
    "description": "Every read and write is scoped to one project, and the browser never reaches a database directly.\n\nThis document is GENERATED from `server/db/openapi.yaml` by `tools/api-docs/build_public_openapi.py`; it is the public projection of that contract and is never edited by hand. Operations that are internal, worker-facing, admin-only or merely planned are absent by construction, not by omission.\n\nThe response shape is documented as implemented: successes are bare JSON objects carrying `correlationId` and `traceId`, and errors are `{\"error\": {code, message, retryable, correlationId}}`. The `{data, meta, error}` envelope described in `docs/contracts/API.md` is implemented only by the Partner Services API.",
    "x-generated-by": "tools/api-docs/build_public_openapi.py",
    "x-source-of-truth": [
      "server/db/openapi.yaml",
      "server/db/partner_openapi.yaml"
    ]
  },
  "servers": [
    {
      "url": "http://127.0.0.1:8600",
      "description": "Local development",
      "x-skylens-environment": "local",
      "x-skylens-configured": true,
      "x-skylens-try-it": true,
      "x-skylens-note": "Started by `npm run dev`. Identity is the X-Skylens-User-Id header, trusted verbatim because no gateway fronts it. CORS is Access-Control-Allow-Origin: * on this surface."
    },
    {
      "url": "https://api.example.skylens.co.il",
      "description": "Production (PLACEHOLDER — requires deployment configuration)",
      "x-skylens-environment": "production",
      "x-skylens-configured": false,
      "x-skylens-try-it": false,
      "x-skylens-note": "PLACEHOLDER — requires deployment configuration. The gateway (functions/project-api-gateway.js) is implemented and unit-tested but not wired: firebase.json has no \"/api/**\" -> projectApiGateway rewrite, and SKYLENS_API_UPSTREAM_URL / SKYLENS_CLOUD_RUN_AUDIENCE / SKYLENS_GATEWAY_SHARED_SECRET are unset. Until then this host resolves to nothing and Try It is disabled against it."
    }
  ],
  "tags": [
    {
      "name": "projects",
      "x-display-name": "Projects",
      "description": ""
    },
    {
      "name": "documents",
      "x-display-name": "Documents",
      "description": ""
    },
    {
      "name": "gis",
      "x-display-name": "GIS Layers and Features",
      "description": ""
    },
    {
      "name": "spatial-analysis",
      "x-display-name": "Spatial Analysis",
      "description": ""
    },
    {
      "name": "elevation",
      "x-display-name": "Elevation",
      "description": ""
    },
    {
      "name": "review",
      "x-display-name": "Entity Review",
      "description": ""
    },
    {
      "name": "reality-models",
      "x-display-name": "Reality Models",
      "description": ""
    },
    {
      "name": "imports",
      "x-display-name": "CAD and BIM Imports",
      "description": ""
    },
    {
      "name": "photos",
      "x-display-name": "Photos and 360 Panoramas",
      "description": ""
    },
    {
      "name": "ai-jobs",
      "x-display-name": "AI Jobs",
      "description": ""
    },
    {
      "name": "agent-memory",
      "x-display-name": "Agent Memory",
      "description": ""
    },
    {
      "name": "viewer",
      "x-display-name": "Viewer",
      "description": ""
    },
    {
      "name": "chat-database",
      "x-display-name": "Chat Database Grounding",
      "description": ""
    },
    {
      "name": "export",
      "x-display-name": "Export",
      "description": ""
    },
    {
      "name": "partners",
      "x-display-name": "Partner Services",
      "description": ""
    }
  ],
  "paths": {
    "/api/projects/{projectId}/extraction-plans": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "ai-jobs"
        ],
        "summary": "Create an immutable extraction plan",
        "responses": {
          "default": {
            "description": "OK"
          }
        },
        "operationId": "postProjectsByProjectIdExtractionPlans",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "ai-jobs",
          "surface": "platform",
          "role": "editor",
          "source": "server/db/extraction_planner.create_plan",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/extraction-plans",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/extraction-plans/{planId}": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "planId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "ai-jobs"
        ],
        "summary": "Get an extraction plan with its work units, cost estimate and current state.",
        "responses": {
          "default": {
            "description": "OK"
          },
          "404": {
            "description": "NOT_FOUND"
          }
        },
        "operationId": "getProjectsByProjectIdExtractionPlansByPlanId",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "ai-jobs",
          "surface": "platform",
          "role": "editor",
          "source": "server/db/extraction_orchestrator.get_plan",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/extraction-plans/77777777-7777-4777-8777-777777777777",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/extraction-plans/{planId}/confirm": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "planId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "ai-jobs"
        ],
        "summary": "Confirm a plan so a job may be started from it. Body {idempotencyKey} is REQUIRED.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/IdempotentCommand"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          },
          "409": {
            "description": "INVALID_STATE | IDEMPOTENCY_CONFLICT"
          },
          "410": {
            "description": "PLAN_EXPIRED"
          }
        },
        "operationId": "postProjectsByProjectIdExtractionPlansByPlanIdConfirm",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "ai-jobs",
          "surface": "platform",
          "role": "editor",
          "idempotency": "body",
          "source": "server/db/extraction_orchestrator.confirm_plan",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/extraction-plans/77777777-7777-4777-8777-777777777777/confirm",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/extraction-plans/{planId}/cancel": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "planId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "ai-jobs"
        ],
        "summary": "Cancel an unconfirmed plan. Body {idempotencyKey} is REQUIRED.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/IdempotentCommand"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          },
          "409": {
            "description": "INVALID_STATE"
          }
        },
        "operationId": "postProjectsByProjectIdExtractionPlansByPlanIdCancel",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "ai-jobs",
          "surface": "platform",
          "role": "editor",
          "idempotency": "body",
          "source": "server/db/extraction_orchestrator.cancel_plan",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/extraction-plans/77777777-7777-4777-8777-777777777777/cancel",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/extraction-jobs": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "ai-jobs"
        ],
        "summary": "List extraction jobs",
        "responses": {
          "default": {
            "description": "OK"
          }
        },
        "operationId": "getProjectsByProjectIdExtractionJobs",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "ai-jobs",
          "surface": "platform",
          "role": "editor",
          "source": "server/db/extraction_orchestrator.list_jobs",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/extraction-jobs",
          "sourceDocument": "server/db/openapi.yaml"
        }
      },
      "post": {
        "tags": [
          "ai-jobs"
        ],
        "summary": "Start a job from a confirmed plan",
        "responses": {
          "default": {
            "description": "OK"
          }
        },
        "operationId": "postProjectsByProjectIdExtractionJobs",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "ai-jobs",
          "surface": "platform",
          "role": "editor",
          "idempotency": "body",
          "source": "server/db/extraction_orchestrator.start_job",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/extraction-jobs",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/extraction-jobs/{jobId}": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "jobId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "ai-jobs"
        ],
        "summary": "Get a job with its per-task progress, lease state and terminal outcome.",
        "responses": {
          "default": {
            "description": "OK"
          },
          "404": {
            "description": "NOT_FOUND"
          }
        },
        "operationId": "getProjectsByProjectIdExtractionJobsByJobId",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "ai-jobs",
          "surface": "platform",
          "role": "editor",
          "source": "server/db/extraction_orchestrator.get_job",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/extraction-jobs/66666666-6666-4666-8666-666666666666",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/extraction-jobs/{jobId}/cancel": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "jobId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "ai-jobs"
        ],
        "summary": "Cancel a non-terminal job. Body {idempotencyKey} is REQUIRED.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/IdempotentCommand"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          },
          "409": {
            "description": "INVALID_STATE"
          }
        },
        "operationId": "postProjectsByProjectIdExtractionJobsByJobIdCancel",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "ai-jobs",
          "surface": "platform",
          "role": "editor",
          "idempotency": "body",
          "source": "server/db/extraction_orchestrator.cancel_job",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/extraction-jobs/66666666-6666-4666-8666-666666666666/cancel",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/extraction-jobs/{jobId}/retry": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "jobId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "ai-jobs"
        ],
        "summary": "Retry a failed job. Body {idempotencyKey} is REQUIRED.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/IdempotentCommand"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          },
          "409": {
            "description": "INVALID_STATE | RETRY_EXHAUSTED"
          }
        },
        "operationId": "postProjectsByProjectIdExtractionJobsByJobIdRetry",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "ai-jobs",
          "surface": "platform",
          "role": "editor",
          "idempotency": "body",
          "source": "server/db/extraction_orchestrator.retry_job",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/extraction-jobs/66666666-6666-4666-8666-666666666666/retry",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/drafts/merge": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "review"
        ],
        "summary": "Merge draft features (body featureIds)",
        "responses": {
          "default": {
            "description": "OK"
          }
        },
        "operationId": "postProjectsByProjectIdDraftsMerge",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "review",
          "surface": "platform",
          "role": "reviewer",
          "idempotency": "body",
          "concurrency": "expectedRevisions",
          "source": "server/db/review_service.merge_features",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/drafts/merge",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/drafts/{draftId}": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "draftId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "review"
        ],
        "summary": "Get one draft feature with its full revision history.",
        "responses": {
          "default": {
            "description": "OK"
          },
          "404": {
            "description": "NOT_FOUND"
          }
        },
        "operationId": "getProjectsByProjectIdDraftsByDraftId",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "review",
          "surface": "platform",
          "role": "viewer",
          "source": "server/db/review_service.get_feature_history",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/drafts/55555555-5555-4555-8555-555555555555",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/drafts/{draftId}/geometry": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "draftId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "patch": {
        "tags": [
          "review"
        ],
        "summary": "Replace a draft's geometry (editor). Body {expectedRevision, geometry, reason, idempotencyKey} — all required.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DraftGeometryEdit"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          },
          "403": {
            "description": "FORBIDDEN"
          },
          "409": {
            "description": "VERSION_CONFLICT"
          }
        },
        "operationId": "patchProjectsByProjectIdDraftsByDraftIdGeometry",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "review",
          "surface": "platform",
          "role": "editor",
          "idempotency": "body",
          "concurrency": "expectedRevision",
          "crs": "GeoJSON EPSG:4326",
          "source": "server/db/review_service.edit_geometry",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/drafts/55555555-5555-4555-8555-555555555555/geometry",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/drafts/{draftId}/approve": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "draftId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "review"
        ],
        "summary": "Approve a draft into an authoritative feature (REVIEWER). Body {expectedRevision, reason, idempotencyKey, observedAt?}.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DraftApprove"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          },
          "403": {
            "description": "FORBIDDEN — needs the reviewer role"
          },
          "409": {
            "description": "VERSION_CONFLICT"
          }
        },
        "operationId": "postProjectsByProjectIdDraftsByDraftIdApprove",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "review",
          "surface": "platform",
          "role": "reviewer",
          "idempotency": "body",
          "concurrency": "expectedRevision",
          "source": "server/db/review_service.approve_feature",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/drafts/55555555-5555-4555-8555-555555555555/approve",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/drafts/{draftId}/reject": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "draftId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "review"
        ],
        "summary": "Reject a draft (REVIEWER). Body {expectedRevision, reason, idempotencyKey}.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DraftDecision"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          },
          "403": {
            "description": "FORBIDDEN"
          },
          "409": {
            "description": "VERSION_CONFLICT"
          }
        },
        "operationId": "postProjectsByProjectIdDraftsByDraftIdReject",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "review",
          "surface": "platform",
          "role": "reviewer",
          "idempotency": "body",
          "concurrency": "expectedRevision",
          "source": "server/db/review_service.reject_feature",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/drafts/55555555-5555-4555-8555-555555555555/reject",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/drafts/{draftId}/split": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "draftId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "review"
        ],
        "summary": "Split one draft into several (REVIEWER). Body {expectedRevision, geometries, reason, idempotencyKey}. Stable-ID rules apply: the parent's identity is preserved in lineage.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DraftSplit"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          },
          "403": {
            "description": "FORBIDDEN"
          },
          "409": {
            "description": "VERSION_CONFLICT"
          }
        },
        "operationId": "postProjectsByProjectIdDraftsByDraftIdSplit",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "review",
          "surface": "platform",
          "role": "reviewer",
          "idempotency": "body",
          "concurrency": "expectedRevision",
          "source": "server/db/review_service.split_feature",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/drafts/55555555-5555-4555-8555-555555555555/split",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/drafts/{draftId}/reclassify": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "draftId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "review"
        ],
        "summary": "Move a draft to another layer / entity type (REVIEWER). Body {expectedRevision, targetLayerId, targetEntityTypeId, reason, idempotencyKey}.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DraftReclassify"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          },
          "403": {
            "description": "FORBIDDEN"
          },
          "409": {
            "description": "VERSION_CONFLICT"
          }
        },
        "operationId": "postProjectsByProjectIdDraftsByDraftIdReclassify",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "review",
          "surface": "platform",
          "role": "reviewer",
          "idempotency": "body",
          "concurrency": "expectedRevision",
          "source": "server/db/review_service.reclassify_feature",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/drafts/55555555-5555-4555-8555-555555555555/reclassify",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/drafts/{draftId}/regenerate": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "draftId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "review"
        ],
        "summary": "Queue an AI regeneration of one draft (async). Body {expectedRevision, reason, idempotencyKey, options?}.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DraftRegenerate"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted — regeneration queued"
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          },
          "403": {
            "description": "FORBIDDEN"
          },
          "409": {
            "description": "VERSION_CONFLICT"
          }
        },
        "operationId": "postProjectsByProjectIdDraftsByDraftIdRegenerate",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "review",
          "surface": "platform",
          "role": "reviewer",
          "idempotency": "body",
          "concurrency": "expectedRevision",
          "source": "server/db/review_service.regenerate_feature",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/drafts/55555555-5555-4555-8555-555555555555/regenerate",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/review-queue": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "review"
        ],
        "summary": "List the review queue",
        "responses": {
          "default": {
            "description": "OK"
          }
        },
        "operationId": "getProjectsByProjectIdReviewQueue",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "review",
          "surface": "platform",
          "role": "reviewer",
          "source": "server/db/review_service.list_review_queue",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/review-queue",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/my-layers": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "viewer"
        ],
        "summary": "Snapshot of the project's user-authored layers, their features, the saved view state and the project AOIs. A layer is SAVED with PUT .../my-layers/{layerId} — there is no POST on this collection (extraction_api.py returns 405).",
        "responses": {
          "default": {
            "description": "OK"
          }
        },
        "operationId": "getProjectsByProjectIdMyLayers",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "viewer",
          "surface": "platform",
          "role": "editor",
          "source": "server/db/extraction_repository.get_my_layers_snapshot",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/my-layers",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/my-layers/view-state": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "viewer"
        ],
        "summary": "Save which layers are active. Body {activeLayers: string[]} (an array is required). POST, not PUT — PUT is 405 (a baseline declaration of `put` here was never implemented).",
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "INVALID_ARGUMENT (activeLayers is not an array)"
          }
        },
        "operationId": "postProjectsByProjectIdMyLayersViewState",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "viewer",
          "surface": "platform",
          "role": "editor",
          "source": "server/db/extraction_repository.save_project_layer_view_state",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/my-layers/view-state",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/my-layers/{layerId}": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "layerId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "put": {
        "tags": [
          "viewer"
        ],
        "summary": "Create or replace one user-authored layer and its features, under optimistic concurrency. layer.layerId MUST equal the path segment; layer.kind is user_draw or entity_layer; layer.provenance.source must be user-authored (user_draw/draw_menu, or approved_building_review for an entity_layer) — the route refuses to let a client forge an authoritative provenance. Every feature needs featureId and geometry. There is no GET on this path (405); read it from the my-layers snapshot.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "layer"
                ],
                "properties": {
                  "expectedRevision": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Optimistic concurrency; a stale value is 409 VERSION_CONFLICT."
                  },
                  "layer": {
                    "type": "object"
                  },
                  "features": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "INVALID_ARGUMENT (layerId mismatch, unsupported kind, non-user provenance, malformed feature)"
          },
          "409": {
            "description": "VERSION_CONFLICT"
          }
        },
        "operationId": "putProjectsByProjectIdMyLayersByLayerId",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "viewer",
          "surface": "platform",
          "role": "editor",
          "concurrency": "expectedRevision",
          "bodyLimitMb": 32,
          "source": "server/db/extraction_repository.upsert_project_layer",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/my-layers/layer_demo_001",
          "sourceDocument": "server/db/openapi.yaml"
        }
      },
      "delete": {
        "tags": [
          "viewer"
        ],
        "summary": "Delete one user-authored layer. Body {expectedRevision} is REQUIRED.",
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          },
          "409": {
            "description": "VERSION_CONFLICT"
          }
        },
        "operationId": "deleteProjectsByProjectIdMyLayersByLayerId",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "viewer",
          "surface": "platform",
          "role": "editor",
          "concurrency": "expectedRevision",
          "source": "server/db/extraction_repository.delete_project_layer",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/my-layers/layer_demo_001",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/trusted-features": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "gis"
        ],
        "summary": "List authoritative/trusted features",
        "responses": {
          "default": {
            "description": "OK"
          }
        },
        "operationId": "getProjectsByProjectIdTrustedFeatures",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "gis",
          "surface": "platform",
          "role": "viewer",
          "source": "server/db/review_service.list_trusted_features",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/trusted-features",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/layer-features": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "gis"
        ],
        "summary": "Query layer features (bbox/crs)",
        "responses": {
          "default": {
            "description": "OK"
          }
        },
        "operationId": "getProjectsByProjectIdLayerFeatures",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "gis",
          "surface": "platform",
          "role": "viewer",
          "crs": "crs=4326 (default) or crs=project",
          "source": "server/db/export_service.query_features",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/layer-features",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/db-catalog": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "includeCounts",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string"
          },
          "description": "'true' adds per-domain live record counts (server-cached ~30s)"
        }
      ],
      "get": {
        "tags": [
          "chat-database"
        ],
        "summary": "Resolved chat exposure catalog for the project (viewer role; skill: skylens-chat-database). Read-only. Domains, filters, aggregates, aliases and optional counts.",
        "responses": {
          "default": {
            "description": "OK"
          },
          "403": {
            "description": "FORBIDDEN"
          },
          "404": {
            "description": "NOT_FOUND"
          }
        },
        "operationId": "getProjectsByProjectIdDbCatalog",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "chat-database",
          "surface": "platform",
          "role": "viewer",
          "source": "server/db/chat_db_query_service.get_catalog",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/db-catalog",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/db-query": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "chat-database"
        ],
        "summary": "Dispatch one frozen-SQL chat-db tool (db_count, db_describe, db_list_models, db_query_gis_features, db_query_entities, db_query_relations, db_spatial_relate, db_list_layers). project_id is bound from the path, never from input; read-only; viewer role. Body: {contractVersion, tool, input}.",
        "responses": {
          "default": {
            "description": "OK"
          },
          "400": {
            "description": "UNKNOWN_TOOL / UNKNOWN_DOMAIN / UNKNOWN_FILTER / SCOPE_TOO_LARGE / INVALID_ARGUMENT"
          },
          "403": {
            "description": "FORBIDDEN"
          },
          "404": {
            "description": "NOT_FOUND"
          },
          "500": {
            "description": "QUERY_TIMEOUT (retryable)"
          }
        },
        "operationId": "postProjectsByProjectIdDbQuery",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "chat-database",
          "surface": "platform",
          "role": "viewer",
          "source": "server/db/chat_db_query_service.dispatch_tool",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/db-query",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/models/{modelId}/thumbnail": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "modelId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "put": {
        "tags": [
          "reality-models"
        ],
        "summary": "Persist a small client-captured preview onto the model's metadata.thumbnailUrl (editor role). Body: {thumbnailUrl} — an image data: URL, <=64KB. Surfaced by GET /models for project cards.",
        "responses": {
          "default": {
            "description": "OK"
          },
          "400": {
            "description": "INVALID_ARGUMENT (not an image data: URL / too large / bad modelId)"
          },
          "403": {
            "description": "FORBIDDEN"
          },
          "404": {
            "description": "NOT_FOUND"
          }
        },
        "operationId": "putProjectsByProjectIdModelsByModelIdThumbnail",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "reality-models",
          "surface": "platform",
          "role": "editor",
          "source": "server/db/reality_model_service.set_model_thumbnail",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/models/44444444-4444-4444-8444-444444444444/thumbnail",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/reality-models/{modelId}/delivery": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        },
        {
          "name": "modelId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        },
        {
          "name": "ttlSeconds",
          "in": "query",
          "required": false,
          "schema": {
            "type": "integer",
            "minimum": 60,
            "maximum": 900,
            "default": 900
          },
          "description": "May only SHORTEN the grant; a larger value is capped, never honoured."
        }
      ],
      "get": {
        "tags": [
          "reality-models"
        ],
        "summary": "Issue a short-lived CloudFront signed-cookie delivery grant for ONE reality model (ADR 0028; action reality.read; flag SKYLENS_REALITY_SIGNED_DELIVERY_ENABLED, default OFF => 404). The three CloudFront cookies are returned as Set-Cookie headers (HttpOnly, Secure, SameSite=None, Domain=<shared parent>, Path=<model prefix>) and NEVER in the body; the viewer never reads a cookie value. Body: {kind, delivery, accessUrl, trustHost, cookieScope:{domain,path}, expiresAt, ttlSeconds} — no bucket, key, prefix or credential. The organization, the storage prefix and the CDN scope are derived server-side from the model row: a caller-supplied organizationId/prefix/bucket is 400 UNKNOWN_FIELD, and a model that is not in {projectId} is 404, indistinguishable from one that does not exist.",
        "responses": {
          "200": {
            "description": "Grant issued; response carries Set-Cookie x3 and Cache-Control: private, no-store"
          },
          "400": {
            "description": "UNKNOWN_FIELD (a client-supplied scope) / INVALID_ARGUMENT (ttlSeconds)"
          },
          "403": {
            "description": "FORBIDDEN (member without reality.read)"
          },
          "404": {
            "description": "NOT_FOUND (flag off, project undisclosed, or model not in this project)"
          },
          "405": {
            "description": "METHOD_NOT_ALLOWED"
          },
          "500": {
            "description": "INTERNAL (signing or the delivery audit trail is unavailable — no grant is issued)"
          }
        },
        "operationId": "getProjectsByProjectIdRealityModelsByModelIdDelivery",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "reality-models",
          "surface": "platform",
          "permission": "reality.read",
          "flag": "SKYLENS_REALITY_SIGNED_DELIVERY_ENABLED",
          "source": "server/db/reality_delivery_service.issue_model_delivery",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/reality-models/44444444-4444-4444-8444-444444444444/delivery",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/gis-layers": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "theme",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string"
          },
          "description": "Filter by theme/category, e.g. Planning"
        }
      ],
      "get": {
        "tags": [
          "gis"
        ],
        "summary": "List cataloged GIS layers (gis.layers) offered to the project, with source provenance and current feature counts",
        "responses": {
          "default": {
            "description": "OK"
          }
        },
        "operationId": "getProjectsByProjectIdGisLayers",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "gis",
          "surface": "platform",
          "role": "viewer",
          "source": "server/db/gis_catalog_service.list_catalog_layers",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/gis-layers",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/gis-layers/{layerCode}/features": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "layerCode",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "Logical layer code, e.g. planning.urban_renewal_declared"
        },
        {
          "name": "bbox",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string"
          },
          "description": "minLon,minLat,maxLon,maxLat in EPSG:4326; server-side filtered"
        },
        {
          "name": "limit",
          "in": "query",
          "required": false,
          "schema": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5000,
            "default": 2000
          }
        }
      ],
      "get": {
        "tags": [
          "gis"
        ],
        "summary": "Current-snapshot features (valid_to IS NULL) of a cataloged GIS layer as EPSG:4326 GeoJSON, bbox-filtered and limited (read-only; authoritative layers)",
        "responses": {
          "default": {
            "description": "OK"
          },
          "400": {
            "description": "INVALID_BBOX or INVALID_LIMIT"
          }
        },
        "operationId": "getProjectsByProjectIdGisLayersByLayerCodeFeatures",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "gis",
          "surface": "platform",
          "role": "viewer",
          "crs": "EPSG:4326 output; bbox in EPSG:4326",
          "source": "server/db/gis_catalog_service.query_layer_features",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/gis-layers/planning.urban_renewal_declared/features",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/gis-imports": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "gis"
        ],
        "summary": "Import an official/authoritative layer (from a frontend upload) into the gis.* catalog: creates data_source/dataset/layer (read-only, theme e.g. Planning) and imports the posted GeoJSON FeatureCollection (body: {name, theme, geojson, sourceCrs, sourceUrl}). Reviewer/admin role.",
        "responses": {
          "201": {
            "description": "Created"
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          }
        },
        "operationId": "postProjectsByProjectIdGisImports",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "gis",
          "surface": "platform",
          "role": "reviewer",
          "bodyLimitMb": 32,
          "source": "server/db/gis_catalog_service.create_and_import_layer",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/gis-imports",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/spatial-analysis/overlays": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "limit",
          "in": "query",
          "required": false,
          "schema": {
            "type": "integer",
            "minimum": 1,
            "maximum": 200,
            "default": 50
          },
          "description": "GET: max overlay runs to list"
        }
      ],
      "get": {
        "tags": [
          "spatial-analysis"
        ],
        "summary": "List persisted spatial-overlay runs for the project (newest first), each with metrics, source references, and derived output layer codes",
        "responses": {
          "default": {
            "description": "OK"
          }
        },
        "operationId": "getProjectsByProjectIdSpatialAnalysisOverlays",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "spatial-analysis",
          "surface": "platform",
          "role": "viewer",
          "source": "server/db/spatial_overlay_service.list_runs",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/spatial-analysis/overlays",
          "sourceDocument": "server/db/openapi.yaml"
        }
      },
      "post": {
        "tags": [
          "spatial-analysis"
        ],
        "summary": "Run a governed polygon overlay (skill: skylens-spatial-overlay-analysis). Body: {spec:{operation,scope,mask,topology,outputs}, outputMode:'preview'|'persist', idempotencyKey}. Topology runs in PostGIS/EPSG:2039; persist materializes derived covered_by_mask/uncovered_by_mask gis.* layers. Preview needs viewer; persist needs reviewer+. Idempotent per (org,project,idempotencyKey).",
        "responses": {
          "201": {
            "description": "Completed"
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          },
          "403": {
            "description": "FORBIDDEN"
          },
          "404": {
            "description": "NOT_FOUND (input layer or project)"
          },
          "429": {
            "description": "QUOTA_EXCEEDED"
          }
        },
        "operationId": "postProjectsByProjectIdSpatialAnalysisOverlays",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "spatial-analysis",
          "surface": "platform",
          "role": "viewer for outputMode=preview, reviewer for persist",
          "idempotency": "body",
          "crs": "topology runs in EPSG:2039",
          "source": "server/db/spatial_overlay_service.run_overlay",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/spatial-analysis/overlays",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/spatial-analysis/overlays/{runId}": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "runId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "spatial-analysis"
        ],
        "summary": "Get a spatial-overlay run manifest (status, operation, source versions, metrics, output layer IDs, lineage, warnings)",
        "responses": {
          "default": {
            "description": "OK"
          },
          "404": {
            "description": "NOT_FOUND"
          }
        },
        "operationId": "getProjectsByProjectIdSpatialAnalysisOverlaysByRunId",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "spatial-analysis",
          "surface": "platform",
          "role": "viewer",
          "source": "server/db/spatial_overlay_service.get_run",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/spatial-analysis/overlays/bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/layer-exports/{format}": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "format",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "enum": [
              "geojson",
              "shapefile",
              "csv",
              "entities_json"
            ]
          },
          "description": "The four formats export_service.FORMATS actually serves. 'kml' was declared here at baseline and has never been implemented."
        },
        {
          "name": "crs",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string",
            "enum": [
              "4326",
              "project"
            ],
            "default": "4326"
          },
          "description": "'4326' or the literal 'project' (core.projects.default_crs_epsg, 2039 by default). Any other value is 400 — there is no on-the-fly reprojection to an arbitrary EPSG."
        },
        {
          "name": "scope",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "layerId",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "entityTypeId",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "reviewState",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "limit",
          "in": "query",
          "required": false,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "offset",
          "in": "query",
          "required": false,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "export"
        ],
        "summary": "Export approved features as a downloadable file (Content-Disposition attachment). NOTE: GET here vs POST .../layers/{id}/export on Firebase — reconcile.",
        "responses": {
          "200": {
            "description": "File body"
          },
          "400": {
            "description": "INVALID_ARGUMENT (unknown format or crs)"
          },
          "403": {
            "description": "FORBIDDEN"
          }
        },
        "operationId": "getProjectsByProjectIdLayerExportsByFormat",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "export",
          "surface": "platform",
          "role": "viewer",
          "crs": "crs=4326 (default) or crs=project",
          "source": "server/db/export_service.export_features",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/layer-exports/geojson",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/photo-collections": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "AUTHORITATIVE tenant boundary for this request"
        },
        {
          "name": "publicationStatus",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string",
            "enum": [
              "draft",
              "ready_for_review",
              "published",
              "rejected",
              "archived"
            ]
          },
          "description": "GET: filter the listing"
        }
      ],
      "get": {
        "tags": [
          "photos"
        ],
        "summary": "List the project's Photo Collections (viewer role). The Viewer layer requests publicationStatus=published.",
        "responses": {
          "default": {
            "description": "OK"
          },
          "403": {
            "description": "FORBIDDEN"
          }
        },
        "operationId": "getProjectsByProjectIdPhotoCollections",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "photos",
          "surface": "platform",
          "role": "viewer",
          "source": "server/db/photo_import_service.list_collections",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/photo-collections",
          "sourceDocument": "server/db/openapi.yaml"
        }
      },
      "post": {
        "tags": [
          "photos"
        ],
        "summary": "Register an immutable Photo Collection version + its assets and return a deterministic, server-derived upload plan (editor role). Idempotent per (projectId, collectionKey, version) with an identical manifest digest; a different manifest for the same version is INVALID_STATE — corrected inventories are NEW versions. The API never proxies image bytes.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "collectionKey",
                  "manifest"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "collectionKey": {
                    "type": "string",
                    "description": "Sanitized server-side to [A-Za-z0-9_-]{1,120}"
                  },
                  "version": {
                    "type": "integer",
                    "minimum": 1,
                    "default": 1
                  },
                  "manifest": {
                    "type": "object",
                    "required": [
                      "files"
                    ],
                    "description": "Client preflight inventory. Paths are validated server-side (no absolute paths, no traversal, no case collisions, no __skylens/ prefix).",
                    "properties": {
                      "files": {
                        "type": "array",
                        "items": {
                          "type": "object"
                        }
                      }
                    }
                  },
                  "organizationId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "CROSS-CHECK ONLY. Must equal core.projects.organization_id for the projectId in the URL, which is verified under FOR SHARE inside the write transaction. A mismatch is 403 FORBIDDEN. Omitting it skips the cross-check; it never selects a tenant."
                  },
                  "panoramaDeclaration": {
                    "type": "object",
                    "description": "Intent recorded on the collection, NOT a registration — panorama groups are created later by /panoramas, after the stitched bytes exist. Manual placement is normalized server-side to positionSource=manual, spatialConfidence=approximate and cannot be promoted.",
                    "properties": {
                      "assetMode": {
                        "type": "string",
                        "enum": [
                          "mixed",
                          "panorama",
                          "source_frames"
                        ],
                        "default": "mixed"
                      },
                      "confirmedEquirectangular": {
                        "type": "boolean",
                        "description": "Permits immersive viewing only. NOT a calibration."
                      },
                      "placement": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                          "center": {
                            "type": "array",
                            "items": {
                              "type": "number"
                            },
                            "minItems": 2,
                            "maxItems": 3,
                            "description": "[lon, lat, heightM?] in EPSG:4326"
                          },
                          "headingDeg": {
                            "type": "number",
                            "minimum": 0,
                            "maximum": 360,
                            "nullable": true,
                            "description": "Typed by the user -> orientationSource=manual, never calibrated"
                          }
                        }
                      }
                    }
                  },
                  "publicationRequested": {
                    "type": "boolean",
                    "default": false,
                    "description": "Intent flag only. Publication still requires the separate REVIEWER-role /publish call."
                  },
                  "captureStartedAt": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true
                  },
                  "captureEndedAt": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true
                  },
                  "storageBucket": {
                    "type": "string",
                    "nullable": true
                  },
                  "platform": {
                    "type": "string",
                    "nullable": true
                  },
                  "camera": {
                    "type": "string",
                    "nullable": true
                  },
                  "operator": {
                    "type": "string",
                    "nullable": true
                  },
                  "mission": {
                    "type": "string",
                    "nullable": true
                  },
                  "sourceCrs": {
                    "type": "string",
                    "nullable": true
                  },
                  "heightDatum": {
                    "type": "string",
                    "nullable": true
                  },
                  "privacy": {
                    "type": "object",
                    "nullable": true
                  },
                  "provenance": {
                    "type": "object",
                    "nullable": true
                  },
                  "metadata": {
                    "type": "object",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Registered; body carries the collection and its uploadPlan (server-derived keys)"
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          },
          "403": {
            "description": "FORBIDDEN — actor not authorized for the project, or organizationId does not own it"
          },
          "404": {
            "description": "NOT_FOUND — the project does not exist"
          },
          "409": {
            "description": "INVALID_STATE — same version, different manifest digest"
          },
          "422": {
            "description": "VALIDATION_FAILED"
          }
        },
        "operationId": "postProjectsByProjectIdPhotoCollections",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "photos",
          "surface": "platform",
          "role": "editor",
          "bodyLimitMb": 32,
          "idempotency": "per (projectId, collectionKey, version) + manifest digest",
          "source": "server/db/photo_import_service.create_collection",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/photo-collections",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/photo-collections/{collectionId}": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "AUTHORITATIVE; the collection lookup is filtered by it, so cross-project access is 404"
        },
        {
          "name": "collectionId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "photos"
        ],
        "summary": "Collection detail: asset counts, camera-solution versions, and panorama groups with projection, center, positionSource, headingDeg, orientationSource and spatialConfidence (viewer role)",
        "responses": {
          "default": {
            "description": "OK"
          },
          "404": {
            "description": "NOT_FOUND"
          }
        },
        "operationId": "getProjectsByProjectIdPhotoCollectionsByCollectionId",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "photos",
          "surface": "platform",
          "role": "viewer",
          "source": "server/db/photo_import_service.get_collection",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/photo-collections/88888888-8888-4888-8888-888888888888",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/photo-collections/{collectionId}/images": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "collectionId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "limit",
          "in": "query",
          "required": false,
          "schema": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000,
            "default": 200
          }
        },
        {
          "name": "offset",
          "in": "query",
          "required": false,
          "schema": {
            "type": "integer",
            "minimum": 0,
            "default": 0
          }
        },
        {
          "name": "role",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string",
            "enum": [
              "original_still",
              "panorama_source_frame",
              "stitched_panorama",
              "solution_artifact",
              "support_document",
              "derivative"
            ]
          }
        }
      ],
      "get": {
        "tags": [
          "photos"
        ],
        "summary": "Paged asset listing for the Viewer layer (viewer role) — delivery references and GNSS/solved positions, never bytes. Solved poses are returned displayOnly=true and never drive picking.",
        "responses": {
          "default": {
            "description": "OK"
          },
          "403": {
            "description": "FORBIDDEN"
          },
          "404": {
            "description": "NOT_FOUND — including a collection that belongs to another project"
          }
        },
        "operationId": "getProjectsByProjectIdPhotoCollectionsByCollectionIdImages",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "photos",
          "surface": "platform",
          "role": "viewer",
          "paginated": "offset",
          "source": "server/db/photo_import_service.list_images",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/photo-collections/88888888-8888-4888-8888-888888888888/images",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/photo-collections/{collectionId}/complete-upload": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "collectionId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "photos"
        ],
        "summary": "Mark direct-to-storage uploads done (editor role). Body {uploaded:[{path}], s3BaseUrl?}. Object URIs are rebuilt from the REGISTERED asset role and the stored prefix — a client cannot relocate an asset outside its collection.",
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          },
          "403": {
            "description": "FORBIDDEN"
          },
          "409": {
            "description": "INVALID_STATE"
          }
        },
        "operationId": "postProjectsByProjectIdPhotoCollectionsByCollectionIdCompleteUpload",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "photos",
          "surface": "platform",
          "role": "editor",
          "source": "server/db/photo_import_service.complete_upload",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/photo-collections/88888888-8888-4888-8888-888888888888/complete-upload",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/photo-collections/{collectionId}/validate": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "collectionId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "photos"
        ],
        "summary": "Re-validate stored bytes from object storage (signature, size, SHA-256, dimensions). Degrades to a documented note when the server has no storage credentials; the collection then keeps client-preflight evidence only.",
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "description": "FORBIDDEN"
          }
        },
        "operationId": "postProjectsByProjectIdPhotoCollectionsByCollectionIdValidate",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "photos",
          "surface": "platform",
          "role": "editor",
          "source": "server/db/photo_validation_service.validate_collection_bytes",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/photo-collections/88888888-8888-4888-8888-888888888888/validate",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/photo-collections/{collectionId}/solutions": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "collectionId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "photos"
        ],
        "summary": "Attach one immutable Bentley BlocksExchange/ATExport camera-solution version (editor role). Body {filename, contentBase64, producer?, producerVersion?}. Lands as 'parsed' or 'needs_review' — never auto-validated.",
        "responses": {
          "201": {
            "description": "Created"
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          },
          "422": {
            "description": "VALIDATION_FAILED"
          }
        },
        "operationId": "postProjectsByProjectIdPhotoCollectionsByCollectionIdSolutions",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "photos",
          "surface": "platform",
          "role": "editor",
          "source": "server/db/photo_import_service.attach_solution",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/photo-collections/88888888-8888-4888-8888-888888888888/solutions",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/photo-collections/{collectionId}/panoramas": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "collectionId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "photos"
        ],
        "summary": "Register reviewed panorama groups (editor role), after the stitched bytes exist and validate. projection defaults to 'unknown': a 2:1 ratio is a candidate, not proof, and the client sends 'equirectangular' only from GPano metadata or explicit per-image user confirmation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "groups"
                ],
                "properties": {
                  "groups": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string",
                          "nullable": true
                        },
                        "stitchedPath": {
                          "type": "string",
                          "nullable": true,
                          "description": "Must already be in this collection's inventory"
                        },
                        "sourcePaths": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "projection": {
                          "type": "string",
                          "enum": [
                            "equirectangular",
                            "partial_sphere",
                            "unknown"
                          ],
                          "default": "unknown"
                        },
                        "widthPx": {
                          "type": "integer",
                          "nullable": true
                        },
                        "heightPx": {
                          "type": "integer",
                          "nullable": true
                        },
                        "center": {
                          "type": "array",
                          "items": {
                            "type": "number"
                          },
                          "minItems": 2,
                          "maxItems": 3,
                          "nullable": true,
                          "description": "[lon, lat, heightM?] EPSG:4326"
                        },
                        "positionSource": {
                          "type": "string",
                          "enum": [
                            "solution",
                            "gnss",
                            "manual",
                            "none"
                          ]
                        },
                        "headingDeg": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 360,
                          "nullable": true
                        },
                        "orientationSource": {
                          "type": "string",
                          "enum": [
                            "metadata",
                            "manual",
                            "unknown"
                          ],
                          "description": "'solution' is REJECTED at import — a solved orientation is claimed by a solution version and confirmed in review. 'manual' requires a headingDeg and is never calibrated."
                        },
                        "spatialConfidence": {
                          "type": "string",
                          "enum": [
                            "approximate",
                            "unplaced"
                          ],
                          "description": "'validated' is REJECTED at import — it is set by review only, and it is the sole gate for metric raycast, entity projection and Cesium camera synchronization. positionSource=manual is forced to 'approximate'."
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created; body carries panoramaGroupIds and count"
          },
          "400": {
            "description": "INVALID_ARGUMENT — including an import-time claim of 'validated' or orientationSource 'solution'"
          },
          "403": {
            "description": "FORBIDDEN"
          },
          "422": {
            "description": "VALIDATION_FAILED — stitchedPath/sourcePath not in the collection"
          }
        },
        "operationId": "postProjectsByProjectIdPhotoCollectionsByCollectionIdPanoramas",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "photos",
          "surface": "platform",
          "role": "editor",
          "crs": "center is [lon, lat, heightM?] EPSG:4326",
          "source": "server/db/photo_import_service.register_panoramas",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/photo-collections/88888888-8888-4888-8888-888888888888/panoramas",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/photo-collections/{collectionId}/panorama-height": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "collectionId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "photos"
        ],
        "summary": "Correct the CAMERA HEIGHT of the collection's placed panorama groups (editor role). A reviewed height is mutable even though the image inventory is not — map picking returns the surface under the cursor, not the altitude the camera flew at. Longitude/latitude are untouched (a different position is a re-placement, not a height correction), and positionSource / orientationSource / spatialConfidence are unchanged, so editing a height can never promote a panorama toward 'validated'.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "heightM"
                ],
                "properties": {
                  "heightM": {
                    "type": "number",
                    "minimum": -500,
                    "maximum": 10000,
                    "description": "Metres. Bounds span the Dead Sea shore to above any panorama-capable camera."
                  },
                  "panoramaGroupId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Optional. Omit to correct every PLACED group in the collection."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated; body carries {updated:[{panoramaGroupId, center}], count, heightM}"
          },
          "400": {
            "description": "INVALID_ARGUMENT — non-numeric or out-of-range height"
          },
          "403": {
            "description": "FORBIDDEN"
          },
          "404": {
            "description": "NOT_FOUND — collection is not in this project"
          },
          "409": {
            "description": "INVALID_STATE — no placed panorama matched (an unplaced panorama has no height to correct)"
          }
        },
        "operationId": "postProjectsByProjectIdPhotoCollectionsByCollectionIdPanoramaHeight",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "photos",
          "surface": "platform",
          "role": "editor",
          "source": "server/db/photo_import_service.set_panorama_height",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/photo-collections/88888888-8888-4888-8888-888888888888/panorama-height",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/photo-collections/{collectionId}/publish": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "collectionId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "photos"
        ],
        "summary": "Publish the collection to the Viewer (REVIEWER role). Upload completion is NOT publication — this is the separate authorized action. Bounds derive from GNSS points, normalized poses and panorama centers; publication never mutates assets.",
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "description": "FORBIDDEN — needs the reviewer role"
          },
          "409": {
            "description": "INVALID_STATE"
          }
        },
        "operationId": "postProjectsByProjectIdPhotoCollectionsByCollectionIdPublish",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "photos",
          "surface": "platform",
          "role": "reviewer",
          "source": "server/db/photo_import_service.publish_collection",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/photo-collections/88888888-8888-4888-8888-888888888888/publish",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/photo-collections/{collectionId}/cancel": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "collectionId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "photos"
        ],
        "summary": "Abandon an unpublished import (editor role). Published collections are immutable — archive instead.",
        "responses": {
          "200": {
            "description": "OK"
          },
          "409": {
            "description": "INVALID_STATE"
          }
        },
        "operationId": "postProjectsByProjectIdPhotoCollectionsByCollectionIdCancel",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "photos",
          "surface": "platform",
          "role": "editor",
          "source": "server/db/photo_import_service.cancel_collection",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/photo-collections/88888888-8888-4888-8888-888888888888/cancel",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/photo-assets/{assetId}/delivery": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "assetId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "ttlSeconds",
          "in": "query",
          "required": false,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "photos"
        ],
        "summary": "Short-lived authorized delivery URL for one asset (viewer role). Presigned when the server holds storage credentials; falls back to the stored https URI otherwise. Originals stay private — this is the ONLY read path the Viewer may use.",
        "responses": {
          "default": {
            "description": "OK"
          },
          "403": {
            "description": "FORBIDDEN"
          },
          "404": {
            "description": "NOT_FOUND"
          }
        },
        "operationId": "getProjectsByProjectIdPhotoAssetsByAssetIdDelivery",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "photos",
          "surface": "platform",
          "role": "viewer",
          "source": "server/db/photo_validation_service.get_asset_delivery",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/photo-assets/99999999-9999-4999-8999-999999999999/delivery",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/ai/dreams": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "status",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "completed",
              "failed",
              "cancelled"
            ]
          },
          "description": "GET: filter by run status"
        },
        {
          "name": "limit",
          "in": "query",
          "required": false,
          "schema": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "default": 25
          }
        },
        {
          "name": "Idempotency-Key",
          "in": "header",
          "required": false,
          "schema": {
            "type": "string",
            "minLength": 8,
            "maxLength": 200
          },
          "description": "POST: replay-safe key; a body idempotencyKey is also accepted, header wins"
        }
      ],
      "get": {
        "tags": [
          "agent-memory"
        ],
        "summary": "List memory-consolidation (dream) runs, newest first. Requires agent_memory.read",
        "responses": {
          "default": {
            "description": "OK"
          }
        },
        "operationId": "getProjectsByProjectIdAiDreams",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "agent-memory",
          "surface": "platform",
          "permission": "agent_memory.read",
          "flag": "SKYLENS_AGENT_DREAMS_ENABLED",
          "source": "server/db/agent_dream_service.list_dreams",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/ai/dreams",
          "sourceDocument": "server/db/openapi.yaml"
        }
      },
      "post": {
        "tags": [
          "agent-memory"
        ],
        "summary": "Queue a dream run. Body {agentKey, scope, scopeRef, selection:{startedAfter,startedBefore,outcomeStatuses,minOutcomeQuality,sessionIds}, instructions, adapterKey}. Selects sessions at request time and freezes the manifest. Produces a CANDIDATE store only — it cannot change what any agent reads. Requires agent_memory.run",
        "responses": {
          "201": {
            "description": "Queued"
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          },
          "403": {
            "description": "FORBIDDEN"
          },
          "409": {
            "description": "IDEMPOTENCY_CONFLICT | INVALID_STATE | FEATURE_DISABLED"
          }
        },
        "operationId": "postProjectsByProjectIdAiDreams",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "agent-memory",
          "surface": "platform",
          "permission": "agent_memory.run",
          "flag": "SKYLENS_AGENT_DREAMS_ENABLED",
          "idempotency": "header or body",
          "source": "server/db/agent_dream_service.create_dream",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/ai/dreams",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/ai/dreams/{dreamId}": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "dreamId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "agent-memory"
        ],
        "summary": "Get one dream run with its session manifest, findings (each with supporting sessions/items/evidence) and evaluation. Requires agent_memory.read",
        "responses": {
          "default": {
            "description": "OK"
          },
          "404": {
            "description": "NOT_FOUND"
          }
        },
        "operationId": "getProjectsByProjectIdAiDreamsByDreamId",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "agent-memory",
          "surface": "platform",
          "permission": "agent_memory.read",
          "source": "server/db/agent_dream_service.get_dream",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/ai/dreams/dddddddd-dddd-4ddd-8ddd-dddddddddddd",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/ai/dreams/{dreamId}/diff": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "dreamId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "agent-memory"
        ],
        "summary": "Structured baseline-vs-candidate diff bucketed as added/merged/modified/superseded/removed/unresolved. Requires agent_memory.read",
        "responses": {
          "default": {
            "description": "OK"
          },
          "404": {
            "description": "NOT_FOUND"
          },
          "409": {
            "description": "INVALID_STATE (no candidate yet)"
          }
        },
        "operationId": "getProjectsByProjectIdAiDreamsByDreamIdDiff",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "agent-memory",
          "surface": "platform",
          "permission": "agent_memory.read",
          "source": "server/db/agent_dream_service.get_diff",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/ai/dreams/dddddddd-dddd-4ddd-8ddd-dddddddddddd/diff",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/ai/dreams/{dreamId}/cancel": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "dreamId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "Idempotency-Key",
          "in": "header",
          "required": false,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "agent-memory"
        ],
        "summary": "Cancel a non-terminal run. The active binding is never touched. Requires agent_memory.run",
        "responses": {
          "default": {
            "description": "OK"
          },
          "409": {
            "description": "INVALID_STATE"
          }
        },
        "operationId": "postProjectsByProjectIdAiDreamsByDreamIdCancel",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "agent-memory",
          "surface": "platform",
          "permission": "agent_memory.run",
          "idempotency": "header or body",
          "source": "server/db/agent_dream_service.cancel_dream",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/ai/dreams/dddddddd-dddd-4ddd-8ddd-dddddddddddd/cancel",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/ai/dreams/{dreamId}/approve": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "dreamId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "Idempotency-Key",
          "in": "header",
          "required": false,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "agent-memory"
        ],
        "summary": "Record an APPROVAL of the candidate. Does NOT activate it — activation is a separate call requiring agent_memory.activate. Requires agent_memory.review",
        "responses": {
          "default": {
            "description": "OK"
          },
          "403": {
            "description": "FORBIDDEN"
          },
          "409": {
            "description": "INVALID_STATE"
          }
        },
        "operationId": "postProjectsByProjectIdAiDreamsByDreamIdApprove",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "agent-memory",
          "surface": "platform",
          "permission": "agent_memory.review",
          "idempotency": "header or body",
          "source": "server/db/agent_dream_service.review_dream",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/ai/dreams/dddddddd-dddd-4ddd-8ddd-dddddddddddd/approve",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/ai/dreams/{dreamId}/reject": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "dreamId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "Idempotency-Key",
          "in": "header",
          "required": false,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "agent-memory"
        ],
        "summary": "Reject the candidate. Body {reason} is REQUIRED and is stored in the audit trail. Requires agent_memory.review",
        "responses": {
          "default": {
            "description": "OK"
          },
          "400": {
            "description": "INVALID_ARGUMENT (missing reason)"
          }
        },
        "operationId": "postProjectsByProjectIdAiDreamsByDreamIdReject",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "agent-memory",
          "surface": "platform",
          "permission": "agent_memory.review",
          "idempotency": "header or body",
          "source": "server/db/agent_dream_service.review_dream",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/ai/dreams/dddddddd-dddd-4ddd-8ddd-dddddddddddd/reject",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/ai/memory-stores": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "agentKey",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "status",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string",
            "enum": [
              "candidate",
              "active",
              "rejected",
              "archived"
            ]
          }
        },
        {
          "name": "active",
          "in": "query",
          "required": false,
          "schema": {
            "type": "boolean"
          },
          "description": "true returns {activeStore} — the currently bound store plus its items and binding version"
        },
        {
          "name": "limit",
          "in": "query",
          "required": false,
          "schema": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "default": 25
          }
        }
      ],
      "get": {
        "tags": [
          "agent-memory"
        ],
        "summary": "List memory stores, or fetch the active one. Requires agent_memory.read",
        "responses": {
          "default": {
            "description": "OK"
          }
        },
        "operationId": "getProjectsByProjectIdAiMemoryStores",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "agent-memory",
          "surface": "platform",
          "permission": "agent_memory.read",
          "source": "server/db/agent_memory_service.list_stores",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/ai/memory-stores",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/ai/memory-stores/{storeId}": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "storeId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "tags": [
          "agent-memory"
        ],
        "summary": "Get one memory store with its typed items, trust labels, confidence, provenance and unresolved flags. Requires agent_memory.read",
        "responses": {
          "default": {
            "description": "OK"
          },
          "404": {
            "description": "NOT_FOUND"
          }
        },
        "operationId": "getProjectsByProjectIdAiMemoryStoresByStoreId",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "agent-memory",
          "surface": "platform",
          "permission": "agent_memory.read",
          "source": "server/db/agent_memory_service.get_store",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/ai/memory-stores/cccccccc-cccc-4ccc-8ccc-cccccccccccc",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/ai/memory-stores/{storeId}/activate": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "storeId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "post": {
        "tags": [
          "agent-memory"
        ],
        "summary": "Atomically make this store active. Body {expectedBindingVersion, reason}. Compare-and-swap: expectedBindingVersion is REQUIRED once a binding exists, and a stale value returns REVISION_CONFLICT so two candidates from one baseline cannot both activate. Requires agent_memory.activate",
        "responses": {
          "default": {
            "description": "OK"
          },
          "403": {
            "description": "FORBIDDEN"
          },
          "409": {
            "description": "REVISION_CONFLICT | INVALID_STATE"
          }
        },
        "operationId": "postProjectsByProjectIdAiMemoryStoresByStoreIdActivate",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "agent-memory",
          "surface": "platform",
          "permission": "agent_memory.activate",
          "concurrency": "expectedBindingVersion",
          "source": "server/db/agent_memory_service.activate_store",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/ai/memory-stores/cccccccc-cccc-4ccc-8ccc-cccccccccccc/activate",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/ai/memory-stores/{storeId}/rollback": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "storeId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "post": {
        "tags": [
          "agent-memory"
        ],
        "summary": "Restore a previously-active store. Same compare-and-swap semantics as activate; binding_version is monotonic (a rollback increments it). A rejected store can never be rolled back to. Requires agent_memory.activate",
        "responses": {
          "default": {
            "description": "OK"
          },
          "409": {
            "description": "REVISION_CONFLICT | INVALID_STATE"
          }
        },
        "operationId": "postProjectsByProjectIdAiMemoryStoresByStoreIdRollback",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "agent-memory",
          "surface": "platform",
          "permission": "agent_memory.activate",
          "concurrency": "expectedBindingVersion",
          "source": "server/db/agent_memory_service.rollback_binding",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/ai/memory-stores/cccccccc-cccc-4ccc-8ccc-cccccccccccc/rollback",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/ai/agent-sessions": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "agentKey",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "startedAfter",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string",
            "format": "date-time"
          }
        },
        {
          "name": "startedBefore",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string",
            "format": "date-time"
          }
        },
        {
          "name": "limit",
          "in": "query",
          "required": false,
          "schema": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "default": 25
          }
        }
      ],
      "get": {
        "tags": [
          "agent-memory"
        ],
        "summary": "List recorded agent sessions eligible for consolidation. Requires agent_memory.read",
        "responses": {
          "default": {
            "description": "OK"
          }
        },
        "operationId": "getProjectsByProjectIdAiAgentSessions",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "agent-memory",
          "surface": "platform",
          "permission": "agent_memory.read",
          "source": "server/db/agent_memory_service.list_sessions",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/ai/agent-sessions",
          "sourceDocument": "server/db/openapi.yaml"
        }
      },
      "post": {
        "tags": [
          "agent-memory"
        ],
        "summary": "Record a completed session. Body {session:{sessionId,agentKey,startedAt,endedAt,outcomeStatus,toolCalls,errorSummary,transcriptUri,contentHash,metadata}}. Transcript BODIES are rejected — pass a transcriptUri into object storage plus its hash. Requires agent_memory.run",
        "responses": {
          "201": {
            "description": "Recorded"
          },
          "400": {
            "description": "INVALID_ARGUMENT (inline transcript, missing startedAt)"
          },
          "409": {
            "description": "DUPLICATE_REQUEST"
          }
        },
        "operationId": "postProjectsByProjectIdAiAgentSessions",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "agent-memory",
          "surface": "platform",
          "permission": "agent_memory.run",
          "source": "server/db/agent_memory_service.record_session",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/ai/agent-sessions",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/ai/agent-context": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "agent-memory"
        ],
        "summary": "Assemble the doc-14 Context Package for one agent turn. Body {intent, agentKey, scope, scopeRef, focus, focusTypes, memoryTokenBudget}. Reads the ACTIVE store only; operational memory is emitted at priority 8 (lowest) and can never override an authoritative spatial source. Requires agent_memory.read",
        "responses": {
          "default": {
            "description": "OK"
          }
        },
        "operationId": "postProjectsByProjectIdAiAgentContext",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "agent-memory",
          "surface": "platform",
          "permission": "agent_memory.read",
          "source": "server/db/agent_context_service.assemble_context",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/ai/agent-context",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects": {
      "get": {
        "tags": [
          "projects"
        ],
        "summary": "List the projects the verified principal can access, newest-updated first.",
        "parameters": [
          {
            "name": "organizationId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "active",
                "on_hold",
                "completed",
                "archived"
              ]
            }
          },
          {
            "name": "projectType",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Opaque keyset cursor from the previous page's nextCursor."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectPage"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          },
          "401": {
            "description": "UNAUTHENTICATED"
          }
        },
        "operationId": "getProjects",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "projects",
          "surface": "platform",
          "permission": "projects.read",
          "paginated": "cursor",
          "source": "server/db/extraction_api.py (PROJECTS_COLLECTION_ROUTE)",
          "examplePath": "/api/projects",
          "sourceDocument": "server/db/openapi.yaml"
        }
      },
      "post": {
        "tags": [
          "projects"
        ],
        "summary": "Create a project inside an organization. Requires projects.create, which only organization_admin and above hold. Idempotent per idempotencyKey: a replay returns 200 with the existing project instead of 201.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "organizationId",
                  "name"
                ],
                "properties": {
                  "organizationId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "name": {
                    "type": "string"
                  },
                  "slug": {
                    "type": "string",
                    "nullable": true
                  },
                  "projectType": {
                    "type": "string",
                    "nullable": true
                  },
                  "description": {
                    "type": "string",
                    "nullable": true
                  },
                  "accessMode": {
                    "type": "string",
                    "default": "organization"
                  },
                  "defaultCrsEpsg": {
                    "type": "integer",
                    "default": 2039,
                    "description": "The project's storage/working CRS. Defaults to EPSG:2039 (Israel TM Grid); geometry is stored in it and EPSG:4326 is derived."
                  },
                  "idempotencyKey": {
                    "type": "string",
                    "nullable": true
                  },
                  "metadata": {
                    "type": "object",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created"
          },
          "200": {
            "description": "Idempotent replay — the project already existed"
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          },
          "403": {
            "description": "FORBIDDEN — needs projects.create"
          }
        },
        "operationId": "postProjects",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "projects",
          "surface": "platform",
          "permission": "projects.create",
          "idempotency": "body",
          "source": "server/db/extraction_api.py (PROJECTS_COLLECTION_ROUTE)",
          "examplePath": "/api/projects",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "tags": [
          "projects"
        ],
        "summary": "Project detail plus its content inventory (reality models, photo collections, approved and planning layer counts).",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          },
          "403": {
            "description": "FORBIDDEN"
          },
          "404": {
            "description": "NOT_FOUND"
          }
        },
        "operationId": "getProjectsByProjectId",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "projects",
          "surface": "platform",
          "permission": "projects.read",
          "source": "server/db/extraction_api.py (PROJECT_DETAIL_ROUTE)",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111",
          "sourceDocument": "server/db/openapi.yaml"
        }
      },
      "patch": {
        "tags": [
          "projects"
        ],
        "summary": "Update mutable project fields. Requires projects.update (project_admin+). Unknown fields are REJECTED, never ignored.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "projectType": {
                    "type": "string"
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "draft",
                      "active",
                      "on_hold",
                      "completed",
                      "archived"
                    ]
                  },
                  "accessMode": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "UNKNOWN_FIELD / INVALID_ARGUMENT"
          },
          "403": {
            "description": "FORBIDDEN"
          }
        },
        "operationId": "patchProjectsByProjectId",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "projects",
          "surface": "platform",
          "permission": "projects.update",
          "source": "server/db/extraction_api.py (PROJECT_DETAIL_ROUTE)",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/archive": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "post": {
        "tags": [
          "projects"
        ],
        "summary": "Archive a project (projects.archive, project_admin+). Body takes no fields.",
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "description": "FORBIDDEN"
          }
        },
        "operationId": "postProjectsByProjectIdArchive",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "projects",
          "surface": "platform",
          "permission": "projects.archive",
          "source": "server/db/extraction_api.py (PROJECT_ARCHIVE_ROUTE)",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/archive",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/resources": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        },
        {
          "name": "kind",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string"
          },
          "description": "GET: filter the listing by resource kind."
        }
      ],
      "get": {
        "tags": [
          "documents"
        ],
        "summary": "List the project's documents and attachments (metadata only — never bytes). Requires resources.read.",
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "description": "FORBIDDEN"
          }
        },
        "operationId": "getProjectsByProjectIdResources",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "documents",
          "surface": "platform",
          "permission": "resources.read",
          "source": "server/db/extraction_api.py (PROJECT_RESOURCES_ROUTE)",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/resources",
          "sourceDocument": "server/db/openapi.yaml"
        }
      },
      "post": {
        "tags": [
          "documents"
        ],
        "summary": "Attach a document to the project (resources.upload, editor+). Either contentBase64 (inline bytes, up to the 32MB body limit) or sourceUrl.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "kind": {
                    "type": "string",
                    "default": "document"
                  },
                  "description": {
                    "type": "string",
                    "nullable": true
                  },
                  "contentType": {
                    "type": "string",
                    "nullable": true
                  },
                  "contentBase64": {
                    "type": "string",
                    "nullable": true,
                    "description": "Base64 bytes. The request body limit for this route is 32MB."
                  },
                  "sourceUrl": {
                    "type": "string",
                    "nullable": true
                  },
                  "metadata": {
                    "type": "object",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created"
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          },
          "403": {
            "description": "FORBIDDEN"
          },
          "413": {
            "description": "PAYLOAD_TOO_LARGE"
          }
        },
        "operationId": "postProjectsByProjectIdResources",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "documents",
          "surface": "platform",
          "permission": "resources.upload",
          "bodyLimitMb": 32,
          "source": "server/db/extraction_api.py (PROJECT_RESOURCES_ROUTE)",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/resources",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/resources/{resourceId}": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        },
        {
          "name": "resourceId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "download",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string",
            "enum": [
              "1",
              "true",
              "yes"
            ]
          },
          "description": "GET: return the stored bytes as well as the metadata."
        }
      ],
      "get": {
        "tags": [
          "documents"
        ],
        "summary": "Get one document's metadata, or its bytes with ?download=1. Requires resources.read.",
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "description": "FORBIDDEN"
          },
          "404": {
            "description": "NOT_FOUND"
          }
        },
        "operationId": "getProjectsByProjectIdResourcesByResourceId",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "documents",
          "surface": "platform",
          "permission": "resources.read",
          "source": "server/db/extraction_api.py (PROJECT_RESOURCE_ROUTE)",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/resources/aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
          "sourceDocument": "server/db/openapi.yaml"
        }
      },
      "delete": {
        "tags": [
          "documents"
        ],
        "summary": "Delete a document. Requires resources.delete — held by human editors and above, and deliberately NOT by the ai_worker service principal.",
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "description": "FORBIDDEN"
          }
        },
        "operationId": "deleteProjectsByProjectIdResourcesByResourceId",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "documents",
          "surface": "platform",
          "permission": "resources.delete",
          "source": "server/db/extraction_api.py (PROJECT_RESOURCE_ROUTE)",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/resources/aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/members": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "tags": [
          "projects"
        ],
        "summary": "List project members and their roles. Requires members.manage (project_admin+).",
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "description": "FORBIDDEN"
          }
        },
        "operationId": "getProjectsByProjectIdMembers",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "projects",
          "surface": "platform",
          "permission": "members.manage",
          "source": "server/db/extraction_api.py (PROJECT_MEMBERS_ROUTE)",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/members",
          "sourceDocument": "server/db/openapi.yaml"
        }
      },
      "post": {
        "tags": [
          "projects"
        ],
        "summary": "Add a member. Requires members.manage AND the Idempotency-Key header. A caller may never grant a role that outranks their own (409 ROLE_RANK_VIOLATION). Returns 200 rather than 201 on an idempotent replay.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKeyRequired"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "userId",
                  "role"
                ],
                "properties": {
                  "userId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "role": {
                    "type": "string",
                    "enum": [
                      "viewer",
                      "editor",
                      "reviewer",
                      "project_admin",
                      "organization_admin",
                      "ai_worker",
                      "integration_service"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created"
          },
          "200": {
            "description": "Idempotent replay"
          },
          "400": {
            "description": "IDEMPOTENCY_KEY_REQUIRED / INVALID_MEMBER_ROLE"
          },
          "403": {
            "description": "FORBIDDEN / ROLE_RANK_VIOLATION"
          },
          "409": {
            "description": "MEMBER_ALREADY_EXISTS"
          }
        },
        "operationId": "postProjectsByProjectIdMembers",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "projects",
          "surface": "platform",
          "permission": "members.manage",
          "idempotency": "header",
          "source": "server/db/extraction_api.py (PROJECT_MEMBERS_ROUTE)",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/members",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/members/{userId}": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        },
        {
          "name": "userId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        },
        {
          "$ref": "#/components/parameters/IdempotencyKeyRequired"
        }
      ],
      "patch": {
        "tags": [
          "projects"
        ],
        "summary": "Change a member's role. Body {role, expectedRole?} — expectedRole makes the change a compare-and-swap (409 VERSION_CONFLICT when stale).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "role"
                ],
                "properties": {
                  "role": {
                    "type": "string"
                  },
                  "expectedRole": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "description": "FORBIDDEN / ROLE_RANK_VIOLATION"
          },
          "404": {
            "description": "MEMBER_NOT_FOUND"
          },
          "409": {
            "description": "VERSION_CONFLICT | LAST_PROJECT_ADMIN"
          }
        },
        "operationId": "patchProjectsByProjectIdMembersByUserId",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "projects",
          "surface": "platform",
          "permission": "members.manage",
          "idempotency": "header",
          "source": "server/db/extraction_api.py (PROJECT_MEMBER_ROUTE)",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/members/33333333-3333-4333-8333-333333333333",
          "sourceDocument": "server/db/openapi.yaml"
        }
      },
      "delete": {
        "tags": [
          "projects"
        ],
        "summary": "Remove a member. Removing the last project_admin is refused (409 LAST_PROJECT_ADMIN).",
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "description": "FORBIDDEN"
          },
          "404": {
            "description": "MEMBER_NOT_FOUND"
          },
          "409": {
            "description": "LAST_PROJECT_ADMIN"
          }
        },
        "operationId": "deleteProjectsByProjectIdMembersByUserId",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "projects",
          "surface": "platform",
          "permission": "members.manage",
          "idempotency": "header",
          "source": "server/db/extraction_api.py (PROJECT_MEMBER_ROUTE)",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/members/33333333-3333-4333-8333-333333333333",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/gis-layers/{layerCode}/search": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "layerCode",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "q",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string"
          },
          "description": "Attribute search text. An empty q returns the layer's leading rows."
        },
        {
          "name": "limit",
          "in": "query",
          "required": false,
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        }
      ],
      "get": {
        "tags": [
          "gis"
        ],
        "summary": "Attribute search within one cataloged GIS layer's current snapshot. Read-only.",
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "INVALID_LIMIT"
          },
          "404": {
            "description": "NOT_FOUND"
          }
        },
        "operationId": "getProjectsByProjectIdGisLayersByLayerCodeSearch",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "gis",
          "surface": "platform",
          "role": "viewer",
          "source": "server/db/gis_catalog_service.search_layer_features",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/gis-layers/planning.urban_renewal_declared/search",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/models": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "status",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string"
          },
          "description": "GET: filter by model status."
        }
      ],
      "get": {
        "tags": [
          "reality-models"
        ],
        "summary": "List the project's Reality Models in CLIENT projection. storagePrefix, bucket and rootKey are never returned: where a model's bytes live is the exact scope a signed CloudFront policy authorizes, so it stays server-side. Ask GET .../reality-models/{modelId}/delivery for an address instead. When SKYLENS_RASTER_PRODUCTS_ENABLED is on, orthophoto and terrain rows are served from reality.raster_products and are hidden from this listing.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ModelList"
                }
              }
            }
          },
          "403": {
            "description": "FORBIDDEN"
          }
        },
        "operationId": "getProjectsByProjectIdModels",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "reality-models",
          "surface": "platform",
          "role": "viewer",
          "source": "server/db/reality_model_service.list_models",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/models",
          "sourceDocument": "server/db/openapi.yaml"
        }
      },
      "post": {
        "tags": [
          "reality-models"
        ],
        "summary": "Register an immutable Reality Model version (editor+). storagePrefix and rootKey are ABSENT from the accepted field set on purpose — the storage identity is derived server-side from (organization, project, model), so a caller that sends one gets 400 UNKNOWN_FIELD rather than a silently ignored field it believes took effect. rootFilename is the single path component a caller may still name, and it is validated to resolve inside the model's own prefix.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "datasetKey",
                  "name",
                  "tilesetUrl"
                ],
                "properties": {
                  "datasetKey": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "tilesetUrl": {
                    "type": "string"
                  },
                  "modelType": {
                    "type": "string",
                    "default": "3d_tiles",
                    "description": "3d_tiles | orthophoto | terrain | mesh — see the reality model architecture doc."
                  },
                  "version": {
                    "type": "integer",
                    "default": 1
                  },
                  "status": {
                    "type": "string",
                    "default": "available"
                  },
                  "storageBucket": {
                    "type": "string",
                    "nullable": true
                  },
                  "rootFilename": {
                    "type": "string",
                    "nullable": true
                  },
                  "sourceCrsEpsg": {
                    "type": "integer",
                    "nullable": true
                  },
                  "captureDate": {
                    "type": "string",
                    "nullable": true
                  },
                  "producer": {
                    "type": "string",
                    "nullable": true
                  },
                  "fileCount": {
                    "type": "integer",
                    "nullable": true
                  },
                  "totalBytes": {
                    "type": "integer",
                    "nullable": true
                  },
                  "bounds": {
                    "type": "object",
                    "nullable": true
                  },
                  "validation": {
                    "type": "object",
                    "nullable": true
                  },
                  "provenance": {
                    "type": "object",
                    "nullable": true
                  },
                  "metadata": {
                    "type": "object",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created"
          },
          "400": {
            "description": "UNKNOWN_FIELD (storagePrefix/rootKey) / INVALID_ARGUMENT"
          },
          "403": {
            "description": "FORBIDDEN"
          }
        },
        "operationId": "postProjectsByProjectIdModels",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "reality-models",
          "surface": "platform",
          "role": "editor",
          "source": "server/db/reality_model_service.register_model",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/models",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/viewer/source-layers": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "types",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string"
          },
          "description": "Comma-separated source-layer types."
        },
        {
          "name": "status",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "published",
          "in": "query",
          "required": false,
          "schema": {
            "type": "boolean",
            "default": true
          }
        },
        {
          "name": "cursor",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "limit",
          "in": "query",
          "required": false,
          "schema": {
            "type": "integer",
            "minimum": 1,
            "maximum": 200,
            "default": 50
          }
        }
      ],
      "get": {
        "tags": [
          "viewer"
        ],
        "summary": "Dynamic Viewer source-layer selector projected over the canonical reality catalog. Flag-gated by SKYLENS_API_VIEWER_SOURCE_LAYERS_ENABLED (default OFF -> 404) and optionally narrowed by a project allowlist.",
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          },
          "404": {
            "description": "NOT_FOUND (flag off or project not in the rollout allowlist)"
          }
        },
        "operationId": "getProjectsByProjectIdViewerSourceLayers",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "viewer",
          "surface": "platform",
          "role": "viewer",
          "paginated": "cursor",
          "flag": "SKYLENS_API_VIEWER_SOURCE_LAYERS_ENABLED",
          "source": "server/db/reality_source_layer_service.list_source_layers",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/viewer/source-layers",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/elevation/sample": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "elevation"
        ],
        "summary": "Sample the project's DSM over an AOI and return height statistics (ground estimate, percentiles, nDSM range).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "bbox"
                ],
                "properties": {
                  "bbox": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    },
                    "minItems": 4,
                    "maxItems": 4,
                    "description": "[minX, minY, maxX, maxY] in bboxEpsg. Axis order is X/easting/longitude first — NOT the EPSG-authority lat/lon order."
                  },
                  "bboxEpsg": {
                    "type": "integer",
                    "default": 4326,
                    "description": "EPSG code the bbox is expressed in. 4326 and the project CRS (2039 by default) are the tested values."
                  },
                  "groundPercentile": {
                    "type": "number",
                    "default": 10.0,
                    "description": "Percentile of sampled elevations treated as ground when deriving nDSM heights."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          },
          "403": {
            "description": "FORBIDDEN"
          },
          "409": {
            "description": "ELEVATION_UNAVAILABLE — the project has no usable DSM"
          }
        },
        "operationId": "postProjectsByProjectIdElevationSample",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "elevation",
          "surface": "platform",
          "permission": "layers.read",
          "crs": "bboxEpsg, default 4326",
          "source": "server/db/elevation_service.sample_aoi",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/elevation/sample",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/elevation/gate": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "elevation"
        ],
        "summary": "Keep only the features whose nDSM height clears minHeightM. Used to drop ground-level false positives from a segmentation result.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "features",
                  "minHeightM"
                ],
                "properties": {
                  "features": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "GeoJSON Features expressed in featureEpsg."
                  },
                  "minHeightM": {
                    "type": "number",
                    "description": "Metres above the derived ground surface."
                  },
                  "featureEpsg": {
                    "type": "integer",
                    "default": 4326
                  },
                  "groundPercentile": {
                    "type": "number",
                    "default": 10.0
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          },
          "403": {
            "description": "FORBIDDEN"
          },
          "409": {
            "description": "ELEVATION_UNAVAILABLE"
          }
        },
        "operationId": "postProjectsByProjectIdElevationGate",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "elevation",
          "surface": "platform",
          "permission": "layers.read",
          "crs": "featureEpsg, default 4326",
          "source": "server/db/elevation_service.gate_features",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/elevation/gate",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/aois/register": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "viewer"
        ],
        "summary": "Register an AOI against an immutable source revision (editor+). Idempotent per idempotencyKey.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "geometry"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "geometry": {
                    "type": "object",
                    "description": "GeoJSON geometry in EPSG:4326."
                  },
                  "realityModelId": {
                    "type": "string",
                    "nullable": true
                  },
                  "sourceModelId": {
                    "type": "string",
                    "nullable": true
                  },
                  "idempotencyKey": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created"
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          },
          "403": {
            "description": "FORBIDDEN"
          }
        },
        "operationId": "postProjectsByProjectIdAoisRegister",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "beta",
          "domain": "viewer",
          "surface": "platform",
          "role": "editor",
          "idempotency": "body",
          "crs": "GeoJSON EPSG:4326",
          "source": "server/db/aoi_registration_service.register_aoi",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/aois/register",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/cad-imports": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "imports"
        ],
        "summary": "Convert an uploaded DWG/DXF/DGN drawing into GIS features (editor+). The 32MB body limit applies to this route.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "filename",
                  "contentBase64"
                ],
                "properties": {
                  "filename": {
                    "type": "string"
                  },
                  "contentBase64": {
                    "type": "string"
                  },
                  "sourceEpsg": {
                    "type": "integer",
                    "nullable": true,
                    "description": "CRS the drawing coordinates are in. Omit only when the file carries a georeference."
                  },
                  "layerFilter": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          },
          "403": {
            "description": "FORBIDDEN"
          },
          "413": {
            "description": "PAYLOAD_TOO_LARGE"
          }
        },
        "operationId": "postProjectsByProjectIdCadImports",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "imports",
          "surface": "platform",
          "role": "editor",
          "bodyLimitMb": 32,
          "source": "server/db/cad_import_service.convert_cad_upload",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/cad-imports",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/architectural-model-imports": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "status",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "imports"
        ],
        "summary": "List architectural (BIM) import sessions.",
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "description": "FORBIDDEN"
          }
        },
        "operationId": "getProjectsByProjectIdArchitecturalModelImports",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "imports",
          "surface": "platform",
          "role": "viewer",
          "source": "server/db/architectural_model_service.list_imports",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/architectural-model-imports",
          "sourceDocument": "server/db/openapi.yaml"
        }
      },
      "post": {
        "tags": [
          "imports"
        ],
        "summary": "Open an architectural-model import session (editor+). The session is the unit of revision; the bytes arrive at /inspect.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "sourceFormat": {
                    "type": "string",
                    "nullable": true,
                    "description": "IFC / IFCZIP / RVT / GLB / ... "
                  },
                  "originalFilename": {
                    "type": "string",
                    "nullable": true
                  },
                  "classification": {
                    "type": "string",
                    "nullable": true
                  },
                  "discipline": {
                    "type": "string",
                    "nullable": true
                  },
                  "buildingEntityId": {
                    "type": "string",
                    "nullable": true
                  },
                  "revision": {
                    "type": "string",
                    "nullable": true
                  },
                  "issueDate": {
                    "type": "string",
                    "nullable": true
                  },
                  "designStatus": {
                    "type": "string",
                    "nullable": true
                  },
                  "revisionNotes": {
                    "type": "string",
                    "nullable": true
                  },
                  "units": {
                    "type": "string",
                    "nullable": true
                  },
                  "declaredCrsEpsg": {
                    "type": "integer",
                    "nullable": true
                  },
                  "idempotencyKey": {
                    "type": "string",
                    "nullable": true
                  },
                  "metadata": {
                    "type": "object",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created"
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          },
          "403": {
            "description": "FORBIDDEN"
          }
        },
        "operationId": "postProjectsByProjectIdArchitecturalModelImports",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "imports",
          "surface": "platform",
          "role": "editor",
          "idempotency": "body",
          "source": "server/db/architectural_model_service.create_import",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/architectural-model-imports",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/architectural-model-imports/{importId}": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "importId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "imports"
        ],
        "summary": "Import session detail: parsed semantics, element counts, placement and publication state.",
        "responses": {
          "200": {
            "description": "OK"
          },
          "404": {
            "description": "NOT_FOUND"
          }
        },
        "operationId": "getProjectsByProjectIdArchitecturalModelImportsByImportId",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "imports",
          "surface": "platform",
          "role": "viewer",
          "source": "server/db/architectural_model_service.get_import",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/architectural-model-imports/eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/architectural-model-imports/{importId}/inspect": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "importId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "imports"
        ],
        "summary": "Upload and parse the model bytes. IFC is read for SEMANTICS by a pure-python STEP parser; render geometry is a separate glTF/3D Tiles asset.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "filename",
                  "contentBase64"
                ],
                "properties": {
                  "filename": {
                    "type": "string"
                  },
                  "contentBase64": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          },
          "413": {
            "description": "PAYLOAD_TOO_LARGE"
          },
          "422": {
            "description": "VALIDATION_FAILED"
          }
        },
        "operationId": "postProjectsByProjectIdArchitecturalModelImportsByImportIdInspect",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "imports",
          "surface": "platform",
          "role": "editor",
          "bodyLimitMb": 1,
          "source": "server/db/architectural_model_service.inspect_import",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/architectural-model-imports/eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee/inspect",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/architectural-model-imports/{importId}/placement": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "importId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "imports"
        ],
        "summary": "Georeference the model. Body {mode, originLon, originLat, originHeight, crsEpsg, trueNorthDeg, notes}.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "mode"
                ],
                "properties": {
                  "mode": {
                    "type": "string"
                  },
                  "originLon": {
                    "type": "number",
                    "nullable": true
                  },
                  "originLat": {
                    "type": "number",
                    "nullable": true
                  },
                  "originHeight": {
                    "type": "number",
                    "nullable": true
                  },
                  "crsEpsg": {
                    "type": "integer",
                    "nullable": true
                  },
                  "trueNorthDeg": {
                    "type": "number",
                    "nullable": true
                  },
                  "notes": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "INVALID_ARGUMENT"
          }
        },
        "operationId": "postProjectsByProjectIdArchitecturalModelImportsByImportIdPlacement",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "imports",
          "surface": "platform",
          "role": "editor",
          "crs": "crsEpsg names the model's own CRS",
          "source": "server/db/architectural_model_service.set_placement",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/architectural-model-imports/eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee/placement",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/architectural-model-imports/{importId}/publish": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "importId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "imports"
        ],
        "summary": "Publish the import as a Reality Model revision. Same rule as POST /models: no storagePrefix, no renderRootKey — the location is derived, not declared.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "datasetKey",
                  "tilesetUrl"
                ],
                "properties": {
                  "datasetKey": {
                    "type": "string"
                  },
                  "tilesetUrl": {
                    "type": "string"
                  },
                  "storageBucket": {
                    "type": "string",
                    "nullable": true
                  },
                  "renderRootFilename": {
                    "type": "string",
                    "nullable": true
                  },
                  "bounds": {
                    "type": "object",
                    "nullable": true
                  },
                  "version": {
                    "type": "integer",
                    "nullable": true
                  },
                  "activate": {
                    "type": "boolean",
                    "default": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "UNKNOWN_FIELD / INVALID_ARGUMENT"
          },
          "403": {
            "description": "FORBIDDEN"
          }
        },
        "operationId": "postProjectsByProjectIdArchitecturalModelImportsByImportIdPublish",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "imports",
          "surface": "platform",
          "role": "editor",
          "source": "server/db/architectural_model_service.publish_import",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/architectural-model-imports/eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee/publish",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/architectural-model-imports/{importId}/cancel": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "importId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "imports"
        ],
        "summary": "Abandon an unpublished import session. Body {reason}.",
        "responses": {
          "200": {
            "description": "OK"
          },
          "409": {
            "description": "INVALID_STATE"
          }
        },
        "operationId": "postProjectsByProjectIdArchitecturalModelImportsByImportIdCancel",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "imports",
          "surface": "platform",
          "role": "editor",
          "source": "server/db/architectural_model_service.cancel_import",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/architectural-model-imports/eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee/cancel",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/projects/{projectId}/bim-models/{modelId}/features/{featureKey}": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "modelId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "featureKey",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "The BIM element key (GlobalId or the import's stable element key)."
        }
      ],
      "get": {
        "tags": [
          "imports"
        ],
        "summary": "Property set of one BIM element — what the Viewer shows when an element is picked.",
        "responses": {
          "200": {
            "description": "OK"
          },
          "404": {
            "description": "NOT_FOUND"
          }
        },
        "operationId": "getProjectsByProjectIdBimModelsByModelIdFeaturesByFeatureKey",
        "security": [
          {
            "firebaseIdToken": []
          },
          {
            "devUserHeader": []
          }
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "imports",
          "surface": "platform",
          "role": "viewer",
          "source": "server/db/architectural_model_service.get_bim_feature",
          "examplePath": "/api/projects/11111111-1111-4111-8111-111111111111/bim-models/44444444-4444-4444-8444-444444444444/features/1a2B3c4D5e6F7g8H9i0Jk",
          "sourceDocument": "server/db/openapi.yaml"
        }
      }
    },
    "/api/partners/v1/external-projects": {
      "get": {
        "summary": "List external projects (cursor-paginated)",
        "description": "Scoped to the verified partner. Ordered by `(updated_at DESC,\npartnerProjectId DESC)` — a deterministic tie-breaker, so a page boundary\nis stable even when many rows share a timestamp. One SQL query per page\nregardless of page size; backed by `idx_partner_projects_catalog`.\n",
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "maximum": 100
            }
          },
          {
            "name": "externalTenantId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "archived"
              ]
            }
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "Substring match on external id or display name."
          },
          {
            "$ref": "#/components/parameters/RequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of external projects.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "object",
                          "properties": {
                            "externalProjects": {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/ExternalProject"
                              }
                            },
                            "nextCursor": {
                              "type": [
                                "string",
                                "null"
                              ]
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_CREDENTIAL"
          },
          "403": {
            "description": "SCOPE_DENIED | PARTNER_SUSPENDED"
          },
          "429": {
            "description": "RATE_LIMITED"
          }
        },
        "operationId": "getPartnersV1ExternalProjects",
        "security": [
          {
            "partnerCredential": [
              "external_projects:read"
            ]
          }
        ],
        "tags": [
          "partners"
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "partners",
          "surface": "partner",
          "scope": "external_projects:read",
          "paginated": "cursor",
          "source": "server/db/partner_api.py (EXTERNAL_PROJECTS_COLLECTION)",
          "examplePath": "/api/partners/v1/external-projects",
          "sourceDocument": "server/db/partner_openapi.yaml"
        }
      }
    },
    "/api/partners/v1/external-projects/{externalProjectId}": {
      "parameters": [
        {
          "name": "externalProjectId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          }
        }
      ],
      "get": {
        "summary": "Retrieve one external project",
        "description": "`externalTenantId` is required when the same external id exists under\nmore than one of the partner's tenants; otherwise the request is\n`INVALID_ARGUMENT` (ambiguous), never an arbitrary pick.\n",
        "parameters": [
          {
            "name": "externalTenantId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/RequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "The external project and its current snapshot.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "$ref": "#/components/schemas/ExternalProject"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "INVALID_ARGUMENT (ambiguous across tenants)"
          },
          "404": {
            "description": "NOT_FOUND. Also returned for another partner's project — the response\nis identical whether or not the resource exists.\n"
          }
        },
        "operationId": "getPartnersV1ExternalProjectsByExternalProjectId",
        "security": [
          {
            "partnerCredential": [
              "external_projects:read"
            ]
          }
        ],
        "tags": [
          "partners"
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "partners",
          "surface": "partner",
          "scope": "external_projects:read",
          "source": "server/db/partner_api.py (EXTERNAL_PROJECT_ITEM)",
          "examplePath": "/api/partners/v1/external-projects/ext_demo_001",
          "sourceDocument": "server/db/partner_openapi.yaml"
        }
      },
      "put": {
        "summary": "Create or update an external project (append-only snapshot)",
        "description": "Upserts the stable catalog record and appends an immutable snapshot.\n\nIdempotent at two levels: an identical body appends no snapshot and\nreturns the existing one (content-addressed by sha256), independently of\nthe `Idempotency-Key` replay cache.\n\n**Creates no `core.projects` or `core.organizations` row.** Naming\nanother partner's tenant does not reach it — it creates a same-named\ntenant under the caller's own partner.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "$ref": "#/components/parameters/RequestId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExternalProjectUpsert"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Existing record updated.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "$ref": "#/components/schemas/ExternalProject"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "201": {
            "description": "Stable record created."
          },
          "400": {
            "description": "EXTERNAL_PROJECT_INVALID | UNKNOWN_FIELD | UNSUPPORTED_VERSION |\nIDEMPOTENCY_KEY_REQUIRED | INVALID_ARGUMENT\n"
          },
          "403": {
            "description": "SCOPE_DENIED (credential lacks external_projects:write)"
          },
          "409": {
            "description": "IDEMPOTENCY_CONFLICT (same key, different body) |\nIDEMPOTENCY_IN_PROGRESS (concurrent request holds the key) |\nSNAPSHOT_VERSION_CONFLICT (same sourceVersion, different body) |\nEXTERNAL_TENANT_INACTIVE\n"
          },
          "413": {
            "description": "PAYLOAD_TOO_LARGE (> 4 MiB)"
          },
          "429": {
            "description": "RATE_LIMITED"
          }
        },
        "operationId": "putPartnersV1ExternalProjectsByExternalProjectId",
        "security": [
          {
            "partnerCredential": [
              "external_projects:write"
            ]
          }
        ],
        "tags": [
          "partners"
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "partners",
          "surface": "partner",
          "scope": "external_projects:write",
          "idempotency": "header",
          "source": "server/db/partner_api.py (EXTERNAL_PROJECT_ITEM)",
          "examplePath": "/api/partners/v1/external-projects/ext_demo_001",
          "sourceDocument": "server/db/partner_openapi.yaml"
        }
      }
    },
    "/api/partners/v1/services": {
      "get": {
        "summary": "List services the partner is entitled to sell",
        "description": "Scoped to the partner's ACTIVE CONTRACT and explicit entitlements. An\nunentitled service is absent, not merely hidden — and its detail route\nreturns 404, so entitlement cannot be probed.\n\nFeeds the customer widget's service cards. `fromPriceMinor` is a display\nanchor only; the authoritative amount always comes from a quote.\n",
        "parameters": [
          {
            "name": "locale",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Selects the copy variant. Production defaults to the repository's English rule; unknown locales fall back to it rather than to null. Hebrew/RTL is enabled by configuration, not code."
          },
          {
            "$ref": "#/components/parameters/RequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "Entitled services.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "object",
                          "properties": {
                            "services": {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/Service"
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "403": {
            "description": "SCOPE_DENIED | PARTNER_SUSPENDED | SERVICE_NOT_AVAILABLE (no active contract)"
          }
        },
        "operationId": "getPartnersV1Services",
        "security": [
          {
            "partnerCredential": [
              "services:read"
            ]
          }
        ],
        "tags": [
          "partners"
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "partners",
          "surface": "partner",
          "scope": "services:read",
          "source": "server/db/partner_api.py (SERVICES_COLLECTION)",
          "examplePath": "/api/partners/v1/services",
          "sourceDocument": "server/db/partner_openapi.yaml"
        }
      }
    },
    "/api/partners/v1/services/{serviceCode}": {
      "parameters": [
        {
          "name": "serviceCode",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^[a-z0-9_]{1,64}$"
          }
        }
      ],
      "get": {
        "summary": "Retrieve one service with its calculator specification",
        "description": "Adds the declarative `calculatorInputs` the widget renders from, the\nrequired project facts, assumptions and exclusions. The frontend never\nhardcodes a field list or its copy.\n",
        "parameters": [
          {
            "name": "locale",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/RequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "Service detail.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "$ref": "#/components/schemas/ServiceDetail"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "NOT_FOUND — absent, or not entitled for this partner"
          }
        },
        "operationId": "getPartnersV1ServicesByServiceCode",
        "security": [
          {
            "partnerCredential": [
              "services:read"
            ]
          }
        ],
        "tags": [
          "partners"
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "partners",
          "surface": "partner",
          "scope": "services:read",
          "source": "server/db/partner_api.py (SERVICE_ITEM)",
          "examplePath": "/api/partners/v1/services/orthophoto_survey",
          "sourceDocument": "server/db/partner_openapi.yaml"
        }
      }
    },
    "/api/partners/v1/quotes": {
      "post": {
        "summary": "Calculate and persist an immutable quote",
        "description": "Prices the request server-side from the versioned service, pricing and\nrule configuration, against the project's immutable snapshot.\n\nThe quote records every version used (service, pricing, contract,\nsnapshot) plus an `inputHash`, so it is reproducible and is never\nre-priced by a later pricing version.\n\nOutcome is one of `fixed`, `range` or `manual_review` — see the Quote\nschema. Acceptance is Phase 3 and is not available here.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "$ref": "#/components/parameters/RequestId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuoteRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Quote calculated.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "$ref": "#/components/schemas/Quote"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "UNKNOWN_FIELD (includes any client-submitted total) |\nUNSUPPORTED_VERSION | INVALID_ARGUMENT | IDEMPOTENCY_KEY_REQUIRED\n"
          },
          "403": {
            "description": "SCOPE_DENIED | SERVICE_NOT_AVAILABLE"
          },
          "404": {
            "description": "NOT_FOUND — service, project or snapshot"
          },
          "409": {
            "description": "IDEMPOTENCY_CONFLICT | IDEMPOTENCY_IN_PROGRESS"
          }
        },
        "operationId": "postPartnersV1Quotes",
        "security": [
          {
            "partnerCredential": [
              "quotes:write"
            ]
          }
        ],
        "tags": [
          "partners"
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "partners",
          "surface": "partner",
          "scope": "quotes:write",
          "idempotency": "header",
          "source": "server/db/partner_api.py (QUOTES_COLLECTION)",
          "examplePath": "/api/partners/v1/quotes",
          "sourceDocument": "server/db/partner_openapi.yaml"
        }
      }
    },
    "/api/partners/v1/quotes/{quoteId}": {
      "parameters": [
        {
          "name": "quoteId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "summary": "Retrieve a quote",
        "description": "Scoped to the verified partner; another partner's quote is 404. `status`\nis derived at read time — a quote past `validUntil` reports `expired` and\n`orderable: false` without any write.\n",
        "parameters": [
          {
            "name": "locale",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/RequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "The quote.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "$ref": "#/components/schemas/Quote"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "NOT_FOUND"
          }
        },
        "operationId": "getPartnersV1QuotesByQuoteId",
        "security": [
          {
            "partnerCredential": [
              "quotes:read"
            ]
          }
        ],
        "tags": [
          "partners"
        ],
        "x-skylens": {
          "status": "stable",
          "domain": "partners",
          "surface": "partner",
          "scope": "quotes:read",
          "source": "server/db/partner_api.py (QUOTE_ITEM)",
          "examplePath": "/api/partners/v1/quotes/ffffffff-ffff-4fff-8fff-ffffffffffff",
          "sourceDocument": "server/db/partner_openapi.yaml"
        }
      }
    }
  },
  "components": {
    "schemas": {
      "DraftApprove": {
        "allOf": [
          {
            "$ref": "#/components/schemas/DraftDecision"
          },
          {
            "type": "object",
            "properties": {
              "observedAt": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When the underlying observation was made, if it differs from the approval time."
              }
            }
          }
        ]
      },
      "DraftDecision": {
        "type": "object",
        "required": [
          "expectedRevision",
          "reason",
          "idempotencyKey"
        ],
        "properties": {
          "expectedRevision": {
            "type": "integer",
            "description": "The revision the caller read. A stale value is 409 VERSION_CONFLICT — the edit is never silently applied on top of someone else's."
          },
          "reason": {
            "type": "string",
            "description": "Recorded in the audit trail; required on every review decision."
          },
          "idempotencyKey": {
            "type": "string"
          }
        }
      },
      "DraftGeometryEdit": {
        "allOf": [
          {
            "$ref": "#/components/schemas/DraftDecision"
          },
          {
            "type": "object",
            "required": [
              "geometry"
            ],
            "properties": {
              "geometry": {
                "type": "object",
                "description": "GeoJSON geometry, EPSG:4326."
              }
            }
          }
        ]
      },
      "DraftReclassify": {
        "allOf": [
          {
            "$ref": "#/components/schemas/DraftDecision"
          },
          {
            "type": "object",
            "required": [
              "targetLayerId",
              "targetEntityTypeId"
            ],
            "properties": {
              "targetLayerId": {
                "type": "string"
              },
              "targetEntityTypeId": {
                "type": "string",
                "description": "Must exist in shared/entity_registry.json."
              }
            }
          }
        ]
      },
      "DraftRegenerate": {
        "allOf": [
          {
            "$ref": "#/components/schemas/DraftDecision"
          },
          {
            "type": "object",
            "properties": {
              "options": {
                "type": "object",
                "nullable": true
              }
            }
          }
        ]
      },
      "DraftSplit": {
        "allOf": [
          {
            "$ref": "#/components/schemas/DraftDecision"
          },
          {
            "type": "object",
            "required": [
              "geometries"
            ],
            "properties": {
              "geometries": {
                "type": "array",
                "items": {
                  "type": "object"
                },
                "description": "Two or more GeoJSON geometries, EPSG:4326."
              }
            }
          }
        ]
      },
      "Envelope": {
        "type": "object",
        "required": [
          "data",
          "meta",
          "error"
        ],
        "properties": {
          "data": {
            "description": "Payload on success; null on error"
          },
          "meta": {
            "type": "object",
            "required": [
              "requestId",
              "timestamp",
              "contractVersion"
            ],
            "properties": {
              "requestId": {
                "type": "string"
              },
              "timestamp": {
                "type": "string",
                "format": "date-time"
              },
              "contractVersion": {
                "type": "string",
                "const": "1.0"
              }
            }
          },
          "error": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "required": [
                  "code",
                  "message",
                  "retryable"
                ],
                "properties": {
                  "code": {
                    "type": "string"
                  },
                  "message": {
                    "type": "string"
                  },
                  "retryable": {
                    "type": "boolean"
                  }
                }
              }
            ]
          }
        }
      },
      "Error": {
        "type": "object",
        "description": "The platform API's error envelope, as implemented. NOTE: this is NOT the {data, meta, error} envelope in docs/contracts/API.md §2.2 — only the Partner API implements that. Successes are bare objects with correlationId and traceId merged in at the top level.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable machine code, e.g. NOT_FOUND, FORBIDDEN, INVALID_ARGUMENT, VERSION_CONFLICT."
              },
              "message": {
                "type": "string",
                "description": "Caller-safe text. An uncoded internal fault is always the generic 'internal error'; the real detail is logged server-side under the same correlationId."
              },
              "retryable": {
                "type": "boolean",
                "description": "True when repeating the identical request may succeed (e.g. QUERY_TIMEOUT)."
              },
              "correlationId": {
                "type": "string",
                "description": "Quote this when contacting support."
              }
            }
          }
        }
      },
      "ExternalProject": {
        "type": "object",
        "properties": {
          "partnerProjectId": {
            "type": "string",
            "format": "uuid"
          },
          "externalProjectId": {
            "type": "string"
          },
          "externalTenantId": {
            "type": "string"
          },
          "displayName": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "archived"
            ]
          },
          "snapshotCount": {
            "type": "integer"
          },
          "sourceUpdatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "skylensProjectLinked": {
            "type": "boolean",
            "description": "Always false in Phase 1. Present so the contract does not change\nshape when lazy activation populates it.\n"
          },
          "currentSnapshot": {
            "type": "object",
            "description": "Normalized, queryable facts plus provenance. These are the inputs the\nPhase 2 pricing calculator will read.\n",
            "properties": {
              "snapshotId": {
                "type": "string",
                "format": "uuid"
              },
              "snapshotHash": {
                "type": "string",
                "description": "sha256 of the canonical payload"
              },
              "sourceVersion": {
                "type": "string"
              },
              "schemaVersion": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "projectType": {
                "type": "string"
              },
              "constructionStage": {
                "type": "string"
              },
              "address": {
                "type": "string"
              },
              "areaSqm": {
                "type": "number"
              },
              "parcelCount": {
                "type": "integer"
              },
              "buildingCount": {
                "type": "integer"
              },
              "maxBuildingHeightM": {
                "type": "number"
              },
              "receivedAt": {
                "type": "string",
                "format": "date-time"
              },
              "location": {
                "type": "object",
                "description": "GeoJSON Point (EPSG:4326)"
              },
              "boundary": {
                "type": "object",
                "description": "GeoJSON MultiPolygon (EPSG:4326)"
              }
            }
          }
        }
      },
      "ExternalProjectUpsert": {
        "type": "object",
        "required": [
          "schemaVersion",
          "externalTenantId"
        ],
        "additionalProperties": false,
        "properties": {
          "schemaVersion": {
            "type": "string",
            "enum": [
              "1.0"
            ]
          },
          "externalTenantId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "externalUserId": {
            "type": "string",
            "maxLength": 200
          },
          "sourceVersion": {
            "type": "string",
            "maxLength": 100,
            "description": "Partner-side version marker. Reusing one with a different body is a\n`SNAPSHOT_VERSION_CONFLICT`: provenance must not fork silently.\n"
          },
          "name": {
            "type": "string",
            "maxLength": 300
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "archived"
            ]
          },
          "projectType": {
            "type": "string",
            "maxLength": 100
          },
          "constructionStage": {
            "type": "string",
            "maxLength": 100
          },
          "address": {
            "type": "string",
            "maxLength": 500
          },
          "location": {
            "type": "object",
            "properties": {
              "longitude": {
                "type": "number"
              },
              "latitude": {
                "type": "number"
              },
              "crs": {
                "type": "string",
                "default": "EPSG:4326"
              }
            }
          },
          "boundary": {
            "type": "object",
            "description": "GeoJSON Polygon or MultiPolygon, max 50,000 vertices. Stored as\ncanonical EPSG:2039 with EPSG:4326 derived by trigger. Structure is\nvalidated in Python before PostGIS sees it, so a malformed geometry\ncan never abort the surrounding transaction.\n",
            "properties": {
              "geometry": {
                "type": "object"
              },
              "crs": {
                "type": "string",
                "default": "EPSG:4326"
              }
            }
          },
          "areaSqm": {
            "type": "number",
            "minimum": 0,
            "description": "Derived from the boundary when omitted."
          },
          "parcels": {
            "type": "array",
            "maxItems": 2000,
            "items": {
              "type": "object"
            }
          },
          "buildings": {
            "type": "array",
            "maxItems": 2000,
            "items": {
              "type": "object"
            }
          },
          "contacts": {
            "type": "array",
            "maxItems": 50,
            "items": {
              "type": "object"
            }
          },
          "documents": {
            "type": "array",
            "maxItems": 200,
            "items": {
              "type": "object"
            }
          },
          "dates": {
            "type": "object"
          },
          "customerOrganization": {
            "type": "object"
          },
          "permissions": {
            "type": "object"
          },
          "deliveryPreferences": {
            "type": "object"
          },
          "metadata": {
            "type": "object",
            "maxProperties": 100
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "IdempotentCommand": {
        "type": "object",
        "required": [
          "idempotencyKey"
        ],
        "properties": {
          "idempotencyKey": {
            "type": "string",
            "description": "Required in the BODY for the extraction lifecycle commands (these predate the Idempotency-Key header)."
          }
        }
      },
      "ModelList": {
        "type": "object",
        "properties": {
          "models": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "Client projection — storagePrefix, bucket and rootKey are deliberately absent.",
              "properties": {
                "modelId": {
                  "type": "string",
                  "format": "uuid"
                },
                "datasetKey": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "modelType": {
                  "type": "string"
                },
                "version": {
                  "type": "integer"
                },
                "status": {
                  "type": "string"
                },
                "bounds": {
                  "type": "object",
                  "nullable": true
                },
                "captureDate": {
                  "type": "string",
                  "nullable": true
                }
              }
            }
          }
        }
      },
      "Page": {
        "type": "object",
        "description": "Cursor pagination. nextCursor is opaque and null on the final page. Collections that are NOT paginated return a plain array under a named key instead.",
        "properties": {
          "nextCursor": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "Project": {
        "type": "object",
        "properties": {
          "projectId": {
            "type": "string",
            "format": "uuid"
          },
          "organizationId": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string",
            "nullable": true
          },
          "projectType": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "active",
              "on_hold",
              "completed",
              "archived"
            ]
          },
          "accessMode": {
            "type": "string"
          },
          "defaultCrsEpsg": {
            "type": "integer"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ProjectPage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Page"
          },
          {
            "type": "object",
            "properties": {
              "projects": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          }
        ]
      },
      "Quote": {
        "type": "object",
        "description": "Customer-safe by construction. There is no commission, cost, margin or\ncontract-version field: the projection names every field it emits.\n",
        "properties": {
          "quoteId": {
            "type": "string",
            "format": "uuid"
          },
          "quoteNumber": {
            "type": "string"
          },
          "quoteType": {
            "type": "string",
            "enum": [
              "fixed",
              "range",
              "manual_review"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "calculated",
              "expired",
              "superseded",
              "accepted"
            ],
            "description": "`expired` is derived at read time from validUntil."
          },
          "orderable": {
            "type": "boolean",
            "description": "True only for a live `fixed` quote. Phase 3 will allow accepting exactly these; a range or manual-review quote is never orderable."
          },
          "currency": {
            "type": "string"
          },
          "serviceCode": {
            "type": "string"
          },
          "serviceName": {
            "type": "string"
          },
          "serviceVersion": {
            "type": "integer"
          },
          "pricingVersion": {
            "type": "integer"
          },
          "externalProjectId": {
            "type": "string"
          },
          "externalTenantId": {
            "type": "string"
          },
          "amounts": {
            "type": "object",
            "description": "`fixed`: subtotalMinor, discountMinor, taxMinor, totalMinor.\n`range`: rangeMinMinor, rangeMaxMinor (never a binding total).\n`manual_review`: empty.\n",
            "properties": {
              "subtotalMinor": {
                "type": "integer"
              },
              "discountMinor": {
                "type": "integer"
              },
              "taxMinor": {
                "type": "integer"
              },
              "totalMinor": {
                "type": "integer"
              },
              "rangeMinMinor": {
                "type": "integer"
              },
              "rangeMaxMinor": {
                "type": "integer"
              }
            }
          },
          "tax": {
            "type": "object",
            "properties": {
              "mode": {
                "type": "string",
                "enum": [
                  "exclusive",
                  "inclusive",
                  "none"
                ]
              },
              "rateBps": {
                "type": "integer"
              },
              "code": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "amountMinor": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            }
          },
          "breakdown": {
            "type": "array",
            "description": "Customer-safe line items only.",
            "items": {
              "type": "object",
              "properties": {
                "code": {
                  "type": "string"
                },
                "type": {
                  "type": "string"
                },
                "amountMinor": {
                  "type": "integer"
                },
                "labelKey": {
                  "type": "string"
                }
              }
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "exclusions": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "missingFacts": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Facts whose absence prevented a fixed price."
          },
          "reviewReasons": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Why the quote needs Skylens review."
          },
          "calculatorInputs": {
            "type": "object"
          },
          "projectFacts": {
            "type": "object",
            "description": "The snapshot facts actually priced — pinned, not current."
          },
          "inputHash": {
            "type": "string",
            "description": "sha256 of versions + facts + inputs; determinism anchor."
          },
          "validUntil": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "QuoteRequest": {
        "type": "object",
        "required": [
          "schemaVersion",
          "externalTenantId",
          "externalProjectId",
          "serviceCode"
        ],
        "additionalProperties": false,
        "properties": {
          "schemaVersion": {
            "type": "string",
            "enum": [
              "1.0"
            ]
          },
          "externalTenantId": {
            "type": "string",
            "maxLength": 200
          },
          "externalProjectId": {
            "type": "string",
            "maxLength": 200
          },
          "externalUserId": {
            "type": "string",
            "maxLength": 200
          },
          "serviceCode": {
            "type": "string",
            "maxLength": 200
          },
          "snapshotId": {
            "type": "string",
            "format": "uuid",
            "description": "Price against a specific snapshot. Defaults to the project's current snapshot. A snapshot belonging to another project is NOT FOUND."
          },
          "locale": {
            "type": "string"
          },
          "inputs": {
            "type": "object",
            "maxProperties": 50,
            "description": "Scalar calculator inputs only. Objects and arrays are rejected, as is any monetary field."
          }
        }
      },
      "Service": {
        "type": "object",
        "description": "Customer-safe service card. Contains no commission or cost.",
        "properties": {
          "serviceCode": {
            "type": "string"
          },
          "category": {
            "type": "string",
            "enum": [
              "progress_monitoring",
              "reality_model",
              "orthophoto_measurement",
              "marketing_media"
            ]
          },
          "name": {
            "type": "string",
            "description": "Resolved for the requested locale"
          },
          "description": {
            "type": "string"
          },
          "iconToken": {
            "type": "string"
          },
          "serviceVersion": {
            "type": "integer"
          },
          "pricingVersion": {
            "type": "integer"
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217"
          },
          "fromPriceMinor": {
            "type": "integer",
            "description": "Integer MINOR units. Display anchor only, not a quote."
          },
          "requiredProjectFacts": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "supportedDeliverables": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "quoteValidityHours": {
            "type": "integer"
          },
          "taxTreatment": {
            "type": "object",
            "description": "Configuration, never a constant.",
            "properties": {
              "mode": {
                "type": "string",
                "enum": [
                  "exclusive",
                  "inclusive",
                  "none"
                ]
              },
              "rateBps": {
                "type": "integer",
                "description": "Basis points; 1700 = 17%"
              },
              "code": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          }
        }
      },
      "ServiceDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Service"
          },
          {
            "type": "object",
            "properties": {
              "minimumTotalMinor": {
                "type": "integer"
              },
              "sla": {
                "type": "object"
              },
              "assumptions": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "text": {
                      "type": "string"
                    }
                  }
                }
              },
              "exclusions": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "text": {
                      "type": "string"
                    }
                  }
                }
              },
              "calculatorInputs": {
                "type": "array",
                "description": "Declarative input spec the widget renders. Labels are already locale-resolved, so no copy is hardcoded in the frontend.",
                "items": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string"
                    },
                    "type": {
                      "type": "string",
                      "enum": [
                        "integer",
                        "number",
                        "enum",
                        "boolean"
                      ]
                    },
                    "required": {
                      "type": "boolean"
                    },
                    "label": {
                      "type": "string"
                    },
                    "unit": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "min": {
                      "type": "number"
                    },
                    "max": {
                      "type": "number"
                    },
                    "default": {},
                    "options": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "value": {},
                          "label": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        ]
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "schema": {
          "type": "string",
          "minLength": 8,
          "maxLength": 200
        },
        "description": "Required on every write. Replaying a key with an identical body returns\nthe stored response with `replayed: true`; replaying it with a different\nbody returns `IDEMPOTENCY_CONFLICT`. Keys are scoped per API consumer, so\ntwo partners cannot collide.\n"
      },
      "IdempotencyKeyRequired": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 200
        },
        "description": "REQUIRED replay-safe key. Replaying it returns the prior result instead of acting twice; reusing one key for a different target is 409 DUPLICATE_REQUEST. Note the scope: the header is honoured by the membership commands, the reality-attach approval and the agent-memory routes. Elsewhere the key travels in the body as `idempotencyKey`, and several routes (exports, document upload) honour neither — see x-skylens-classification."
      },
      "RequestId": {
        "name": "X-Request-ID",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string",
          "maxLength": 200
        },
        "description": "Echoed back in the response header and in `meta.requestId`."
      }
    },
    "securitySchemes": {
      "devUserHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Skylens-User-Id",
        "description": "Development only: the internal principal UUID, trusted verbatim. Never use it against a deployed environment."
      },
      "firebaseIdToken": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Authorization: Bearer <Firebase ID token>. The only authentication a browser or external client should use."
      },
      "partnerCredential": {
        "type": "http",
        "scheme": "bearer",
        "description": "`Authorization: Bearer <keyId>.<secret>`\n\nThe secret is a 256-bit random value stored only as a SHA-256 hash and\nshown exactly once at issuance. Credentials carry scopes, a status\n(`active` | `rotating` | `revoked`) and an optional expiry; all are\nre-checked on every request, so revocation is immediate.\n\nScopes: `external_projects:read`, `external_projects:write`,\n`services:read`, `quotes:read`, `quotes:write`. They are separately\ngrantable — a partner may sync projects without being able to price them.\n\nNever place a partner credential in browser JavaScript.\n"
      }
    }
  },
  "x-skylens": {
    "contractVersion": "1",
    "domains": [
      {
        "id": "getting-started",
        "title": "Getting Started",
        "kind": "guide"
      },
      {
        "id": "authentication",
        "title": "Authentication",
        "kind": "guide"
      },
      {
        "id": "projects",
        "title": "Projects",
        "kind": "reference"
      },
      {
        "id": "documents",
        "title": "Documents",
        "kind": "reference"
      },
      {
        "id": "gis",
        "title": "GIS Layers and Features",
        "kind": "reference"
      },
      {
        "id": "spatial-analysis",
        "title": "Spatial Analysis",
        "kind": "reference"
      },
      {
        "id": "elevation",
        "title": "Elevation",
        "kind": "reference"
      },
      {
        "id": "entities",
        "title": "Entities",
        "kind": "reference"
      },
      {
        "id": "review",
        "title": "Entity Review",
        "kind": "reference"
      },
      {
        "id": "reality-models",
        "title": "Reality Models",
        "kind": "reference"
      },
      {
        "id": "imports",
        "title": "CAD and BIM Imports",
        "kind": "reference"
      },
      {
        "id": "photos",
        "title": "Photos and 360 Panoramas",
        "kind": "reference"
      },
      {
        "id": "ai-jobs",
        "title": "AI Jobs",
        "kind": "reference"
      },
      {
        "id": "agent-memory",
        "title": "Agent Memory",
        "kind": "reference"
      },
      {
        "id": "scene-graph",
        "title": "Scene Graph",
        "kind": "reference"
      },
      {
        "id": "viewer",
        "title": "Viewer",
        "kind": "reference"
      },
      {
        "id": "chat-database",
        "title": "Chat Database Grounding",
        "kind": "reference"
      },
      {
        "id": "export",
        "title": "Export",
        "kind": "reference"
      },
      {
        "id": "partners",
        "title": "Partner Services",
        "kind": "reference"
      },
      {
        "id": "errors",
        "title": "Errors",
        "kind": "guide"
      },
      {
        "id": "pagination",
        "title": "Pagination and Idempotency",
        "kind": "guide"
      },
      {
        "id": "geometry",
        "title": "Geometry and CRS",
        "kind": "guide"
      },
      {
        "id": "changelog",
        "title": "Changelog",
        "kind": "guide"
      }
    ],
    "surfaces": {
      "platform": {
        "title": "Skylens Platform API",
        "router": "server/db/extraction_api.py",
        "localBaseUrl": "http://127.0.0.1:8600",
        "auth": [
          "firebaseIdToken",
          "devUserHeader"
        ],
        "envelope": "bare"
      },
      "entity": {
        "title": "Entity API",
        "router": "server/db/entity_api.py",
        "localBaseUrl": "http://127.0.0.1:8500",
        "auth": [
          "devUserHeader"
        ],
        "envelope": "bare"
      },
      "partner": {
        "title": "Partner Services API",
        "router": "server/db/partner_api.py",
        "localBaseUrl": "http://127.0.0.1:8700",
        "auth": [
          "partnerCredential"
        ],
        "envelope": "data-meta-error"
      },
      "firebase": {
        "title": "Firebase Hosting rewrites",
        "router": "functions/index.js",
        "localBaseUrl": "https://skylens-viewer.web.app",
        "auth": [],
        "envelope": "bare"
      }
    },
    "environments": [
      {
        "id": "local",
        "title": "Local development",
        "baseUrl": "http://127.0.0.1:8600",
        "configured": true,
        "tryIt": true,
        "note": "Started by `npm run dev`. Identity is the X-Skylens-User-Id header, trusted verbatim because no gateway fronts it. CORS is Access-Control-Allow-Origin: * on this surface."
      },
      {
        "id": "production",
        "title": "Production",
        "baseUrl": "https://api.example.skylens.co.il",
        "configured": false,
        "tryIt": false,
        "note": "PLACEHOLDER — requires deployment configuration. The gateway (functions/project-api-gateway.js) is implemented and unit-tested but not wired: firebase.json has no \"/api/**\" -> projectApiGateway rewrite, and SKYLENS_API_UPSTREAM_URL / SKYLENS_CLOUD_RUN_AUDIENCE / SKYLENS_GATEWAY_SHARED_SECRET are unset. Until then this host resolves to nothing and Try It is disabled against it."
      }
    ],
    "planned": [
      {
        "method": "GET",
        "path": "/api/projects/{projectId}/summary",
        "domain": "projects",
        "documentedIn": "docs/contracts/API.md (from the retired 07_SKYLENS_API_CONTRACT)",
        "note": "No route regex matches. GET /api/projects/{projectId} already returns a content inventory."
      },
      {
        "method": "GET",
        "path": "/api/projects/{projectId}/viewer/bootstrap",
        "domain": "viewer",
        "documentedIn": "three skill contracts",
        "note": "Referenced as the canonical Viewer bootstrap; implemented on no surface."
      },
      {
        "method": "GET",
        "path": "/api/projects/{projectId}/viewer/layers",
        "domain": "viewer",
        "documentedIn": "docs/contracts/API.md",
        "note": "The implemented reads are /viewer/source-layers and /my-layers."
      },
      {
        "method": "PATCH",
        "path": "/api/projects/{projectId}/viewer/preferences",
        "domain": "viewer",
        "documentedIn": "docs/contracts/API.md",
        "note": "No preferences route exists."
      },
      {
        "method": "POST",
        "path": "/api/projects/{projectId}/viewer/sessions",
        "domain": "viewer",
        "documentedIn": "docs/contracts/API.md",
        "note": "No session route exists."
      },
      {
        "method": "POST",
        "path": "/api/projects/{projectId}/spatial-query",
        "domain": "gis",
        "documentedIn": "docs/contracts/API.md",
        "note": "The implemented spatial reads are GET /gis-layers/{code}/features (bbox) and POST /spatial-analysis/overlays."
      },
      {
        "method": "GET",
        "path": "/api/projects/{projectId}/layers/{layerId}/features",
        "domain": "gis",
        "documentedIn": "docs/contracts/API.md",
        "note": "This exact path exists on no surface; the code has /layer-features and /gis-layers/{code}/features."
      },
      {
        "method": "GET",
        "path": "/api/projects/{projectId}/entities",
        "domain": "entities",
        "documentedIn": "docs/contracts/API.md",
        "note": "Entity CRUD is FLAT (/api/entities?projectId=) on the withheld :8500 surface, not project-scoped in the path."
      },
      {
        "method": "GET",
        "path": "/api/projects/{projectId}/entities/{entityId}/versions",
        "domain": "entities",
        "documentedIn": "docs/contracts/API.md",
        "note": "entity_service.get_entity_versions exists in-process; no HTTP route reaches it."
      },
      {
        "method": "GET",
        "path": "/api/projects/{projectId}/entities/{entityId}/evidence",
        "domain": "entities",
        "documentedIn": "docs/contracts/API.md",
        "note": "No route."
      },
      {
        "method": "GET",
        "path": "/api/projects/{projectId}/entities/{entityId}/captures",
        "domain": "entities",
        "documentedIn": "docs/contracts/API.md",
        "note": "No route."
      },
      {
        "method": "GET",
        "path": "/api/projects/{projectId}/entities/{entityId}/documents",
        "domain": "entities",
        "documentedIn": "docs/contracts/API.md",
        "note": "No route."
      },
      {
        "method": "GET",
        "path": "/api/projects/{projectId}/entities/{entityId}/tasks",
        "domain": "entities",
        "documentedIn": "docs/contracts/API.md",
        "note": "No route, and no tasks.manage action in server/db/authz_actions.py."
      },
      {
        "method": "GET",
        "path": "/api/projects/{projectId}/reality-models",
        "domain": "reality-models",
        "documentedIn": "docs/contracts/API.md",
        "note": "The implemented collection is GET /api/projects/{projectId}/models. Only the .../reality-models/{modelId}/delivery sub-path uses this spelling."
      },
      {
        "method": "GET",
        "path": "/api/projects/{projectId}/tilesets/{tilesetId}",
        "domain": "reality-models",
        "documentedIn": "docs/contracts/API.md",
        "note": "No route. Tiles are delivered by CDN under a signed grant."
      },
      {
        "method": "POST",
        "path": "/api/projects/{projectId}/reality-models/{modelId}/activate",
        "domain": "reality-models",
        "documentedIn": "docs/contracts/API.md",
        "note": "No route; activation happens at import publish time."
      },
      {
        "method": "GET",
        "path": "/api/projects/{projectId}/scene-graph/entities/{entityId}",
        "domain": "scene-graph",
        "documentedIn": "skylens-manage-admin-control-plane skill contract",
        "note": "server/db/scene_graph_service.py is reached only in-process. NO HTTP surface exists for the scene graph."
      },
      {
        "method": "POST",
        "path": "/api/projects/{projectId}/scene-graph/query",
        "domain": "scene-graph",
        "documentedIn": "skill contract",
        "note": "No HTTP surface."
      },
      {
        "method": "GET",
        "path": "/api/projects/{projectId}/scene-graph/nodes/{nodeId}/neighbors",
        "domain": "scene-graph",
        "documentedIn": "skill contract",
        "note": "No HTTP surface."
      },
      {
        "method": "POST",
        "path": "/api/projects/{projectId}/scene-graph/edges",
        "domain": "scene-graph",
        "documentedIn": "skill contract",
        "note": "No HTTP surface."
      },
      {
        "method": "POST",
        "path": "/api/projects/{projectId}/ai/jobs",
        "domain": "ai-jobs",
        "documentedIn": "docs/contracts/API.md",
        "note": "The implemented lifecycle is POST /extraction-plans then POST /extraction-jobs. Deliberately two steps: a plan is confirmed before it costs anything."
      },
      {
        "method": "GET",
        "path": "/api/projects/{projectId}/ai/jobs/{jobId}/results",
        "domain": "ai-jobs",
        "documentedIn": "docs/contracts/API.md",
        "note": "Results are published as ai_draft layers and read through the review queue."
      }
    ],
    "demoIdentifiers": {
      "projectId": "11111111-1111-4111-8111-111111111111",
      "organizationId": "22222222-2222-4222-8222-222222222222",
      "userId": "33333333-3333-4333-8333-333333333333",
      "modelId": "44444444-4444-4444-8444-444444444444",
      "draftId": "55555555-5555-4555-8555-555555555555",
      "jobId": "66666666-6666-4666-8666-666666666666",
      "planId": "77777777-7777-4777-8777-777777777777",
      "collectionId": "88888888-8888-4888-8888-888888888888",
      "assetId": "99999999-9999-4999-8999-999999999999",
      "resourceId": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
      "runId": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
      "storeId": "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
      "dreamId": "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
      "importId": "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
      "quoteId": "ffffffff-ffff-4fff-8fff-ffffffffffff",
      "layerId": "layer_demo_001",
      "layerCode": "planning.urban_renewal_declared",
      "entityType": "building",
      "featureKey": "1a2B3c4D5e6F7g8H9i0Jk",
      "format": "geojson",
      "serviceCode": "orthophoto_survey",
      "externalProjectId": "ext_demo_001",
      "aoiId": "aoi_demo_001",
      "entityId": "entity_demo_001",
      "observationId": "observation_demo_001",
      "token": "grant_demo_token",
      "workerId": "worker_demo_001",
      "taskId": "task_demo_001",
      "artifactId": "artifact_demo_001",
      "candidateId": "candidate_demo_001",
      "dreamId2": "dream_demo_001"
    },
    "census": {
      "declaredOperations": 160,
      "byStatus": {
        "stable": 66,
        "beta": 31,
        "internal": 58,
        "deprecated": 5
      },
      "byVisibility": {
        "public": 97,
        "internal": 63
      },
      "published": 97,
      "withheld": 63,
      "plannedNotOperations": 22,
      "note": "byStatus and byVisibility are two views of the SAME declaredOperations set and must not be added together. plannedNotOperations is a DISJOINT set: those routes have no path on any surface and are not counted as operations."
    }
  }
}
