{
  "openapi": "3.1.0",
  "info": {
    "title": "GroundIT — Work",
    "version": "0.1.0",
    "license": {
      "name": "Proprietary — GroundIT/Sysmedac internal"
    },
    "description": "The team task board/list and assignments (create, assign, prioritise, status lifecycle, the no-active-task idle scan), daily/weekly timesheets and work entries (decimal hours, never float), the manager sign-off that produces the immutable certified-hours record consumed by `pay` and Finance by event (`work.timesheet.approved`), and the lightweight project register time rolls up to. Every CursorPage list honours page[size] from the bracketed query parameter under Express's extended parser and implements real bidirectional keyset continuation: pass page.next_cursor as page[after] or page.prev_cursor as page[before]; the two cursor parameters are mutually exclusive. Cursors are bound to the operation's selected sort and direction. See ../../api-docs/00-api-overview-and-conventions.md.\n"
  },
  "servers": [
    {
      "url": "/api/v1"
    }
  ],
  "security": [
    {
      "bearerJWT": []
    }
  ],
  "tags": [
    {
      "name": "work",
      "description": "Task board, assignments, timesheets, sign-off, and projects."
    },
    {
      "name": "task_board"
    },
    {
      "name": "task"
    },
    {
      "name": "task_assignment"
    },
    {
      "name": "task_attachment"
    },
    {
      "name": "timesheet"
    },
    {
      "name": "work_entry"
    },
    {
      "name": "project"
    },
    {
      "name": "milestone",
      "description": "Dated checkpoints inside a project (work.milestones, WRK-F06/F09)."
    },
    {
      "name": "project_allocation",
      "description": "Effective-dated capacity commitments of an employee to a project (work.project_allocations, WRK-F14)."
    },
    {
      "name": "workload",
      "description": "Capacity/load reads — the assignee × week heatmap and the leave-aware assignment picker (WRK-F13)."
    },
    {
      "name": "portfolio",
      "description": "All-projects KPI, health and escalation reads (WRK-F14/F16)."
    },
    {
      "name": "team_metrics",
      "description": "Team-lead velocity tiles over the caller's reports (WRK-F18)."
    },
    {
      "name": "task_dependency",
      "description": "Typed BLOCKS edges between tasks (work.task_dependencies, WRK-F10)."
    },
    {
      "name": "task_comment",
      "description": "The task's comment thread with @mentions (work.task_comments, WRK-F07)."
    },
    {
      "name": "task_watcher",
      "description": "The task's explicit watch list (work.task_watchers, WRK-F07)."
    },
    {
      "name": "task_checklist_item",
      "description": "The task's checklist — lightweight checked/unchecked lines (work.task_checklist_items, WRK-F07's checklist facet). Distinct from SUBTASKS (`work.tasks.parent_task_id`), which stay full child tasks; neither replaces the other.\n"
    },
    {
      "name": "task_activity",
      "description": "Append-only task activity trail (work.task_activity, WRK-F07)."
    },
    {
      "name": "saved_view",
      "description": "Named, restorable filter sets over board/list/calendar (work.saved_views, WRK-F08)."
    },
    {
      "name": "template",
      "description": "Reusable project/task structures and their publish/instantiate lifecycle (work.work_templates, WRK-F11)."
    },
    {
      "name": "task_recurrence",
      "description": "Recurrence rules materialized by the jobs tier (work.task_recurrences, WRK-F11)."
    },
    {
      "name": "my_work",
      "description": "The self-scoped personal execution queue (WRK-F15)."
    },
    {
      "name": "project_share",
      "description": "Client-visible signed-link project shares — Launch: Later, post-S12 (WRK-F20)."
    },
    {
      "name": "import",
      "description": "Trello/Jira/CSV work import batches — Launch: Later, post-S12 (WRK-F21)."
    },
    {
      "name": "team",
      "description": "Delivery squads — the work-module team a board binds to (work.teams, ADR 0037). NOT the RLS `TEAM` scope, which is the one-level reportee closure; the two words are deliberately kept apart everywhere in this module.\n"
    },
    {
      "name": "team_member",
      "description": "Explicit squad roster with org-unit seed provenance (work.team_members, ADR 0037)."
    },
    {
      "name": "project_team",
      "description": "The project ↔ team join — where cross-team work is modelled (work.project_teams, ADR 0037)."
    },
    {
      "name": "iteration",
      "description": "Board-owned sprints — the SCRUM lens' only extra table (work.iterations, ADR 0037). A LENS over one task data model, never a second data model: `tasks.iteration_id IS NULL` is the backlog, and switching a board's methodology hides the lens without clearing a single link.\n"
    }
  ],
  "x-reuse-anchors": {
    "parameters": {
      "path_id": {
        "$ref": "#/components/parameters/PathId"
      },
      "page_size": {
        "$ref": "#/components/parameters/PageSize"
      },
      "page_after": {
        "$ref": "#/components/parameters/PageAfter"
      },
      "page_before": {
        "$ref": "#/components/parameters/PageBefore"
      },
      "sort_param": {
        "$ref": "#/components/parameters/SortParam"
      },
      "accept_language": {
        "$ref": "#/components/parameters/AcceptLanguage"
      },
      "idempotency_key": {
        "$ref": "#/components/parameters/IdempotencyKey"
      },
      "if_match": {
        "$ref": "#/components/parameters/IfMatch"
      }
    },
    "responses": {
      "unauthorized": {
        "$ref": "#/components/responses/Unauthorized"
      },
      "forbidden": {
        "$ref": "#/components/responses/Forbidden"
      },
      "not_found": {
        "$ref": "#/components/responses/NotFound"
      },
      "conflict": {
        "$ref": "#/components/responses/Conflict"
      },
      "unprocessable": {
        "$ref": "#/components/responses/UnprocessableEntity"
      },
      "unprocessable_work_entry": {
        "description": "Field-level validation failed. In addition to the usual field rules, an unresolvable `project_id` — a stale id, a soft-deleted or archived project, or one belonging to another tenant — is reported as a `/project_id` field error with rule `not_found`, NOT as a `404`. A project the caller can see but does not own is NOT an error at all: the register is tenant-shared and time may be logged against any `ACTIVE` project (a non-`ACTIVE` one is the `409 STATE_TRANSITION_INVALID` above).\n",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/ValidationProblem"
            },
            "example": {
              "type": "about:blank",
              "title": "Unprocessable Entity",
              "status": 422,
              "code": "VALIDATION_FAILED",
              "instance": "/api/v1/work-entries",
              "errors": [
                {
                  "pointer": "/project_id",
                  "rule": "not_found",
                  "message": "That project does not exist, or you do not have access to it."
                }
              ]
            }
          }
        }
      },
      "allocation_not_writable": {
        "description": "Authenticated and permitted to READ this allocation, but not to write it — the per-command write confinement migration 0131 §3 puts on `work.project_allocations`. Distinguished from a generic 403 by `type: urn:groundit:problem:work:project-allocation-not-writable`, with `code: SCOPE_DENIED` and `status: 403`.\n**403, never 404.** The usual \"absent or RLS-masked, deliberately indistinguishable\" posture (`_shared.yaml` `NotFound`, security 02 §4) does **not** apply here: the caller can see the row — a list just rendered it — so masking the write refusal as a 404 would contradict the response they already hold. Nothing is disclosed by saying so.\nEvery other 403 cause for this operation — token not granted, tenant suspended — keeps `type: about:blank` and its own `code`, exactly as `Forbidden` describes. **`detail` never names another employee, a rate or an amount**: it says the caller's scope does not permit the change and who to ask, not who holds what.\n",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "urn:groundit:problem:work:project-allocation-not-writable",
              "title": "Forbidden",
              "status": 403,
              "code": "SCOPE_DENIED",
              "detail": "You can view this allocation, but your access scope does not permit changing it. Ask the project owner or an HR admin to make this change.",
              "instance": "/api/v1/project-allocations/{id}",
              "correlation_id": "018f2c7a-0000-7000-8000-0000000000c1"
            }
          }
        }
      },
      "allocation_window_overlap": {
        "description": "Refused: the window this write asks for overlaps another allocation of the **same member on the same project**. `type` is `urn:groundit:problem:work:allocation-window-overlap`, `code` is `VALIDATION_FAILED`, and `errors[0].pointer` is the date field to move (`/effective_from`, or `/effective_to` when the end date is what was changed). `detail` names the conflicting window's dates and percentage so the screen can say which row is in the way.\nPer `(tenant, employee, project)` the live windows **partition time** — migration `0231` enforces it with the `work_project_allocations_no_overlap` exclusion constraint, and the write doors check the same `daterange(effective_from, effective_to, '[]')` predicate in-transaction so the refusal can name the other row. A client receives this `type` either way; it cannot tell, and does not need to tell, which of the two caught it.\n**The taper is NOT this error.** A new phase starting after an existing OPEN-ENDED window is the shape real staffing needs, and `POST /project-allocations` honours it by narrowing the incumbent to the day before, in the same transaction, returning `201` with a `PREDECESSOR_WINDOW_CLOSED` warning. Only a genuine collision reaches here.\n",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "urn:groundit:problem:work:allocation-window-overlap",
              "title": "Unprocessable Entity",
              "status": 422,
              "code": "VALIDATION_FAILED",
              "detail": "This member already has a 60.00 % allocation to this project for 2026-10-01 – 2026-10-20. Allocation windows on one project cannot overlap — end or split the existing window first.",
              "errors": [
                {
                  "pointer": "/effective_from",
                  "rule": "overlap",
                  "message": "This member already has a 60.00 % allocation to this project for 2026-10-01 – 2026-10-20. Allocation windows on one project cannot overlap — end or split the existing window first."
                }
              ],
              "instance": "/api/v1/project-allocations",
              "correlation_id": "018f2c7a-0000-7000-8000-0000000000c3"
            }
          }
        }
      },
      "close_undispositioned": {
        "description": "Close-out refused: one or more non-terminal tasks carry no explicit disposition. `type` is `urn:groundit:problem:work:project-close-undispositioned-tasks`, `code` is `STATE_TRANSITION_INVALID`, and **`detail` names the stranded tasks** by business number and title — a close that silently strands work is the failure mode this ritual exists to prevent (`WRK-F17`), so the refusal must be actionable without a second round-trip. The other 409 causes on this operation (idempotency-key reuse with a different body) keep `type: about:blank`.\n",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "urn:groundit:problem:work:project-close-undispositioned-tasks",
              "title": "Conflict",
              "status": 409,
              "code": "STATE_TRANSITION_INVALID",
              "detail": "2 open task(s) have no close-out disposition: TSK-000041 Migrate seeds, TSK-000052 Cutover runbook.",
              "instance": "/api/v1/projects/018f2c7a-0000-7000-8000-0000000000b2/close",
              "correlation_id": "018f2c7a-0000-7000-8000-0000000000c2"
            }
          }
        }
      },
      "archive_via_close_only": {
        "description": "Refused: `status: ARCHIVED` is not settable on the general project edit. `type` is `urn:groundit:problem:work:project-archive-via-close-only` and `code` is `STATE_TRANSITION_INVALID`; `detail` names `POST /projects/{id}/close` — the operation that actually archives, because archiving is a **ritual and not a field** (`WRK-S23`). Every other 409 cause on this operation (idempotency-key reuse with a different body, an already-archived project) keeps `type: about:blank`.\n",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "urn:groundit:problem:work:project-archive-via-close-only",
              "title": "Conflict",
              "status": 409,
              "code": "STATE_TRANSITION_INVALID",
              "detail": "A project is archived by closing it out, not by setting its status: POST /api/v1/projects/{id}/close applies the open-task dispositions, stamps the allocation windows and records the close note in one transaction.",
              "instance": "/api/v1/projects/018f2c7a-0000-7000-8000-0000000000b2",
              "correlation_id": "018f2c7a-0000-7000-8000-0000000000c3"
            }
          }
        }
      },
      "locked": {
        "$ref": "#/components/responses/Locked"
      },
      "too_many": {
        "$ref": "#/components/responses/TooManyRequests"
      },
      "precondition_required": {
        "$ref": "#/components/responses/PreconditionRequired"
      },
      "precondition_failed": {
        "$ref": "#/components/responses/PreconditionFailed"
      }
    },
    "headers": {
      "etag": {
        "$ref": "#/components/headers/ETag"
      },
      "location": {
        "$ref": "#/components/headers/Location"
      },
      "idem_replayed": {
        "$ref": "#/components/headers/IdempotencyReplayed"
      }
    }
  },
  "paths": {
    "/projects/{id}/access": {
      "get": {
        "operationId": "work.project.access",
        "summary": "Resolve effective project capabilities",
        "description": "Project-local roles govern project work (ADR 0072). Owner/HR Admin may administer all projects in their workspace. Explicit roles override inherited team Member access. Role permissions are restricted to project operations; they grant no payroll or timesheet approval authority. Template edits affect future copies only. Protected reads and writes resolve current access before returning data, including idempotent replays. Workspace-level grants for project cost create/update/delete/void are capability-probed before the project-local read and are merged into capabilities when held; the probe is fail-closed when unavailable.",
        "tags": [
          "work",
          "project"
        ],
        "x-token": "work.project.access",
        "x-realizes-features": [
          "WRK-F25"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_roles",
          "work.project_members",
          "work.project_role_templates"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectCapabilityAccess"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/projects/{id}/roles": {
      "get": {
        "operationId": "work.project_role.list",
        "summary": "List project roles",
        "description": "Project-local roles govern project work (ADR 0072). Owner/HR Admin may administer all projects in their workspace. Explicit roles override inherited team Member access. Role permissions are restricted to project operations; they grant no payroll or timesheet approval authority. Template edits affect future copies only. Protected reads and writes resolve current access before returning data, including idempotent replays.",
        "tags": [
          "work",
          "project"
        ],
        "x-token": "work.project_role.list",
        "x-realizes-features": [
          "WRK-F25"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_roles",
          "work.project_members",
          "work.project_role_templates"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectRolePage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "operationId": "work.project_role.create",
        "summary": "Create a project-local role",
        "description": "Project-local roles govern project work (ADR 0072). Owner/HR Admin may administer all projects in their workspace. Explicit roles override inherited team Member access. Role permissions are restricted to project operations; they grant no payroll or timesheet approval authority. Template edits affect future copies only. Protected reads and writes resolve current access before returning data, including idempotent replays.",
        "tags": [
          "work",
          "project"
        ],
        "x-token": "work.project_role.create",
        "x-realizes-features": [
          "WRK-F25"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_roles",
          "work.project_members",
          "work.project_role_templates"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectRoleInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Successful result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectRole"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        }
      }
    },
    "/projects/{id}/roles/{roleId}": {
      "patch": {
        "operationId": "work.project_role.update",
        "summary": "Update a project-local role",
        "description": "Project-local roles govern project work (ADR 0072). Owner/HR Admin may administer all projects in their workspace. Explicit roles override inherited team Member access. Role permissions are restricted to project operations; they grant no payroll or timesheet approval authority. Template edits affect future copies only. Protected reads and writes resolve current access before returning data, including idempotent replays.",
        "tags": [
          "work",
          "project"
        ],
        "x-token": "work.project_role.update",
        "x-realizes-features": [
          "WRK-F25"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_roles",
          "work.project_members",
          "work.project_role_templates"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "roleId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "description": "Current version as a quoted v-prefixed ETag.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectRoleInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectRole"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        }
      },
      "delete": {
        "operationId": "work.project_role.delete",
        "summary": "Delete an unassigned project role",
        "description": "Project-local roles govern project work (ADR 0072). Owner/HR Admin may administer all projects in their workspace. Explicit roles override inherited team Member access. Role permissions are restricted to project operations; they grant no payroll or timesheet approval authority. Template edits affect future copies only. Protected reads and writes resolve current access before returning data, including idempotent replays.",
        "tags": [
          "work",
          "project"
        ],
        "x-token": "work.project_role.delete",
        "x-realizes-features": [
          "WRK-F25"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_roles",
          "work.project_members",
          "work.project_role_templates"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "roleId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "description": "Current version as a quoted v-prefixed ETag.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Explicit assignment or unassigned role removed."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        }
      }
    },
    "/projects/{id}/members": {
      "get": {
        "operationId": "work.project_member.list",
        "summary": "List effective direct and inherited participants",
        "description": "Project-local roles govern project work (ADR 0072). Owner/HR Admin may administer all projects in their workspace. Explicit roles override inherited team Member access. Role permissions are restricted to project operations; they grant no payroll or timesheet approval authority. Template edits affect future copies only. Protected reads and writes resolve current access before returning data, including idempotent replays.",
        "tags": [
          "work",
          "project"
        ],
        "x-token": "work.project_member.list",
        "x-realizes-features": [
          "WRK-F25"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_roles",
          "work.project_members",
          "work.project_role_templates"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectMemberPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/projects/{id}/members/{employeeId}": {
      "put": {
        "operationId": "work.project_member.update",
        "summary": "Assign or replace an explicit project role",
        "description": "Project-local roles govern project work (ADR 0072). Owner/HR Admin may administer all projects in their workspace. Explicit roles override inherited team Member access. Role permissions are restricted to project operations; they grant no payroll or timesheet approval authority. Template edits affect future copies only. Protected reads and writes resolve current access before returning data, including idempotent replays.",
        "tags": [
          "work",
          "project"
        ],
        "x-token": "work.project_member.update",
        "x-realizes-features": [
          "WRK-F25"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_roles",
          "work.project_members",
          "work.project_role_templates"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "employeeId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectMemberInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectMember"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        }
      },
      "delete": {
        "operationId": "work.project_member.delete",
        "summary": "Remove explicit role and fall back to any team membership",
        "description": "Project-local roles govern project work (ADR 0072). Owner/HR Admin may administer all projects in their workspace. Explicit roles override inherited team Member access. Role permissions are restricted to project operations; they grant no payroll or timesheet approval authority. Template edits affect future copies only. Protected reads and writes resolve current access before returning data, including idempotent replays.",
        "tags": [
          "work",
          "project"
        ],
        "x-token": "work.project_member.delete",
        "x-realizes-features": [
          "WRK-F25"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_roles",
          "work.project_members",
          "work.project_role_templates"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "employeeId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Explicit assignment or unassigned role removed."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        }
      }
    },
    "/employees/{id}/project-access": {
      "get": {
        "operationId": "work.project_member.list_employee",
        "summary": "List an employee's effective project access",
        "description": "Project-local roles govern project work (ADR 0072). Owner/HR Admin may administer all projects in their workspace. Explicit roles override inherited team Member access. Role permissions are restricted to project operations; they grant no payroll or timesheet approval authority. Template edits affect future copies only. Protected reads and writes resolve current access before returning data, including idempotent replays.",
        "tags": [
          "work",
          "project"
        ],
        "x-token": "work.project_member.list_employee",
        "x-realizes-features": [
          "WRK-F25"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_roles",
          "work.project_members",
          "work.project_role_templates"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmployeeProjectAccessPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/project-role-templates": {
      "get": {
        "operationId": "work.project_role_template.list",
        "summary": "List reusable project role templates",
        "description": "Project-local roles govern project work (ADR 0072). Owner/HR Admin may administer all projects in their workspace. Explicit roles override inherited team Member access. Role permissions are restricted to project operations; they grant no payroll or timesheet approval authority. Template edits affect future copies only. Protected reads and writes resolve current access before returning data, including idempotent replays.",
        "tags": [
          "work",
          "project"
        ],
        "x-token": "work.project_role_template.list",
        "x-realizes-features": [
          "WRK-F25"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_roles",
          "work.project_members",
          "work.project_role_templates"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "responses": {
          "200": {
            "description": "Successful result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectRoleTemplatePage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "operationId": "work.project_role_template.create",
        "summary": "Create a workspace project-role template",
        "description": "Project-local roles govern project work (ADR 0072). Owner/HR Admin may administer all projects in their workspace. Explicit roles override inherited team Member access. Role permissions are restricted to project operations; they grant no payroll or timesheet approval authority. Template edits affect future copies only. Protected reads and writes resolve current access before returning data, including idempotent replays.",
        "tags": [
          "work",
          "project"
        ],
        "x-token": "work.project_role_template.create",
        "x-realizes-features": [
          "WRK-F25"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_roles",
          "work.project_members",
          "work.project_role_templates"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectRoleInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Successful result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectRoleTemplate"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        }
      }
    },
    "/project-role-templates/{id}": {
      "patch": {
        "operationId": "work.project_role_template.update",
        "summary": "Update template for future project copies",
        "description": "Project-local roles govern project work (ADR 0072). Owner/HR Admin may administer all projects in their workspace. Explicit roles override inherited team Member access. Role permissions are restricted to project operations; they grant no payroll or timesheet approval authority. Template edits affect future copies only. Protected reads and writes resolve current access before returning data, including idempotent replays.",
        "tags": [
          "work",
          "project"
        ],
        "x-token": "work.project_role_template.update",
        "x-realizes-features": [
          "WRK-F25"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_roles",
          "work.project_members",
          "work.project_role_templates"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "description": "Current version as a quoted v-prefixed ETag.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectRoleInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectRoleTemplate"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        }
      }
    },
    "/task-boards": {
      "get": {
        "operationId": "work.task_board.list",
        "summary": "List task boards (mobile task picker / team board switcher)",
        "description": "Shared board catalogue used by the mobile task picker and the manager/HR Admin team board switcher. Employees may read this catalogue to choose a board; board creation and settings remain manager/HR Admin operations (db 06 §1 `task_boards`). Rows are bounded by the board's `visibility` (ADR 0037): a `TENANT` board — which is the default, and what every board predating migration `0109` was backfilled to — is listed for every employee, while a `TEAM` or `PRIVATE` board appears only for the caller it admits. The scope narrowed from `tenant` to `team` with `0109` for exactly this reason; nothing that was listed before it is missing now unless an owner deliberately narrowed it.\n\n**The `department_id` filter is gone (migration `0177`, boards leg B7, #873), and it fails OPEN.** Unlike the board write bodies — which run through an allow-list and now `422` on the retired fields — unknown *query* keys on this operation are ignored, not rejected. A client still sending `?department_id=…` therefore receives the **unfiltered** page rather than an error. That is a narrower result set becoming a wider one, not an authorization change: RLS bounds this page exactly as before, and `work_task_boards_visible` never referenced `department_id`. Filter by `team_id`'s scoping instead.\n",
        "tags": [
          "work",
          "task_board"
        ],
        "x-token": "work.task_board.list",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S08"
        ],
        "x-touches-entities": [
          "work.task_boards"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "is_active",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            }
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `name`, `-name`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of task boards.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskBoardPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "operationId": "work.task_board.create",
        "summary": "Create a task board",
        "description": "Creates a board. Scope is `team_id` (NULL = a workspace board) and `project_id`; the lens is `methodology`. New boards start with `columns=[]` and are configured via `work.task_board.update` (db 06 §1). **`board_type` and `department_id` are gone** — migration `0177` dropped both columns (boards leg B7, #873); sending either is now a `422` naming an unknown field, not a silently ignored write.\n",
        "tags": [
          "work",
          "task_board"
        ],
        "x-token": "work.task_board.create",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S08"
        ],
        "x-touches-entities": [
          "work.task_boards",
          "work.teams",
          "work.projects",
          "people.employees"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskBoardCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Task board created.",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskBoard"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/task-boards/{id}": {
      "get": {
        "operationId": "work.task_board.get",
        "summary": "Get one task board",
        "description": "One board's config — `columns` (stage lanes), `methodology` (the ADR 0037 lens), `wip_limit` per lane (db 06 §1).",
        "tags": [
          "work",
          "task_board"
        ],
        "x-token": "work.task_board.get",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S08"
        ],
        "x-touches-entities": [
          "work.task_boards"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          }
        ],
        "responses": {
          "200": {
            "description": "The task board.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskBoard"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "operationId": "work.task_board.update",
        "summary": "Update a task board (board settings)",
        "description": "Edits `columns`, `methodology`, `visibility`, `wip_limit`, `is_active` — the **Board settings** `PT-MODAL` on `WRK-S08` (fsd 05 §W1, \"no separate ID\"). Archiving (`is_active=false`) keeps the board's tasks; closure is a service rule, not a cascade (db 06 §1). `visibility` is the per-board read control introduced with ADR 0037 (migration 0109): it narrows who may READ the board and its cards, and never widens who may write them.\n",
        "tags": [
          "work",
          "task_board"
        ],
        "x-token": "work.task_board.update",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S08"
        ],
        "x-touches-entities": [
          "work.task_boards"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskBoardUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated task board.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskBoard"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/task-boards/{id}/archive": {
      "post": {
        "operationId": "work.task_board.archive",
        "summary": "Archive a task board (retire the board, keep its tasks)",
        "description": "Sets `is_active = false`. **Keeps the board's tasks** — closure is a service rule, not a cascade (db 06 §1, ADR 0037), same posture as `work.team.archive`. Archiving a second time is refused (`409`): a replay must not silently re-close a board somebody just re-opened. `work.task_board.update` still carries `is_active` — that PATCH is also the un-archive, this operation exists only for the one-way, replay-safe close. Once archived the board accepts no new cards: `work.task.create` and `work.task.move` **onto** it are refused with `409`, while moving cards **off** it still works — the only way an archived board gets emptied out.\n",
        "tags": [
          "work",
          "task_board"
        ],
        "x-token": "work.task_board.archive",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S08"
        ],
        "x-touches-entities": [
          "work.task_boards"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "The archived task board.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskBoard"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/task-boards/{id}/tasks": {
      "get": {
        "operationId": "work.task.list_board",
        "summary": "One board's cards (board read)",
        "description": "This board's own cards — the read `WRK-S08` uses to render one board. It replaces the previous behaviour where the board surface read `work.task.list_team` (every task in the tenant, capped at one page) and filtered by `board_id` client-side. Pagination here is a **correctness requirement, not an optimization** (ADR 0037): the old fetch-and-filter silently lost the 201st visible card with no error.\nAuthorization is two facts in order. First, the board must be readable under `work_task_boards_visible` (migration 0109) — a board the caller may not see 404s exactly as it would for a board that never existed. Only then are the cards read under `work_tasks_visible`, whose board arm admits the cards of a readable board at any non-`SELF` scope. This is **read-broad, write-scoped** (ADR 0037): the token is granted to `employee`, so any employee may read any board they can see, including boards belonging to teams they are not on — it confers no write, and task writes remain the assignee / task-assignment / manager-of-assignee arms, unchanged. A caller who has a task on a board they may not otherwise read still gets `404` here: a board read is a board read, not \"you can see something on it\".\n",
        "tags": [
          "work",
          "task"
        ],
        "x-token": "work.task.list_board",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S08"
        ],
        "x-touches-entities": [
          "work.tasks",
          "work.task_boards",
          "work.task_assignments",
          "people.employees"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/TaskStatus"
            }
          },
          {
            "name": "assignee_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "project_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "team_id",
            "in": "query",
            "required": false,
            "description": "Narrow to the cards of one squad (`work.teams`). A task carries no `team_id` — ADR 0037 §Board binding puts the team on the BOARD — so this resolves the squad to its LIVE roster (`work.team_members`, `left_at IS NULL`) and admits a card whose `assignee_id` is on that roster **or** which has an ACTIVE `work.task_assignments` row for a roster member. That is the same definition of \"on this squad\" `work.team_metrics.get` counts throughput over, so the board facet and the squad's own metrics can never disagree about whose work it is. An unreadable or unknown squad answers `404`; a squad whose members have all left matches no card, which is the honest answer and not \"no filter\".\n",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "priority",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/TaskPriority"
            }
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `rank`, `-rank`, `due_date`, `-due_date`, `created_at`, `-created_at`, `status`, `-status`. Default `rank` — the board's own lane order.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of the board's own cards.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/tasks": {
      "get": {
        "operationId": "work.task.list",
        "summary": "My tasks (assigned to me)",
        "description": "The employee's own assigned tasks — status/priority filters and counts, and the *Task Category* picker source on the add-work-entry form (`WRK-S01`, `WRK-S06`). `Overdue` is **derived** (`due_date < today AND status NOT IN (DONE, CANCELLED)`), never a stored status value (db 06 §1).\n",
        "tags": [
          "work",
          "task"
        ],
        "x-token": "work.task.list",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S01",
          "WRK-S06"
        ],
        "x-touches-entities": [
          "work.tasks",
          "work.task_assignments",
          "work.projects",
          "org.departments",
          "people.employees"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/TaskStatus"
            }
          },
          {
            "name": "priority",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/TaskPriority"
            }
          },
          {
            "name": "due_date[from]",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "due_date[to]",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "project_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "milestone_id",
            "in": "query",
            "required": false,
            "description": "Narrow to the tasks filed under one milestone (`work.tasks.milestone_id`, a plain column predicate). Added at #1430 so a milestone can be OPENED — `WRK-S14`'s milestone rows link to a milestone view whose task list is exactly this read — rather than only counted. The same whitelist backs `work.task.list` and `work.task.list_team`, so both accept it.\n",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "is_overdue",
            "in": "query",
            "required": false,
            "description": "Filters on the derived overdue rule, not a stored column.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `due_date`, `-due_date`, `priority`, `rank`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of the caller's own assigned tasks.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "operationId": "work.task.create",
        "summary": "Create a task (self-assign or manager-assign)",
        "description": "Creates a task on a board and writes its initial `OWNER` `work.task_assignments` row in the same transaction — self-assigned by default, or to `assignee_id` (assigning **another** employee is a Manager/HR Admin gate, deferred to RBAC). `board_id` is **required** — the \"resolved from `department_id`\" convenience this line used to promise was designed and never built (`GAP-41`), and `0177` has now dropped `task_boards.department_id` outright (#873), so there is nothing left to resolve from. Emits `work.task.assigned` (`WRK-S02`, `WRK-S08`). An archived project_id is refused with 409 STATE_TRANSITION_INVALID; an inaccessible project is 404. **Leave-aware advisories (#801).** The response carries a non-blocking `warnings[]`: `LEAVE_COLLISION` when the assignee has approved leave overlapping `start_date`→`due_date`, and `NON_WORKING_DAY` when `due_date` is a holiday for them. The task is created either way — a manager may legitimately schedule across an absence and needs to be told, not stopped. A task created with no assignee gets no advisory. Both codes reuse **this operation's existing token** — nothing new is minted and nothing is gated differently. See `WorkWarning` for the two `detail` shapes and for why `LEAVE_PROJECTION_UNAVAILABLE` means something narrower here than on the allocation path.\n",
        "tags": [
          "work",
          "task"
        ],
        "x-token": "work.task.create",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S02",
          "WRK-S08"
        ],
        "x-touches-entities": [
          "work.tasks",
          "work.task_assignments",
          "work.task_boards",
          "work.projects",
          "people.employees",
          "xc.approved_leave_projection",
          "org.holiday_calendars"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": "work.task.assigned",
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Task created (default `status=BACKLOG`), owner assignment written; `warnings[]` carries any leave/holiday advisory about the assignee — advisory only, the task exists.",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/tasks/team": {
      "get": {
        "operationId": "work.task.list_team",
        "summary": "Team task board (Kanban / list)",
        "description": "The manager's team board — every task on a board they can read, grouped by `status`/`columns` and ordered by `rank` for the Kanban lanes (`WRK-S08`). Board scope is `team_id`/`project_id` plus `visibility` (ADR 0037); the department scoping this line used to describe went with `task_boards.department_id` in `0177` (#873). Also serves the sub-task drill (`parent_task_id` children, `WRK-S09`).\n",
        "tags": [
          "work",
          "task"
        ],
        "x-token": "work.task.list_team",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S08",
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.tasks",
          "work.task_boards",
          "work.projects",
          "people.employees"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "board_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "project_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "assignee_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "milestone_id",
            "in": "query",
            "required": false,
            "description": "Narrow to the tasks filed under one milestone (`work.tasks.milestone_id`, a plain column predicate). Added at #1430 so a milestone can be OPENED — `WRK-S14`'s milestone rows link to a milestone view whose task list is exactly this read — rather than only counted. The same whitelist backs `work.task.list` and `work.task.list_team`, so both accept it.\n",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "priority",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/TaskPriority"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/TaskStatus"
            }
          },
          {
            "name": "team_id",
            "in": "query",
            "required": false,
            "description": "Narrow to the cards of one squad (`work.teams`). A task carries no `team_id` — ADR 0037 §Board binding puts the team on the BOARD — so this resolves the squad to its LIVE roster (`work.team_members`, `left_at IS NULL`) and admits a card whose `assignee_id` is on that roster **or** which has an ACTIVE `work.task_assignments` row for a roster member. That is the same definition of \"on this squad\" `work.team_metrics.get` counts throughput over, so the board facet and the squad's own metrics can never disagree about whose work it is. An unreadable or unknown squad answers `404`; a squad whose members have all left matches no card, which is the honest answer and not \"no filter\".\n",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "parent_task_id",
            "in": "query",
            "required": false,
            "description": "Filter to a task's sub-tasks (self-reference, `WRK-S09`).",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `rank`, `due_date`, `-due_date`, `priority`, `status`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of tasks in the manager's team scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/tasks/export": {
      "post": {
        "operationId": "work.task.export",
        "summary": "Export the team task board — NOT IMPLEMENTED (501)",
        "description": "**Answers `501 Not Implemented` (#1661).** It is published, the token is real and granted, and it will be implemented — but there is no board register behind it today, and until #1661 it answered `202 { id, status: \"QUEUED\" }` with a fabricated id behind no job, no report run and no outbox event. A client told an export is coming stops looking for the data, which is a worse failure than a refusal; a `404` would be worse again, since it would send a client hunting for a typo in a path that is correct.\n\nFor logged hours, `work.timesheet.export` is the working export and the `detail` says so.\n",
        "tags": [
          "work",
          "task"
        ],
        "x-token": "work.task.export",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S08"
        ],
        "x-touches-entities": [
          "work.tasks"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskExportRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "501": {
            "description": "Not implemented — see the description. `detail` names the working alternative.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/tasks/{id}": {
      "get": {
        "operationId": "work.task.get",
        "summary": "Task detail",
        "description": "One task in full — assignment ledger, sub-tasks, and an attachments count. Serves both the employee's own task view (`WRK-S03`) and the manager's full detail (`WRK-S09`, status history + reassign); `team` scope is the RLS ceiling and naturally includes the assignee's own row.\n",
        "tags": [
          "work",
          "task"
        ],
        "x-token": "work.task.get",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S03",
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.tasks",
          "work.task_assignments",
          "work.task_attachments",
          "work.projects",
          "org.departments",
          "people.employees",
          "org.designations"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          }
        ],
        "responses": {
          "200": {
            "description": "The task with its assignment ledger and sub-tasks.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskDetail"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "operationId": "work.task.update",
        "summary": "Edit a task's own fields (inline edit / detail edit)",
        "description": "Partial update of a task's **descriptive** fields — `title`, `description`, `priority`, `due_date`, `estimated_hours`, `milestone_id` — **and its project home, `project_id`** — from the inline row edit and the detail/side-panel edit (`WRK-S08`, `WRK-S09`, `WRK-S15`, `WRK-S16`, `WRK-S17`). Deliberately **not** the two stateful writes: `status` is `work.task.transition` (lifecycle rules, `work.task.status_changed`) and lane position is `work.task.move` (`board_id` + `rank` + `wip_limit`); a request carrying either is `422`. `milestone_id` is service-validated to belong to the task's project (db 06 §1/§4). **Re-homing (#1419).** `project_id` moves the task to another project (`null` detaches it) on the same terms `work.project.close`'s `MOVE` disposition has always used: the destination must be `ACTIVE` (else `409` `STATE_TRANSITION_INVALID`), `milestone_id` resets to `NULL` unless the same body names a milestone on the new project, and a `MILESTONE_CHANGED` activity row carrying `field: 'project_id'` records the move. No new token — this is a task edit, which is exactly why `work.project.close` demands `work.task.update` alongside its own token to do the same thing. **Leave-aware advisories (#801).** The response carries a non-blocking `warnings[]`: `LEAVE_COLLISION` when the task's CURRENT assignee has approved leave overlapping the task's dates after this edit, and `NON_WORKING_DAY` when the resulting `due_date` is a holiday for them — which is the case this screen exists for: moving a date onto someone's leave. The edit is applied either way. This door does not write `assignee_id` (see `TaskUpdate`), so the advisory is always about the assignee the task already has; an unassigned task gets none. Both codes reuse **this operation's existing token** — nothing new is minted and nothing is gated differently. See `WorkWarning` for the two `detail` shapes and for why `LEAVE_PROJECTION_UNAVAILABLE` means something narrower here than on the allocation path.\n",
        "tags": [
          "work",
          "task"
        ],
        "x-token": "work.task.update",
        "x-realizes-features": [
          "WRK-F01",
          "WRK-F07",
          "WRK-F08",
          "WRK-F13"
        ],
        "x-screens": [
          "WRK-S08",
          "WRK-S09",
          "WRK-S15",
          "WRK-S16",
          "WRK-S17"
        ],
        "x-touches-entities": [
          "work.tasks",
          "work.projects",
          "work.milestones",
          "work.task_activity",
          "people.employees",
          "xc.approved_leave_projection",
          "org.holiday_calendars"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated task; `warnings[]` carries any leave/holiday advisory about its assignee — advisory only, the edit is applied.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/tasks/{id}/start": {
      "post": {
        "operationId": "work.task.start",
        "summary": "Start a task",
        "description": "The assignee begins work — `status` `TODO`/`BACKLOG` → `IN_PROGRESS`, stamps `last_activity_at` (`WRK-S03` **Start Task**). The remaining lifecycle (`BLOCKED`, `IN_REVIEW`, `DONE`, `CANCELLED`) is the full-set `work.task.transition` action (`WRK-S09`); `WRK-S03` has no transcribed control for it (fsd 05 §1.4, coverage note 4). The returned task is the committed row and its required `status` therefore confirms `IN_PROGRESS`. An optional `note` records why work began in the transition audit/event; it is not a task comment.\n",
        "tags": [
          "work",
          "task"
        ],
        "x-token": "work.task.start",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S03"
        ],
        "x-touches-entities": [
          "work.tasks"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": "work.task.status_changed",
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskStartInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The committed task, with `status` confirmed as `IN_PROGRESS`.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/tasks/{id}/submit-review": {
      "post": {
        "operationId": "work.task.submit_review",
        "summary": "Submit a task for review",
        "description": "The assignee declares their own work ready — `status` → `IN_REVIEW` from any active state, stamps `last_activity_at` (`WRK-S03`, `WRK-S15`). The sibling of `work.task.start`, and the second and last status write an ordinary `employee` holds: the target is FIXED, exactly as it is on `start`, because the token is the authority and `DONE` stays behind `work.task.transition` (manager / project_manager). Body is the same `TaskTransitionActionInput` the transition takes minus `status` — an optional `note` and an optional `mentions` list, both of which ride the `work.task.status_changed` event so the person the mover wanted to reach is actually told (#1410, #1414).\n",
        "tags": [
          "work",
          "task"
        ],
        "x-token": "work.task.submit_review",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S03",
          "WRK-S15"
        ],
        "x-touches-entities": [
          "work.tasks",
          "work.task_watchers"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": "work.task.status_changed",
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskSubmitReviewInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Task moved to `IN_REVIEW`.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/tasks/{id}/transition": {
      "post": {
        "operationId": "work.task.transition",
        "summary": "Transition a task's status (full lifecycle)",
        "description": "The manager's full status control — `→ BLOCKED` ↔, `→ IN_REVIEW`, `→ DONE` (sets `completed_at`), `→ CANCELLED` — from any non-terminal state (`WRK-S09`). Stamps `last_activity_at`; feeds the role-aware dashboard (`XC-F09`) and the idle scan (`WRK-F02`).\nThe lifecycle accepts DIRECT jumps, not a single-step ladder (#1410): any active state (`BACKLOG`/`TODO`/`IN_PROGRESS`/`BLOCKED`) may go straight to `IN_REVIEW` or straight to `DONE`, and `IN_REVIEW → DONE` is unchanged. `BACKLOG → TODO` and `TODO → IN_PROGRESS` remain the only single-rung moves; `DONE`/`CANCELLED` stay terminal. Closing a finished card is one save, one version and one audit row — it no longer manufactures a review record nobody asked for.\n`note` and `mentions` are read, not merely accepted (#1414): the note lands on the audit row and on the `work.task.status_changed` payload, and each mention becomes a `MENTIONED` watcher so the fan-out reaches the named person even if they watched nothing before.\n",
        "tags": [
          "work",
          "task"
        ],
        "x-token": "work.task.transition",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.tasks"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": "work.task.status_changed",
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskTransitionInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Task transitioned.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/tasks/{id}/move": {
      "post": {
        "operationId": "work.task.move",
        "summary": "Move a task on the Kanban board (drag)",
        "description": "Drag-and-drop re-lane/re-rank — writes `status` (from the target lane's `maps_to_status`) and a fractional `rank` between neighbours, avoiding a full re-sequence (`WRK-S08`, same pattern as the recruit pipeline). Honours the lane's `wip_limit`. **A board change on its own is now expressible (#1419):** `status` is optional and defaults to the task's current status, so the task-detail board picker (`WRK-S09`) can re-home a card between boards without asserting a lifecycle position. `rank` keeps its existing meaning — absent means unranked, where `work.task.create`'s quick-add already puts a new card in its lane.\n",
        "tags": [
          "work",
          "task"
        ],
        "x-token": "work.task.move",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S08",
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.tasks",
          "work.task_boards"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": "work.task.status_changed",
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskMoveInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Task moved.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/tasks/{id}/work-entries": {
      "get": {
        "operationId": "work.work_entry.list_by_task",
        "summary": "Time logged against a task",
        "description": "The **Logged time** panel on the manager's task detail — work entries recorded against this task, across employees (`WRK-S09`).",
        "tags": [
          "work",
          "work_entry"
        ],
        "x-token": "work.work_entry.list_by_task",
        "x-realizes-features": [
          "WRK-F03"
        ],
        "x-screens": [
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.work_entries",
          "work.tasks"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `work_date`, `-work_date`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of work entries logged against the task.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkEntryPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/tasks/{id}/assignments": {
      "get": {
        "operationId": "work.task_assignment.list",
        "summary": "A task's assignment ledger",
        "description": "The authoritative \"who is on what\" ledger for a task — live `ACTIVE` rows plus history (`WRK-S09`, db 06 §1).",
        "tags": [
          "work",
          "task_assignment"
        ],
        "x-token": "work.task_assignment.list",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.task_assignments",
          "people.employees"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/AssignmentStatus"
            }
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `assigned_at`, `-assigned_at`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of assignment ledger rows for the task.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskAssignmentPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "operationId": "work.task_assignment.create",
        "summary": "Assign (or reassign / add a collaborator to) a task",
        "description": "Adds an `ACTIVE` assignment row — the manager's **quick-assign** from the idle-capacity tile (`WRK-S08`) or **Reassign** / add-collaborator on the task detail (`WRK-S09`). Assigning another employee is a Manager/HR Admin gate (defer to RBAC); self-assign is covered by `work.task.create`. Emits `work.task.assigned`. **Leave-aware advisories (#801).** The response carries a non-blocking `warnings[]`: `LEAVE_COLLISION` when the person being assigned has approved leave overlapping the task's existing dates, and `NON_WORKING_DAY` when the task's `due_date` is a holiday for them. This is the acceptance case `WRK-F13` has always described — *warn at the moment of choosing* — answered on the write as well as on `work.assignment_context.get`, because a quick-assign chip need not open a picker first. Both codes reuse **this operation's existing token** — nothing new is minted and nothing is gated differently. See `WorkWarning` for the two `detail` shapes and for why `LEAVE_PROJECTION_UNAVAILABLE` means something narrower here than on the allocation path.\n",
        "tags": [
          "work",
          "task_assignment"
        ],
        "x-token": "work.task_assignment.create",
        "x-realizes-features": [
          "WRK-F01",
          "WRK-F13"
        ],
        "x-screens": [
          "WRK-S08",
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.task_assignments",
          "work.tasks",
          "people.employees",
          "xc.approved_leave_projection",
          "org.holiday_calendars"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": "work.task.assigned",
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskAssignmentCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Assignment created; `warnings[]` carries any leave/holiday advisory about the new assignee — advisory only, the row exists.",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskAssignment"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/task-assignments/{id}/release": {
      "post": {
        "operationId": "work.task_assignment.release",
        "summary": "Release or reassign-out an assignment",
        "description": "Closes a live `ACTIVE` assignment (`status → RELEASED`/`REASSIGNED`, stamps `released_at`) — the counterpart of `work.task_assignment.create` when reassigning (`WRK-S09`). Emits `work.task.status_changed`.\n",
        "tags": [
          "work",
          "task_assignment"
        ],
        "x-token": "work.task_assignment.release",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.task_assignments"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": "work.task.status_changed",
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskAssignmentReleaseInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Assignment released.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskAssignment"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/task-assignments/idle-status": {
      "get": {
        "operationId": "work.task_assignment.idle_status",
        "summary": "My idle-capacity status",
        "description": "Whether the caller currently has zero `ACTIVE` `OWNER`/`COLLABORATOR` assignments for ≥3 days — the employee idle banner on `WRK-S01` (`WRK-F02`). Read from the jobs-tier scan's projection (`XC-F08`), not computed on the request path.\n",
        "tags": [
          "work",
          "task_assignment"
        ],
        "x-token": "work.task_assignment.idle_status",
        "x-realizes-features": [
          "WRK-F02"
        ],
        "x-screens": [
          "WRK-S01"
        ],
        "x-touches-entities": [
          "work.task_assignments"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [],
        "responses": {
          "200": {
            "description": "The caller's idle-capacity status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdleStatus"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/task-assignments/idle-alerts": {
      "get": {
        "operationId": "work.task_assignment.idle_alerts",
        "summary": "Team idle-capacity alerts",
        "description": "The manager's **No-Task Alert** tile — team members with no `ACTIVE` assignment ≥3 days (`WRK-S08`, `WRK-F02`). Optionally narrowed to one squad's live roster (`team_id`, #1420), so `WRK-S22`'s idle panel follows the same cohort as the tile strip above it instead of always answering about the caller's reporting line.\n",
        "tags": [
          "work",
          "task_assignment"
        ],
        "x-token": "work.task_assignment.idle_alerts",
        "x-realizes-features": [
          "WRK-F02"
        ],
        "x-screens": [
          "WRK-S08",
          "WRK-S22"
        ],
        "x-touches-entities": [
          "work.task_assignments",
          "people.employees",
          "work.teams",
          "work.team_members"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "team_id",
            "in": "query",
            "required": false,
            "description": "Narrow to the members of one squad (`work.teams`). Resolved to that squad's LIVE roster (`work.team_members`, `left_at IS NULL`) — the same definition of \"on this squad\" that `work.team_metrics.get` counts over and `work.task.list_team` filters cards by, so a squad-scoped My Team screen cannot show a tile strip about one group of people over an idle panel about another. Absent ⇒ every idle person the caller's own access admits. An empty value is ABSENT, not malformed; a malformed one is `422`. An unreadable or unknown squad answers `404`.\n",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          }
        ],
        "responses": {
          "200": {
            "description": "Page of team members currently idle ≥3 days.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdleAlertPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/tasks/{id}/attachments": {
      "get": {
        "operationId": "work.task_attachment.list",
        "summary": "A task's attachments",
        "description": "Files attached to a task, each resolved to a presigned, expiring download URL (`XC-F07`) — integrity verified against `content_hash`; the `storage_key` never transits the API (`WRK-S03`, `WRK-S09`, db 06 §1). **This list is the complete inventory**: a file posted with a comment carries `comment_id` and is returned here too (#1418), so the panel can show it with an \"on comment\" reference instead of pretending it is unrelated. It is also where the thread resolves its download handles — `TaskComment.attachments` carries metadata only. A file whose comment has since been **soft-deleted stays in this list**, `comment_id` intact: the comment is tombstoned, the evidence is not.\n",
        "tags": [
          "work",
          "task_attachment"
        ],
        "x-token": "work.task_attachment.list",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S03",
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.task_attachments",
          "work.task_comments",
          "people.employees"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          }
        ],
        "responses": {
          "200": {
            "description": "Page of the task's attachments.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskAttachmentPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "operationId": "work.task_attachment.create",
        "summary": "Attach a file to a task",
        "description": "Records metadata for a file already uploaded via the documents capability (`XC-F07` — Gallery / Attach File / Take Picture, `WRK-S02` *Attach From* sheet); the bytes never transit this API, only the resulting `storage_key` reference. An optional **`comment_id`** binds the file to one live comment on this same task at registration time (#1418); it is validated against the path task and refused with `422` otherwise. Omitted, the row is a task-level attachment exactly as before. The comment composer does **not** use this field — it registers unbound and binds via `attachment_ids` on `work.task_comment.create`, so an abandoned draft degrades to a task-level attachment rather than an orphan.\n",
        "tags": [
          "work",
          "task_attachment"
        ],
        "x-token": "work.task_attachment.create",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S02",
          "WRK-S03"
        ],
        "x-touches-entities": [
          "work.task_attachments",
          "work.tasks",
          "work.task_comments",
          "people.employees"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskAttachmentCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Attachment recorded.",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskAttachment"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/timesheets": {
      "post": {
        "operationId": "work.timesheet.create",
        "summary": "Open (get-or-create) a timesheet for a period",
        "description": "Ensures the caller's `DRAFT` timesheet exists for a `DAILY`/`WEEKLY` period (unique per `employee_id, period_type, period_start`) — opened implicitly when an employee first views a new period on `WRK-S04`/`WRK-S05`, or logs its first entry. `period_end` is derived from the market work week (`XC-F01`, `legal_entity_id`); for `DAILY` it equals `period_start`.\n",
        "tags": [
          "work",
          "timesheet"
        ],
        "x-token": "work.timesheet.create",
        "x-realizes-features": [
          "WRK-F03"
        ],
        "x-screens": [
          "WRK-S04",
          "WRK-S05"
        ],
        "x-touches-entities": [
          "work.timesheets",
          "org.legal_entities",
          "people.employees"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TimesheetCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Timesheet opened (`DRAFT`), or the caller's existing one for the period.",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Timesheet"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "get": {
        "operationId": "work.timesheet.list",
        "summary": "My timesheets",
        "description": "The caller's own daily/weekly timesheets, most recent period first (`WRK-S04`). Every row carries the RLS-filtered, newest-first `decision_log`, including rejection comments, so an employee can see why a sheet was returned without a second request. This deliberate correlated-subquery cost keeps list and detail rows on one truthful read shape.\n",
        "tags": [
          "work",
          "timesheet"
        ],
        "x-token": "work.timesheet.list",
        "x-realizes-features": [
          "WRK-F03"
        ],
        "x-screens": [
          "WRK-S04"
        ],
        "x-touches-entities": [
          "work.timesheets",
          "work.timesheet_approvals"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "period_type",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/TimesheetPeriod"
            }
          },
          {
            "name": "period_from",
            "in": "query",
            "required": false,
            "description": "Inclusive lower bound on `period_start` — compiled as `period_start >= period_from` (#1428). Supersedes the `period_start[from]` bracket form this document carried before #1428: that pair was declared in planning round 7 but never implemented, so a client sending it received an unfiltered page.\n",
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "period_to",
            "in": "query",
            "required": false,
            "description": "Inclusive upper bound on `period_start` — compiled as `period_start <= period_to` (#1428). Either bound may be sent alone. A `period_to` earlier than `period_from` is a `422`, never an empty page: a transposed pair must not read as \"nothing was submitted\".\n",
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/TimesheetStatus"
            }
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `period_start`, `-period_start`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of the caller's own timesheets.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TimesheetPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/timesheets/team": {
      "get": {
        "operationId": "work.timesheet.list_team",
        "summary": "Team timesheet grid (manager & Finance)",
        "description": "Team timesheets by period and status for oversight and costing — `total_hours`/`billable_hours` per row (`WRK-S10`). Finance's costing context is read-only here; approved hours cross to `pay` only by the `work.timesheet.approved` event, never a join. Every row deliberately carries the RLS-filtered, newest-first `decision_log`: the grid needs its latest `REVIEWED` chip and inline `REJECTED` reason, and uses the same read shape as detail rather than making per-row requests.\n",
        "tags": [
          "work",
          "timesheet"
        ],
        "x-token": "work.timesheet.list_team",
        "x-realizes-features": [
          "WRK-F03"
        ],
        "x-screens": [
          "WRK-S10"
        ],
        "x-touches-entities": [
          "work.timesheets",
          "work.timesheet_approvals",
          "work.work_entries",
          "people.employees",
          "work.teams",
          "work.team_members"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "employee_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "period_type",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/TimesheetPeriod"
            }
          },
          {
            "name": "period_from",
            "in": "query",
            "required": false,
            "description": "Inclusive lower bound on `period_start` — compiled as `period_start >= period_from` (#1428). Supersedes the `period_start[from]` bracket form this document carried before #1428: that pair was declared in planning round 7 but never implemented, so a client sending it received an unfiltered page.\n",
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "period_to",
            "in": "query",
            "required": false,
            "description": "Inclusive upper bound on `period_start` — compiled as `period_start <= period_to` (#1428). Either bound may be sent alone. A `period_to` earlier than `period_from` is a `422`, never an empty page: a transposed pair must not read as \"nothing was submitted\".\n",
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/TimesheetStatus"
            }
          },
          {
            "name": "team_id",
            "in": "query",
            "required": false,
            "description": "Narrow to the sheets of one squad's LIVE roster (`work.team_members`, `left_at IS NULL`) — the same definition of \"on this squad\" `work.team_metrics.get` counts over and `work.task.list_team` filters cards by (#1420). Absent ⇒ every sheet the caller's own access admits. An empty value is ABSENT, not malformed; a malformed one is `422`. An unreadable or unknown squad answers `404`.\n",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "project_id",
            "in": "query",
            "required": false,
            "description": "Sheets carrying at least one `work.work_entries` line on this project (#1420). A timesheet is a PERIOD belonging to a person and holds no `project_id` of its own, so this is an EXISTS over its lines — deliberately *any*, not *only*: a week split across two projects belongs to both leads' review queues, and dropping it from one would hide hours from somebody who has to review them. `total_hours` stays the sheet's WHOLE total and is never re-cut to the project.\n",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "legal_entity_id",
            "in": "query",
            "required": false,
            "description": "Branch axis (`WRK-S10` org scope, #1434) — `work.timesheets.legal_entity_id`, a column on the sheet itself.\n",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "department_id",
            "in": "query",
            "required": false,
            "description": "Department axis (#1434). A property of the PERSON, so it is applied as a correlated `EXISTS` over the RLS-filtered `people.employees` — it can only narrow the rows the caller's own scope already admits.\n",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "org_unit_id",
            "in": "query",
            "required": false,
            "description": "Team / org-unit axis (#1434, ADR 0037) — `people.employees.org_unit_id`, applied the same way as `department_id`.\n",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `period_start`, `-period_start`, `total_hours`, `status`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of team timesheets.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TimesheetPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/timesheets/all": {
      "get": {
        "operationId": "work.timesheet.list_all",
        "summary": "All-employee timesheet grid (HR & Finance)",
        "description": "The tenant-wide lens on `WRK-S10` (#1434). Same row shape and same filters as `/timesheets/team`; what differs is reach. `/timesheets/team` is confined by the ownership policy on `work.timesheets` to the caller's own sheet and their DIRECT reports' — one hop, no skip-level — which leaves branch- and department-shaped questions (\"what did Acme India book this month\") unanswerable for anyone who is not already tenant-wide by accident of another grant. This operation is that reach stated explicitly: `work.timesheet.list_all` is requested at `TENANT` scope, so the caller must hold a tenant-wide grant AND this token, and the ownership policy's existing `app.current_access_scope() = 'TENANT'` arm admits the rows. No RLS policy is rewritten — the arm was always there, and this is the way to reach it.\nA caller who holds neither is refused 403 by the permission gate; a caller who holds the token without a tenant-wide grant is refused 403 (`SCOPE_DENIED`). Neither is served an empty page.\n",
        "tags": [
          "work",
          "timesheet"
        ],
        "x-token": "work.timesheet.list_all",
        "x-realizes-features": [
          "WRK-F03"
        ],
        "x-screens": [
          "WRK-S10"
        ],
        "x-touches-entities": [
          "work.timesheets",
          "work.timesheet_approvals",
          "people.employees"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "employee_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "period_type",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/TimesheetPeriod"
            }
          },
          {
            "name": "period_from",
            "in": "query",
            "required": false,
            "description": "Inclusive lower bound on `period_start` — compiled as `period_start >= period_from` (#1428). The all-employee lens is the same grid one lens wider, so it honours the range the team read honours; the never-implemented `period_start[from]` bracket form #1428 removed is not reintroduced here.\n",
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "period_to",
            "in": "query",
            "required": false,
            "description": "Inclusive upper bound on `period_start` — compiled as `period_start <= period_to` (#1428). Either bound may be sent alone. A `period_to` earlier than `period_from` is a `422`, never an empty page.\n",
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/TimesheetStatus"
            }
          },
          {
            "name": "team_id",
            "in": "query",
            "required": false,
            "description": "Narrow to the sheets of one squad's LIVE roster (`work.team_members`, `left_at IS NULL`) — the same definition of \"on this squad\" `work.team_metrics.get` counts over and `work.task.list_team` filters cards by (#1420). Absent ⇒ every sheet the caller's own access admits. An empty value is ABSENT, not malformed; a malformed one is `422`. An unreadable or unknown squad answers `404`.\n",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "project_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "legal_entity_id",
            "in": "query",
            "required": false,
            "description": "Branch axis (`WRK-S10` org scope, #1434) — `work.timesheets.legal_entity_id`, a column on the sheet itself.\n",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "department_id",
            "in": "query",
            "required": false,
            "description": "Department axis (#1434). A property of the PERSON, so it is applied as a correlated `EXISTS` over the RLS-filtered `people.employees` — it can only narrow the rows the caller's own scope already admits.\n",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "org_unit_id",
            "in": "query",
            "required": false,
            "description": "Team / org-unit axis (#1434, ADR 0037) — `people.employees.org_unit_id`, applied the same way as `department_id`.\n",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `period_start`, `-period_start`, `total_hours`, `status`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of every timesheet in the tenant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TimesheetPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/timesheets/export": {
      "post": {
        "operationId": "work.timesheet.export",
        "summary": "Export a timesheet register (member × day × task)",
        "description": "One row per work ENTRY over the requested window — member × day × task/activity with hours, the billable flag, the certifying sheet's number and status, and the per-day markers (`leave`, `holiday`, `weekly_off`, `ot_approved_hours`) the read model carries. `project_id` narrows it to one project, which is the `WRK-S10` by-project drill-down's **Export** (`/work/timesheets/project/{id}`); omitted, it is every project the caller's own scope admits (`WRK-S10` **Export**, `XC-F08`).\n\n**`CSV` or `XLSX`, rendered SYNCHRONOUSLY**, PUT to object storage and returned as a time-limited presigned GET — the bytes never transit the API (db-docs/00 §14). Both date bounds are REQUIRED: an unbounded export is every hour ever logged, and the 5 000-row cap would refuse it at the far end of an expensive scan instead of up front. Past the cap the answer is a `422` naming `work_date_to`, never a truncated file.\n\nThe identical register is also seeded as the `TIMESHEET_REGISTER` `admin.report_definitions` row, which `admin.report_run.create` runs through the jobs tier with a 50 000-row cap for a tenant-wide period — same columns, same markers, same writers.\n\n**History (#1661):** this operation was specified as a 200 `FileDownload` from the start, and the implementation answered `202 { id, status: \"QUEUED\" }` behind no job, no report run and no outbox event. The code now matches this document; no consumer contract changed.\n",
        "tags": [
          "work",
          "timesheet"
        ],
        "x-token": "work.timesheet.export",
        "x-realizes-features": [
          "WRK-F03"
        ],
        "x-screens": [
          "WRK-S10"
        ],
        "x-touches-entities": [
          "work.timesheets",
          "work.work_entries",
          "work.projects",
          "work.tasks",
          "people.employees",
          "xc.object_refs"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TimesheetExportRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Presigned download handle for the generated export.",
            "headers": {
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileDownloadRef"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/timesheets/pending-approval": {
      "get": {
        "operationId": "work.timesheet.list_for_approval",
        "summary": "Manager's timesheet approvals queue",
        "description": "`SUBMITTED` timesheets awaiting the manager's decision, plus a missing-submissions view (`WRK-S11`). Each queue row carries its RLS-filtered, newest-first `decision_log` so prior `REVIEWED`/decision evidence is visible before sign-off. Routed via the unified approvals inbox (`XC-F12`); honours delegation (`XC-F14`).\n",
        "tags": [
          "work",
          "timesheet"
        ],
        "x-token": "work.timesheet.list_for_approval",
        "x-realizes-features": [
          "WRK-F04"
        ],
        "x-screens": [
          "WRK-S11"
        ],
        "x-touches-entities": [
          "work.timesheets",
          "work.timesheet_approvals",
          "people.employees"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "employee_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "period_from",
            "in": "query",
            "required": false,
            "description": "Inclusive lower bound on `period_start` — compiled as `period_start >= period_from` (#1428). `WRK-S11`'s range control scopes the QUEUE through this operation, so the KPI band and the table are computed from one bounded page rather than from all history.\n",
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "period_to",
            "in": "query",
            "required": false,
            "description": "Inclusive upper bound on `period_start` — compiled as `period_start <= period_to` (#1428). A `period_to` earlier than `period_from` is a `422`, never an empty page.\n",
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `submitted_at`, `-submitted_at`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of timesheets awaiting sign-off in the manager's scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TimesheetPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/timesheets/approve-batch": {
      "post": {
        "operationId": "work.timesheet.approve_batch",
        "summary": "Batch-approve submitted timesheets",
        "description": "Approves multiple `SUBMITTED` timesheets in one call — each certified at its full `total_hours` (`WRK-S11` **Approve Selected**, \"Confirm X approvals\"). Per-item optimistic-concurrency uses the item's own `if_match` (a bulk call cannot carry a single `If-Match` header); a per-item failure is rolled back to its own savepoint and does not roll back the others. The response reports every row's `APPROVED` or `FAILED` outcome plus totals. Appends one immutable `work.timesheet_approvals` row and emits exactly one `work.timesheet.approved` event per success. For one compatibility release, legacy clients may send `id` in place of `timesheet_id` and may include deprecated `approved_hours`; the server ignores that value and certifies `total_hours`.\n",
        "tags": [
          "work",
          "timesheet"
        ],
        "x-token": "work.timesheet.approve_batch",
        "x-realizes-features": [
          "WRK-F04"
        ],
        "x-screens": [
          "WRK-S11"
        ],
        "x-touches-entities": [
          "work.timesheets",
          "work.timesheet_approvals"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": "work.timesheet.approved",
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TimesheetApproveBatchRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Per-item approval outcomes.",
            "headers": {
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TimesheetApproveBatchResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/timesheets/{id}": {
      "get": {
        "operationId": "work.timesheet.get",
        "summary": "My timesheet detail (weekly or day)",
        "description": "The caller's own timesheet with its decision history (`decision_log`) — `WRK-S04` (weekly summary + day roll-up rows) and `WRK-S05` (`period_type=DAILY` detail). `Target`/`Remaining` are **derived** from the market work-week schedule (`XC-F01`), never stored columns (fsd 05 §1.8). Entry rows are served by the work-entries endpoints, not embedded here.\n",
        "tags": [
          "work",
          "timesheet"
        ],
        "x-token": "work.timesheet.get",
        "x-realizes-features": [
          "WRK-F03"
        ],
        "x-screens": [
          "WRK-S04",
          "WRK-S05"
        ],
        "x-touches-entities": [
          "work.timesheets",
          "work.work_entries",
          "work.timesheet_approvals"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          }
        ],
        "responses": {
          "200": {
            "description": "The timesheet with entries and derived target/remaining.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TimesheetDetail"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/timesheets/{id}/review": {
      "get": {
        "operationId": "work.timesheet.get_team",
        "summary": "Timesheet review breakdown (manager sign-off)",
        "description": "The team-readable timesheet and its decision history (`decision_log`) — read-only, feeds `WRK-S07` (§M sign-off), `WRK-S10` (row expand), and `WRK-S11` (review-first before Approve/Reject).\n",
        "tags": [
          "work",
          "timesheet"
        ],
        "x-token": "work.timesheet.get_team",
        "x-realizes-features": [
          "WRK-F03",
          "WRK-F04"
        ],
        "x-screens": [
          "WRK-S07",
          "WRK-S10",
          "WRK-S11"
        ],
        "x-touches-entities": [
          "work.timesheets",
          "work.timesheet_approvals",
          "work.work_entries",
          "people.employees"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          }
        ],
        "responses": {
          "200": {
            "description": "The timesheet, its employee, and the entries breakdown.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TimesheetTeamDetail"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "operationId": "work.timesheet.review",
        "summary": "Record a first-level review of a timesheet (not an approval)",
        "description": "Appends a `work.timesheet_approvals` row with **`decision = 'REVIEWED'`** — the team lead's *Mark reviewed* on `WRK-S11`'s REVIEW lane and `WRK-S22`'s *Timesheets to review* tab. **It is not an approval, and the boundary is part of the contract** (db 06 §2 BOS addendum, ADR 0026): a `REVIEWED` row writes **no** `approved_hours`, **never moves `work.timesheets.status`** (the timesheet stays `SUBMITTED`), and is **excluded from the latest-effective-decision resolver** — the PM/HR sign-off (`work.timesheet.approve`) remains the authoritative decision. The row is append-only and frozen on write like every row in that table; notifies the approver (`XC-F05`). This token is deliberately **not** `work.timesheet.approve` and grants nothing of it; a lead holding only `work.timesheet.review` never sees the approve action (fsd 05 `WRK-S22`). *Return to employee* is the existing `work.timesheet.reject` with its required comment, not a variant of this operation. **Recorded ONCE per timesheet** (#1421): a second review — by this lead or by any other — is refused with `409` `urn:groundit:problem:work:timesheet-already-reviewed`, naming the reviewer who already stamped it. \"Reviewed\" is a fact about the sheet, not about the viewer, which is how every surface already words it; and because a review moves no `version` and each click carries a fresh `Idempotency-Key`, this refusal is the only thing that can stop the sheet accumulating one row per click. There is no undo: the row is append-only, and a mistaken review is corrected by the authoritative sign-off, not by removing the stamp.\n",
        "tags": [
          "work",
          "timesheet"
        ],
        "x-token": "work.timesheet.review",
        "x-realizes-features": [
          "WRK-F18",
          "WRK-F04"
        ],
        "x-screens": [
          "WRK-S11",
          "WRK-S22"
        ],
        "x-touches-entities": [
          "work.timesheet_approvals",
          "work.timesheets"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TimesheetReviewInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Review recorded; the timesheet's own `status` is unchanged (still `SUBMITTED`).",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TimesheetApproval"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "**The timesheet already carries a `REVIEWED` row** — from ANY reviewer, not just this one (#1421). `type` is `urn:groundit:problem:work:timesheet-already-reviewed`, `code` stays `STATE_TRANSITION_INVALID`, `detail` names the existing reviewer and the date, and the body carries `decided_by`/`decided_by_id` — the same two declared members `xc.approval_inbox.decide` publishes on its decide-race, so a client renders the disabled *Reviewed* state without a follow-up `GET`. **Review is recorded once per timesheet**: it is a stamp, so neither of the mechanisms that normally stop a double-submit can see a repeat — `If-Match` passes forever because a review never moves `work.timesheets.version`, and a client mints a fresh `Idempotency-Key` per click by design (`04 §1`). The rule is held in the schema by a partial unique index on `(tenant_id, timesheet_id) WHERE decision = 'REVIEWED'` (migration `0207`, `db-docs/06 §2`), so a concurrent double click gets the same `409` with a `detail` that asks the client to refresh rather than naming the winner. The other `409` cause on this operation (an `Idempotency-Key` replayed with a different body) keeps `type: about:blank` and `code: IDEMPOTENCY_KEY_REUSE`. *Undo* is deliberately not offered: the row is append-only (`db-docs/00 §8`).\n",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/TimesheetReviewConflictProblem"
                },
                "example": {
                  "type": "urn:groundit:problem:work:timesheet-already-reviewed",
                  "title": "Conflict",
                  "status": 409,
                  "code": "STATE_TRANSITION_INVALID",
                  "detail": "This timesheet was already reviewed by Priya Sharma on 2026-07-22.",
                  "decided_by": "Priya Sharma",
                  "decided_by_id": "018f2c7a-0000-7000-8000-0000000000a7",
                  "instance": "/api/v1/timesheets/018f2c7a-0000-7000-8000-0000000000b4/review",
                  "correlation_id": "018f2c7a-0000-7000-8000-0000000000c4"
                }
              }
            }
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/timesheets/{id}/submit": {
      "post": {
        "operationId": "work.timesheet.submit",
        "summary": "Submit a timesheet for sign-off",
        "description": "`status` `DRAFT` → `SUBMITTED`, sets `submitted_at`, routes to `manager_id` via the unified approvals inbox (`XC-F12`) — **Submit Timesheet** (`WRK-S04`, weekly) or **Submit Day Timesheet** (`WRK-S05`, `period_end = period_start`). Emits `work.timesheet.submitted`.\n",
        "tags": [
          "work",
          "timesheet"
        ],
        "x-token": "work.timesheet.submit",
        "x-realizes-features": [
          "WRK-F03"
        ],
        "x-screens": [
          "WRK-S04",
          "WRK-S05"
        ],
        "x-touches-entities": [
          "work.timesheets"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": "work.timesheet.submitted",
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApprovalDecisionInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Submitted for sign-off.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Timesheet"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/timesheets/{id}/recall": {
      "post": {
        "operationId": "work.timesheet.recall",
        "summary": "Recall a pending timesheet submission",
        "description": "Withdraws a `SUBMITTED` timesheet back to `DRAFT` semantics before it is decided (`status → RECALLED`). The `§M` control for this is **design-pending** (fsd 05 §1.4 coverage note 4); the `RECALLED` enum state and this endpoint are the data-true wire contract regardless.\n",
        "tags": [
          "work",
          "timesheet"
        ],
        "x-token": "work.timesheet.recall",
        "x-realizes-features": [
          "WRK-F03"
        ],
        "x-screens": [
          "WRK-S04"
        ],
        "x-touches-entities": [
          "work.timesheets"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApprovalDecisionInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recalled to `DRAFT` semantics.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Timesheet"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/timesheets/{id}/approve": {
      "post": {
        "operationId": "work.timesheet.approve",
        "summary": "Approve a timesheet (sign-off)",
        "description": "Appends an **immutable** `work.timesheet_approvals` row (`decision=APPROVED`, `approved_hours`), sets the timesheet `status=APPROVED`. Four-eyes enforced (`approver_id <> timesheets.employee_id`). Certified hours + the project-tagged billable split cross to `pay` and Finance **only by the `work.timesheet.approved` event**, never a join (`WRK-S07` §M, `WRK-S11` §W). Honours delegation (`XC-F14`, `delegated_from`).\n\n`approved_hours` is **optional**: omitted, the server certifies the sheet's own `total_hours` (the batch path's rule). This operation stays the **direct** decision surface — the mobile `WRK-S07` contract and the in-process seam the unified approval inbox dispatches to. Where an XC-F12 envelope exists, decide it through `xc.approval_inbox.decide` instead: deciding the timesheet directly leaves that envelope `PENDING` until the reconciler closes it.\n",
        "tags": [
          "work",
          "timesheet"
        ],
        "x-token": "work.timesheet.approve",
        "x-realizes-features": [
          "WRK-F04"
        ],
        "x-screens": [
          "WRK-S07",
          "WRK-S11"
        ],
        "x-touches-entities": [
          "work.timesheets",
          "work.timesheet_approvals",
          "work.work_entries"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": "work.timesheet.approved",
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TimesheetApproveInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Approved and certified.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Timesheet"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/timesheets/{id}/reject": {
      "post": {
        "operationId": "work.timesheet.reject",
        "summary": "Reject a timesheet",
        "description": "Appends an **immutable** `work.timesheet_approvals` row (`decision=REJECTED`, `comments` required), sets the timesheet `status=REJECTED` for fix-and-resubmit. Four-eyes enforced (`WRK-S07` §M, `WRK-S11` §W). Emits `work.timesheet.rejected`.\n",
        "tags": [
          "work",
          "timesheet"
        ],
        "x-token": "work.timesheet.reject",
        "x-realizes-features": [
          "WRK-F04"
        ],
        "x-screens": [
          "WRK-S07",
          "WRK-S11"
        ],
        "x-touches-entities": [
          "work.timesheets",
          "work.timesheet_approvals"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": "work.timesheet.rejected",
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TimesheetRejectInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rejected.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Timesheet"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/task-timers": {
      "post": {
        "operationId": "work.task_timer.start",
        "summary": "Start a task timer",
        "description": "Opens a `RUNNING` timer on a task for the calling employee — the `WRK-S09` Time panel's **Start ⏱**. One live timer per person is a database rule (`work_task_timers_live_key`), so a caller who already holds a `RUNNING` or `PAUSED` timer is refused `409` (`urn:groundit:problem:work:timer-already-running`, whose `detail` names the task and `timer_id` that holds it) unless they send `stop_active: true`, which stops the incumbent in the same transaction and starts this one. Self only — starting a timer *for* someone else is not an operation.\n",
        "tags": [
          "work",
          "task_timer"
        ],
        "x-token": "work.task_timer.start",
        "x-realizes-features": [
          "WRK-F12",
          "WRK-F09"
        ],
        "x-screens": [
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.task_timers",
          "work.tasks"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskTimerStart"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The running timer.",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskTimer"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "This employee already holds a live timer and `stop_active` was not set. The problem document's `type` is `urn:groundit:problem:work:timer-already-running` and its `detail` names the task the incumbent is on, so the client can offer \"stop that one and start here\" rather than a bare refusal.\n",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/task-timers/active": {
      "get": {
        "operationId": "work.task_timer.get_active",
        "summary": "My live timer",
        "description": "The calling employee's `RUNNING` or `PAUSED` timer, or `null` when there is none — the read behind the app-shell running-timer indicator and the `WRK-S09` Time panel's chip. Deliberately **not** a list: one live timer per person is the invariant, so a list would invite a client to render a set that can never have two members. Returns `200` with a `null` body rather than `404`: \"you have no timer running\" is an ANSWER, not a missing resource, and a `404` here would make every shell mount log a spurious not-found.\n",
        "tags": [
          "work",
          "task_timer"
        ],
        "x-token": "work.task_timer.get_active",
        "x-realizes-features": [
          "WRK-F12"
        ],
        "x-screens": [
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.task_timers",
          "work.tasks"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "responses": {
          "200": {
            "description": "The live timer, or `null`.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/TaskTimer"
                    },
                    {
                      "type": "null"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/task-timers/{id}/pause": {
      "post": {
        "operationId": "work.task_timer.pause",
        "summary": "Pause a running timer",
        "description": "Closes the current segment: the seconds since `started_at` are banked into `accumulated_seconds`, `started_at` is cleared, and the timer becomes `PAUSED`. It stays the caller's one live timer — pausing does not free the slot, because a paused timer is work in progress, not work abandoned. Pausing an already-`PAUSED` timer is `409`.\n",
        "tags": [
          "work",
          "task_timer"
        ],
        "x-token": "work.task_timer.pause",
        "x-realizes-features": [
          "WRK-F12"
        ],
        "x-screens": [
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.task_timers"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "The paused timer.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskTimer"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/task-timers/{id}/resume": {
      "post": {
        "operationId": "work.task_timer.resume",
        "summary": "Resume a paused timer",
        "description": "Opens a new segment on a `PAUSED` timer — `started_at` is set to now and the status returns to `RUNNING`; `accumulated_seconds` is untouched. Resuming a `RUNNING` or `STOPPED` timer is `409`.\n",
        "tags": [
          "work",
          "task_timer"
        ],
        "x-token": "work.task_timer.resume",
        "x-realizes-features": [
          "WRK-F12"
        ],
        "x-screens": [
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.task_timers"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "The running timer.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskTimer"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/task-timers/{id}/stop": {
      "post": {
        "operationId": "work.task_timer.stop",
        "summary": "Stop a timer and read back the exact elapsed time",
        "description": "Banks the open segment, sets `stopped_at`, and freezes the timer at `STOPPED`. **It writes no work entry.** The response carries `elapsed_seconds` and `suggested_hours` — the elapsed time in hours to **two decimal places, with no quarter-hour floor** — which the client uses to pre-fill the log-time modal. Logging is still `work.work_entry.create`, which optionally takes `timer_id` and links the two in one transaction. The stopped row is deliberately LEFT IN PLACE: if the person cancels the log-time dialog their time is still on the server and the panel can offer it back. Only `work.task_timer.discard` retires a timer. Stopping an already-`STOPPED` timer is `409`.\n",
        "tags": [
          "work",
          "task_timer"
        ],
        "x-token": "work.task_timer.stop",
        "x-realizes-features": [
          "WRK-F12",
          "WRK-F03"
        ],
        "x-screens": [
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.task_timers"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "The stopped timer, carrying the exact elapsed figure to log.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskTimer"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/task-timers/{id}/discard": {
      "post": {
        "operationId": "work.task_timer.discard",
        "summary": "Discard a timer without logging it",
        "description": "Retires a timer the person does not want to log — a soft delete, because `DELETE` is revoked on this table. The row survives for the record; it stops being the caller's live timer and stops being offered back by the Time panel. This is the ONLY operation that ends a timer's life, and it is deliberately separate from `stop`: \"I have finished this stretch\" and \"throw this away\" are different intentions, and a single verb that meant both would make an accidental stop unrecoverable. Discarding an already-discarded timer is `404`.\n",
        "tags": [
          "work",
          "task_timer"
        ],
        "x-token": "work.task_timer.discard",
        "x-realizes-features": [
          "WRK-F12"
        ],
        "x-screens": [
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.task_timers"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "The discarded timer.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskTimer"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/work-entries": {
      "post": {
        "operationId": "work.work_entry.create",
        "summary": "Log a work entry",
        "description": "Adds decimal hours (`numeric(9,2)`, never float) against a task and/or project on a date, and re-rolls the parent timesheet's `total_hours`/`billable_hours` in the same transaction (`WRK-S06` **Save Entry**). When both `start_time`/`end_time` are supplied, `hours` may be server-derived as their difference; `hours` remains the stored authoritative figure. Billable time requires a `project_id`. Editable only while the parent timesheet is `DRAFT`/`REJECTED`.\n",
        "tags": [
          "work",
          "work_entry"
        ],
        "x-token": "work.work_entry.create",
        "x-realizes-features": [
          "WRK-F03",
          "WRK-F05",
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S06"
        ],
        "x-touches-entities": [
          "work.work_entries",
          "work.timesheets",
          "work.tasks",
          "work.projects"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WorkEntryCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Work entry logged; parent timesheet totals re-rolled.",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkEntry"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "The parent `timesheet_id` does not resolve for this caller. Scoped to the addressed parent only — a `project_id` that does not resolve is a `422` field error, never this (#805).\n",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "description": "Field-level validation failed. In addition to the usual field rules, an unresolvable `project_id` — a stale id, a soft-deleted or archived project, or one belonging to another tenant — is reported as a `/project_id` field error with rule `not_found`, NOT as a `404`. A project the caller can see but does not own is NOT an error at all: the register is tenant-shared and time may be logged against any `ACTIVE` project (a non-`ACTIVE` one is the `409 STATE_TRANSITION_INVALID` above).\n",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblem"
                },
                "example": {
                  "type": "about:blank",
                  "title": "Unprocessable Entity",
                  "status": 422,
                  "code": "VALIDATION_FAILED",
                  "instance": "/api/v1/work-entries",
                  "errors": [
                    {
                      "pointer": "/project_id",
                      "rule": "not_found",
                      "message": "That project does not exist, or you do not have access to it."
                    }
                  ]
                }
              }
            }
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "get": {
        "operationId": "work.work_entry.list",
        "summary": "Work entries for a day, timesheet, task or project",
        "description": "The **Logged Task (N)** entry rows on the day-detail screen — first/last entry, duration,\ntask/project (`WRK-S05`) — and the rows behind `WRK-S10`'s per-day breakdown, `WRK-S11`'s\nexpanded approval row and the per-person time drilldown wired from both (work leg L5, #877).\n\nEach row carries the `task` and `project` embeds declared on `WorkEntry`. Both have been in\nthis schema since the round-8 traceability audit and were populated by nothing until #877;\nthey exist so a period of entries resolves its tasks and projects in one read rather than one\n`GET /tasks/{id}` per distinct task.\n\n**`task` is null when the caller cannot read the task**, which is a real and reachable state,\nnot an error: `work.tasks` is confined by its own RLS overlay, which above `self` scope is\nBOARD-scoped. An entry logged against a task on a board the caller cannot see — a colleague's\nprivate board, another squad's team board — therefore arrives with its hours and its\n`task_id` and with `task: null`. Clients must render that as *\"a task you cannot read\"*, not\nas missing data, and must exclude it from any rate they derive. `project` has no such\nhazard — `work.projects` is tenant-readable since migration `0101` — so a null `project`\nmeans the entry carries no `project_id`, nothing more.\n\nEmployees may use this same filtered read for their own task log. The employee grant is\nadmitted at the route gate, while the work-entry timesheet policy still limits returned rows\nto the caller's own timesheet; managers and project managers retain their team-scoped read.\n",
        "tags": [
          "work",
          "work_entry"
        ],
        "x-token": "work.work_entry.list",
        "x-realizes-features": [
          "WRK-F03"
        ],
        "x-screens": [
          "WRK-S05",
          "WRK-S10",
          "WRK-S11"
        ],
        "x-touches-entities": [
          "work.work_entries",
          "work.tasks",
          "work.projects"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "timesheet_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "work_date",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "work_date_from",
            "in": "query",
            "required": false,
            "description": "Inclusive lower bound on `work_date`. With `work_date_to` it reads one **period** rather than one day — the window `/work/timesheets/project/{projectId}` (`WRK-S10`'s by-project drill-down, #1427) opens on, and the mirror of `period_from`/`period_to` on `GET /timesheets/team`. `work_date` (the single-day equality filter) still composes with both; a day inside a window is a legal, if redundant, request.\n",
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "work_date_to",
            "in": "query",
            "required": false,
            "description": "Inclusive upper bound on `work_date`. A `work_date_to` that PRECEDES `work_date_from` is a `422` field error on `/work_date_to`, never a silently empty page.\n",
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "task_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "project_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `work_date`, `start_time`, `entered_at`, `created_at` (prefix `-` to descend).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of the work entries the caller's scope admits.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkEntryPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/work-entries/{id}": {
      "get": {
        "operationId": "work.work_entry.get",
        "summary": "Get one work entry",
        "description": "One entry, for prefilling the edit form (`WRK-S06`, edit mode).",
        "tags": [
          "work",
          "work_entry"
        ],
        "x-token": "work.work_entry.get",
        "x-realizes-features": [
          "WRK-F03"
        ],
        "x-screens": [
          "WRK-S06"
        ],
        "x-touches-entities": [
          "work.work_entries"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          }
        ],
        "responses": {
          "200": {
            "description": "The work entry.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkEntry"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "operationId": "work.work_entry.update",
        "summary": "Edit a work entry",
        "description": "Edits an entry and re-rolls the parent timesheet's totals — gated to the parent timesheet being `DRAFT`/`REJECTED` (frozen once `APPROVED` by the immutable sign-off, `WRK-S05`/`WRK-S06`).\n",
        "tags": [
          "work",
          "work_entry"
        ],
        "x-token": "work.work_entry.update",
        "x-realizes-features": [
          "WRK-F03",
          "WRK-F05",
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S06"
        ],
        "x-touches-entities": [
          "work.work_entries",
          "work.timesheets"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WorkEntryUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated work entry; parent timesheet totals re-rolled.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkEntry"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "description": "Field-level validation failed. In addition to the usual field rules, an unresolvable `project_id` — a stale id, a soft-deleted or archived project, or one belonging to another tenant — is reported as a `/project_id` field error with rule `not_found`, NOT as a `404`. A project the caller can see but does not own is NOT an error at all: the register is tenant-shared and time may be logged against any `ACTIVE` project (a non-`ACTIVE` one is the `409 STATE_TRANSITION_INVALID` above).\n",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblem"
                },
                "example": {
                  "type": "about:blank",
                  "title": "Unprocessable Entity",
                  "status": 422,
                  "code": "VALIDATION_FAILED",
                  "instance": "/api/v1/work-entries",
                  "errors": [
                    {
                      "pointer": "/project_id",
                      "rule": "not_found",
                      "message": "That project does not exist, or you do not have access to it."
                    }
                  ]
                }
              }
            }
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/projects": {
      "get": {
        "operationId": "work.project.list",
        "summary": "List projects (register / picker)",
        "description": "The lightweight project register — the `§W` grid (`WRK-S12`) and the `§M` picker source (filtered `status=ACTIVE`, `WRK-S02`/`WRK-S06`).\n**The projection is gated on the caller's grant scope.** `work.project.list` is granted to `employee` so the picker is reachable from mobile, but `work.project.get` — the full row — is granted only to `manager`/`hr_admin`. A caller **without** a `TENANT` or `PLATFORM` grant therefore receives the picker projection and the commercial columns `client_name`, `budget_hours` and `budget_amount` are **omitted** from every item (see the `Project` schema). `billing_type` is not gated: it is already an enumerated query filter on this operation. A tenant-wide caller receives the full row, unchanged.\n",
        "tags": [
          "work",
          "project"
        ],
        "x-token": "work.project.list",
        "x-realizes-features": [
          "WRK-F05"
        ],
        "x-screens": [
          "WRK-S12",
          "WRK-S02",
          "WRK-S06"
        ],
        "x-touches-entities": [
          "work.projects",
          "work.project_teams",
          "org.legal_entities",
          "org.departments",
          "people.employees"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/ProjectStatus"
            }
          },
          {
            "name": "billing_type",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/ProjectBillingType"
            }
          },
          {
            "name": "department_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "legal_entity_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "team_id",
            "in": "query",
            "required": false,
            "description": "Narrow to the projects one squad (`work.teams`) is staffed on — the by-team edge read (#1433). A project carries no `team_id`; the edge is a row in `work.project_teams`, so this matches a project with a live edge to that squad. It is what makes \"which projects is this team on\" ONE request: before it, a client had to read the whole register and call `work.project_team.list` once per project, which is why the team home hid its staffing control past 25 projects.\n**Confined to what the caller may already know.** This operation is tenant-shared so the project picker is not confined, but the staffing graph is not tenant-shared: a caller without a `TENANT`/`PLATFORM` grant matches only edges to a squad they are on, a project they own, or a project they are currently allocated to. An unknown or out-of-reach squad therefore matches no project — `200` with an empty page, never `404`, so this cannot be used as a squad-existence oracle. `?team_id=` with no value is treated as absent.\n",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `name`, `-name`, `status`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of projects.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "operationId": "work.project.create",
        "summary": "Create a project",
        "description": "Creates the register/costing context tasks and time tag to (`WRK-S12` **+ New Project**).",
        "tags": [
          "work",
          "project"
        ],
        "x-token": "work.project.create",
        "x-realizes-features": [
          "WRK-F05"
        ],
        "x-screens": [
          "WRK-S12"
        ],
        "x-touches-entities": [
          "work.projects",
          "org.legal_entities",
          "org.departments",
          "people.employees"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Project created.",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/projects/{id}": {
      "get": {
        "operationId": "work.project.get",
        "summary": "Get one project",
        "description": "One project's register detail (`WRK-S12`).",
        "tags": [
          "work",
          "project"
        ],
        "x-token": "work.project.get",
        "x-realizes-features": [
          "WRK-F05"
        ],
        "x-screens": [
          "WRK-S12"
        ],
        "x-touches-entities": [
          "work.projects"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          }
        ],
        "responses": {
          "200": {
            "description": "The project.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "operationId": "work.project.update",
        "summary": "Update a project (incl. lifecycle)",
        "description": "Edits register fields and drives the lifecycle (`PLANNED → ACTIVE → ON_HOLD ↔ → COMPLETED → ARCHIVED`; `CANCELLED` from any non-terminal state, `WRK-S12`). Time may only be logged against an `ACTIVE` project (service-validated at `work.work_entry.create`); an `ARCHIVED` project is read-only.\n",
        "tags": [
          "work",
          "project"
        ],
        "x-token": "work.project.update",
        "x-realizes-features": [
          "WRK-F05"
        ],
        "x-screens": [
          "WRK-S12"
        ],
        "x-touches-entities": [
          "work.projects"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated project.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/projects/{id}/time-rollup": {
      "get": {
        "operationId": "work.project.time_rollup",
        "summary": "Project time roll-up (costing / billing view)",
        "description": "Logged vs `budget_hours`, billable split, and a per-task/per-employee breakdown — an aggregate over project-tagged `work_entries` only (`project_id NOT NULL`), never a cross-schema join (`WRK-S14` *Time & utilization*, which absorbed the superseded `WRK-S13`). Consumed downstream by `pay`/Finance via module API/events, not written here.\n",
        "tags": [
          "work",
          "project"
        ],
        "x-token": "work.project.time_rollup",
        "x-realizes-features": [
          "WRK-F05"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.projects",
          "work.work_entries",
          "work.tasks",
          "people.employees"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          }
        ],
        "responses": {
          "200": {
            "description": "The project's time roll-up.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectTimeRollup"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/projects/{id}/time-rollup/export": {
      "post": {
        "operationId": "work.project.time_rollup_export",
        "summary": "Export a project's time roll-up — NOT IMPLEMENTED (501)",
        "description": "**Answers `501 Not Implemented` (#1661)**, for the same reason as `work.task.export`: it was the third byte-identical `202 { id, status: \"QUEUED\" }` stub behind no job at all.\n\nUnlike the task export this one has a working replacement TODAY, and the `detail` names it: `work.timesheet.export` with this `project_id` returns the same project's hours at LINE level (a day roll-up derives from it; the reverse does not), synchronously, in CSV or XLSX.\n",
        "tags": [
          "work",
          "project"
        ],
        "x-token": "work.project.time_rollup_export",
        "x-realizes-features": [
          "WRK-F05"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.projects",
          "work.work_entries"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "work_date_from": {
                    "$ref": "#/components/schemas/DateOnlyRef"
                  },
                  "work_date_to": {
                    "$ref": "#/components/schemas/DateOnlyRef"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "501": {
            "description": "Not implemented — use `work.timesheet.export` with this `project_id`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/projects/{id}/overview": {
      "get": {
        "operationId": "work.project.overview",
        "summary": "Project 360 overview aggregate",
        "description": "The single read `WRK-S14` opens with — register header, the derived **health** chip together with **every formula input rendered beside it** (overdue-task %, burn vs estimate, stale-task count, blocked count, `health_computed_at`, and whether `health_override` is set), the milestone roll-up, task roll-ups by status and by assignee, the blocked-work count, and the estimate-vs-approved burn. Burn actuals count **approved** `work_entries` only and the payload states how many hours are still pending, so a project is never shown as further along than its signed-off time (`WRK-F09`). Health is **read, never recomputed on this path** — the jobs tier (`XC-F08`) computes and stamps it, and where the inputs are missing the payload reports `NOT_ENOUGH_DATA` rather than defaulting to green.\n\n**Who this admits, and what actually confines the rows — read this before reasoning about `x-rls-scope`.** `tenant` on this operation carries the meaning `02 §4` defines for it — *no RESTRICTIVE overlay beyond tenant isolation, any authorized principal in the tenant* — and **not** \"TENANT-access-scope sessions only\". The two are different vocabularies and conflating them is the likely reading error here: the handler serves a caller at **any** session access scope, which is precisely what makes the `manager` grant in `security-docs/seed/grant-rules.yaml` usable at all. Confinement is supplied by an **explicit row predicate in the handler** rather than by an overlay — the ADR 0037 participation arm: the project's `owner_id`, an employee holding a live `work.project_allocations` window, or a member of a team linked through `work.project_teams` — with tenant FORCE-RLS as the floor underneath. A caller outside that arm receives **`404`, not `403`**, so a refusal does not disclose that the project exists. Money is gated by **column omission, never by zeroing**: a participation-only reader's projection simply does not carry `budget_amount` / `currency_code`, so an absent budget can never be mistaken for a zero one. **The overlay axis and the predicate are one control.** Anyone \"tightening\" this to a narrower `x-rls-scope` without replacing the predicate will silently break the manager and participant reads, which is why this note is on the contract rather than in a commit message.\n",
        "tags": [
          "work",
          "project"
        ],
        "x-token": "work.project.overview",
        "x-realizes-features": [
          "WRK-F06",
          "WRK-F09"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.projects",
          "work.milestones",
          "work.tasks",
          "work.work_entries",
          "work.task_dependencies",
          "work.project_allocations",
          "people.employees"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          }
        ],
        "responses": {
          "200": {
            "description": "The project 360 aggregate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectOverview"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/projects/{id}/utilization": {
      "get": {
        "operationId": "work.project.utilization",
        "summary": "Project utilization (allocations × approved hours)",
        "description": "The *Time & utilization* tab's per-member picture (`WRK-S14`) and the per-project row the portfolio drills into (`WRK-S18`): **approved** hours ÷ (`allocation_pct` × work-week capacity) per member, the billable split, logged vs `budget_hours`, and estimate-vs-actual burn. Capacity comes from the market work week resolved through `legal_entity_id` (`XC-F01`) — never a hard-coded Monday–Friday. **Expenditure is now stated; margin still is not (#1431).** The payload carries an `expenditure` block — recurring + one-time cost lines and salary derived from the allocation percentages — for a caller who also holds `work.project_cost.list`, and `cost_unavailable_reason` has been narrowed to the sentence about MARGIN and BILL RATES (`BIL-F03`) alone. It no longer claims cost is unknowable, because it is not: the owner's model needs no rate card, and every input already lives in the tenant's own data.\n\n**Admission and confinement are `work.project.overview`'s — read that operation's note.** In short: `x-rls-scope: tenant` means *no RESTRICTIVE overlay*, not *TENANT-access-scope callers only*; the rows are confined by the explicit ADR 0037 participation predicate (owner ∪ live allocation ∪ `work.project_teams`); an out-of-arm caller gets `404`, not `403`; and money is withheld by **column omission**, never by zeroing.\n",
        "tags": [
          "work",
          "project"
        ],
        "x-token": "work.project.utilization",
        "x-realizes-features": [
          "WRK-F14",
          "WRK-F09"
        ],
        "x-screens": [
          "WRK-S14",
          "WRK-S18"
        ],
        "x-touches-entities": [
          "work.projects",
          "work.project_allocations",
          "work.project_costs",
          "work.work_entries",
          "work.tasks",
          "people.employees",
          "org.legal_entities",
          "pay.employee_compensation"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Window start; defaults to the project's `start_date`.",
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Window end; defaults to today (or `archived_at` for an archived project).",
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The project's utilization and burn aggregate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectUtilization"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/projects/{id}/cashbook/overview": {
      "get": {
        "operationId": "work.project_cashbook.overview",
        "summary": "Read project cash overview",
        "description": "Manager-only. Omitted `to` defaults to the tenant's civil today from TenantCache localization (UTC fallback), or the opening date when a previously configured opening is later; omitted `from` is the first day of the resolved `to` month. Explicit dates remain authoritative. Cash figures use classified one-time project costs only; recurring and historical unclassified costs remain in the expenditure roll-up and never alter cash. No currency conversion occurs. Priceable salary disclosure writes a SALARY DATA_ACCESS audit row; callers without salary authority receive withheld salary totals.",
        "tags": [
          "work",
          "project_cost"
        ],
        "x-token": "work.project_cost.list",
        "x-realizes-features": [
          "WRK-F14"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_cashbook_entries",
          "work.project_costs",
          "work.projects",
          "people.employees",
          "audit.access_log"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cashbook overview",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectCashbookOverview"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/projects/{id}/cashbook/days/{date}": {
      "get": {
        "operationId": "work.project_cashbook.day",
        "summary": "Read one daily cashbook",
        "description": "Includes current opening/closing balance, worker count and non-voided entries. `409` until opening balance is configured.",
        "tags": [
          "work",
          "project_cost"
        ],
        "x-token": "work.project_cost.list",
        "x-realizes-features": [
          "WRK-F14"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_cashbook_entries",
          "work.project_cashbook_worker_counts",
          "work.project_costs"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "date",
            "in": "path",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Daily cashbook",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectCashbookDay"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Cashbook not configured."
          }
        }
      }
    },
    "/projects/{id}/cashbook/opening-balance": {
      "put": {
        "operationId": "work.project_cashbook.opening_balance.create",
        "summary": "Configure starting balance",
        "description": "One-time setup; zero is allowed. Date must not be future, currency must match the project, and earlier classified cashbook activity is refused.",
        "tags": [
          "work",
          "project_cost"
        ],
        "x-token": "work.project_cost.create",
        "x-realizes-features": [
          "WRK-F14"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_cashbook_entries",
          "audit.audit_log"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectCashbookOpeningCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Configured balance",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectCashbookOpening"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        }
      },
      "patch": {
        "operationId": "work.project_cashbook.opening_balance.update",
        "summary": "Correct starting balance",
        "description": "Optimistic audited correction. Date cannot move after the earliest existing classified activity.",
        "tags": [
          "work",
          "project_cost"
        ],
        "x-token": "work.project_cost.update",
        "x-realizes-features": [
          "WRK-F14"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_cashbook_entries",
          "audit.audit_log"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectCashbookOpeningUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Corrected balance",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectCashbookOpening"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          }
        }
      }
    },
    "/projects/{id}/cashbook/days/{date}/worker-count": {
      "put": {
        "operationId": "work.project_cashbook.worker_count.update",
        "summary": "Set daily worker count",
        "description": "Upserts one count per date; If-Match is required when updating an existing row.",
        "tags": [
          "work",
          "project_cost"
        ],
        "x-token": "work.project_cost.create",
        "x-realizes-features": [
          "WRK-F14"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_cashbook_worker_counts",
          "audit.audit_log"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "date",
            "in": "path",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": false,
            "$ref": "#/components/parameters/IfMatch"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectCashbookWorkerCount"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Worker count",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectCashbookWorkerCount"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          }
        }
      }
    },
    "/projects/{id}/cashbook/days/{date}/cash-count": {
      "post": {
        "operationId": "work.project_cashbook.cash_count.create",
        "summary": "Record an end-of-day cash count",
        "description": "Optional. Compares the cash physically counted with the book balance for the day and posts the difference (counted - book) so the cashbook matches the cash in hand. `notes` is required when they differ. Counting again on the same day replaces the earlier count. On a project with no cashbook yet the count becomes the opening balance. Future days are refused.",
        "tags": [
          "work",
          "project_cost"
        ],
        "x-token": "work.project_cost.create",
        "x-realizes-features": [
          "WRK-F14"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_cashbook_entries",
          "work.project_costs",
          "audit.audit_log"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "date",
            "in": "path",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "counted_amount"
                ],
                "properties": {
                  "counted_amount": {
                    "$ref": "#/components/schemas/MoneyRef"
                  },
                  "notes": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 2000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Cash count",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectCashbookCashCount"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        }
      }
    },
    "/projects/{id}/cashbook/receipts": {
      "post": {
        "operationId": "work.project_cashbook.receipt.create",
        "summary": "Record received money",
        "tags": [
          "work",
          "project_cost"
        ],
        "x-token": "work.project_cost.create",
        "x-realizes-features": [
          "WRK-F14"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_cashbook_entries",
          "audit.audit_log"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectCashbookMovementCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Receipt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectCashbookEntry"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        }
      }
    },
    "/projects/{id}/cashbook/repayments": {
      "post": {
        "operationId": "work.project_cashbook.repayment.create",
        "summary": "Record employee repayment",
        "description": "Pays an employee back from the project balance. Any amount and any employee is allowed; anything beyond what that employee is owed is recorded as an advance (see employee_balances on the overview) and later employee-paid expenses absorb it.",
        "tags": [
          "work",
          "project_cost"
        ],
        "x-token": "work.project_cost.create",
        "x-realizes-features": [
          "WRK-F14"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_cashbook_entries",
          "work.project_costs",
          "audit.audit_log"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectCashbookRepaymentCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Repayment",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectCashbookEntry"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        }
      }
    },
    "/projects/{id}/cashbook/entries/{entryId}": {
      "patch": {
        "operationId": "work.project_cashbook.entry.update",
        "summary": "Edit receipt or repayment",
        "tags": [
          "work",
          "project_cost"
        ],
        "x-token": "work.project_cost.update",
        "x-realizes-features": [
          "WRK-F14"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_cashbook_entries",
          "work.project_costs",
          "audit.audit_log"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "entryId",
            "in": "path",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectCashbookEntryUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated entry",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectCashbookEntry"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          }
        }
      }
    },
    "/projects/{id}/cashbook/entries/{entryId}/void": {
      "post": {
        "operationId": "work.project_cashbook.entry.void",
        "summary": "Void receipt or repayment",
        "description": "Append-only void with reason; original remains auditable and leaves derived balances.",
        "tags": [
          "work",
          "project_cost"
        ],
        "x-token": "work.project_cost.void",
        "x-realizes-features": [
          "WRK-F14"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_cashbook_entries",
          "audit.audit_log"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": true,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "entryId",
            "in": "path",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "reason"
                ],
                "additionalProperties": false,
                "properties": {
                  "reason": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 500
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Voided entry",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectCashbookEntry"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          }
        }
      }
    },
    "/projects/{id}/costs": {
      "get": {
        "operationId": "work.project_cost.list",
        "summary": "A project's cost ledger and its expenditure roll-up",
        "description": "**Expenditure by permission (2026-09-25, migration 0264):** a caller who also holds the service-enforced `work.project_cost.all_projects` (the `expenditure` role) reaches every project's ledger, cashbook and report for the `work.project_cost*` family — not only projects they participate in. It is never a route of its own and widens nothing outside the cost family.\n\nThe *Costs* tab on `WRK-S14`. Returns the project's `work.project_costs` lines — recurring (rent, laptop rental, electricity, priced per `cadence` over an effective-dated window) and one-time (a licence, a device, priced on `incurred_on`) — each with the **spend to date** the line has actually accrued, plus the read-only **salary-derived** lines computed from `work.project_allocations` × `pay.employee_compensation`, plus the same `expenditure` block `work.project.utilization` carries.\n\n**Salary lines are derived, never stored.** For each allocation window overlapping the reporting window, `monthly_cost = ctc_amount / 12 × allocation_pct / 100` and the line's cost is that figure times the elapsed months of the window clipped to `[effective_from, min(effective_to, today)]`. `ctc_amount` is ANNUAL cost-to-company (db 07 §1.1) and exists only for `pay_model = MONTHLY_SALARY`; a `DAILY_WAGE` or `PIECE_RATE` allocation is priced from a rate card or a piece-rate catalogue and has no CTC, so it is returned with `cost: null` and an `unpriced_reason` and is **counted in `unpriced_allocation_count`** rather than folded in as a zero. Storing the figure instead would be a second, stale copy of `pay.employee_compensation` that a salary revision would silently stop updating.\n\n**Currencies are grouped, never summed across.** Each cost line carries its own `currency_code`. `by_currency` always states the per-currency totals; the scalar `total` is populated **only** when every contributing line shares one currency, and is `null` with `mixed_currency: true` otherwise. There is no conversion here — no rate is stated anywhere in this module, and an unconverted sum is a number that can be believed and is wrong.\n",
        "tags": [
          "work",
          "project_cost"
        ],
        "x-token": "work.project_cost.list",
        "x-realizes-features": [
          "WRK-F14",
          "WRK-F09"
        ],
        "x-screens": [
          "WRK-S14",
          "WRK-S18"
        ],
        "x-touches-entities": [
          "work.project_costs",
          "work.projects",
          "work.project_allocations",
          "pay.employee_compensation",
          "people.employees"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "description": "Restrict the ledger to one kind. The `expenditure` roll-up is unaffected — it always states both.",
            "schema": {
              "$ref": "#/components/schemas/ProjectCostKind"
            }
          },
          {
            "name": "as_of",
            "in": "query",
            "required": false,
            "description": "Accrual cut-off for spend-to-date and the salary derivation; defaults to the tenant's civil today from TenantCache localization (UTC fallback). Values later than that day are capped to it.",
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "One expenditure category code (system list or a tenant code). Uppercase.",
            "schema": {
              "type": "string",
              "maxLength": 40
            }
          },
          {
            "name": "payment_method",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/ProjectCostPaymentMethod"
            }
          },
          {
            "name": "paid_by",
            "in": "query",
            "required": false,
            "description": "Employee who paid (`work.project_costs.paid_by_employee_id`).",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "incurred_from",
            "in": "query",
            "required": false,
            "description": "Start of the date window. **Overlap semantics, not equality:** a `ONE_TIME` line matches on `incurred_on`; a `RECURRING` line matches when its `[effective_from, effective_to]` window OVERLAPS the range, so a rent line that ran through the quarter belongs to that quarter's view even though it was never incurred on a day inside it.\n",
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "incurred_to",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "has_receipt",
            "in": "query",
            "required": false,
            "description": "`true` = only lines carrying a receipt, `false` = only lines without one. Absent = either.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "include_voided",
            "in": "query",
            "required": false,
            "description": "Voided lines are excluded by default. They are never deleted — the row survives with its reason so a correction stays reconstructable — and they leave every total whether or not they are listed.\n",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Case-insensitive search over `label`, `payee` and `invoice_no`.",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The project's cost ledger, its derived salary lines, and the expenditure roll-up.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectCostLedger"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "operationId": "work.project_cost.create",
        "summary": "Add a cost line to a project",
        "description": "Inline-row add on the *Costs* tab (`WRK-S14`). `kind` decides which date columns the body must carry, and the database enforces the binding (`work_project_costs_kind_binding_check`, migration `0206`): a `RECURRING` line requires `cadence` and `effective_from` and refuses `incurred_on`; a `ONE_TIME` line requires `incurred_on` and refuses `cadence`, `effective_from` and `effective_to`. `amount` is the amount **per cadence period** for a recurring line, not the amount to date.\n\n`currency_code` is required on every line and is NOT defaulted from the project: a project may hold a budget in one currency and incur a cost in another, and silently stamping the project's currency onto a foreign invoice is exactly the fabrication the no-cross-currency-sum rule exists to prevent. A line whose currency differs from `work.projects.currency_code` is accepted and reported back in `by_currency`, and suppresses the scalar `total`.\n\nCost lines cannot be added to an archived project (`423`).\n",
        "tags": [
          "work",
          "project_cost"
        ],
        "x-token": "work.project_cost.create",
        "x-realizes-features": [
          "WRK-F14"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_costs",
          "work.projects"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectCostCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Cost line created.",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectCost"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/projects/{id}/costs/{costId}": {
      "patch": {
        "operationId": "work.project_cost.update",
        "summary": "Edit a cost line",
        "description": "Inline edit on the *Costs* tab. Every field of the create body is optional here and the SAME kind-binding CHECK applies to the merged row, so an edit that would leave a `RECURRING` line without a `cadence` is refused rather than silently repriced. `kind` itself is **not** editable — switching a line between recurring and one-time changes what its stored amount MEANS (per-period vs total), and a silently reinterpreted amount is the defect this refusal exists to prevent; delete the line and add the other kind.\n\nEnding a recurring line is `effective_to`, not a delete: the closed window IS the record of what the project incurred while it ran, and the spend-to-date arithmetic reads it.\n",
        "tags": [
          "work",
          "project_cost"
        ],
        "x-token": "work.project_cost.update",
        "x-realizes-features": [
          "WRK-F14"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_costs"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "costId",
            "in": "path",
            "required": true,
            "description": "UUIDv7 of the `work.project_costs` row.",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectCostUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated cost line.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectCost"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "delete": {
        "operationId": "work.project_cost.delete",
        "summary": "Remove a cost line (soft) — deprecated, use void",
        "deprecated": true,
        "description": "**Deprecated by `POST …/costs/{costId}/void` (#1597), and it now PERFORMS a void** with the stock reason *\"Deleted via the deprecated delete endpoint.\"* — the response is still `204`.\n\nIt used to set `deleted_at` itself, which left three holes the void path does not have: it skipped the archived-project guard, it allowed a delete on top of an existing void (burying the first reason), and it tombstoned the line while leaving its receipt rows live — so `…/attachments/{id}/download-url` still resolved for a line nobody could see. Routing it to void closes all three rather than teaching a second removal path the same rules, and it is the more honest record: the owner's decision for this round was \"removal = void with a mandatory reason, append-only\". Receipts stay attached because the LINE stays — it is visible under `include_voided`, and a receipt vanishing from a line still on the ledger would be the same hole in reverse.\n\nKept only for contract compatibility with clients built against the #1431 surface; the web app no longer calls it. Ending a recurring line that genuinely ran is `effective_to` on the PATCH above, not this.\n",
        "tags": [
          "work",
          "project_cost"
        ],
        "x-token": "work.project_cost.delete",
        "x-realizes-features": [
          "WRK-F14"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_costs"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "costId",
            "in": "path",
            "required": true,
            "description": "UUIDv7 of the `work.project_costs` row.",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "204": {
            "description": "Cost line soft-deleted; it leaves every expenditure total."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/projects/{id}/costs/{costId}/void": {
      "post": {
        "operationId": "work.project_cost.void",
        "summary": "Void a cost line, with a mandatory reason",
        "description": "**The append-only removal (#1597), and the replacement for `DELETE`.** The row is not deleted: `voided_at`, `voided_by` and `void_reason` are stamped, `version` moves, and the line leaves **every** total — its own `spend_to_date` goes to `\"0.00\"` so a struck-through row and the tiles above it can never disagree. It comes back in the ledger only when the caller asks for `include_voided`.\n\n`409` `STATE_TRANSITION_INVALID` when the line is already voided: a second void would overwrite the first reason and the first voider, which are the two facts the void exists to record. A voided line also refuses `PATCH` — re-enter the spend as a new line, which is what append-only means here.\n\nIts own token, granted to exactly the roles that hold **`work.project_cost.delete`** — `operationId` IS the token (ADR 0015), so an operation cannot enforce another's id, and identical grants are what keep this from being a new authority: it is the delete for this table, under its own name.\n",
        "tags": [
          "work",
          "project_cost"
        ],
        "x-token": "work.project_cost.void",
        "x-realizes-features": [
          "WRK-F24",
          "WRK-F14"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_costs",
          "work.projects"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "costId",
            "in": "path",
            "required": true,
            "description": "UUIDv7 of the `work.project_costs` row.",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectCostVoid"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The voided cost line, carrying its reason and stamps.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectCost"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Refused: `status: ARCHIVED` is not settable on the general project edit. `type` is `urn:groundit:problem:work:project-archive-via-close-only` and `code` is `STATE_TRANSITION_INVALID`; `detail` names `POST /projects/{id}/close` — the operation that actually archives, because archiving is a **ritual and not a field** (`WRK-S23`). Every other 409 cause on this operation (idempotency-key reuse with a different body, an already-archived project) keeps `type: about:blank`.\n",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "example": {
                  "type": "urn:groundit:problem:work:project-archive-via-close-only",
                  "title": "Conflict",
                  "status": 409,
                  "code": "STATE_TRANSITION_INVALID",
                  "detail": "A project is archived by closing it out, not by setting its status: POST /api/v1/projects/{id}/close applies the open-task dispositions, stamps the allocation windows and records the close note in one transaction.",
                  "instance": "/api/v1/projects/018f2c7a-0000-7000-8000-0000000000b2",
                  "correlation_id": "018f2c7a-0000-7000-8000-0000000000c3"
                }
              }
            }
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/projects/{id}/costs/{costId}/attachments/{attachmentId}/download-url": {
      "get": {
        "operationId": "work.project_cost.attachment_url",
        "summary": "A short-lived download URL for one receipt",
        "description": "Mints a **freshly signed, five-minute** GET for one receipt on a cost line (#1597).\n\nURLs are minted on demand and are **never** returned by the ledger list: an operation that handed back live object URLs for every receipt would turn each page load into a bulk disclosure of receipt images, and the links would still resolve long after the session that produced them ended. The list carries file metadata only (`id`, `file_name`, `content_type`, `size_bytes`, `created_at`) — not even the `storage_key`.\n\n`work.project_cost.attachment_url` is granted to exactly the roles that hold **`work.project_cost.list`**: seeing the ledger and seeing the receipt behind a line are one disclosure. A caller outside the project's participation arm gets **404**, not 403.\n",
        "tags": [
          "work",
          "project_cost"
        ],
        "x-token": "work.project_cost.attachment_url",
        "x-realizes-features": [
          "WRK-F24"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_cost_attachments",
          "work.project_costs",
          "xc.object_refs"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "costId",
            "in": "path",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "attachmentId",
            "in": "path",
            "required": true,
            "description": "UUIDv7 of the `work.project_cost_attachments` row.",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A short-lived presigned GET for the receipt.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AttachmentDownloadUrl"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/projects/{id}/costs/exports": {
      "post": {
        "operationId": "work.project_cost.export",
        "summary": "Export the filtered cost ledger as CSV",
        "description": "The *Costs* tab's **Export** control (#1597). Takes the **same filter vocabulary** as the list and runs it through the same builder, so the file can never describe a different set than the grid the operator is looking at.\n\nThe bytes never come back through the API: the CSV is uploaded to object storage and a **short-lived presigned GET** is returned. The presigned URL is minted OUTSIDE the idempotency ledger on both the fresh and the replayed path, so a retry hands back the same artifact through a fresh link instead of one that expired while the caller was retrying.\n\n**Salary lines are not in it.** They are derived from `pay.employee_compensation`, not ledger rows, and a CSV mixing them with petty cash would be a per-employee compensation extract wearing an expenditure filename. Capped at **5 000 rows** — past that the answer is `422` \"narrow the filters\", because a file nobody can open is not an export. Every cell is formula-neutralised (`=`, `+`, `-`, `@` get a leading apostrophe): a `label` and a `payee` are free text an operator typed.\n\n`work.project_cost.export` is granted to exactly the roles that hold `work.project_cost.list` — exporting what you may already read is the same disclosure, recorded on the audit chain (`work.project_cost.exported`) with the filter set and the row count.\n",
        "tags": [
          "work",
          "project_cost"
        ],
        "x-token": "work.project_cost.export",
        "x-realizes-features": [
          "WRK-F24"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_costs",
          "work.projects",
          "work.project_cost_attachments",
          "xc.object_refs",
          "audit.audit_log"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectCostExportRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A short-lived presigned GET for the generated CSV.",
            "headers": {
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectCostExportHandle"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/project-costs/report": {
      "get": {
        "operationId": "work.project_cost.report",
        "summary": "Tenant-wide expenditure report",
        "description": "Logged expenditure across every project the caller may report on, folded by project, category, month or payment method (#1597).\n\n**`ONE_TIME` lines only.** A recurring line is a **run rate**, not a logged expenditure: \"rent, ₹40,000 a month, since January\" has no date inside the report window on which anybody spent anything, and folding it in would mean choosing between counting it once (wrong for a quarter) and accruing it (a number the tenant never entered). The project's own *Costs* tab states both kinds; this answers *what did we actually pay out between these two dates*.\n\n**Currency is part of every group key and is never summed across** (db 00 §6). A project that bought a part in SAR and paid its crew in INR appears as two group rows; one row with a single total would be a number that can be believed and is wrong. `totals_by_currency` is the footer, and there is deliberately no scalar grand total anywhere in the payload.\n\nLines with no category, or no payment method, are folded under `UNCATEGORISED` / `UNSPECIFIED` rather than dropped — that count is what tells a manager the picker is not being used, and hiding it would leave the groups silently failing to add up to the totals.\n\nThe range defaults to the **current calendar month**. Voided lines are excluded unless `include_voided` is set — and even when included they contribute **zero** to `total` and `tax_total`, as does a line dated after today: every figure here goes through the same accrual the *Costs* tab runs, so the two surfaces can never disagree about what has been spent. Such rows are still counted in `count`.\n\nCapped at **5 000 matching lines** — the folding happens in the API, so an unbounded tenant-wide read is refused with `422` \"narrow the range or filters\" rather than served slowly. Same figure as the CSV export beside it, deliberately: a report that rendered a range its own export then refused would be the worse of the two failures.\n",
        "tags": [
          "work",
          "project_cost"
        ],
        "x-token": "work.project_cost.report",
        "x-realizes-features": [
          "WRK-F24"
        ],
        "x-screens": [
          "WRK-S28"
        ],
        "x-touches-entities": [
          "work.project_costs",
          "work.projects",
          "work.project_cost_attachments",
          "work.project_cost_categories",
          "work.project_allocations",
          "work.project_teams"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "incurred_from",
            "in": "query",
            "required": false,
            "description": "Start of the window; defaults to the first of the current calendar month.",
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "incurred_to",
            "in": "query",
            "required": false,
            "description": "End of the window; defaults to today.",
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "project_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 40
            }
          },
          {
            "name": "payment_method",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/ProjectCostPaymentMethod"
            }
          },
          {
            "name": "include_voided",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "group_by",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "project",
                "category",
                "month",
                "payment_method"
              ],
              "default": "project"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The folded expenditure report and its per-currency totals.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectExpenditureReport"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/project-costs/report/exports": {
      "post": {
        "operationId": "work.project_cost.report_export",
        "summary": "Export the tenant-wide expenditure report as CSV",
        "description": "The report's filters, at **line level** rather than folded (#1597) — deliberately, because a grouped CSV is a picture of the page and the reason Finance exports is to do arithmetic the page does not do. Same 5 000-row cap, same formula neutralisation, same upload-then-presign discipline and the same audit row (`work.project_cost.report_exported`) as the per-project export.\n",
        "tags": [
          "work",
          "project_cost"
        ],
        "x-token": "work.project_cost.report_export",
        "x-realizes-features": [
          "WRK-F24"
        ],
        "x-screens": [
          "WRK-S28"
        ],
        "x-touches-entities": [
          "work.project_costs",
          "work.projects",
          "work.project_cost_attachments",
          "xc.object_refs",
          "audit.audit_log"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectExpenditureReportExportRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A short-lived presigned GET for the generated CSV.",
            "headers": {
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectCostExportHandle"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/project-cost-categories": {
      "get": {
        "operationId": "work.project_cost_category.list",
        "summary": "Expenditure categories — the system list merged with the tenant's own",
        "description": "One endpoint rather than two (#1597). A picker that had to call twice and concatenate would eventually concatenate in a different order somewhere, and the same category would sort differently on two screens.\n\nThe **fourteen system codes** (`EQUIPMENT`, `TOOLS`, `MATERIALS`, `CONSUMABLES`, `TRANSPORT`, `FUEL`, `FOOD`, `LABOUR`, `SITE_EXPENSES`, `RENT`, `UTILITIES`, `SOFTWARE`, `PROFESSIONAL_FEES`, `MISC`) are a **constant in the API**, not per-tenant rows: a system list stored per tenant is a list that drifts per tenant, and the cross-project report would stop meaning one thing across the estate the moment somebody renamed `FUEL`. They are always `active` and carry no `id`. `work.project_cost_categories` holds only what a tenant added.\n",
        "tags": [
          "work",
          "project_cost"
        ],
        "x-token": "work.project_cost_category.list",
        "x-realizes-features": [
          "WRK-F24"
        ],
        "x-screens": [
          "WRK-S14",
          "WRK-S28"
        ],
        "x-touches-entities": [
          "work.project_cost_categories"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "responses": {
          "200": {
            "description": "The merged category list, in display order.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ProjectCostCategory"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "operationId": "work.project_cost_category.manage",
        "summary": "Add a tenant expenditure category",
        "description": "Adds one code to this workspace's own half of the list (#1597). `code` is uppercased on the way in, so the report can never split one category across two spellings.\n\n**A code that collides with a system code is refused** (`422`): a tenant code shadowing a built-in one would make the same code mean two things depending on which list a reader consulted, and the report groups by the code. A duplicate tenant code is a `409`.\n",
        "tags": [
          "work",
          "project_cost"
        ],
        "x-token": "work.project_cost_category.manage",
        "x-realizes-features": [
          "WRK-F24"
        ],
        "x-screens": [
          "WRK-S28"
        ],
        "x-touches-entities": [
          "work.project_cost_categories",
          "audit.audit_log"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectCostCategoryCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Category created.",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectCostCategory"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/project-cost-categories/{id}": {
      "patch": {
        "operationId": "work.project_cost_category.update",
        "summary": "Rename, re-sort or deactivate a tenant expenditure category",
        "description": "`code` is deliberately **not** editable (#1597). It is the value stored on every line that ever used the category, and editing it would either orphan those lines — there is no FK to cascade through, by design — or silently re-label history. **Deactivating is the supported retirement**: the code stops being offered in the picker and every line already carrying it is untouched, because deactivating a category is \"stop offering this\", not \"rewrite what happened\".\n\nIts own token, granted to the same roles as `work.project_cost_category.manage` — `operationId` IS the token (ADR 0015), so an operation cannot enforce another's id; keeping the grants identical is what keeps the authority undivided.\n",
        "tags": [
          "work",
          "project_cost"
        ],
        "x-token": "work.project_cost_category.update",
        "x-realizes-features": [
          "WRK-F24"
        ],
        "x-screens": [
          "WRK-S28"
        ],
        "x-touches-entities": [
          "work.project_cost_categories",
          "audit.audit_log"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectCostCategoryUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated category.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectCostCategory"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/projects/{id}/close": {
      "post": {
        "operationId": "work.project.close",
        "summary": "Close out and archive a project",
        "description": "The staged close-out (`WRK-S23`) applied as **one transaction**: every open task carries an explicit disposition (`MOVE` to a receiving `ACTIVE` project, or `CANCEL`), open `work.project_allocations` windows are stamped with `effective_to` (never deleted — the history is the utilization record), `archived_at` is set, `status` becomes `ARCHIVED`, and the close note lands on the project activity trail (it is **not** a `work.projects` column, db 06 §3). **`409` when any non-terminal task is left undispositioned** — the response names them, since a close that silently strands work is the failure mode this ritual exists to prevent (`WRK-F17`). An `ARCHIVED` project rejects writes afterwards. Emits `work.project.closed`, which `billing` may consume for final invoicing — an event, never a cross-schema write (ADR 0026 §(d)). Reversal is not offered here.\n",
        "tags": [
          "work",
          "project"
        ],
        "x-token": "work.project.close",
        "x-realizes-features": [
          "WRK-F17"
        ],
        "x-screens": [
          "WRK-S23"
        ],
        "x-touches-entities": [
          "work.projects",
          "work.tasks",
          "work.project_allocations",
          "work.milestones",
          "work.task_activity",
          "work.work_entries",
          "work.timesheets"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": "work.project.closed",
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectCloseRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Project archived; dispositions applied and allocation windows closed.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectCloseResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Close-out refused: one or more non-terminal tasks carry no explicit disposition. `type` is `urn:groundit:problem:work:project-close-undispositioned-tasks`, `code` is `STATE_TRANSITION_INVALID`, and **`detail` names the stranded tasks** by business number and title — a close that silently strands work is the failure mode this ritual exists to prevent (`WRK-F17`), so the refusal must be actionable without a second round-trip. The other 409 causes on this operation (idempotency-key reuse with a different body) keep `type: about:blank`.\n",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "example": {
                  "type": "urn:groundit:problem:work:project-close-undispositioned-tasks",
                  "title": "Conflict",
                  "status": 409,
                  "code": "STATE_TRANSITION_INVALID",
                  "detail": "2 open task(s) have no close-out disposition: TSK-000041 Migrate seeds, TSK-000052 Cutover runbook.",
                  "instance": "/api/v1/projects/018f2c7a-0000-7000-8000-0000000000b2/close",
                  "correlation_id": "018f2c7a-0000-7000-8000-0000000000c2"
                }
              }
            }
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/projects/{id}/health-override": {
      "post": {
        "operationId": "work.project.health_override",
        "summary": "Override (or clear) a project's computed health",
        "description": "Sets `health` by hand and flips `health_override = true`, stamping `health_computed_at` — a deliberately **separate gate** from general project edit, so \"who may overrule the formula\" is answerable on its own (`WRK-S14`). A `reason` is **required** on override: it is not a `work.projects` column (db 06 §3) but is captured to the audit trail (`XC-F06`) as the action's side-effect and rendered as *\"Set by <name> · <reason>\"* beside the computed value, which stays visible. The recompute job does not overwrite an overridden value. `clear: true` hands the project back to the formula.\n",
        "tags": [
          "work",
          "project"
        ],
        "x-token": "work.project.health_override",
        "x-realizes-features": [
          "WRK-F06"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.projects"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectHealthOverrideInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Health overridden (or handed back to the formula).",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectHealth"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/projects/{id}/activity": {
      "get": {
        "operationId": "work.project.activity",
        "summary": "The project's activity feed",
        "description": "The *Activity* tab of `WRK-S14`, newest first — what changed on this project and on the work inside it.\n**It is a UNION of two origins, and the shape deliberately does not say which.** Task-level events are `work.task_activity` rows for the project's tasks; project-level events (status, delivery model, ownership, health override, close-out, milestone and allocation changes) have **no product-facing table** — `work.task_activity.task_id` is `NOT NULL` with a composite FK to `work.tasks`, so a project-scoped row is not storable there, and this round deliberately does **not** widen that published table. Those events are read from `audit.audit_log`, which is already their source of truth (the health-override `reason` is specified to live there and explicitly *not* as a `work.projects` column). Each item carries a `scope` — `TASK` or `PROJECT` — describing the **subject** of the event, never its storage. A client filters and renders on `scope` + `kind` alone; the origin table is not a contract and may change without a version bump.\n**Two contract guarantees, load-bearing and not implementation notes.**\n(1) **ALLOW-LIST, never a window onto audit.** `ProjectActivityKind` is a *closed* set. An event whose kind is not in that set is not projected, whatever it is. This is the whole reason the feed can read `audit.audit_log` at all: `audit` is the tenant-invisible, hash-chained evidence plane, and a feed that forwarded arbitrary audit rows would make it tenant-visible by the back door. Widening the set is an api-docs change first, reviewed as a disclosure change — it is not a service-layer decision.\n(2) **NO MONEY VALUES AND NO PII EVER LEAVE THIS ENDPOINT.** `ProjectActivityDetail` carries the **name** of what changed and never the values it changed between: there is no `from`, no `to`, no amount and no free text copied out of an audit payload. *It may say the budget changed; it must not say to what.* Money and commercial columns (`budget_amount`, `budget_hours`, `client_name`) stay behind the existing `WorkService.projectListSelect()` column gating on `work.project.get`/`.list`, where the permission matrix expresses them — this feed is not a second, ungated route to the same numbers. Comment BODIES are excluded for the same reason: `COMMENTED` is **not** an allow-listed kind here, and a caller who may read a task's thread reads it through `work.task_comment.list`, under that token.\n`actor_id` is **null for system/jobs** actions — the health recompute (`XC-F08`), recurrence materialization, the SLA scan. Rows are immutable: a mistaken entry is superseded by a later one, never corrected in place.\n\n**Admission and confinement are `work.project.overview`'s — read that operation's note**, and read it with extra care here, because this is the one operation on the screen that reaches the audit plane. The handler requests `ANY`, never `TENANT`: it states its own row boundary through the explicit ADR 0037 participation predicate (owner ∪ live allocation ∪ `work.project_teams`), and an out-of-arm caller gets `404`, not `403`. `x-rls-scope` is therefore declared **`team`** — the widest reach a caller without a tenant-wide grant actually has here, and what the two personas this token is granted to (Manager, PM — both `scope_type: SELF`) can satisfy. Declaring `tenant` to mean *no RESTRICTIVE overlay* is the catalogue-vs-runtime lie #934 exists to stop: the grant matrix reads the declaration, so a `tenant` claim promises reach the engine refuses (the same correction #953 made to `work.workload.read`). Note the grant deliberately differs from its two neighbours: `finance` holds `work.project.overview` and `.utilization` but **not** this token, because widening a disclosure surface is a separate decision from widening a cost read (`security-docs/02 §3`).\n",
        "tags": [
          "work",
          "project"
        ],
        "x-token": "work.project.activity",
        "x-realizes-features": [
          "WRK-F06",
          "WRK-F07",
          "WRK-F17"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.task_activity",
          "work.tasks",
          "work.projects",
          "work.milestones",
          "work.project_allocations",
          "audit.audit_log",
          "people.employees"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": true,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "description": "Filter to one allow-listed kind. Repeat the parameter to request several; an unknown value is a `422` field error, never a silently empty page — a client must not be able to mistake \"that kind is not projected\" for \"nothing happened\".\n",
            "schema": {
              "$ref": "#/components/schemas/ProjectActivityKind"
            }
          },
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "description": "Filter to task-level or project-level events.",
            "schema": {
              "$ref": "#/components/schemas/ProjectActivityScope"
            }
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `-occurred_at`, `occurred_at`. Default `-occurred_at`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of the project's activity, newest first, across both origins.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectActivityPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/projects/{id}/milestones": {
      "get": {
        "operationId": "work.milestone.list",
        "summary": "List a project's milestones",
        "description": "The *Milestones* tab grid (`WRK-S14`), ordered by `sort_order` then `due_date`, each row carrying its **derived** roll-up — task counts by status (\"7 of 11 done\") and estimate vs approved actual for the milestone's tasks. Nothing in the roll-up is stored, which is what keeps a milestone from becoming a second, stale copy of the truth (db 06 §4).\n",
        "tags": [
          "work",
          "milestone"
        ],
        "x-token": "work.milestone.list",
        "x-realizes-features": [
          "WRK-F06",
          "WRK-F09"
        ],
        "x-screens": [
          "WRK-S14",
          "WRK-S17"
        ],
        "x-touches-entities": [
          "work.milestones",
          "work.tasks",
          "work.work_entries"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/MilestoneStatus"
            }
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `sort_order`, `due_date`, `-due_date`, `name`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of the project's milestones with their derived roll-ups.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MilestonePage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "operationId": "work.milestone.create",
        "summary": "Add a milestone to a project",
        "description": "Inline-row add on the *Milestones* tab (`WRK-S14`) — `name` is required and tenant-unique within the project. A `due_date` outside the project's `start_date`…`end_date` window returns a **warning** in the payload, not a rejection (fsd 05 `WRK-S14`); the parent-window check is service-validated because a CHECK cannot reference the parent row (db 06 §4).\n",
        "tags": [
          "work",
          "milestone"
        ],
        "x-token": "work.milestone.create",
        "x-realizes-features": [
          "WRK-F06"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.milestones",
          "work.projects"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MilestoneCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Milestone created.",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Milestone"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/milestones/{id}": {
      "get": {
        "operationId": "work.milestone.get",
        "summary": "One milestone with its derived roll-up",
        "description": "A single `work.milestones` row and the same **derived** roll-up `work.milestone.list` returns — task counts by status, and estimate vs **approved** actual over the milestone's tasks. Nothing in the roll-up is stored (db 06 §4), which is what keeps a milestone from becoming a second, stale copy of the truth. Exists as its own read because a milestone is deep-linkable from a task's breadcrumb and from the calendar (`WRK-S17`), where the caller has an id and no project context to list against.\n",
        "tags": [
          "work",
          "milestone"
        ],
        "x-token": "work.milestone.get",
        "x-realizes-features": [
          "WRK-F06",
          "WRK-F09"
        ],
        "x-screens": [
          "WRK-S14",
          "WRK-S17"
        ],
        "x-touches-entities": [
          "work.milestones",
          "work.tasks",
          "work.work_entries"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          }
        ],
        "responses": {
          "200": {
            "description": "The milestone with its derived roll-up.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Milestone"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "operationId": "work.milestone.update",
        "summary": "Update a milestone (inline edit)",
        "description": "Edits `name`, `due_date`, `sort_order`, `description` and drives the **stored, PM-owned** status (`PLANNED → ON_TRACK → DONE`, with `AT_RISK` branching off `ON_TRACK` and back, and `DROPPED` reachable from any non-`DONE` state). The status is **not derived** — the workspace may *suggest* `AT_RISK` from the same roll-up that feeds `projects.health`, but what is stored is what the PM committed to. `DONE` carries the completion semantics on its own; there is **no `completed_at` column** (db 06 §4). A status change lands on the project activity feed (`work.project.activity`, `MILESTONE_STATUS_CHANGED`).\n**It does NOT notify today, and this spec will not pretend otherwise.** `asyncapi/domain-events.asyncapi.yaml` declares no `work.milestone.*` channel, so the `XC-F05` notification an earlier draft of this description promised cannot fire — there is nothing to publish to and nothing subscribed. Declaring a channel here would imply wiring and consumers that are not built, and a declared-but-unconsumed channel is a worse artifact than a documented absence. Tracked as issue **#914**; recorded here rather than quietly dropped, per api-docs `00 §6`.\n",
        "tags": [
          "work",
          "milestone"
        ],
        "x-token": "work.milestone.update",
        "x-realizes-features": [
          "WRK-F06",
          "WRK-F09"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.milestones"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MilestoneUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated milestone.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Milestone"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/project-allocations": {
      "get": {
        "operationId": "work.project_allocation.list",
        "summary": "List project allocations",
        "description": "The effective-dated allocation table behind `WRK-S19` and the *Team & allocations* tab on `WRK-S14` — one row per employee × project × window. Per `(employee, project)` the live windows **partition time** (#1663, migration `0231`): they cannot overlap, so a member's PHASES on a project read as an ordered, gapless-or-gapped sequence rather than a set that has to be resolved. Pass `include_history=true` to get every phase; without it the read returns the window containing `as_of` (defaulting to today), which is now at most one row per pair by construction rather than by tie-breaking. Each row carries its derived open-task load and the amber **overbooking warning** where the member's live `allocation_pct` — the peak per project, summed across projects, never a sum of rows — exceeds 100 %.\n",
        "tags": [
          "work",
          "project_allocation"
        ],
        "x-token": "work.project_allocation.list",
        "x-realizes-features": [
          "WRK-F14",
          "WRK-F13"
        ],
        "x-screens": [
          "WRK-S19",
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_allocations",
          "work.projects",
          "work.tasks",
          "people.employees"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "project_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "employee_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "as_of",
            "in": "query",
            "required": false,
            "description": "Window resolution date; defaults to today. Closed historical windows are returned when `include_history=true`.",
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "include_history",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `effective_from`, `-effective_from`, `allocation_pct`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of allocations with derived load and overbooking warnings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectAllocationPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "operationId": "work.project_allocation.create",
        "summary": "Allocate a member to a project",
        "description": "Writes one effective-dated `work.project_allocations` row (`WRK-S19` **Allocate**) and lands on the project activity feed (`work.project.activity`, `ALLOCATION_ADDED`). **It does not notify the member yet.** fsd 05 `WRK-S19` specifies an `XC-F05` notification on allocation, and this description previously asserted one; there is **no outbox channel for `work.project_allocation.*`** (`x-emits-event: null`, above, is the truthful reading), so nothing is dispatched and no consumer should wait for it — `work.milestone.update` carries the same gap. Tracked as [#914](https://github.com/Sysmeadac/GroundIT/issues/914) — corrected here rather than left standing, because a consumer that reads this line as a promise builds against a message that never arrives. `allocation_pct` is a **percentage** (`0 < pct <= 100` per row, decimal — the deliberate db 06 §5 deviation from fraction-typed ratios, since it is the number the operator typed and never an operand of money arithmetic). A member's **summed** percentage across projects may exceed 100: that returns a populated `warnings[]` with the colliding projects and window — **a warning, never an error** (`WRK-F13`/`F14`). An overlap with an approved-leave window is specified to warn the same way, but `LEAVE_COLLISION` is **not emitted yet** (#916, see `WorkWarning`); today that path answers with the standing `LEAVE_PROJECTION_UNAVAILABLE` caveat instead. A hard block would make a real, temporary over-allocation unrecordable. **Windows on ONE project cannot overlap, and that IS an error (#1663).** Per `(employee, project)` the live windows partition time: the `work_project_allocations_no_overlap` exclusion constraint (migration `0231`) is the guarantee, and this door checks the same predicate in-transaction so the refusal can name the other row (`422`, `urn:groundit:problem:work:allocation-window-overlap`). Read that alongside the paragraph above rather than against it — a member summed past 100 % ACROSS projects is a judgement the business is allowed to make; two windows claiming the same member on the same project for the same days is a contradiction no reader can interpret. The **taper** is the one shape that is not a collision. A phase starting after an existing OPEN-ENDED window narrows that window to the day before, in the same transaction, and returns `201` with a `PREDECESSOR_WINDOW_CLOSED` warning naming the window it closed — so \"80 % for two weeks, then 20 %\" is one POST. `POST /project-allocations/{id}/split` is the explicit form of the same operation against a window the caller names. Allocating against an `ARCHIVED` project is refused (`409`).\n",
        "tags": [
          "work",
          "project_allocation"
        ],
        "x-token": "work.project_allocation.create",
        "x-realizes-features": [
          "WRK-F14",
          "WRK-F13"
        ],
        "x-screens": [
          "WRK-S19"
        ],
        "x-touches-entities": [
          "work.project_allocations",
          "work.projects",
          "people.employees"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectAllocationCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Allocation created; `warnings` is populated (non-blocking) where the member is overbooked or on approved leave in the window.",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectAllocation"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Authenticated and permitted to READ this allocation, but not to write it — the per-command write confinement migration 0131 §3 puts on `work.project_allocations`. Distinguished from a generic 403 by `type: urn:groundit:problem:work:project-allocation-not-writable`, with `code: SCOPE_DENIED` and `status: 403`.\n**403, never 404.** The usual \"absent or RLS-masked, deliberately indistinguishable\" posture (`_shared.yaml` `NotFound`, security 02 §4) does **not** apply here: the caller can see the row — a list just rendered it — so masking the write refusal as a 404 would contradict the response they already hold. Nothing is disclosed by saying so.\nEvery other 403 cause for this operation — token not granted, tenant suspended — keeps `type: about:blank` and its own `code`, exactly as `Forbidden` describes. **`detail` never names another employee, a rate or an amount**: it says the caller's scope does not permit the change and who to ask, not who holds what.\n",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "example": {
                  "type": "urn:groundit:problem:work:project-allocation-not-writable",
                  "title": "Forbidden",
                  "status": 403,
                  "code": "SCOPE_DENIED",
                  "detail": "You can view this allocation, but your access scope does not permit changing it. Ask the project owner or an HR admin to make this change.",
                  "instance": "/api/v1/project-allocations/{id}",
                  "correlation_id": "018f2c7a-0000-7000-8000-0000000000c1"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "description": "Refused: the window this write asks for overlaps another allocation of the **same member on the same project**. `type` is `urn:groundit:problem:work:allocation-window-overlap`, `code` is `VALIDATION_FAILED`, and `errors[0].pointer` is the date field to move (`/effective_from`, or `/effective_to` when the end date is what was changed). `detail` names the conflicting window's dates and percentage so the screen can say which row is in the way.\nPer `(tenant, employee, project)` the live windows **partition time** — migration `0231` enforces it with the `work_project_allocations_no_overlap` exclusion constraint, and the write doors check the same `daterange(effective_from, effective_to, '[]')` predicate in-transaction so the refusal can name the other row. A client receives this `type` either way; it cannot tell, and does not need to tell, which of the two caught it.\n**The taper is NOT this error.** A new phase starting after an existing OPEN-ENDED window is the shape real staffing needs, and `POST /project-allocations` honours it by narrowing the incumbent to the day before, in the same transaction, returning `201` with a `PREDECESSOR_WINDOW_CLOSED` warning. Only a genuine collision reaches here.\n",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "example": {
                  "type": "urn:groundit:problem:work:allocation-window-overlap",
                  "title": "Unprocessable Entity",
                  "status": 422,
                  "code": "VALIDATION_FAILED",
                  "detail": "This member already has a 60.00 % allocation to this project for 2026-10-01 – 2026-10-20. Allocation windows on one project cannot overlap — end or split the existing window first.",
                  "errors": [
                    {
                      "pointer": "/effective_from",
                      "rule": "overlap",
                      "message": "This member already has a 60.00 % allocation to this project for 2026-10-01 – 2026-10-20. Allocation windows on one project cannot overlap — end or split the existing window first."
                    }
                  ],
                  "instance": "/api/v1/project-allocations",
                  "correlation_id": "018f2c7a-0000-7000-8000-0000000000c3"
                }
              }
            }
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/project-allocations/{id}": {
      "patch": {
        "operationId": "work.project_allocation.update",
        "summary": "Update, re-date or end an allocation",
        "description": "Adjusts `allocation_pct`, `effective_from`, `effective_to` or `role_note` (`WRK-S19`). **Ending an allocation stamps `effective_to` — never a hard delete**, because the closed window *is* the utilization record (db 06 §5). `role_note` is a single free-text label (\"tech lead\", \"QA\") and explicitly **not** an RBAC role or a `people` designation. Overbooking and leave-collision remain non-blocking `warnings[]` — with the same caveat as `create`: `LEAVE_COLLISION` is specified but unemitted (#916). **`effective_from` is writable since #1663.** It was immutable, so a window whose start was mistyped could only be ended and re-created under a new id — losing the audit thread and orphaning the row the utilization record points at. It is accepted here and validated by the general window rule: the window this PATCH would leave behind must not overlap any sibling window of the same member on the same project, checked against ALL siblings (dragging a start date backwards across its predecessor is the same contradiction as dragging an end date forwards across its successor). A crossing is refused with `422` `urn:groundit:problem:work:allocation-window-overlap`, naming the other window. `employee_id` and `project_id` remain immutable: re-pointing a window at somebody else, or at another project, is a new allocation and not an edit.\n",
        "tags": [
          "work",
          "project_allocation"
        ],
        "x-token": "work.project_allocation.update",
        "x-realizes-features": [
          "WRK-F14",
          "WRK-F13"
        ],
        "x-screens": [
          "WRK-S19"
        ],
        "x-touches-entities": [
          "work.project_allocations"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectAllocationUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated allocation.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectAllocation"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Authenticated and permitted to READ this allocation, but not to write it — the per-command write confinement migration 0131 §3 puts on `work.project_allocations`. Distinguished from a generic 403 by `type: urn:groundit:problem:work:project-allocation-not-writable`, with `code: SCOPE_DENIED` and `status: 403`.\n**403, never 404.** The usual \"absent or RLS-masked, deliberately indistinguishable\" posture (`_shared.yaml` `NotFound`, security 02 §4) does **not** apply here: the caller can see the row — a list just rendered it — so masking the write refusal as a 404 would contradict the response they already hold. Nothing is disclosed by saying so.\nEvery other 403 cause for this operation — token not granted, tenant suspended — keeps `type: about:blank` and its own `code`, exactly as `Forbidden` describes. **`detail` never names another employee, a rate or an amount**: it says the caller's scope does not permit the change and who to ask, not who holds what.\n",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "example": {
                  "type": "urn:groundit:problem:work:project-allocation-not-writable",
                  "title": "Forbidden",
                  "status": 403,
                  "code": "SCOPE_DENIED",
                  "detail": "You can view this allocation, but your access scope does not permit changing it. Ask the project owner or an HR admin to make this change.",
                  "instance": "/api/v1/project-allocations/{id}",
                  "correlation_id": "018f2c7a-0000-7000-8000-0000000000c1"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "description": "Refused: the window this write asks for overlaps another allocation of the **same member on the same project**. `type` is `urn:groundit:problem:work:allocation-window-overlap`, `code` is `VALIDATION_FAILED`, and `errors[0].pointer` is the date field to move (`/effective_from`, or `/effective_to` when the end date is what was changed). `detail` names the conflicting window's dates and percentage so the screen can say which row is in the way.\nPer `(tenant, employee, project)` the live windows **partition time** — migration `0231` enforces it with the `work_project_allocations_no_overlap` exclusion constraint, and the write doors check the same `daterange(effective_from, effective_to, '[]')` predicate in-transaction so the refusal can name the other row. A client receives this `type` either way; it cannot tell, and does not need to tell, which of the two caught it.\n**The taper is NOT this error.** A new phase starting after an existing OPEN-ENDED window is the shape real staffing needs, and `POST /project-allocations` honours it by narrowing the incumbent to the day before, in the same transaction, returning `201` with a `PREDECESSOR_WINDOW_CLOSED` warning. Only a genuine collision reaches here.\n",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "example": {
                  "type": "urn:groundit:problem:work:allocation-window-overlap",
                  "title": "Unprocessable Entity",
                  "status": 422,
                  "code": "VALIDATION_FAILED",
                  "detail": "This member already has a 60.00 % allocation to this project for 2026-10-01 – 2026-10-20. Allocation windows on one project cannot overlap — end or split the existing window first.",
                  "errors": [
                    {
                      "pointer": "/effective_from",
                      "rule": "overlap",
                      "message": "This member already has a 60.00 % allocation to this project for 2026-10-01 – 2026-10-20. Allocation windows on one project cannot overlap — end or split the existing window first."
                    }
                  ],
                  "instance": "/api/v1/project-allocations",
                  "correlation_id": "018f2c7a-0000-7000-8000-0000000000c3"
                }
              }
            }
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/project-allocations/{id}/split": {
      "post": {
        "operationId": "work.project_allocation.split",
        "summary": "Split an allocation into two adjacent phases",
        "description": "Re-phases one allocation from a date (`WRK-S19` **Split from date**, #1663): the named window is closed the day BEFORE `effective_from`, and a successor window opens on `effective_from` carrying the incumbent's own `effective_to`. Both writes are one transaction, so the pair is only ever observed whole.\n**Adjacency is by construction, not by validation.** Splitting an open-ended window yields `[from, split-1]` + `[split, ∞)`; splitting a closed one yields `[from, split-1]` + `[split, to]`. The successor's start is the day after the incumbent's new end and neither date is a free parameter, so there is **no gap and no overlap** — which is why this is an operation rather than a recipe of two calls the client sequences itself (between those two calls the table would state something false, and before migration `0231` the overlapping half would have double-counted in the heatmap and the overbooking advisory).\n`allocation_pct` and `role_note` default to the incumbent's, so a body carrying only `effective_from` is a pure date split. `employee_id` and `project_id` are not arguments at all — the successor can only ever land on the pair the incumbent already names.\n**Authorized by `work.project_allocation.update`, deliberately not a new token.** The successor is the continuation of the window being edited, not an independent staffing decision, and a separate token would have to be granted to every role that can already end an allocation just to restore capability those roles have today. The split is refused with `403` `urn:groundit:problem:work:project-allocation-not-writable` under exactly the same `0131` write confinement as a PATCH of the same row.\nThe split date must fall **strictly after** the window starts and on or before it ends (`422`): on the first day there is nothing to split — that is a PATCH of the percentage — and past the last day there is no window to continue, which would silently create a detached future row under an operation whose whole contract is adjacency.\n",
        "tags": [
          "work",
          "project_allocation"
        ],
        "x-token": "work.project_allocation.update",
        "x-realizes-features": [
          "WRK-F14",
          "WRK-F13"
        ],
        "x-screens": [
          "WRK-S19",
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_allocations",
          "work.projects"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectAllocationSplit"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The NEW phase, with the closed incumbent in `superseded` so the screen can render both halves from this response rather than re-reading the list to discover what happened to the row the user was looking at. `Location` points at the successor.\n",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectAllocationSplitResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Authenticated and permitted to READ this allocation, but not to write it — the per-command write confinement migration 0131 §3 puts on `work.project_allocations`. Distinguished from a generic 403 by `type: urn:groundit:problem:work:project-allocation-not-writable`, with `code: SCOPE_DENIED` and `status: 403`.\n**403, never 404.** The usual \"absent or RLS-masked, deliberately indistinguishable\" posture (`_shared.yaml` `NotFound`, security 02 §4) does **not** apply here: the caller can see the row — a list just rendered it — so masking the write refusal as a 404 would contradict the response they already hold. Nothing is disclosed by saying so.\nEvery other 403 cause for this operation — token not granted, tenant suspended — keeps `type: about:blank` and its own `code`, exactly as `Forbidden` describes. **`detail` never names another employee, a rate or an amount**: it says the caller's scope does not permit the change and who to ask, not who holds what.\n",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "example": {
                  "type": "urn:groundit:problem:work:project-allocation-not-writable",
                  "title": "Forbidden",
                  "status": 403,
                  "code": "SCOPE_DENIED",
                  "detail": "You can view this allocation, but your access scope does not permit changing it. Ask the project owner or an HR admin to make this change.",
                  "instance": "/api/v1/project-allocations/{id}",
                  "correlation_id": "018f2c7a-0000-7000-8000-0000000000c1"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "description": "Refused: the window this write asks for overlaps another allocation of the **same member on the same project**. `type` is `urn:groundit:problem:work:allocation-window-overlap`, `code` is `VALIDATION_FAILED`, and `errors[0].pointer` is the date field to move (`/effective_from`, or `/effective_to` when the end date is what was changed). `detail` names the conflicting window's dates and percentage so the screen can say which row is in the way.\nPer `(tenant, employee, project)` the live windows **partition time** — migration `0231` enforces it with the `work_project_allocations_no_overlap` exclusion constraint, and the write doors check the same `daterange(effective_from, effective_to, '[]')` predicate in-transaction so the refusal can name the other row. A client receives this `type` either way; it cannot tell, and does not need to tell, which of the two caught it.\n**The taper is NOT this error.** A new phase starting after an existing OPEN-ENDED window is the shape real staffing needs, and `POST /project-allocations` honours it by narrowing the incumbent to the day before, in the same transaction, returning `201` with a `PREDECESSOR_WINDOW_CLOSED` warning. Only a genuine collision reaches here.\n",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "example": {
                  "type": "urn:groundit:problem:work:allocation-window-overlap",
                  "title": "Unprocessable Entity",
                  "status": 422,
                  "code": "VALIDATION_FAILED",
                  "detail": "This member already has a 60.00 % allocation to this project for 2026-10-01 – 2026-10-20. Allocation windows on one project cannot overlap — end or split the existing window first.",
                  "errors": [
                    {
                      "pointer": "/effective_from",
                      "rule": "overlap",
                      "message": "This member already has a 60.00 % allocation to this project for 2026-10-01 – 2026-10-20. Allocation windows on one project cannot overlap — end or split the existing window first."
                    }
                  ],
                  "instance": "/api/v1/project-allocations",
                  "correlation_id": "018f2c7a-0000-7000-8000-0000000000c3"
                }
              }
            }
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/allocations/heatmap": {
      "get": {
        "operationId": "work.workload.read",
        "summary": "Workload heatmap (assignee × week)",
        "description": "The `WRK-S20` grid: rows are assignees, columns are **weeks shaped by the market work week** (`XC-F01` — Mon–Fri India / Sun–Thu KSA, never a hard-coded Monday), cells are committed vs capacity. *Committed* is `Σ tasks.estimated_hours` spread across each task's `start_date`→`due_date` window; *capacity* is `allocation_pct` × work-week hours **minus approved leave**, where leave comes from the event-fed approved-leave **projection** (`XC-F09` pattern) and never a cross-schema read into `leave`. Each cell states its arithmetic so a red cell can be argued with. A member with neither estimates nor allocation is `NO_DATA` — **never drawn as green** — and every row reports `unestimated_open_task_count` so the heatmap says what it cannot see rather than under-reporting silently. When the leave projection returns nothing for the window the response sets `leave_included=false` instead of quietly presenting a gross capacity as a net one; the `formula` string states which of the two arithmetics actually ran, so the legend on screen can never claim a netting that did not happen. **`work.workload.read` is also the token the `WRK-S19` candidate panel's leave view resolves to** (fsd 05 `WRK-S19`/`WRK-S20` Access lines) — it exposes dates and status only, never leave reasons or medical detail. **Reach — `x-rls-scope: team`, narrowed from `tenant` (#934).** A caller holding a tenant-wide grant (HR, Finance) sees every active employee who carries a task or an allocation, exactly as before. A caller without one — `manager` and `project_manager`, the two personas fsd 05 names for `WRK-S20`, both `scope_type: SELF` — sees **themselves and their direct reports** (`people.employees.manager_id`) and no one else; the grid, the leave netting and every cell are computed over that same row set, so the heatmap never counts people it will not show. The declaration said `tenant` while the handler demanded a tenant-wide grant, which denied those two personas outright (`SCOPE_DENIED`) and left the screen hr_admin-only in practice; the handler now requests `ANY` and confines the rows itself. A caller with neither a tenant-wide grant nor a linked employee record has no team to answer with and is **denied**, not shown an empty grid.\n",
        "tags": [
          "work",
          "workload"
        ],
        "x-token": "work.workload.read",
        "x-realizes-features": [
          "WRK-F13"
        ],
        "x-screens": [
          "WRK-S20",
          "WRK-S19"
        ],
        "x-touches-entities": [
          "work.tasks",
          "work.task_assignments",
          "work.project_allocations",
          "people.employees",
          "org.legal_entities"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": true,
            "description": "First week's anchor date; snapped to the market work week's first day.",
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": true,
            "description": "Last week's anchor date. Defaults on the client to `from` + 8 weeks (`WRK-S20`).",
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "department_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "project_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "employee_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "legal_entity_id",
            "in": "query",
            "required": false,
            "description": "The **branch** axis (#1432) — `people.employees.legal_entity_id`, which is what \"branch\" means in this product (owner ruling). Narrows the rows exactly as `department_id` does, on a column the employee query already selects. Additive on a shipped operation, so an empty value (`?legal_entity_id=`) reads as *no filter* rather than a 422; a malformed one is still a 422.\n",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Free-text search over `full_name` and `employee_no` (case-insensitive, substring), applied **server-side** alongside the other filters (#1432). The grid is a scoped read: filtering the rendered rows in the browser instead would leave the empty state describing a set the server never computed, and would leave the per-row cells counting people the grid no longer shows.\n",
            "schema": {
              "type": "string",
              "maxLength": 120
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The assignee × week load grid, with its formula and coverage caveats stated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkloadHeatmap"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/employees/{employeeId}/assignment-context": {
      "get": {
        "operationId": "work.assignment_context.get",
        "summary": "Leave-aware assignment context for one candidate",
        "description": "The read behind the **leave-aware assignee picker** — the same component `WRK-S08`, `WRK-S09`, `WRK-S16` and `WRK-S19` all use, so *\"who can take this?\"* gives the same answer everywhere. Returns the candidate's open-task load (`status ∉ {DONE, CANCELLED}`), their summed live `allocation_pct`, and their **approved-leave windows** — dates and status only, sourced from the event-fed projection (`XC-F09` pattern), **never a cross-schema read into `leave`** and never a leave reason or medical detail (`WRK-F13`). A candidate on leave across the window is **flagged, not hidden**. When the projection returns no window `leave_available=false` is returned so the picker can say *\"Leave data unavailable — showing load and allocation only\"* rather than implying the person is free — and, because `false` also covers *\"this candidate simply has no leave booked\"*, the picker must render it as an absence of information, never as a green light (see `leave_available` below). The `WRK-S19` Access line names this projection read `work.workload.read`; it is minted as its own operation here because an OpenAPI `operationId` is unique per operation, and the security round should treat the two as one grant family. **Second consumer since 2026-08-24 (#880, work leg L8), and it is a PAGE, not a picker.** The web route `/work/people/{employeeId}` renders this operation whole — load, `current_allocation_pct`, the `allocations` array (which the picker never drew), the leave windows and both warnings — as the work module's view of one person. It is deliberately **not** a tab on the people profile, which is the PII surface and needs `people.*` tokens. That route has no `WRK-S**` id (`design-docs/04` `G-74` ②), so it cannot be added to `x-screens`; it is named here instead, because a consumer that exists only in code is how an \"unwired\" operation stays quietly unwired. Nothing about the contract changed for it. **Leave-aware advisories and the `due_date` parameter (2026-08-24, #801).** `warnings[]` now really carries the two codes the picker was always meant to show: `LEAVE_COLLISION` whenever the candidate's approved leave overlaps the requested `from`→`to` window, and `NON_WORKING_DAY` when the new, **optional** `due_date` query parameter names a day that is a holiday for that candidate. `due_date` is purely additive — omit it and this operation answers exactly as it did before, minus any holiday advisory, since there is then no date to test. The holiday source is `org.holiday_calendars`, resolved through the candidate's own `legal_entity_id` + `work_location_id`; the leave source is unchanged and is still `xc.approved_leave_projection` alone. Both entities are named in `x-touches-entities` for the first time here — the projection read has been live since #437 and was simply never declared.\n",
        "tags": [
          "work",
          "workload"
        ],
        "x-token": "work.assignment_context.get",
        "x-realizes-features": [
          "WRK-F13"
        ],
        "x-screens": [
          "WRK-S19",
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.tasks",
          "work.task_assignments",
          "work.project_allocations",
          "people.employees",
          "xc.approved_leave_projection",
          "org.holiday_calendars"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "employeeId",
            "in": "path",
            "required": true,
            "description": "The candidate employee's id (UUIDv7).",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Window start for the leave/collision check; defaults to today.",
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Window end for the leave/collision check.",
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "due_date",
            "in": "query",
            "required": false,
            "description": "The due date the caller is about to write (#801). **Optional and additive** — supplied, it is the date `NON_WORKING_DAY` is judged against and the date `LEAVE_COLLISION.detail.on_due_date` answers for; omitted, neither is affected except that no holiday advisory can be produced. It is deliberately NOT folded into `from`/`to`, which bound the leave window the picker draws and are frequently wider than the one date being committed.\n",
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The candidate's load, allocation and approved-leave windows, plus any leave/holiday advisory in `warnings[]`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssignmentContext"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/portfolio/summary": {
      "get": {
        "operationId": "work.portfolio.read",
        "summary": "Portfolio dashboard aggregate",
        "description": "Every project in the caller's scope at once (`WRK-S18`) — the KPI strip (active projects, at-risk count, utilization, overdue tasks), per-project rows with health chip (marked when `health_override` is set), burn, budget consumption and team size, and the right-hand **escalation feed**. Two honesty rules are part of the contract: **the KPI strip is computed over the same scoped row set as the grid** and states its scope in words, so a KPI never counts rows the grid may not show; and a KPI that cannot be computed is returned as `null` with a reason rather than as a zero that reads like a real number. Budget consumption is `null` — not `0` — when no budget is set. The escalation feed carries **notifications, not approvals** — deliberately **not** routed to the approvals inbox (`WRK-F16`, ADR 0027 §(e)). **The feed is a read of raises that really happened.** Until issue #440 it was not: the handler re-derived \"what is overdue right now\" from `work.tasks` on the request path and returned it in this shape with `escalation_level` fixed at `1`, `threshold` a constant string, `notified` always `[]` and `employee` always `null` (`GAP-57`, now closed). It now reads `work.task_escalations` — the append-only ledger a jobs-tier sweep writes, one row per breach per breach-window, audited under `XC-F06` and notified through `XC-F05`/`XC-F08` from the same transaction — ordered `raised_at DESC`, 50 rows, over the same scoped project set the `projects` array was built from, so the panel and the grid describe the same rows. **Three consequences a consumer must code against.** ① `raised_at` is the instant the escalation was **raised**, not the instant the breach began; the placeholder returned the task's `due_date` under this name, so a client that treated it as a due date must stop. `breach_duration_hours` is likewise the age **stamped at raise time**, not recomputed against `now()`, so a duration does not grow while the panel is open. ② `notified` is built from the ledger's stored recipient array, so its **length is the routing decision**: an id whose employee row the caller may not read through RLS still appears, as an `EmployeeRef` carrying `id` alone, rather than being dropped and silently shortening the count. An empty array means the raise reached nobody — a real answer, never `null`. ③ **`IDLE_ASSIGNEE` never appears on this endpoint, by construction rather than by filter.** An idle raise names an employee and no project (the ledger's shape constraint gives those rows `project_id IS NULL` — a person with no open assignment belongs to no project), and this operation is defined over the caller's scoped project set, so a project-scoped read cannot match one. Those rows surface on employee-keyed reads; this is not the endpoint to look for them on, and their absence here is not evidence that nobody is idle. Row visibility is `work_task_escalations_subject_visible` (RLS), not the project filter — the filter is the feed's scope, not its security boundary. Read-only — no project is edited here. `x-rls-scope: org_unit` is the division-console axis (ADR 0026 §(b)); a PM sees the projects they own or are allocated to and Finance its legal entities, through the same overlay.\n\n**MONEY ON THE GRID (#1431).** Each row now carries `budget_amount`, `spent_to_date` and `money_budget_consumed_pct`, and the summary carries `expenditure_rollup` — but **only for a caller who also holds `work.project_cost.list`**. This operation's own token is granted more broadly than the commercial read, so the money fields are withheld by column omission for everybody else; they are absent, never zero. `money_budget_consumed_pct` is deliberately named apart from `burn.budget_consumed_pct`: the first is money, the second is hours, and the two answer different questions.\n",
        "tags": [
          "work",
          "portfolio"
        ],
        "x-token": "work.portfolio.read",
        "x-realizes-features": [
          "WRK-F14",
          "WRK-F16"
        ],
        "x-screens": [
          "WRK-S18"
        ],
        "x-touches-entities": [
          "work.projects",
          "work.project_allocations",
          "work.project_costs",
          "work.tasks",
          "work.task_escalations",
          "work.task_dependencies",
          "work.task_assignments",
          "work.work_entries",
          "people.employees",
          "org.departments",
          "org.legal_entities",
          "pay.employee_compensation"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "org_unit",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "owner_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "department_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "legal_entity_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/ProjectStatus"
            }
          },
          {
            "name": "health",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/ProjectHealthValue"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The portfolio KPI strip, scoped project rows, and the escalation feed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PortfolioSummary"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/team/metrics": {
      "get": {
        "operationId": "work.team_metrics.get",
        "summary": "Team velocity tiles (lead's My Team strip)",
        "description": "The `WRK-S22` tile strip over **one of two cohorts, and `cohort.kind` says which one was used.**\n\nWithout `team_id`: the caller's **direct and indirect reports**, resolved by the existing `team` RLS overlay — no new scope machinery and no `team_lead` role (ADR 0026 §(a)). This is the original and still the default behaviour; an existing client that sends only `weeks` sees no change.\n\nWith `team_id`: the **live roster of that `work.teams` squad** (`work.team_members`, `left_at IS NULL`), per ADR 0037 §Consequences — *\"`getTeamMetrics` switches to membership when a team is in context so board and tiles agree.\"* The two cohorts are unrelated sets: a squad member need not report to the caller, and a report need not be on any squad. An unreadable or unknown `team_id` is a **404**, never an empty tile strip that would read as \"this squad has no work\".\n\n**Every figure — the cohort included — is computed inside the caller's own RLS scope.** `work.team_members` is RESTRICTIVE-policied (migration `0109` §9b), so a `TEAM`-scoped manager who can see a squad only because they manage its lead reads only the members who are their own reports. `cohort.count` is therefore what *this caller* can see, never a privileged total. Note the corollary: such a caller can see the squad but only part of its roster, so a `count` of 0 with `kind=TEAM_MEMBERSHIP` means \"no members visible to you\", **not** \"no members\" — the 404 covers an unreadable team, not a partially-readable roster.\n\n**What `team_id` widens.** No token, scope or policy changes, but the set of people a caller may aggregate over does grow: a holder of this token with no direct reports used to get four zeroes, and as a mere MEMBER of a squad now reads that squad's throughput. It composes reads the caller already had — board `visibility` is backfilled `TENANT`, and `work.team_member.list` already exposes the roster to the same roles — but it is a widening, and is documented as one.\n\nThroughput is `completed_at`-based per week over the stated window; completion rate and on-time % each return **their own denominator**, because a rate with a hidden denominator is not a metric. On-time % excludes tasks with no due date and says so. With too little history the tiles return `insufficient_history=true` rather than a 0 % or 100 % computed from two data points.\n",
        "tags": [
          "work",
          "team_metrics"
        ],
        "x-token": "work.team_metrics.get",
        "x-realizes-features": [
          "WRK-F18"
        ],
        "x-screens": [
          "WRK-S22"
        ],
        "x-touches-entities": [
          "work.tasks",
          "work.task_assignments",
          "work.timesheets",
          "work.task_dependencies",
          "people.employees",
          "work.teams",
          "work.team_members"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "weeks",
            "in": "query",
            "required": false,
            "description": "Trailing window in weeks (the tile states the window it used). Default 8.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 52,
              "default": 8
            }
          },
          {
            "name": "team_id",
            "in": "query",
            "required": false,
            "description": "A `work.teams` id. Present ⇒ the cohort is that squad's live membership instead of the caller's reportee closure, and `cohort.kind` returns `TEAM_MEMBERSHIP` (ADR 0037). Absent ⇒ the reportee closure, `cohort.kind` = `REPORTS`. Unreadable or unknown ⇒ 404.\n",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "project_id",
            "in": "query",
            "required": false,
            "description": "A `work.projects` id (#1420). Narrows the TASKS the tiles are computed from (`work.tasks.project_id`) without changing the cohort — the same people, one project's work — and is echoed back as `project_id` so a strip never states a scope it did not use. A plain column predicate like `project_id` everywhere else in this module, so an unknown id is an honest zero rather than a 404; only `team_id` 404s, because an unreadable SQUAD would otherwise read as \"this squad has no work\". `cohort.count` stays the whole roster. `idle_report_count` and `pending_review_count` do NOT take this axis: idleness is a capacity fact about a person (narrowing it would call somebody working full time on another project idle), and the review count is the caller's own inbox.\n",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The cohort's velocity tiles, each with its window and denominator stated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TeamMetrics"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/tasks/{id}/dependencies": {
      "get": {
        "operationId": "work.task_dependency.list",
        "summary": "A task's dependencies (both directions)",
        "description": "The *Dependencies* panel on `WRK-S09`, returned as **two labelled groups read from the same stored `BLOCKS` edges** — rows where this task is `task_id` render as **blocked by**, rows where it is `depends_on_task_id` render as **blocks**. There are not two stored types; `dep_type = 'BLOCKS'` is the only launch value, and the enum exists so `FINISH_START`-style relations can be added append-only when `WRK-F19`'s Gantt needs them (db 06 §6). Each edge reports whether its blocker is still live, because the derived **blocked badge** and the assignee-declared `tasks.status = BLOCKED` are different things and can legitimately disagree — surfacing both is the point.\n",
        "tags": [
          "work",
          "task_dependency"
        ],
        "x-token": "work.task_dependency.list",
        "x-realizes-features": [
          "WRK-F10"
        ],
        "x-screens": [
          "WRK-S09",
          "WRK-S24"
        ],
        "x-touches-entities": [
          "work.task_dependencies",
          "work.tasks"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          }
        ],
        "responses": {
          "200": {
            "description": "The task's `blocked_by` and `blocks` edges.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskDependencyView"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "operationId": "work.task_dependency.create",
        "summary": "Add a dependency edge to a task",
        "description": "Creates one `BLOCKS` edge — `task_id` (this task, or the named one) is blocked by `depends_on_task_id`. **Cycle rejection is service-enforced, not a DB constraint** (a CHECK cannot walk an arbitrary-depth graph): the write path performs the reachability walk before insert and returns **`409`** naming the explicit chain (`\"TASK-1622 → TASK-1640 → TASK-1622\"`), so the panel can stay open and say why (`WRK-S09`, db 06 §6). Self-edges are refused (`422`). Both ends are tenant-bound but may sit on **different boards and different projects** — dependencies are deliberately not confined to one board.\n",
        "tags": [
          "work",
          "task_dependency"
        ],
        "x-token": "work.task_dependency.create",
        "x-realizes-features": [
          "WRK-F10"
        ],
        "x-screens": [
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.task_dependencies",
          "work.tasks",
          "work.task_activity"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskDependencyCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Dependency created; a `DEPENDENCY_ADDED` activity row is appended.",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskDependency"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/task-dependencies/{id}": {
      "delete": {
        "operationId": "work.task_dependency.delete",
        "summary": "Remove a dependency edge",
        "description": "Drops one `BLOCKS` edge (`WRK-S09`). Removing the **last** live blocking edge — like terminating the blocker — emits the unblock event that notifies the waiting assignee (`WRK-F10`, `XC-F05`) and writes an `UNBLOCKED` `work.task_activity` row; the reverse index on `depends_on_task_id` is what the unblock fan-out walks (db 06 §6).\n",
        "tags": [
          "work",
          "task_dependency"
        ],
        "x-token": "work.task_dependency.delete",
        "x-realizes-features": [
          "WRK-F10"
        ],
        "x-screens": [
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.task_dependencies",
          "work.task_activity"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "204": {
            "description": "Dependency removed."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/tasks/{id}/comments": {
      "get": {
        "operationId": "work.task_comment.list",
        "summary": "A task's comment thread",
        "description": "The thread on `WRK-S09`, in `created_at` order. A **soft-deleted** comment is returned **in position** as a tombstone (`is_deleted=true`, body omitted) so the surrounding reply context and the trail keep their shape — a hard delete would silently re-write a conversation other people are still reading (db 06 §7). An edited comment carries `edited_at` and the UI shows \"edited\". Each row embeds its **`attachments`** (#1418) — id, file name, mime type and size, no presigned handle: one `LEFT JOIN LATERAL` over the `(tenant_id, comment_id)` partial index and zero storage round-trips, with the expiring URL resolved from `work.task_attachment.list` for the same task. A tombstone keeps its attachments: the comment is soft-deleted, the evidence is not.\n",
        "tags": [
          "work",
          "task_comment"
        ],
        "x-token": "work.task_comment.list",
        "x-realizes-features": [
          "WRK-F07"
        ],
        "x-screens": [
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.task_comments",
          "work.task_attachments",
          "people.employees"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `created_at`, `-created_at`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of the task's comments, tombstones in place.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskCommentPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "operationId": "work.task_comment.create",
        "summary": "Post a comment on a task",
        "description": "Posts to the thread with an **@mention list resolved at post time** and stored on the row, so the notification fan-out and the watcher auto-add never re-parse the body and a later rename does not rewrite history (db 06 §7). Posting writes a `COMMENTED` activity row and **auto-adds the author (`source = EXPLICIT`) and each mentioned employee (`source = MENTIONED`) to `work.task_watchers`** — except where that person previously un-watched: a tombstoned watcher row is **never resurrected** by an auto-adder (db 06 §7). It then emits **`work.task.commented`**, which is what actually notifies: the jobs-tier consumer `work.notify_task_commented` fans that event out to every live watcher of the task, `IN_APP` + watcher-scoped `SSE` (`XC-F05` → notifications centre `XC-S13`). Two consequences follow from the fan-out reading `work.task_watchers` rather than the mention array, and both are contract: the **author is subtracted** and never notified of their own comment, and a mentioned person who had previously un-watched stays un-watched and is therefore **not** notified — the tombstone outranks the mention. `WRK-S15`'s *Recently mentioned* strip is unaffected either way: it reads `work.task_comments.mentions` directly, not the notification rail. Mention autocomplete is bounded by the caller's scope.\nSince #1418 the post may also carry **`attachment_ids`** — attachments already registered on this same task and still unbound, bound to the new comment **inside the comment's own transaction**, so the sentence and its evidence land together or not at all. Any id naming another task, another tenant, or a row that already answers a different comment fails the whole post with `422`. The `201` body echoes the bound rows in `attachments`.\n",
        "tags": [
          "work",
          "task_comment"
        ],
        "x-token": "work.task_comment.create",
        "x-realizes-features": [
          "WRK-F07"
        ],
        "x-screens": [
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.task_comments",
          "work.task_attachments",
          "work.task_watchers",
          "work.task_activity",
          "work.tasks",
          "people.employees"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": "work.task.commented",
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskCommentCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Comment posted; the task's live watchers notified, author excluded.",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskComment"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/task-comments/{id}": {
      "patch": {
        "operationId": "work.task_comment.update",
        "summary": "Edit your own comment",
        "description": "In-place edit by the **author only**, stamping `edited_at`; the prior body is captured by the audit trail (`XC-F06`). A comment is **collaboration, not evidence**, which is why this table is deliberately mutable, unlike the append-only `work.task_activity` (db 06 §7). **Re-resolving mentions on edit does not notify anyone** — the watcher fan-out registers on `work.task.commented` only, and an edit does not emit it — but it DOES **auto-add** each newly-@mentioned employee to `work.task_watchers` (`source = MENTIONED`), so an edit can change who is notified about this task from the next comment onwards. Two axes on this operation under-declare that: `x-emits-event` reads `null` while the handler emits `work.task.comment.updated` (an address no AsyncAPI channel carries), and `x-touches-entities` omits `work.task_watchers`. Both are registered as `GAP-58` rather than patched here, because cataloguing that event means minting an AsyncAPI channel, which this leg is scoped out of; correcting one axis alone would imply the other had been reviewed.\n",
        "tags": [
          "work",
          "task_comment"
        ],
        "x-token": "work.task_comment.update",
        "x-realizes-features": [
          "WRK-F07"
        ],
        "x-screens": [
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.task_comments"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskCommentUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Comment edited; `edited_at` stamped.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskComment"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "delete": {
        "operationId": "work.task_comment.delete",
        "summary": "Delete a comment (soft)",
        "description": "**Soft delete** — the standard `deleted_at` tombstones the row and `work.task_comment.list` keeps rendering a *\"comment deleted\"* placeholder in position, preserving reply context (db 06 §7). The row is never removed, so the thread's shape and the activity trail stay honest.\n",
        "tags": [
          "work",
          "task_comment"
        ],
        "x-token": "work.task_comment.delete",
        "x-realizes-features": [
          "WRK-F07"
        ],
        "x-screens": [
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.task_comments"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "responses": {
          "204": {
            "description": "Comment soft-deleted; the thread renders a tombstone in its place."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/tasks/{id}/checklist-items": {
      "get": {
        "operationId": "work.task_checklist_item.list",
        "summary": "A task's checklist",
        "description": "The task's checklist lines in `rank` order — the order `WRK-S09`'s *Checklist* region renders and the order a drag rewrites (db 06 §7, migration `0184`). **Not subtasks:** a subtask is a full child `Task` (`work.tasks.parent_task_id`) that can be assigned, scheduled and boarded; a checklist item is a lightweight checked/unchecked line. Both regions ship on `WRK-S09` and neither replaces the other. A removed line is **absent**, not tombstoned — unlike `work.task_comment.list`, whose tombstones exist to preserve reply context a checklist does not have; a struck-through ghost would only make the \"3 of 7\" progress counter disagree with the list under it.\n",
        "tags": [
          "work",
          "task_checklist_item"
        ],
        "x-token": "work.task_checklist_item.list",
        "x-realizes-features": [
          "WRK-F07"
        ],
        "x-screens": [
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.task_checklist_items",
          "work.tasks",
          "people.employees"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `rank`, `-rank`, `created_at`, `-created_at`. Default `rank`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of the task's live checklist items, in rank order.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskChecklistItemPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "operationId": "work.task_checklist_item.create",
        "summary": "Add a checklist line to a task",
        "description": "Adds one line. `rank` is **optional**: omitted, the item lands at the END of the list (`max(rank) + 1`), which is what a composer at the bottom of the panel means; sent explicitly it is a midpoint between two neighbours, on `work.tasks.rank`'s fractional convention, so an insert-between never renumbers the tail. A new line is always **open** — there is no `is_done` on this body, because an item that arrives already ticked carries a `done_at` nobody can defend, and the tick is one `PATCH` away. A write against a task whose project is `ARCHIVED` is refused `409`, the same rule every other `work` write states. **No `work.task_activity` row and no event:** the activity vocabulary is a closed enum with no checklist member and growing it is its own change — the audit log still records every write. Same posture as `work.task_watcher.create`.\n",
        "tags": [
          "work",
          "task_checklist_item"
        ],
        "x-token": "work.task_checklist_item.create",
        "x-realizes-features": [
          "WRK-F07"
        ],
        "x-screens": [
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.task_checklist_items",
          "work.tasks",
          "work.projects"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskChecklistItemCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Checklist item added.",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskChecklistItem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/checklist-items/{itemId}": {
      "patch": {
        "operationId": "work.task_checklist_item.update",
        "summary": "Rename, tick or reorder a checklist item",
        "description": "The one write behind all three gestures — rename, tick/un-tick, and the **reorder** (`rank`). **`is_done`, `done_at` and `done_by` move in a single statement:** ticking stamps both, un-ticking clears both, and the row can never carry a completion stamp on a line that renders as open (a `CHECK` constraint enforces it at the database as the second line). `If-Match` is required, so a client toggling optimistically loses a race with `412` and snaps back rather than silently overwriting a colleague's tick. A write against a task whose project is `ARCHIVED` is refused `409`.\n",
        "tags": [
          "work",
          "task_checklist_item"
        ],
        "x-token": "work.task_checklist_item.update",
        "x-realizes-features": [
          "WRK-F07"
        ],
        "x-screens": [
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.task_checklist_items",
          "work.tasks",
          "work.projects",
          "people.employees"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "itemId",
            "in": "path",
            "required": true,
            "description": "The checklist item's id (UUIDv7).",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskChecklistItemUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Checklist item updated; the done stamp matches `is_done`.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskChecklistItem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "delete": {
        "operationId": "work.task_checklist_item.delete",
        "summary": "Remove a checklist item",
        "description": "**Soft delete** — the mutable profile's `deleted_at`, exactly as `work.task_dependency.delete` does. The row keeps its history; `work.task_checklist_item.list` filters it out, so the list and the progress counter agree. A removal against a task whose project is `ARCHIVED` is refused `409`.\n",
        "tags": [
          "work",
          "task_checklist_item"
        ],
        "x-token": "work.task_checklist_item.delete",
        "x-realizes-features": [
          "WRK-F07"
        ],
        "x-screens": [
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.task_checklist_items",
          "work.tasks",
          "work.projects"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "itemId",
            "in": "path",
            "required": true,
            "description": "The checklist item's id (UUIDv7).",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "responses": {
          "204": {
            "description": "Checklist item removed; the row is tombstoned, not erased."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/tasks/{id}/watchers": {
      "get": {
        "operationId": "work.task_watcher.list",
        "summary": "Who is watching a task",
        "description": "The *Watchers* facepile on `WRK-S09`, and the read the *Watch* toggle needs to know which state to render. Returns only **live** watches — a soft-deleted row is an un-watch, and un-watching is durable (the tombstone *is* the suppression record the assign/@mention auto-adders must not resurrect, db 06 §7), so a tombstone is absent here rather than returned with a flag. `source` travels with each row because *watching because you were mentioned* and *watching because you chose to* are different things to the person deciding whether to un-watch. **What a watch means, in one sentence — this is the text the `WRK-S09` *Watch* tooltip renders:** everyone on this list is told when the task is commented on, changes status or lane, or changes assignee, and nobody else is; the person who made the change is never told about their own. That is the whole of it — a watch is not a permission, it grants no read the caller did not already have, and it carries no dependency, due-date or attachment notice.\n",
        "tags": [
          "work",
          "task_watcher"
        ],
        "x-token": "work.task_watcher.list",
        "x-realizes-features": [
          "WRK-F07"
        ],
        "x-screens": [
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.task_watchers",
          "work.tasks",
          "people.employees"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `created_at`, `-created_at`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of the task's live watchers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskWatcherPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "operationId": "work.task_watcher.create",
        "summary": "Watch a task",
        "description": "Adds (or **re-activates**) the caller as a watcher with `source = 'EXPLICIT'` — the *Watch* toggle on `WRK-S09`. **A watch buys exactly three notifications**, and naming them is the point: a comment on the task (`work.task.commented`), a status or lane change (`work.task.status_changed`) and an assignee change (`work.task.assigned`). All three are already-emitted `xc.outbox` events; the jobs-tier consumers **`work.notify_task_commented`** / **`work.notify_task_status_changed`** / **`work.notify_task_assigned`** (one job body, `apps/jobs/src/jobs/work-watcher-notify.ts`) read `work.task_watchers` for the task, **subtract the actor**, and emit one `IN_APP` notification row plus one watcher-scoped `SSE` frame per remaining recipient (`XC-F05` → notifications centre `XC-S13`; api-docs `10 §1.3.2`). There are **no dependency notifications** — this description used to claim them, nothing has ever emitted one, and `work.task_dependencies` has no producer on any channel; a watcher learns about a block from the card, not from the bell. Auto-watch is **server-side** and there is no client call for any of it. **The invariant is unconditional: every path that writes `work.task_assignments` auto-watches the assignee** — worth stating as an invariant rather than a list, because the three paths reach it differently. `work.task.create` and `work.task_assignment.create` share the one `WorkService.insertAssignment` funnel, which is where the rule lives, so those two can no longer disagree with each other; `work.task.bulk_update` calls the same `addWatcher` per row whose `assignee_id` the batch changes; and the desk → work conversion job (`apps/jobs/src/jobs/desk-work-conversion-jobs.ts`), which writes `work.task_assignments` with its own SQL rather than through the funnel, inserts the matching `work.task_watchers` row itself. `source = ASSIGNED` on all three. Separately, `work.task_comment.create` / `work.task_comment.update` add each @mentioned employee (`source = MENTIONED`), with `work.task_comment.create` additionally adding the **author** (`source = EXPLICIT`). The unique key spans all rows including tombstoned ones: **only an explicit watch clears `deleted_at`**, so a single un-watch is never undone by the next mention (db 06 §7) — and because the fan-out reads the watcher table and nothing else, an un-watch is also the one durable way to stop being notified.\n",
        "tags": [
          "work",
          "task_watcher"
        ],
        "x-token": "work.task_watcher.create",
        "x-realizes-features": [
          "WRK-F07",
          "WRK-F15"
        ],
        "x-screens": [
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.task_watchers",
          "work.tasks",
          "people.employees"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskWatcherCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Watching (row created or an earlier un-watch cleared).",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskWatcher"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "delete": {
        "operationId": "work.task_watcher.delete",
        "summary": "Unwatch a task",
        "description": "**Un-watching is durable** — it soft-deletes the row, and that tombstone *is* the suppression record: the auto-adders (assign, mention) must not resurrect it (db 06 §7). Defaults to the caller; `employee_id` removes another watcher where the caller is permitted to.\n",
        "tags": [
          "work",
          "task_watcher"
        ],
        "x-token": "work.task_watcher.delete",
        "x-realizes-features": [
          "WRK-F07"
        ],
        "x-screens": [
          "WRK-S09"
        ],
        "x-touches-entities": [
          "work.task_watchers"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "employee_id",
            "in": "query",
            "required": false,
            "description": "Defaults to the caller (self-unwatch).",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "204": {
            "description": "No longer watching; the tombstone suppresses future auto-adds."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/tasks/{id}/activity": {
      "get": {
        "operationId": "work.task_activity.list",
        "summary": "A task's activity trail",
        "description": "The append-only trail `WRK-S09` renders, newest first — every field change, assignment, dependency event, comment and time log, rendered from the typed `payload` diff whose shape varies by `activity_type` (db 06 §7). **This is not the audit log and neither is derived from the other**: `audit.audit_log` is the tenant-invisible, hash-chained evidence plane; this is the task's story as told to anyone who can see the task. It is immutable — a mistaken entry is superseded by a later row, never corrected in place — and `actor_id` is **null for system/jobs** actions (recurrence materialization, the SLA scan). The same rows project into the `WRK-S14` *Activity* feed.\n",
        "tags": [
          "work",
          "task_activity"
        ],
        "x-token": "work.task_activity.list",
        "x-realizes-features": [
          "WRK-F07"
        ],
        "x-screens": [
          "WRK-S09",
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.task_activity",
          "people.employees"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": true,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "activity_type",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/TaskActivityType"
            }
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `-created_at`, `created_at`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of the task's activity rows, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskActivityPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/tasks/bulk": {
      "post": {
        "operationId": "work.task.bulk_update",
        "summary": "Apply a bulk edit across selected tasks",
        "description": "The multi-select bulk action bar shared by `WRK-S08` and `WRK-S16` — set `status`, `assignee_id`, `priority` or `milestone_id` over a list of task ids. **Authorization is the per-row tokens, not this one:** fsd 05 §W1 is explicit that \"bulk actions = the same per-row tokens (no bulk bypass)\", so every item is evaluated against `work.task.update` / `work.task.transition` / `work.task_assignment.create` exactly as if edited individually, and this operation confers no wider authority — a point the security round should carry forward when it adopts the token. **Partial failures are explicit**: results are returned per item with the reason, a failure does not roll back the rest, and per-item optimistic concurrency travels in the body (`if_match`) because a bulk call cannot carry one `If-Match` header — the same shape as `work.timesheet.approve_batch`. Assignee changes write `work.task_assignments` and auto-watch, exactly as the single-row path does. **Leave-aware advisories (#801), and they are PER ROW.** Each **successful** item result carries its own `warnings[]` — `LEAVE_COLLISION` / `NON_WORKING_DAY` for that row's assignee against that row's dates. A bulk edit spans many people and many due dates, so one collapsed banner would name none of them; failed items are untouched and carry their `error_code`/`message` instead. **An advisory never fires on an un-assign through this door**, because `assignee_id` and `due_date` are applied with `COALESCE` here (see `TaskBulkUpdateRequest`) and therefore cannot be *cleared* through it at all (`design-docs/04` `G-81`). Reuses this operation's existing token; nothing new is minted.\n",
        "tags": [
          "work",
          "task"
        ],
        "x-token": "work.task.bulk_update",
        "x-realizes-features": [
          "WRK-F08",
          "WRK-F01",
          "WRK-F13"
        ],
        "x-screens": [
          "WRK-S08",
          "WRK-S16"
        ],
        "x-touches-entities": [
          "work.tasks",
          "work.task_assignments",
          "work.task_watchers",
          "work.task_activity",
          "work.milestones",
          "people.employees",
          "xc.approved_leave_projection",
          "org.holiday_calendars"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": "work.task.status_changed",
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskBulkUpdateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Per-item outcomes; failures are reported, never silently dropped, and each successful item carries its own `warnings[]`.",
            "headers": {
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskBulkUpdateResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/saved-views": {
      "get": {
        "operationId": "work.saved_view.list",
        "summary": "List saved views for a work surface",
        "description": "The saved-views control shared by `WRK-S08` (board), `WRK-S16` (list) and `WRK-S17` (calendar) — **one filter model, one `work.saved_views` set**, so switching surfaces keeps your filters. A view binds to **one** `surface`; the same filter set saved for two surfaces is two rows, because the group/sort keys are not portable between them. Returns the caller's own views plus every `is_shared` view in the tenant. **A shared view hands over a *filter*, not a result set** — every row it later returns is still cut by the reader's own RLS overlays and tokens, so two people opening the same view can legitimately see different rows (db 06 §9, ADR 0026 §(b)).\n",
        "tags": [
          "work",
          "saved_view"
        ],
        "x-token": "work.saved_view.list",
        "x-realizes-features": [
          "WRK-F08"
        ],
        "x-screens": [
          "WRK-S08",
          "WRK-S16",
          "WRK-S17"
        ],
        "x-touches-entities": [
          "work.saved_views",
          "people.employees"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "surface",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/ViewSurface"
            }
          },
          {
            "name": "is_shared",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            }
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `name`, `-name`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of the caller's own and tenant-shared views.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SavedViewPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "operationId": "work.saved_view.create",
        "summary": "Save a view",
        "description": "Stores a named filter/group/sort set for one surface. **The sort and group-by keys live *inside* `filters`** (fsd 05 §1.7) so a shared view reproduces the grouping its author saw. `filters` is an **opaque preference blob, never joined**: the ids inside are not FKs and are not validated on read, so a view naming a deleted project simply returns nothing instead of erroring — the right failure mode for a preference (db 06 §9). New views are private (`is_shared=false`).\n",
        "tags": [
          "work",
          "saved_view"
        ],
        "x-token": "work.saved_view.create",
        "x-realizes-features": [
          "WRK-F08"
        ],
        "x-screens": [
          "WRK-S08",
          "WRK-S16",
          "WRK-S17"
        ],
        "x-touches-entities": [
          "work.saved_views"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SavedViewCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "View saved (private).",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SavedView"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/saved-views/{id}": {
      "patch": {
        "operationId": "work.saved_view.update",
        "summary": "Rename a saved view or change its filters",
        "description": "Edits `name` and `filters`. **Only the owner (or a tenant admin) may edit or delete a view** — a shared view stays owned by `owner_id` (db 06 §9). Flipping tenant visibility is the separate `work.saved_view.share` gate, not a field on this body.\n",
        "tags": [
          "work",
          "saved_view"
        ],
        "x-token": "work.saved_view.update",
        "x-realizes-features": [
          "WRK-F08"
        ],
        "x-screens": [
          "WRK-S08",
          "WRK-S16",
          "WRK-S17"
        ],
        "x-touches-entities": [
          "work.saved_views"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SavedViewUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated view.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SavedView"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "delete": {
        "operationId": "work.saved_view.delete",
        "summary": "Delete a saved view",
        "description": "Owner-only (or tenant admin) removal of a saved view; a shared view disappears for everyone (db 06 §9).",
        "tags": [
          "work",
          "saved_view"
        ],
        "x-token": "work.saved_view.delete",
        "x-realizes-features": [
          "WRK-F08"
        ],
        "x-screens": [
          "WRK-S08",
          "WRK-S16",
          "WRK-S17"
        ],
        "x-touches-entities": [
          "work.saved_views"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "responses": {
          "204": {
            "description": "View deleted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/saved-views/{id}/share": {
      "post": {
        "operationId": "work.saved_view.share",
        "summary": "Share a saved view tenant-wide (or make it private again)",
        "description": "Flips `work.saved_views.is_shared`. **A deliberately separate gate** — fsd 05 §1.7 names `work.saved_view.share` as its own token because *publishing a view beyond yourself* is a different decision from saving one. Sharing is **private or tenant-wide, nothing in between: there is no `TEAM` tier** (db 06 §9). `is_shared = true` is **tenant-visible, not access-granting** — a shared view **never widens the rows a viewer may see**; RLS and the caller's own scope still bound every result, and the UI must not present a shared view as a shared result set.\n",
        "tags": [
          "work",
          "saved_view"
        ],
        "x-token": "work.saved_view.share",
        "x-realizes-features": [
          "WRK-F08"
        ],
        "x-screens": [
          "WRK-S08",
          "WRK-S16"
        ],
        "x-touches-entities": [
          "work.saved_views"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SavedViewShareInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Visibility changed.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SavedView"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/work-templates": {
      "get": {
        "operationId": "work.template.list",
        "summary": "List work templates",
        "description": "The *Templates* tab grid on `WRK-S21`. **Only `PUBLISHED` templates are instantiable**, and a `DRAFT` says so rather than failing at apply (db 06 §8). Retiring a template is the standard soft-delete — there is no archive status.\n",
        "tags": [
          "work",
          "template"
        ],
        "x-token": "work.template.list",
        "x-realizes-features": [
          "WRK-F11"
        ],
        "x-screens": [
          "WRK-S21"
        ],
        "x-touches-entities": [
          "work.work_templates"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "template_kind",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/TemplateKind"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/TemplateStatus"
            }
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `name`, `-name`, `status`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of work templates.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkTemplatePage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "operationId": "work.template.create",
        "summary": "Create a work template (draft)",
        "description": "Creates a `DRAFT` template (`WRK-S21` *Details → Structure → Review & publish*). `template_kind` `PROJECT` spins up a whole engagement (milestones, boards, task tree); `TASK` spins up a block of tasks with sub-tasks. Structure lives in `payload` with **relative `offset_days`, never absolute dates**, so a template ages well; `milestone_ref`/`parent_ref` are **template-local string keys** resolved at instantiation, not uuids — nothing inside `payload` is an FK and none of it is queried (db 06 §8).\n",
        "tags": [
          "work",
          "template"
        ],
        "x-token": "work.template.create",
        "x-realizes-features": [
          "WRK-F11"
        ],
        "x-screens": [
          "WRK-S21"
        ],
        "x-touches-entities": [
          "work.work_templates"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WorkTemplateCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Template created as `DRAFT`.",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkTemplate"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/work-templates/{id}": {
      "patch": {
        "operationId": "work.template.update",
        "summary": "Edit a work template",
        "description": "Edits `name`, `description` and `payload`. **Editing a template never mutates already-created projects or tasks** — instantiation is a copy, not a link, and there is no back-reference by design (db 06 §8). Republishing an edited template **overwrites** the existing `payload`, so *\"which template version created this project?\"* is deliberately **not answerable** until a tenant asks for it; that is recorded rather than implied away.\n",
        "tags": [
          "work",
          "template"
        ],
        "x-token": "work.template.update",
        "x-realizes-features": [
          "WRK-F11"
        ],
        "x-screens": [
          "WRK-S21"
        ],
        "x-touches-entities": [
          "work.work_templates"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WorkTemplateUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated template.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkTemplate"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/work-templates/{id}/publish": {
      "post": {
        "operationId": "work.template.publish",
        "summary": "Publish a template (make it instantiable)",
        "description": "Sets `status = 'PUBLISHED'`, after which the template is offered in the instantiate picker. A **deliberately separate gate** from `work.template.update` (fsd 05 `WRK-S21`) — publishing changes what everyone in the tenant can spin up; without it the editor saves drafts and the Publish CTA is disabled *with its reason*. A template with an empty structure cannot publish (`422`). There is **no `published_at` column and no version tracking** — the standard `updated_at` carries the timing (db 06 §8). Emits `work.template.published`.\n",
        "tags": [
          "work",
          "template"
        ],
        "x-token": "work.template.publish",
        "x-realizes-features": [
          "WRK-F11"
        ],
        "x-screens": [
          "WRK-S21"
        ],
        "x-touches-entities": [
          "work.work_templates"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": "work.template.published",
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApprovalDecisionInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Template published and now instantiable.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkTemplate"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/work-templates/{id}/instantiate": {
      "post": {
        "operationId": "work.template.instantiate",
        "summary": "Apply a published template (create the project/task set)",
        "description": "Instantiates a `PUBLISHED` template — **one transaction**: a `PROJECT` template creates the project, its milestones, board(s) and task tree; a `TASK` template creates the task block against an existing target project. `offset_days` resolve against `start_date` at apply time. **`dry_run=true` returns the same `preview` the `WRK-S21` apply modal shows without writing anything**, so a PM sees exactly what will be created before committing. Applying a `DRAFT` is refused (`409`), as is targeting an `ARCHIVED` project. On failure **nothing is created**.\n",
        "tags": [
          "work",
          "template"
        ],
        "x-token": "work.template.instantiate",
        "x-realizes-features": [
          "WRK-F11"
        ],
        "x-screens": [
          "WRK-S21",
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.work_templates",
          "work.projects",
          "work.milestones",
          "work.task_boards",
          "work.tasks"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WorkTemplateInstantiateInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Structure created (or, for `dry_run=true`, the preview of what would be created — nothing written).",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkTemplateInstantiateResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/task-recurrences": {
      "get": {
        "operationId": "work.task_recurrence.list",
        "summary": "List recurrence rules and their materialization history",
        "description": "The *Recurrence* tab on `WRK-S21`. **There is no `name` column** — the source's own name is the rule's display label, and only one rule exists per source at launch (db 06 §8). Each row carries `last_materialized_period` — the period key (`2026-W31`, `2026-08`, …) that **is the idempotency guard** — alongside the advisory `next_run_at`, which is scheduling state and **not** the guard. The materialization history lists **failures with their reason**: a skipped or failed run is listed, never omitted.\n",
        "tags": [
          "work",
          "task_recurrence"
        ],
        "x-token": "work.task_recurrence.list",
        "x-realizes-features": [
          "WRK-F11"
        ],
        "x-screens": [
          "WRK-S21"
        ],
        "x-touches-entities": [
          "work.task_recurrences",
          "work.work_templates",
          "work.tasks"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "source_type",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/RecurrenceSourceType"
            }
          },
          {
            "name": "is_active",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            }
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `next_run_at`, `-next_run_at`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of recurrence rules with their materialization state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskRecurrencePage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "operationId": "work.task_recurrence.create",
        "summary": "Create a recurrence rule",
        "description": "Attaches an RRULE-shaped rule to a **published template or a seed task** — `(source_type, source_id)` is **polymorphic with no FK** (db-docs/00 §13) and resolvability is validated at write. `recurrence` is an **RFC-5545 RRULE subset** (`freq`/`interval`/`byday`/`bymonthday`/`timezone`/ `until`/`count`; no `BYSETPOS`, no `EXDATE` at launch) and **unsupported keys are rejected at write, not silently dropped** — quietly discarding a rule the operator typed is the failure mode that erodes trust in a scheduler. A rule's start is when it is created; there are no separate `start_date`/`end_date` columns, and the end folds into `recurrence.until`. Weekday options follow the market work week (`XC-F01`). Materialization runs in the jobs tier (`XC-F08`), **idempotent per rule per period**; created tasks carry `work.tasks.origin_ref` naming the rule.\n",
        "tags": [
          "work",
          "task_recurrence"
        ],
        "x-token": "work.task_recurrence.create",
        "x-realizes-features": [
          "WRK-F11"
        ],
        "x-screens": [
          "WRK-S21"
        ],
        "x-touches-entities": [
          "work.task_recurrences",
          "work.work_templates",
          "work.tasks"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskRecurrenceCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Recurrence rule created.",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskRecurrence"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/task-recurrences/{id}": {
      "patch": {
        "operationId": "work.task_recurrence.update",
        "summary": "Update a recurrence rule (incl. the active toggle)",
        "description": "Edits `recurrence` or flips `is_active`. **Pausing stops materialization without deleting the history** (db 06 §8) — the rule and its `last_materialized_period` stay, so re-activating does not replay. A missed window (worker down over a weekend) materializes the **current** period on recovery and does **not** backfill skipped ones; backfilling would flood a board with work nobody is going to do.\n",
        "tags": [
          "work",
          "task_recurrence"
        ],
        "x-token": "work.task_recurrence.update",
        "x-realizes-features": [
          "WRK-F11"
        ],
        "x-screens": [
          "WRK-S21"
        ],
        "x-touches-entities": [
          "work.task_recurrences"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskRecurrenceUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated recurrence rule.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskRecurrence"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/my-work": {
      "get": {
        "operationId": "work.my_work.get",
        "summary": "My Work — the personal execution queue",
        "description": "The page an IC opens every morning (`WRK-S15`): a today strip (hours logged today, this period's timesheet status) plus the five sections — **Overdue**, **Due soon** (next 7 days), **Assigned to me**, **Blocked / flagged** (with **who or what blocks it**, not just a badge), and **Recently mentioned** (last 14 days). Sections come back **absent, not empty**, so the page never renders five \"no items\" panels. Rows feed from the **same `XC-F09` projections as the Work tab — there is no second aggregation path** (`WRK-F15`). Strictly **self-scoped**: there is deliberately **no employee selector**, and a request for someone else's queue is refused rather than silently re-scoped to the caller — a manager's view of a team is `work.team_metrics.get` (`WRK-S22`) or the board. The screen's own Access line names `work.task.list` and `work.work_entry.create` under self scope for the rows and the inline log-time; this aggregate read is minted as its own token because it is one composed read, not five list calls. The today-strip timesheet carries its newest-first, RLS-filtered `decision_log`, so a currently rejected sheet renders the latest rejection comment inline.\n",
        "tags": [
          "work",
          "my_work"
        ],
        "x-token": "work.my_work.get",
        "x-realizes-features": [
          "WRK-F15",
          "WRK-F12"
        ],
        "x-screens": [
          "WRK-S15"
        ],
        "x-touches-entities": [
          "work.tasks",
          "work.task_assignments",
          "work.task_dependencies",
          "work.task_comments",
          "work.timesheets",
          "work.timesheet_approvals",
          "work.work_entries",
          "work.projects"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "due_soon_days",
            "in": "query",
            "required": false,
            "description": "Horizon for the *Due soon* section. Default 7 (`WRK-S15`).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 60,
              "default": 7
            }
          },
          {
            "name": "mentioned_days",
            "in": "query",
            "required": false,
            "description": "Look-back for the *Recently mentioned* section. Default 14 (`WRK-S15`).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 90,
              "default": 14
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's own queue; empty sections are omitted rather than returned empty.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MyWork"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/projects/{id}/shares": {
      "get": {
        "operationId": "work.project_share.list",
        "summary": "List a project's client share links · Launch: Later — post-S12",
        "description": "**Launch: Later — post-S12.** The curator panel's active-share list on `WRK-S14` (*Share with client*) — each share's field allowlist, expiry, revocation state, and its **access log**: every access is logged and visible to the curator (`WRK-F20`). **This operation must not be built from this spec alone.** `WRK-S25` is the module's only external, unauthenticated access surface, and the **security round owes it a signed-link threat model before build** — expiring signed URLs, per-share revocation, no session, and **no enumeration**. The entities (`work.project_shares`, `work.project_share_access`) are **proposed names, not modelled in db 06** (db 06 §10 states the omission deliberately: the columns *are* the threat model, and guessing at a security boundary inside an authoritative doc is the worst place to guess). Hence `x-touches-entities` names only the real entities the curated projection reads.\n",
        "tags": [
          "work",
          "project_share"
        ],
        "x-token": "work.project_share.list",
        "x-realizes-features": [
          "WRK-F20"
        ],
        "x-screens": [
          "WRK-S25",
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.projects",
          "work.milestones"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          }
        ],
        "responses": {
          "200": {
            "description": "Page of the project's shares with expiry, revocation state and access log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectSharePage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "operationId": "work.project_share.create",
        "summary": "Mint a client share link for a project · Launch: Later — post-S12",
        "description": "**Launch: Later — post-S12, and gated on the security round, not only on build capacity.** Creates a curated, expiring signed link for the tenant's client (`WRK-S25`). Two constraints are contract, not UI: **nothing is on by default** — `field_allowlist` requires at least one explicit field group (*name · status · % complete · milestone names + dates · curated updates*) and may **never** name assignee names, comments, task detail, hours or rates; and **`expires_at` is required** — a share with no expiry is not offered. Curated updates are written *for* the client, not a republished internal feed. The external page has **no principal and no token** — its authorization *is* the signed link, which is exactly why the **threat model is a precondition, not a follow-up**, and why the entity is not modelled yet (db 06 §10). Every access is logged (`WRK-F20`).\n",
        "tags": [
          "work",
          "project_share"
        ],
        "x-token": "work.project_share.create",
        "x-realizes-features": [
          "WRK-F20"
        ],
        "x-screens": [
          "WRK-S25",
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.projects",
          "work.milestones"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectShareCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Share created; the signed link is returned once, at mint time.",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectShare"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/project-shares/{id}/revoke": {
      "post": {
        "operationId": "work.project_share.revoke",
        "summary": "Revoke a client share link · Launch: Later — post-S12",
        "description": "**Launch: Later — post-S12.** Revocation takes effect **immediately**, and a revoked link then returns **the same generic response as an unknown or expired link** — there is no \"this was revoked\" oracle and no enumeration signal (`WRK-F20`, `WRK-S25`). The revoked share stays listed for the curator with its access history.\n",
        "tags": [
          "work",
          "project_share"
        ],
        "x-token": "work.project_share.revoke",
        "x-realizes-features": [
          "WRK-F20"
        ],
        "x-screens": [
          "WRK-S25",
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.projects"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApprovalDecisionInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Share revoked; the link is dead immediately.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectShare"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/imports": {
      "post": {
        "operationId": "work.import.create",
        "summary": "Configure a work import and queue its dry run · Launch: Later — post-S12",
        "description": "**Launch: Later — post-S12.** Creates an import batch from a `TRELLO` board JSON, a `JIRA` issue export or a `CSV` (`WRK-S26` stages 1–4) and **queues the dry run**, hence `202`: parsing and mapping run in the jobs tier (`XC-F08`), never on the request path. **Nothing is written to `work.tasks` at this stage.** The dry-run result carries counts to create/update/skip, **the full error and warning list with row numbers**, and a sample of rendered tasks. Two mapping rules are contract: **unmapped source columns are listed explicitly as \"will be ignored\", never dropped silently**, and an unmatched source user is shown by name and **left unassigned, never guessed**. Remote attachment fetching is **off by default** — explicit opt-in only (`WRK-F21`). `work.import_batches` is a **proposed entity, not modelled in db 06** (§10 defers the staging model to the import round), so `x-touches-entities` names the real write targets the batch eventually lands in; the `work.import_batch.*` events are proposed alongside it.\n",
        "tags": [
          "work",
          "import"
        ],
        "x-token": "work.import.create",
        "x-realizes-features": [
          "WRK-F21"
        ],
        "x-screens": [
          "WRK-S26"
        ],
        "x-touches-entities": [
          "work.tasks",
          "work.task_boards",
          "work.projects",
          "work.task_attachments"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "async",
        "x-emits-event": "work.import_batch.dry_run_requested",
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ImportBatchCreate"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Batch created and its dry run queued; poll `work.import.get` for progress and results.",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportBatch"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/imports/{id}": {
      "get": {
        "operationId": "work.import.get",
        "summary": "Get an import batch — mapping, dry-run result, outcome · Launch: Later — post-S12",
        "description": "**Launch: Later — post-S12.** The `WRK-S26` progress and result read: batch status, the mapping, the dry-run preview (with **per-row errors and warnings by row number**), and after a run the outcome — created / skipped / failed **with per-row reasons**. A partial import reports **exactly** what landed, and the batch can be re-run idempotently. A parse failure returns the parser's own reason and the offending line, never a bare \"invalid file\".\n",
        "tags": [
          "work",
          "import"
        ],
        "x-token": "work.import.get",
        "x-realizes-features": [
          "WRK-F21"
        ],
        "x-screens": [
          "WRK-S26"
        ],
        "x-touches-entities": [
          "work.tasks",
          "work.projects"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          }
        ],
        "responses": {
          "200": {
            "description": "The import batch with its mapping, dry-run result and outcome.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportBatch"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/imports/{id}/execute": {
      "post": {
        "operationId": "work.import.execute",
        "summary": "Run an import batch · Launch: Later — post-S12",
        "description": "**Launch: Later — post-S12.** Runs the batch in the jobs tier (`XC-F08`), **idempotent per import batch**, so a re-run after a partial failure cannot double-create; created tasks carry `work.tasks.origin_ref` naming the batch and the source record, so `WRK-S09` can say where they came from. Refused (`409`) unless a dry run has completed **with no blocking errors**. This is a **deliberately separate token from `work.import.create`** (fsd 05 `WRK-S26` Access line), since dry-run is safe and the run writes at volume. Rows land inside the caller's scope; an import never creates data outside it.\n",
        "tags": [
          "work",
          "import"
        ],
        "x-token": "work.import.execute",
        "x-realizes-features": [
          "WRK-F21"
        ],
        "x-screens": [
          "WRK-S26"
        ],
        "x-touches-entities": [
          "work.tasks",
          "work.task_boards",
          "work.projects",
          "work.task_attachments"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "async",
        "x-emits-event": "work.import_batch.execution_requested",
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "responses": {
          "202": {
            "description": "Import run queued; poll `work.import.get` for the per-row outcome.",
            "headers": {
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportBatch"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/teams": {
      "get": {
        "operationId": "work.team.list",
        "summary": "List teams (delivery squads)",
        "description": "The squads visible to the caller (db 06 §12 `teams`, ADR 0037). Row reach is the `work_teams_member_visible` policy: the squads the caller leads or belongs to, plus — for a `TEAM`-scoped manager — those led by a direct report, plus the whole register for a tenant-granted caller. `status=ACTIVE` is the filter the Teams nav counts against; teams are never auto-created, so an empty page is the expected first-run state and must render as such, not as an error.\n",
        "tags": [
          "work",
          "team"
        ],
        "x-token": "work.team.list",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S08",
          "WRK-S22"
        ],
        "x-touches-entities": [
          "work.teams"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/TeamStatus"
            }
          },
          {
            "name": "lead_employee_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "org_unit_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "member_employee_id",
            "in": "query",
            "required": false,
            "description": "Only the squads this employee is a live member of; rows then carry `membership_id`, `member_role`, `joined_at`, `membership_version`.",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "description": "`member_ids` adds each squad's live members' employee ids (`member_ids`).",
            "schema": {
              "type": "string",
              "enum": [
                "member_ids"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `name`, `-name`, `team_key`, `-team_key`, `created_at`, `-created_at`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of teams.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TeamPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "operationId": "work.team.create",
        "summary": "Create a team",
        "description": "Files a new delivery squad. `team_key` is the short handle a team is cited by (unique per tenant, case-insensitively) — there is no business number. `lead_employee_id` defaults to the caller, and migration 0109's WITH CHECK admits only the caller or one of their direct reports as lead, so a squad can never be attributed to a stranger. `org_unit_id` is a **soft anchor** with no FK: it records where the squad came from and is what `work.team_member.sync_from_org_unit` seeds off, and it never couples the two lifecycles (db 06 §12).\n",
        "tags": [
          "work",
          "team"
        ],
        "x-token": "work.team.create",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S22"
        ],
        "x-touches-entities": [
          "work.teams",
          "work.team_members",
          "people.employees",
          "org.org_units"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TeamCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Team created. The lead is enrolled as a `LEAD` member in the same transaction.",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Team"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/my-teams": {
      "get": {
        "operationId": "work.team.list_mine",
        "summary": "My teams (the caller's own live memberships)",
        "description": "The squads the caller is a live member of, each carrying the caller's own `member_role` and `joined_at`. Runs at `self` scope — it answers only \"which squads am I on\", which is exactly what `app.current_team_ids()` resolves for RLS, and needs none of the register's reach. This is the read the Teams nav's progressive disclosure and the board picker's \"my teams\" grouping are built on (ADR 0037 §Progressive disclosure).\n",
        "tags": [
          "work",
          "team"
        ],
        "x-token": "work.team.list_mine",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S08",
          "WRK-S22"
        ],
        "x-touches-entities": [
          "work.teams",
          "work.team_members"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Filter on the TEAM's status; defaults to `ACTIVE` because an archived squad is history, not navigation.",
            "schema": {
              "$ref": "#/components/schemas/TeamStatus"
            }
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `name`, `-name`, `joined_at`, `-joined_at`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of the caller's team memberships.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MyTeamPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/teams/{id}": {
      "get": {
        "operationId": "work.team.get",
        "summary": "Get one team",
        "description": "One squad's record — key, name, lead, org-unit anchor, status and settings (db 06 §12).",
        "tags": [
          "work",
          "team"
        ],
        "x-token": "work.team.get",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S22"
        ],
        "x-touches-entities": [
          "work.teams"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          }
        ],
        "responses": {
          "200": {
            "description": "The team.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Team"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "operationId": "work.team.update",
        "summary": "Update a team",
        "description": "Edits name, description, lead, org-unit anchor and settings. `status` is deliberately **not** writable here — retiring a squad is `work.team.archive`, which also closes the roster, and a bare status flip would leave `app.current_team_ids()` still returning the archived squad (the resolver reads `team_members` only, by design — db 06 §12). Handing the lead over rewrites `lead_employee_id`; migration 0109's `FOR UPDATE ... USING` pre-image arm is what stops a stranger taking a squad over by naming themselves.\n",
        "tags": [
          "work",
          "team"
        ],
        "x-token": "work.team.update",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S22"
        ],
        "x-touches-entities": [
          "work.teams",
          "work.team_members",
          "people.employees",
          "org.org_units"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TeamUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated team.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Team"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/teams/{id}/archive": {
      "post": {
        "operationId": "work.team.archive",
        "summary": "Archive a team (retire the squad, keep its history)",
        "description": "Sets `status='ARCHIVED'` **and closes every live membership row** (`left_at = now()`) in one transaction. Both halves are required, and this is the reason archiving is an operation rather than a `status` field on the PATCH: `app.current_team_ids()` reads `work.team_members` alone and never joins `work.teams`, precisely so it cannot recurse through that table's own policy (db 06 §12). A squad archived without closing its roster would therefore keep granting its members the board reads it was archived to end. **There is no delete** — a squad is history the moment it has produced any work, and `ON DELETE RESTRICT` on both children says so in the schema.\n",
        "tags": [
          "work",
          "team"
        ],
        "x-token": "work.team.archive",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S22"
        ],
        "x-touches-entities": [
          "work.teams",
          "work.team_members"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "The archived team.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Team"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/teams/{id}/summary": {
      "get": {
        "operationId": "work.team.summary",
        "summary": "Team summary (roster, boards, projects and task counts)",
        "description": "One squad's at-a-glance counts for the team home header: live members by `member_role`, the boards bound to the team, the projects it is staffed on, and its tasks by open/done. **Every denominator here is what the CALLER may read**, not a privileged total — the counts are computed inside the caller's own RLS scope, so a member and an HR Admin can legitimately see different numbers on a `PRIVATE` board. Stating that is deliberate: a rate with a hidden denominator is not a metric (fsd 05 §W4).\n",
        "tags": [
          "work",
          "team"
        ],
        "x-token": "work.team.summary",
        "x-realizes-features": [
          "WRK-F01",
          "WRK-F18"
        ],
        "x-screens": [
          "WRK-S22"
        ],
        "x-touches-entities": [
          "work.teams",
          "work.team_members",
          "work.task_boards",
          "work.tasks",
          "work.project_teams"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          }
        ],
        "responses": {
          "200": {
            "description": "The team summary.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TeamSummary"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/teams/{id}/members": {
      "get": {
        "operationId": "work.team_member.list",
        "summary": "List a team's members",
        "description": "The squad's roster (db 06 §12 `team_members`). Live rows by default; `include_past=true` returns closed memberships too, which is what makes an employee who left and rejoined legible rather than a unique-key collision. `source` distinguishes a hand-added member from one produced by the one-shot org-unit seed.\n",
        "tags": [
          "work",
          "team_member"
        ],
        "x-token": "work.team_member.list",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S22"
        ],
        "x-touches-entities": [
          "work.team_members",
          "people.employees"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "include_past",
            "in": "query",
            "required": false,
            "description": "Include memberships already closed (`left_at` set). Defaults to false.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "member_role",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/TeamMemberRole"
            }
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `joined_at`, `-joined_at`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of team members.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TeamMemberPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "operationId": "work.team_member.create",
        "summary": "Add a member to a team",
        "description": "Enrols one employee on the squad with `source='EXPLICIT'`. Confined by `work_team_members_lead_writes_insert` to the team's lead (or a `TEAM`-scoped manager, for their own reports) — **membership is deliberately not self-service**, because an employee who could add themselves to a squad would thereby grant themselves that squad's board reads (db 06 §12). Re-adding an employee who already has a live row is a `409`, not a duplicate; re-adding one who previously left opens a **new** row and keeps the old one.\n",
        "tags": [
          "work",
          "team_member"
        ],
        "x-token": "work.team_member.create",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S22"
        ],
        "x-touches-entities": [
          "work.team_members",
          "work.teams",
          "people.employees"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TeamMemberCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Member added.",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TeamMember"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/teams/{id}/members/sync-from-org-unit": {
      "post": {
        "operationId": "work.team_member.sync_from_org_unit",
        "summary": "Seed the roster from an org unit (one-shot, re-runnable)",
        "description": "Enrols the `ACTIVE` employees of an org unit that are not already on the squad, stamping each new row `source='ORG_UNIT_SYNC'`. `org_unit_id` defaults to the team's own anchor. **This is a seed, not a sync** (ADR 0037 §Entity model): it is re-runnable and idempotent — a second run over an unchanged unit adds nothing — it **never removes** anybody, and no background job keeps the two in lockstep. Members who are on the squad but no longer in the unit are reported as `drift` for a human to act on, which is the whole reason `source` is stored: a seeded row stays distinguishable from a deliberate hand-added one. **Reach note:** the source roster is read through `people.employees` RLS, so a run by a tenant-granted caller seeds the whole unit while a `TEAM`-scoped lead's run seeds the part of it their own scope admits; `source_visible` reports what the run could actually see.\n",
        "tags": [
          "work",
          "team_member"
        ],
        "x-token": "work.team_member.sync_from_org_unit",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S22"
        ],
        "x-touches-entities": [
          "work.team_members",
          "work.teams",
          "people.employees",
          "org.org_units"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TeamMemberSyncRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Seed result — what was added, what was already there, and what has drifted.",
            "headers": {
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TeamMemberSyncResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/team-members/{id}": {
      "patch": {
        "operationId": "work.team_member.update",
        "summary": "Change a member's role on the team",
        "description": "Moves one live membership between `LEAD`, `MEMBER` and `GUEST`. `member_role` is the only writable field — moving an employee between squads is a remove plus an add, so the roster keeps both facts. Note that `member_role='LEAD'` is a roster role and does **not** by itself make the holder `teams.lead_employee_id`; the squad's write authority is that column, changed through `work.team.update`.\n",
        "tags": [
          "work",
          "team_member"
        ],
        "x-token": "work.team_member.update",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S22"
        ],
        "x-touches-entities": [
          "work.team_members"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TeamMemberUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated membership.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TeamMember"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "delete": {
        "operationId": "work.team_member.delete",
        "summary": "Remove a member from the team",
        "description": "**Closes** the membership (`left_at = now()`) rather than deleting the row — the partial unique key is on the live window precisely so a member who leaves and rejoins keeps their history instead of colliding with it (db 06 §12). The employee stops being returned by `app.current_team_ids()` from the next statement onward, so the squad's board reads end with the membership.\n",
        "tags": [
          "work",
          "team_member"
        ],
        "x-token": "work.team_member.delete",
        "x-realizes-features": [
          "WRK-F01"
        ],
        "x-screens": [
          "WRK-S22"
        ],
        "x-touches-entities": [
          "work.team_members"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "204": {
            "description": "Membership closed."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/projects/{id}/teams": {
      "get": {
        "operationId": "work.project_team.list",
        "summary": "List the teams staffed on a project",
        "description": "The project ↔ team edges (db 06 §12 `project_teams`). This is where cross-team work lives: a board never spans teams, so when work genuinely crosses squads the **project** carries them and the project 360 unions their boards (ADR 0037 §Board binding). These edges also feed the board read policy's participation arm — a board bound to this project is readable by the members of every team listed here.\n",
        "tags": [
          "work",
          "project_team"
        ],
        "x-token": "work.project_team.list",
        "x-realizes-features": [
          "WRK-F06",
          "WRK-F14"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_teams",
          "work.teams",
          "work.projects"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `created_at`, `-created_at`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of project ↔ team links.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectTeamPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "operationId": "work.project_team.create",
        "summary": "Staff a team on a project",
        "description": "Links one squad to the project. Confined by `work_project_teams_writes_insert` to the team's lead or the project's owner — staffing a project with a squad is either end's decision and nobody else's (db 06 §12). Re-linking a team that is already live on the project is a `409`; a previously unlinked team can be re-linked, since the unique key is on the live window.\n",
        "tags": [
          "work",
          "project_team"
        ],
        "x-token": "work.project_team.create",
        "x-realizes-features": [
          "WRK-F06",
          "WRK-F14"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_teams",
          "work.teams",
          "work.projects"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectTeamCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Team staffed on the project.",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectTeam"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/project-teams/{id}": {
      "delete": {
        "operationId": "work.project_team.delete",
        "summary": "Unstaff a team from a project",
        "description": "Drops one project ↔ team edge (soft, `deleted_at`). The squad's own boards and history are untouched; what ends is the **participation** the edge granted — members of the unlinked team stop reading this project's boards through the participation arm from the next statement on.\n",
        "tags": [
          "work",
          "project_team"
        ],
        "x-token": "work.project_team.delete",
        "x-realizes-features": [
          "WRK-F06",
          "WRK-F14"
        ],
        "x-screens": [
          "WRK-S14"
        ],
        "x-touches-entities": [
          "work.project_teams"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "204": {
            "description": "Team unstaffed."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/task-boards/{id}/iterations": {
      "get": {
        "operationId": "work.iteration.list",
        "summary": "List a board's iterations (sprints)",
        "description": "The board's planning windows, oldest-configured first (`sort_order`, then `id`). Rows are bounded by `work_iterations_board_visible` (migration 0109), which inherits the RLS-filtered board rather than restating board visibility — so iteration reach can never drift from board reach, and a `PRIVATE` board's sprints stay private without a second predicate to keep in sync. An unreadable board is a `404` on this path, not an empty page: the handler probes the board first, because \"no sprints\" and \"not your board\" are different answers.\nPaginated like every other `work` list — `page[size]` 1–200, bidirectional keyset cursors. `status` filters `PLANNED` / `ACTIVE` / `COMPLETED`; the sprint lens asks for `ACTIVE` to render the board and for `PLANNED` to render the planning drawer.\n",
        "tags": [
          "work",
          "iteration"
        ],
        "x-token": "work.iteration.list",
        "x-realizes-features": [
          "WRK-F01",
          "WRK-F08"
        ],
        "x-screens": [
          "WRK-S08"
        ],
        "x-touches-entities": [
          "work.iterations",
          "work.task_boards"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/IterationStatus"
            }
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `sort_order`, `-sort_order`, `start_date`, `-start_date`, `name`, `-name`, `created_at`, `-created_at`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of the board's iterations.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IterationPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "operationId": "work.iteration.create",
        "summary": "Create an iteration on a board",
        "description": "Files a new sprint, always `PLANNED` — a sprint is never born running, because `work_iterations_board_active_key` allows exactly one `ACTIVE` row per board and creating straight into `ACTIVE` would make a routine plan-ahead action a race. Starting it is `work.iteration.activate`.\nThe board is **not** re-checked for `methodology = 'SCRUM'`. That is deliberate: methodology is a LENS over one task data model (ADR 0037), and refusing to plan a sprint on a board that is currently rendering Kanban would re-introduce exactly the data-loss coupling the ADR forbids — a team could not prepare their first sprint before switching, and a switch away would strand the plan. The lens decides what is *rendered*, never what may *exist*.\nWrites follow the board's OWN write arms (`work_iterations_board_writes_insert`) — the board's owner, or a `TEAM`-scoped manager of that owner. The widened board READ of leg B3 deliberately does not hand sprint planning to everyone who can see the board.\n",
        "tags": [
          "work",
          "iteration"
        ],
        "x-token": "work.iteration.create",
        "x-realizes-features": [
          "WRK-F01",
          "WRK-F08"
        ],
        "x-screens": [
          "WRK-S08"
        ],
        "x-touches-entities": [
          "work.iterations",
          "work.task_boards"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/IterationCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Iteration created, `status = PLANNED`.",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Iteration"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/iterations/{id}": {
      "get": {
        "operationId": "work.iteration.get",
        "summary": "Get one iteration",
        "description": "One sprint's record — name, goal, window, status and board. An iteration owns no time and no estimate of its own (db 06 §13): velocity is an aggregate over its tasks' `estimate_points` and their approved work entries, which is why nothing rolled-up is projected here.\n",
        "tags": [
          "work",
          "iteration"
        ],
        "x-token": "work.iteration.get",
        "x-realizes-features": [
          "WRK-F01",
          "WRK-F08"
        ],
        "x-screens": [
          "WRK-S08"
        ],
        "x-touches-entities": [
          "work.iterations"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          }
        ],
        "responses": {
          "200": {
            "description": "The iteration.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Iteration"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "operationId": "work.iteration.update",
        "summary": "Update an iteration",
        "description": "Edits `name`, `goal`, `start_date`, `end_date` and `sort_order`. **`status` is deliberately not writable here** — the two real transitions are `work.iteration.activate` and `work.iteration.close`, and both of them do more than set a column (the one-`ACTIVE` partial unique, and the rollover that decides where unfinished work goes). A bare status flip would be a door around both.\n`board_id` is not writable either: an iteration belongs to one board for its whole life, and re-pointing one would silently strand every task that carries its `iteration_id` on a board the iteration no longer belongs to — the incoherent row this leg's cross-board guard exists to refuse from the other direction.\n",
        "tags": [
          "work",
          "iteration"
        ],
        "x-token": "work.iteration.update",
        "x-realizes-features": [
          "WRK-F01",
          "WRK-F08"
        ],
        "x-screens": [
          "WRK-S08"
        ],
        "x-touches-entities": [
          "work.iterations"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/IterationUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated iteration.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Iteration"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/iterations/{id}/activate": {
      "post": {
        "operationId": "work.iteration.activate",
        "summary": "Start an iteration (PLANNED → ACTIVE)",
        "description": "Moves a `PLANNED` sprint to `ACTIVE`. **At most one `ACTIVE` iteration exists per board**, enforced by the partial unique `work_iterations_board_active_key` (migration 0109) rather than by application discipline — \"two sprints are open at once\" is a state the board cannot render and a service-level check cannot prevent under concurrency.\nA collision therefore surfaces as a clean **`409 STATE_TRANSITION_INVALID`** naming the sprint that is already running, never as a raw `23505`: the handler pre-checks so the common case gets a useful message, and the index catches the race so the pre-check is not the boundary. Re-activating an already-`ACTIVE` sprint is also a `409` — an idempotent replay of the SAME `Idempotency-Key` returns the stored response instead, which is the difference between \"I already did this\" and \"somebody else is running a different sprint\".\nClosing a sprint (`work.iteration.close`) is what frees the slot; a `COMPLETED` sprint can never be re-activated.\n",
        "tags": [
          "work",
          "iteration"
        ],
        "x-token": "work.iteration.activate",
        "x-realizes-features": [
          "WRK-F01",
          "WRK-F08"
        ],
        "x-screens": [
          "WRK-S08"
        ],
        "x-touches-entities": [
          "work.iterations"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "The now-`ACTIVE` iteration.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Iteration"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/iterations/{id}/close": {
      "post": {
        "operationId": "work.iteration.close",
        "summary": "Close an iteration, with rollover of the unfinished work",
        "description": "Stamps `status = 'COMPLETED'` and `closed_at` (the table's `work_iterations_closed_check` makes the pair inseparable) and, in the same transaction, moves the sprint's **incomplete** tasks somewhere they remain workable. `rollover_to_iteration_id` names the receiving sprint; **`null` (or omitted) means the backlog**, i.e. `tasks.iteration_id = NULL`, because `iteration_id IS NULL` *is* the backlog — there is no backlog entity and no \"backlog sprint\" row to keep in sync (db 06 §13).\n**Incomplete** means `status NOT IN ('DONE','CANCELLED')`. Finished cards are deliberately **left attributed to the closed sprint**: they are what the sprint delivered, and moving them would erase the only record from which velocity can be computed.\n**The response reports what actually moved, and it is not a promise.** The rollover `UPDATE` runs under the caller's own `work_tasks_assignee_writes_update` arms — assignee ∨ a member of the task's assignments ∨ (at `TEAM` scope) the manager of the assignee — which are NARROWER than the iteration write arms that admitted the close itself. A board owner closing a sprint full of other people's cards will therefore move some and not others, and that is a normal outcome, not an error. So `moved` and `not_moved` are both returned **with their task ids**: the close is honest about the cards it could not carry rather than reporting a total it did not achieve. Nothing is deleted and no card loses its `milestone_id`; an unmoved card simply stays attributed to the closed sprint and is still reachable by filtering on it.\nClosing an already-`COMPLETED` iteration is a `409`. A `PLANNED` sprint may be closed (cancelling a plan is legitimate); its rollover behaves identically.\n",
        "tags": [
          "work",
          "iteration"
        ],
        "x-token": "work.iteration.close",
        "x-realizes-features": [
          "WRK-F01",
          "WRK-F08",
          "WRK-F09"
        ],
        "x-screens": [
          "WRK-S08"
        ],
        "x-touches-entities": [
          "work.iterations",
          "work.tasks"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "team",
        "x-append-only": false,
        "x-entitlement": "work",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/IterationCloseRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The closed iteration and the rollover outcome.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IterationCloseResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerJWT": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Product session token. Two mint paths, one contract (ADR 0010): employees/managers authenticate against Keycloak (mobile + web); workspace staff arrive from the One portal via bridge-token SSO (`POST /api/sso/exchange` verifies the platform's Ed25519 token and mints the product session). The token carries IDENTITY ONLY — `sub`, `tenantUid`, `principal_class`, MFA level, session ref, `exp`. Roles/permissions are re-resolved server-side per request. Enforcement is layered: gateway (TLS/WAF/routing only — NEVER trusted for auth) → NestJS auth guard (validates token, builds the request auth-context) → entitlement middleware (subscription-status → feature-flag → numeric-limit, ADR 0009) → `SET LOCAL app.tenant_id` / `app.user_id` → Postgres FORCED RLS. `tenantUid` is NEVER a path, query, or body parameter.\n"
      }
    },
    "schemas": {
      "ProjectRoleInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1
          },
          "description": {
            "type": "string"
          },
          "permissions": {
            "type": "array",
            "uniqueItems": true,
            "items": {
              "type": "string"
            }
          },
          "template_key": {
            "type": "string"
          },
          "version": {
            "type": "integer",
            "minimum": 1
          }
        }
      },
      "ProjectMemberInput": {
        "type": "object",
        "required": [
          "role_id"
        ],
        "additionalProperties": false,
        "properties": {
          "role_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "ProjectRole": {
        "type": "object",
        "required": [
          "id",
          "project_id",
          "role_key",
          "name",
          "description",
          "permissions",
          "version"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "project_id": {
            "type": "string",
            "format": "uuid"
          },
          "role_key": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "permissions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "version": {
            "type": "integer"
          }
        }
      },
      "ProjectRoleTemplate": {
        "type": "object",
        "required": [
          "id",
          "role_key",
          "name",
          "description",
          "permissions",
          "version"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "role_key": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "permissions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "version": {
            "type": "integer"
          }
        }
      },
      "ProjectCapabilityAccess": {
        "type": "object",
        "required": [
          "project_id",
          "role_id",
          "role_key",
          "source",
          "capabilities"
        ],
        "properties": {
          "project_id": {
            "type": "string",
            "format": "uuid"
          },
          "role_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "role_key": {
            "type": [
              "string",
              "null"
            ]
          },
          "source": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "DIRECT",
              "TEAM",
              "ADMIN",
              null
            ]
          },
          "capabilities": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "ProjectMember": {
        "type": "object",
        "required": [
          "employee_id",
          "employee_name",
          "role_id",
          "role_key",
          "role_name",
          "source",
          "version"
        ],
        "properties": {
          "employee_id": {
            "type": "string",
            "format": "uuid"
          },
          "employee_name": {
            "type": "string"
          },
          "role_id": {
            "type": "string",
            "format": "uuid"
          },
          "role_key": {
            "type": "string"
          },
          "role_name": {
            "type": "string"
          },
          "source": {
            "type": "string",
            "enum": [
              "DIRECT",
              "TEAM"
            ]
          },
          "version": {
            "type": [
              "integer",
              "null"
            ]
          }
        }
      },
      "EmployeeProjectAccess": {
        "type": "object",
        "required": [
          "project_id",
          "project_name",
          "role_id",
          "role_key",
          "role_name",
          "source",
          "version"
        ],
        "properties": {
          "project_id": {
            "type": "string",
            "format": "uuid"
          },
          "project_name": {
            "type": "string"
          },
          "role_id": {
            "type": "string",
            "format": "uuid"
          },
          "role_key": {
            "type": "string"
          },
          "role_name": {
            "type": "string"
          },
          "source": {
            "type": "string",
            "enum": [
              "DIRECT",
              "TEAM"
            ]
          },
          "version": {
            "type": [
              "integer",
              "null"
            ]
          }
        }
      },
      "ProjectRolePage": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProjectRole"
            }
          }
        }
      },
      "ProjectRoleTemplatePage": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProjectRoleTemplate"
            }
          }
        }
      },
      "ProjectMemberPage": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProjectMember"
            }
          }
        }
      },
      "EmployeeProjectAccessPage": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EmployeeProjectAccess"
            }
          }
        }
      },
      "UuidRef": {
        "$ref": "#/components/schemas/Uuid"
      },
      "DateOnlyRef": {
        "$ref": "#/components/schemas/DateOnly"
      },
      "TimestampRef": {
        "$ref": "#/components/schemas/Timestamp"
      },
      "DecimalHoursRef": {
        "$ref": "#/components/schemas/DecimalHours"
      },
      "RateRef": {
        "$ref": "#/components/schemas/Rate"
      },
      "MoneyRef": {
        "$ref": "#/components/schemas/Money"
      },
      "BusinessNoRef": {
        "$ref": "#/components/schemas/BusinessNo"
      },
      "FileDownloadRef": {
        "$ref": "#/components/schemas/FileDownload"
      },
      "AuditMetaRef": {
        "$ref": "#/components/schemas/AuditMeta"
      },
      "AppendOnlyMetaRef": {
        "$ref": "#/components/schemas/AppendOnlyMeta"
      },
      "CursorPageRef": {
        "$ref": "#/components/schemas/CursorPage"
      },
      "BoardType": {
        "type": "string",
        "enum": [
          "KANBAN",
          "LIST"
        ],
        "description": "**Retired.** The `work.board_type` column and its Postgres enum were dropped by migration `0177` (boards leg B7, #873); the board lens is `methodology` (`BoardMethodology`). This schema survives for exactly one reason: `WorkTemplatePayload.boards[]` still ACCEPTS the field, because published templates already stored in a tenant's `work.work_templates.payload` carry it and a contract leg may retire a column without invalidating tenant rows that mention it. The value is **validated and then ignored** — instantiation writes no board type. It appears on no board request or response.\n"
      },
      "BoardVisibility": {
        "type": "string",
        "enum": [
          "PRIVATE",
          "TEAM",
          "TENANT"
        ],
        "description": "`work.board_visibility` (db 06 §1, ADR 0037). Who may READ the board and its cards: `TENANT` everyone in the workspace (the default, and what every pre-existing board was backfilled to by migration 0109) · `TEAM` the board's team, plus anyone currently participating in the board's project · `PRIVATE` the board's owner alone. Mutation is unaffected — writes stay with the assignee/team-role/manager arms whatever this says.\n"
      },
      "TaskPriority": {
        "type": "string",
        "enum": [
          "LOW",
          "MEDIUM",
          "HIGH",
          "URGENT"
        ],
        "description": "work.task_priority (db 06 §1)."
      },
      "TaskStatus": {
        "type": "string",
        "enum": [
          "BACKLOG",
          "TODO",
          "IN_PROGRESS",
          "BLOCKED",
          "IN_REVIEW",
          "DONE",
          "CANCELLED"
        ],
        "description": "work.task_status (db 06 §1). `Overdue` is a derived surfacing, not a value of this enum."
      },
      "AssignmentRole": {
        "type": "string",
        "enum": [
          "OWNER",
          "COLLABORATOR",
          "REVIEWER"
        ],
        "description": "work.assignment_role (db 06 §1)."
      },
      "AssignmentStatus": {
        "type": "string",
        "enum": [
          "ACTIVE",
          "COMPLETED",
          "REASSIGNED",
          "RELEASED"
        ],
        "description": "work.assignment_status (db 06 §1)."
      },
      "TimesheetPeriod": {
        "type": "string",
        "enum": [
          "DAILY",
          "WEEKLY"
        ],
        "description": "work.timesheet_period (db 06 §2)."
      },
      "TimesheetStatus": {
        "type": "string",
        "enum": [
          "DRAFT",
          "SUBMITTED",
          "APPROVED",
          "REJECTED",
          "RECALLED"
        ],
        "description": "work.timesheet_status (db 06 §2)."
      },
      "EntryActivity": {
        "type": "string",
        "enum": [
          "WORK",
          "MEETING",
          "SUPPORT",
          "TRAVEL",
          "TRAINING",
          "OTHER"
        ],
        "description": "work.entry_activity (db 06 §2)."
      },
      "ApprovalDecision": {
        "type": "string",
        "enum": [
          "APPROVED",
          "REJECTED",
          "REVIEWED"
        ],
        "description": "work.approval_decision (db 06 §2). `REVIEWED` is **append-only enum growth** added by the BOS round (db 06 §2 addendum 2026-07-31, ADR 0026) for the first-level, **non-authoritative** review lane (`work.timesheet.review`, `WRK-S11`/`WRK-S22`): a `REVIEWED` row carries **no** `approved_hours`, never moves `work.timesheets.status`, and is **excluded from the latest-effective-decision resolver**. `APPROVED`/`REJECTED` are unchanged.\n"
      },
      "ProjectBillingType": {
        "type": "string",
        "enum": [
          "NON_BILLABLE",
          "INTERNAL",
          "TIME_AND_MATERIAL",
          "FIXED_PRICE"
        ],
        "description": "work.project_billing_type (db 06 §3)."
      },
      "ProjectStatus": {
        "type": "string",
        "enum": [
          "PLANNED",
          "ACTIVE",
          "ON_HOLD",
          "COMPLETED",
          "CANCELLED",
          "ARCHIVED"
        ],
        "description": "work.project_status (db 06 §3)."
      },
      "ApprovalDecisionInput": {
        "type": "object",
        "description": "Generic optional-note body shared by the simple state-changing actions in this file.",
        "additionalProperties": false,
        "properties": {
          "note": {
            "type": "string"
          }
        }
      },
      "EmployeeRef": {
        "type": "object",
        "description": "Minimal `people.employees` projection (own read-model — `people` owns the write model, 00 §5).",
        "readOnly": true,
        "properties": {
          "id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "employee_no": {
            "$ref": "#/components/schemas/BusinessNoRef"
          },
          "full_name": {
            "type": "string"
          },
          "designation_title": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "DepartmentRef": {
        "type": "object",
        "description": "Minimal `org.departments` projection.",
        "readOnly": true,
        "properties": {
          "id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "LegalEntityRef": {
        "type": "object",
        "description": "Minimal `org.legal_entities` projection — resolves work-week/currency/pack.",
        "readOnly": true,
        "properties": {
          "id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "name": {
            "type": "string"
          },
          "currency_code": {
            "$ref": "#/components/schemas/CurrencyCode"
          }
        }
      },
      "ProjectRef": {
        "type": "object",
        "description": "Minimal `work.projects` projection for embedding on tasks/entries.",
        "readOnly": true,
        "properties": {
          "id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "project_no": {
            "$ref": "#/components/schemas/BusinessNoRef"
          },
          "code": {
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "type": "string"
          }
        }
      },
      "TaskRef": {
        "type": "object",
        "description": "Minimal `work.tasks` projection for embedding (sub-tasks, entry task links).\n\n`estimated_hours`, `due_date` and `completed_at` were added additively by work leg L5 (#877)\nso the per-person time drilldown can state hours-against-estimate and due-date adherence from\nthe entry list it already reads, without a per-task fan-out and without a new aggregate\nendpoint. They are the delivery facts about a card; every existing property is unchanged, and\nnone of the three is required.\n\n`estimated_hours` is a **decimal string**, never a float — the same rule `hours` follows.\n`completed_at` is populated for `DONE` tasks by construction (`work_tasks_done_check`) and is\nnull otherwise, so *\"completed\"* and *\"has a completion date\"* are the same predicate.\n",
        "readOnly": true,
        "properties": {
          "id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "task_no": {
            "$ref": "#/components/schemas/BusinessNoRef"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/TaskStatus"
          },
          "estimated_hours": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DecimalHoursRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "due_date": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DateOnlyRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "completed_at": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/TimestampRef"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "TimerStatus": {
        "type": "string",
        "enum": [
          "RUNNING",
          "PAUSED",
          "STOPPED"
        ],
        "description": "`work.timer_status` (migration `0178`). `RUNNING` has an open segment (`started_at` set), `PAUSED` has none (`started_at` null, seconds banked in `accumulated_seconds`), `STOPPED` is frozen (`stopped_at` set). A table CHECK pins each state to the columns it may and may not carry, so a row can never claim to be running with no segment open.\n"
      },
      "TaskTimer": {
        "description": "work.task_timers — one person's timer on one task (db 06 §14). **Server-owned state, client-owned tick:** nothing counts seconds server-side; a client renders `accumulated_seconds + (now − started_at)` locally and re-reads on mount, on focus and on a slow interval. One live (`RUNNING` or `PAUSED`) timer per employee is a database rule.\n",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "task_id",
              "employee_id",
              "status",
              "accumulated_seconds",
              "elapsed_seconds",
              "suggested_hours"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "task_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "employee_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "status": {
                "$ref": "#/components/schemas/TimerStatus"
              },
              "started_at": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/TimestampRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "When the CURRENT segment began; null whenever no segment is open (PAUSED, STOPPED). The client ticks from this."
              },
              "accumulated_seconds": {
                "type": "integer",
                "minimum": 0,
                "description": "Seconds banked by segments that have already closed. Whole seconds — the two-decimal figure lives on the work entry, never here."
              },
              "stopped_at": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/TimestampRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "work_entry_id": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "The entry logged FROM this timer, linked in the same transaction by `work.work_entry.create`. Null on a stopped-but-not-yet-logged timer, which is the cancel-safety design and not debris — see `work.task_timer.stop`.\n"
              },
              "elapsed_seconds": {
                "type": "integer",
                "readOnly": true,
                "minimum": 0,
                "description": "Derived, computed at read time: `accumulated_seconds` plus the open segment when RUNNING. Not a stored column."
              },
              "suggested_hours": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/DecimalHoursRef"
                  }
                ],
                "readOnly": true,
                "description": "Derived: `elapsed_seconds` as hours to **two decimal places, with no quarter-hour floor**. It is what the log-time modal pre-fills. The predecessor rounded UP to the next quarter and reported fifteen minutes for a two-minute task; `work.work_entry.create` has always accepted any two-decimal value, so the floor was a client rule stricter than the contract it claimed to mirror.\n"
              },
              "task": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/TaskRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          },
          {
            "$ref": "#/components/schemas/AuditMetaRef"
          }
        ]
      },
      "TaskTimerStart": {
        "type": "object",
        "description": "Which task to start timing, and what to do about a timer that is already live.",
        "required": [
          "task_id"
        ],
        "additionalProperties": false,
        "properties": {
          "task_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "stop_active": {
            "type": "boolean",
            "default": false,
            "description": "When the caller already holds a live timer, `false` (the default) refuses with `409` and names the incumbent, and `true` stops it and starts this one in the same transaction. Opt-in rather than automatic: silently stopping a timer someone forgot they had running is how elapsed time goes missing, and the refusal is what lets the UI ask first.\n"
          }
        }
      },
      "TaskBoardColumn": {
        "type": "object",
        "description": "One stage lane in `task_boards.columns` (jsonb array, db 06 §1). Schemaless layout config, not queried/joined.",
        "additionalProperties": false,
        "properties": {
          "key": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "order": {
            "type": "integer"
          },
          "maps_to_status": {
            "$ref": "#/components/schemas/TaskStatus"
          },
          "wip_limit": {
            "type": [
              "integer",
              "null"
            ]
          }
        }
      },
      "TaskBoard": {
        "description": "`work.task_boards` — a board (db 06 §1). Scoped by `team_id` (NULL = a workspace board) and `project_id`; its lens is `methodology`. **`board_type` and `department_id` were removed** by migration `0177` (boards leg B7, #873) — a client still reading either is reading a field the server no longer sends.\n",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "name",
              "is_active"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "name": {
                "type": "string"
              },
              "owner_id": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "team_id": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "The delivery squad that owns the board; null = a workspace board that belongs to no squad (migration 0109)."
              },
              "project_id": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "The project the board is scoped to; null = a cross-project team board (migration 0109)."
              },
              "methodology": {
                "type": "string",
                "enum": [
                  "KANBAN",
                  "LIST",
                  "SCRUM",
                  "PHASE"
                ],
                "default": "KANBAN",
                "description": "The lens the board is drawn through (ADR 0037). One task data model; the methodology chooses the lens and never clears `iteration_id` or milestone linkage when switched.\n"
              },
              "settings": {
                "type": "object",
                "default": {},
                "description": "Per-lens board settings (`work_task_boards_settings_object` CHECK requires a JSON object, never an array)."
              },
              "is_default": {
                "type": "boolean",
                "default": false,
                "description": "The squad's (or the workspace's) default board; at most one per team and one workspace-wide, enforced by partial unique indexes.\n"
              },
              "columns": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TaskBoardColumn"
                }
              },
              "description": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "visibility": {
                "$ref": "#/components/schemas/BoardVisibility"
              },
              "is_active": {
                "type": "boolean"
              }
            }
          },
          {
            "$ref": "#/components/schemas/AuditMetaRef"
          }
        ]
      },
      "TaskBoardCreate": {
        "type": "object",
        "required": [
          "name"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1
          },
          "owner_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "team_id": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "The delivery squad that owns the board; omitted/null = a workspace board that belongs to no squad."
          },
          "project_id": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "The project the board is scoped to; omitted/null = a cross-project team board."
          },
          "methodology": {
            "type": "string",
            "enum": [
              "KANBAN",
              "LIST",
              "SCRUM",
              "PHASE"
            ],
            "default": "KANBAN",
            "description": "The lens the board is drawn through (ADR 0037). One task data model; the methodology chooses the lens and never clears `iteration_id` or milestone linkage when switched.\n"
          },
          "settings": {
            "type": "object",
            "default": {},
            "description": "Per-lens board settings (`work_task_boards_settings_object` CHECK requires a JSON object, never an array)."
          },
          "is_default": {
            "type": "boolean",
            "default": false,
            "description": "The squad's (or the workspace's) default board; at most one per team and one workspace-wide, enforced by partial unique indexes.\n"
          },
          "visibility": {
            "default": "TENANT",
            "description": "A board created without an opinion is a workspace board — ADR 0037's read-broad posture; narrowing is the deliberate act.\n",
            "allOf": [
              {
                "$ref": "#/components/schemas/BoardVisibility"
              }
            ]
          },
          "description": {
            "type": "string"
          }
        }
      },
      "TaskBoardUpdate": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1
          },
          "team_id": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "The delivery squad that owns the board; null = a workspace board that belongs to no squad."
          },
          "project_id": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "The project the board is scoped to; null = a cross-project team board."
          },
          "methodology": {
            "type": "string",
            "enum": [
              "KANBAN",
              "LIST",
              "SCRUM",
              "PHASE"
            ],
            "default": "KANBAN",
            "description": "The lens the board is drawn through (ADR 0037). One task data model; the methodology chooses the lens and never clears `iteration_id` or milestone linkage when switched.\n"
          },
          "settings": {
            "type": "object",
            "default": {},
            "description": "Per-lens board settings (`work_task_boards_settings_object` CHECK requires a JSON object, never an array)."
          },
          "is_default": {
            "type": "boolean",
            "default": false,
            "description": "The squad's (or the workspace's) default board; at most one per team and one workspace-wide, enforced by partial unique indexes.\n"
          },
          "columns": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TaskBoardColumn"
            }
          },
          "owner_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "description": {
            "type": "string"
          },
          "visibility": {
            "$ref": "#/components/schemas/BoardVisibility"
          },
          "is_active": {
            "type": "boolean"
          }
        }
      },
      "TaskBoardPage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TaskBoard"
                }
              }
            }
          }
        ]
      },
      "Task": {
        "description": "work.tasks — a unit of assignable work on a board (db 06 §1). `is_overdue` is derived, not stored.",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "task_no",
              "board_id",
              "title",
              "priority",
              "status"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "task_no": {
                "$ref": "#/components/schemas/BusinessNoRef"
              },
              "board_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "project_id": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "parent_task_id": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "title": {
                "type": "string"
              },
              "description": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "url": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uri"
              },
              "assignee_id": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "priority": {
                "$ref": "#/components/schemas/TaskPriority"
              },
              "status": {
                "$ref": "#/components/schemas/TaskStatus"
              },
              "rank": {
                "type": [
                  "string",
                  "null"
                ],
                "pattern": "^(?:0|[1-9]\\d{0,2})(?:\\.\\d{1,6})?$",
                "description": "numeric(9,6) fractional lane order, as a string — the field `work.task.create` and `work.task.move` both write TODAY (`WorkService.optionalRank`). db 06 §1 (BOS addendum) plans to supersede it with `board_rank` at the contract step of the expand→migrate→contract cycle (ADR 0011, issue #420), but the migrate step — a write path onto `board_rank` — has not landed, so `rank` is NOT deprecated live and is the one field a client can depend on for lane order (`GAP-41`).\n"
              },
              "board_rank": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Lexicographic (LexoRank-style) ordering key within a board lane — the **intended** writer's column for drag ordering once the migrate step lands (db 06 §1 BOS addendum, fsd 05 §1 gap 4b, issue #420). The column exists (migration `0079`, expand-step backfilled from `rank`) and is readable here, but as of this spec revision **no operation writes it** — `WRK-S16`/`WRK-S17` still bind `rank`, not this field (`GAP-41`). Treat as read-only/informational until the migrate step closes #420.\n"
              },
              "milestone_id": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "The milestone this task rolls up to; null = untargeted. Service-validated to belong to the task's project (db 06 §1/§4)."
              },
              "origin_ref": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/TaskOriginRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Provenance when the task was created from another record — a converted desk ticket (`DSK-F04`), a recurrence rule, or an import batch. **Displayed, never joined** (db-docs/00 §6.2); the trail carries the matching activity row.\n"
              },
              "start_date": {
                "$ref": "#/components/schemas/DateOnlyRef"
              },
              "due_date": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/DateOnlyRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "estimated_hours": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/DecimalHoursRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "iteration_id": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "The board-owned sprint this card is committed to — **`null` IS THE BACKLOG** (db 06 §13, ADR 0037). There is no backlog entity and no \"backlog iteration\" row; a card with no iteration simply has none. The iteration always belongs to the card's own board: an assignment across boards is refused `422`, never written.\nShipped by migration 0109 (leg B1) and projected here for the first time in leg B5 (#871) — before this revision every task read silently withheld it, which is why the SCRUM lens had nothing to group by. **A methodology switch never clears it**: the KANBAN lens stops rendering the sprint grouping and switching back restores it intact, which is the no-data-loss property this leg round-trip tests.\n"
              },
              "estimate_points": {
                "type": [
                  "string",
                  "null"
                ],
                "pattern": "^(?:0|[1-9]\\d{0,3})(?:\\.\\d{1,2})?$",
                "description": "Story points, `numeric(6,2)` as a string — 0 to 9999.99, at most two fractional digits. **A SEPARATE column from `estimated_hours`, and deliberately so** (ADR 0037): points are a relative sizing convention, hours are a duration, and letting points into the billing/burn denominator would corrupt every project-health and utilization figure that reads `estimated_hours`. Nothing in `work.project.overview`, `.utilization` or the health job reads this field, and the jobs tier has a regression test asserting so.\nAlso shipped by 0109 and unprojected until leg B5 (#871). Absent (`null`) means unsized — **never rendered as 0**, exactly like `estimated_hours` on `WRK-S08`.\n"
              },
              "completed_at": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/TimestampRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "last_activity_at": {
                "$ref": "#/components/schemas/TimestampRef"
              },
              "is_overdue": {
                "type": "boolean",
                "readOnly": true,
                "description": "Derived: due_date < today AND status NOT IN (DONE, CANCELLED). Not a stored column (db 06 §1)."
              }
            }
          },
          {
            "$ref": "#/components/schemas/AuditMetaRef"
          },
          {
            "type": "object",
            "readOnly": true,
            "properties": {
              "project": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/ProjectRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "warnings": {
                "type": "array",
                "description": "Non-blocking leave/holiday advisories about the task's ASSIGNEE, computed at write time (#801). **Populated on `work.task.create` and `work.task.update` only** — the reads (`work.task.get`, the list/page operations) do not compute them and omit the field, so a client must not infer \"clean\" from its absence on a read. `[]` means the write was checked and the assignee is clear. Codes: `LEAVE_COLLISION`, `NON_WORKING_DAY` and — only when the tenant's leave projection is empty and nothing collided — `LEAVE_PROJECTION_UNAVAILABLE`. A task with **no assignee** gets no advisory at all, because the holiday calendar is resolved through the assignee (`design-docs/04` `G-81`).\n",
                "items": {
                  "$ref": "#/components/schemas/WorkWarning"
                }
              }
            }
          }
        ]
      },
      "TaskCreate": {
        "type": "object",
        "description": "`board_id` is REQUIRED — `WorkService.createTask`'s `this.only(...)` allow-list has never accepted `department_id` as a board-resolution shortcut; that convenience was designed (see the description this replaces) but not built, and sending `department_id` 422s live (`GAP-41`, `planning/api-docs/05-traceability-and-coverage.md`). As of `0177` the column it would have resolved through no longer exists (#873), so the shortcut is retired, not pending. `assignee_id` defaults to the caller (self-assign); assigning another employee is a Manager/HR Admin gate (RBAC). The initial assignment is always written with `assignment_role` `OWNER` — creation does not accept a role override (`this.only(...)` does not list `assignment_role` either, same `GAP-41`); assign a second person with a different role via `POST /tasks/{id}/assignments` (`TaskAssignmentCreate`) after the task exists. `status` defaults to `BACKLOG` and `rank` to the board's own placement rule when omitted — both are accepted, just previously undocumented here. `milestone_id` IS accepted here as of #1425 — filing a card under a phase no longer needs a second `PATCH /tasks/{id}` — and carries the same cross-project rule the PATCH does.\n",
        "required": [
          "board_id",
          "title"
        ],
        "additionalProperties": false,
        "properties": {
          "board_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "project_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "milestone_id": {
            "$ref": "#/components/schemas/UuidRef",
            "description": "File the new card under a milestone of `project_id` in the same write (#1425). Service-validated exactly as `TaskUpdate.milestone_id` is: the composite FK is tenant-wide, so a milestone belonging to another project — or one sent with no `project_id` at all — is a 422, never a silent accept.\n"
          },
          "parent_task_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "title": {
            "type": "string",
            "minLength": 1
          },
          "description": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "priority": {
            "$ref": "#/components/schemas/TaskPriority"
          },
          "status": {
            "$ref": "#/components/schemas/TaskStatus"
          },
          "rank": {
            "type": "string",
            "pattern": "^(?:0|[1-9]\\d{0,2})(?:\\.\\d{1,6})?$",
            "description": "Legacy fractional lane position, numeric(9,6) — 0 to 999.999999, up to six fractional digits (same validator `TaskMoveInput.rank` uses). Omit to let the board place the card."
          },
          "start_date": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "due_date": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "estimated_hours": {
            "$ref": "#/components/schemas/DecimalHoursRef"
          },
          "assignee_id": {
            "$ref": "#/components/schemas/UuidRef"
          }
        }
      },
      "TaskUpdate": {
        "type": "object",
        "description": "Partial edit of a task's descriptive fields **and its project home**. Excludes `status` (`work.task.transition`) and lane position (`work.task.move`); excludes `board_id` (still `work.task.move`, which since #1419 accepts a board change on its own) and `parent_task_id` re-parenting, which v1 does not transcribe. `project_id` became writable here in #1419: it was create-only on every task-level write path, so a quick-add card could never join a project afterwards without being re-created, losing its number, thread, attachments and logged hours.\n",
        "additionalProperties": false,
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "priority": {
            "$ref": "#/components/schemas/TaskPriority"
          },
          "due_date": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DateOnlyRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "estimated_hours": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DecimalHoursRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "milestone_id": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "Re-target the milestone this task rolls up to; null clears it. Must belong to the task's project after this edit — i.e. to `project_id` when the same body re-homes the task (db 06 §4)."
          },
          "project_id": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "Re-home the task to another project; `null` detaches it. The destination must be an `ACTIVE` project of the caller''s tenant — anything else is `409` `STATE_TRANSITION_INVALID`, the same refusal `work.project.close` gives a `MOVE` disposition. Changing it **clears `milestone_id`**, because a milestone belongs to the project the task is leaving (db 06 §4); send a `milestone_id` on the NEW project in the same body to keep one. The task''s `board_id` is untouched — change it with `work.task.move`.\n"
          }
        }
      },
      "TaskTransitionInput": {
        "type": "object",
        "description": "`status` names the field being written (`work.tasks.status`) — matching this corpus's own convention for a lifecycle-transition body (`EnquiryTransitionInput.stage`, `people.onboarding.advance`'s `stage`: name the payload key after the entity's own field, not a generic `to_<field>`). Was `to_status` here; renamed to match the LIVE `WorkService.transitionTask` body and the rest of the corpus (`GAP-41`).\n",
        "required": [
          "status"
        ],
        "additionalProperties": false,
        "properties": {
          "status": {
            "$ref": "#/components/schemas/TaskStatus"
          },
          "note": {
            "type": "string",
            "maxLength": 2000,
            "description": "Why the card moved. Persisted on the transition's audit row and carried on the `work.task.status_changed` payload — it was accepted and discarded before #1414."
          },
          "mentions": {
            "$ref": "#/components/schemas/TaskMentions"
          }
        }
      },
      "TaskStartInput": {
        "type": "object",
        "description": "`work.task.start` has a fixed target of `IN_PROGRESS`; a client cannot choose a status here. `note` is a short hand-off reason stored on the status-change audit row and emitted in the `work.task.status_changed` payload. It does not create a `work.task_comments` row.\n",
        "additionalProperties": false,
        "properties": {
          "note": {
            "type": "string",
            "maxLength": 2000
          },
          "mentions": {
            "$ref": "#/components/schemas/TaskMentions"
          }
        }
      },
      "TaskSubmitReviewInput": {
        "type": "object",
        "description": "`work.task.submit_review`'s body. Identical to `TaskTransitionInput` minus `status`: the target is fixed at `IN_REVIEW` by the token, exactly as `work.task.start`'s is fixed at `IN_PROGRESS`. Every field is optional, and an empty body is valid.\n",
        "additionalProperties": false,
        "properties": {
          "note": {
            "type": "string",
            "maxLength": 2000
          },
          "mentions": {
            "$ref": "#/components/schemas/TaskMentions"
          }
        }
      },
      "TaskMentions": {
        "type": "array",
        "description": "People to tag on this write. TWO element shapes are accepted, deliberately: a bare employee uuid (what the web clients send) and `{ employee_id }` (what the API has always required). Narrowing the wire to one would break whichever client is not deployed in the same minute. Each id is resolved against ACTIVE employees of the caller's own tenant — an unresolvable id is dropped, never stored — and each resolved employee becomes a `MENTIONED` watcher.\n",
        "items": {
          "anyOf": [
            {
              "$ref": "#/components/schemas/UuidRef"
            },
            {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "employee_id"
              ],
              "properties": {
                "employee_id": {
                  "$ref": "#/components/schemas/UuidRef"
                }
              }
            }
          ]
        }
      },
      "TaskMoveInput": {
        "type": "object",
        "description": "LIVE `WorkService.moveTask` (`this.only(...)`) accepts exactly `board_id`, `status`, `rank` — `board_id` names the destination board and is REQUIRED (moving a card, including across boards, always names where it lands; the earlier omission here was incomplete, not a design choice), `status` is OPTIONAL and defaults to the task's stored status (#1419), and `status`/`rank` are named after the entity's own fields, matching `TaskTransitionInput`'s rename above. The DB has carried a `board_rank` (LexoRank-style text) column since migration `0079` (issue #420, db-docs/06 BOS addendum) for the eventual expand→migrate→contract cutover (ADR 0011), but no write path accepts it yet — sending it 422s live, same as `to_status` did. It is intentionally NOT a property below until the migrate step lands; see `GAP-41` for the corrected record (`05-traceability-and-coverage.md`).\n",
        "required": [
          "board_id"
        ],
        "additionalProperties": false,
        "properties": {
          "board_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "status": {
            "allOf": [
              {
                "$ref": "#/components/schemas/TaskStatus"
              }
            ],
            "description": "OPTIONAL since #1419 — omitted, the task KEEPS its current status. \"Same lane, different board\" was previously inexpressible: this is the only door that writes `board_id`, and it demanded a `status` alongside, so a caller re-homing a card had to assert a lifecycle position it had no business asserting. A drag-and-drop still sends it (from the target lane's `maps_to_status`); the task-detail board picker does not.\n"
          },
          "rank": {
            "type": "string",
            "pattern": "^(?:0|[1-9]\\d{0,2})(?:\\.\\d{1,6})?$",
            "description": "Fractional lane position, numeric(9,6) — 0 to 999.999999, up to six fractional digits (`WorkService.optionalRank`). The only ranking field the service reads today; NOT deprecated live despite `board_rank` existing in the DB (see schema description)."
          }
        }
      },
      "TaskExportRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "board_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "status": {
            "$ref": "#/components/schemas/TaskStatus"
          },
          "assignee_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "project_id": {
            "$ref": "#/components/schemas/UuidRef"
          }
        }
      },
      "TaskPage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          }
        ]
      },
      "TaskDetail": {
        "description": "WRK-S03/WRK-S09 task detail — the task plus its assignment ledger, sub-tasks, and an attachments count.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Task"
          },
          {
            "type": "object",
            "properties": {
              "assignee_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "ref→people.employees.full_name (`assignee_id`), resolved after the authorized task read. `null` when unassigned or the employee cannot be resolved."
              },
              "created_by_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Display name for audit actor `created_by`; resolves both current principals and historical employee ids."
              },
              "updated_by_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Display name for audit actor `updated_by`; resolves both current principals and historical employee ids."
              },
              "assigned_by": {
                "$ref": "#/components/schemas/EmployeeRef"
              },
              "assignments": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TaskAssignment"
                },
                "description": "Live and historical assignment ledger rows (`WRK-S09`)."
              },
              "sub_tasks": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TaskRef"
                }
              },
              "attachments_count": {
                "type": "integer"
              },
              "logged_hours": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/DecimalHoursRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Derived: Σ work.work_entries.hours for this task (WRK-S09 Logged time)."
              }
            }
          }
        ]
      },
      "TaskAssignment": {
        "description": "work.task_assignments — the assignment ledger (db 06 §1).",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "task_id",
              "assignee_id",
              "assignment_role",
              "status",
              "assigned_at"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "task_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "assignee_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "assigned_by": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "assignment_role": {
                "$ref": "#/components/schemas/AssignmentRole"
              },
              "status": {
                "$ref": "#/components/schemas/AssignmentStatus"
              },
              "assigned_at": {
                "$ref": "#/components/schemas/TimestampRef"
              },
              "accepted_at": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/TimestampRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "released_at": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/TimestampRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          },
          {
            "$ref": "#/components/schemas/AuditMetaRef"
          },
          {
            "type": "object",
            "readOnly": true,
            "properties": {
              "warnings": {
                "type": "array",
                "description": "Non-blocking leave/holiday advisories about the assignee this row names, against the task's dates (#801). **Populated on `work.task_assignment.create` only** — the ledger reads (`work.task.get`'s `assignments`, the assignment page) omit it. Never blocking: the assignment is written either way.\n",
                "items": {
                  "$ref": "#/components/schemas/WorkWarning"
                }
              }
            }
          }
        ]
      },
      "TaskAssignmentCreate": {
        "type": "object",
        "required": [
          "assignee_id"
        ],
        "additionalProperties": false,
        "properties": {
          "assignee_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "assignment_role": {
            "$ref": "#/components/schemas/AssignmentRole"
          }
        }
      },
      "TaskAssignmentReleaseInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "outcome": {
            "type": "string",
            "enum": [
              "RELEASED",
              "REASSIGNED"
            ],
            "default": "RELEASED"
          },
          "note": {
            "type": "string"
          }
        }
      },
      "TaskAssignmentPage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TaskAssignment"
                }
              }
            }
          }
        ]
      },
      "IdleStatus": {
        "type": "object",
        "readOnly": true,
        "required": [
          "is_idle"
        ],
        "properties": {
          "is_idle": {
            "type": "boolean"
          },
          "active_assignment_count": {
            "type": "integer"
          },
          "idle_since": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/TimestampRef"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "IdleAlert": {
        "type": "object",
        "readOnly": true,
        "properties": {
          "employee": {
            "$ref": "#/components/schemas/EmployeeRef"
          },
          "idle_since": {
            "$ref": "#/components/schemas/TimestampRef"
          },
          "last_active_task": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/TaskRef"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "IdleAlertPage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/IdleAlert"
                }
              }
            }
          }
        ]
      },
      "TaskAttachment": {
        "type": "object",
        "description": "work.task_attachments (db 06 §1). `storage_key` never transits the API; `download` is the presigned, expiring handle (XC-F07). Column profile mirrors an append-only child record — no edit/delete op.\n",
        "readOnly": true,
        "required": [
          "id",
          "task_id",
          "file_name",
          "mime_type",
          "uploaded_at"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "task_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "comment_id": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "The comment this file was posted with (db 06 §1, migration `0203`). **Null = a task-level attachment** — the original meaning, unchanged. A set `comment_id` NARROWS the row, it never relocates it: the file still belongs to the task and still appears in this list, so the Attachments panel stays a complete inventory and can show an \"on comment\" reference. **A soft-deleted comment keeps its attachments** — they remain reachable here, never cascaded and never hidden, so deleting a comment removes the sentence and never the evidence.\n"
          },
          "file_name": {
            "type": "string"
          },
          "mime_type": {
            "type": "string"
          },
          "byte_size": {
            "type": [
              "integer",
              "null"
            ]
          },
          "content_hash": {
            "type": [
              "string",
              "null"
            ]
          },
          "uploaded_by": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "uploaded_at": {
            "$ref": "#/components/schemas/TimestampRef"
          },
          "download": {
            "$ref": "#/components/schemas/FileDownloadRef"
          }
        }
      },
      "TaskAttachmentCreate": {
        "type": "object",
        "description": "Metadata for a file already uploaded via the documents capability (XC-F07); `storage_key` is that upload's returned reference.",
        "required": [
          "file_name",
          "mime_type",
          "storage_key"
        ],
        "additionalProperties": false,
        "properties": {
          "file_name": {
            "type": "string",
            "minLength": 1
          },
          "mime_type": {
            "type": "string"
          },
          "storage_key": {
            "type": "string"
          },
          "content_hash": {
            "type": "string"
          },
          "byte_size": {
            "type": "integer",
            "minimum": 0
          },
          "comment_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              }
            ],
            "description": "Optional. Binds the file to one **live comment on this same task** at registration time (#1418). A comment on another task, in another tenant, or already soft-deleted is a `422` on `/comment_id` — never a silent task-level attachment. Omit for a task-level attachment, which is what the Attachments panel writes. The composer takes the other route: it registers the upload with no `comment_id` and binds it via `attachment_ids` on `work.task_comment.create`, so an abandoned draft leaves a task-level file rather than orphaned bytes.\n"
          }
        }
      },
      "TaskAttachmentPage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TaskAttachment"
                }
              }
            }
          }
        ]
      },
      "Timesheet": {
        "description": "work.timesheets — a daily or weekly timesheet for one employee over a bounded period (db 06 §2).",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "timesheet_no",
              "employee_id",
              "period_type",
              "period_start",
              "period_end",
              "status",
              "total_hours",
              "billable_hours"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "timesheet_no": {
                "$ref": "#/components/schemas/BusinessNoRef"
              },
              "employee_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "legal_entity_id": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "period_type": {
                "$ref": "#/components/schemas/TimesheetPeriod"
              },
              "period_start": {
                "$ref": "#/components/schemas/DateOnlyRef"
              },
              "period_end": {
                "$ref": "#/components/schemas/DateOnlyRef"
              },
              "status": {
                "$ref": "#/components/schemas/TimesheetStatus"
              },
              "total_hours": {
                "$ref": "#/components/schemas/DecimalHoursRef"
              },
              "billable_hours": {
                "$ref": "#/components/schemas/DecimalHoursRef"
              },
              "approved_hours": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/DecimalHoursRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Immutable certified hours written to the parent only when the latest decision approves it."
              },
              "submitted_at": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/TimestampRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "manager_id": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "target_hours": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/DecimalHoursRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Derived from the market work-week schedule (XC-F01, legal_entity_id) — not a stored column (fsd 05 §1.8)."
              }
            }
          },
          {
            "$ref": "#/components/schemas/AuditMetaRef"
          },
          {
            "type": "object",
            "readOnly": true,
            "properties": {
              "legal_entity": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/LegalEntityRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "decision_log": {
                "type": "array",
                "description": "The append-only decision history for this timesheet, newest first — the *Reviewed by Priya · 22 Jul* chip on `WRK-S10`, the review state and decision log on `WRK-S11` (db 06 §2 addendum, ADR 0026). Includes `REVIEWED` rows; the effective `APPROVED`/ `REJECTED` decision is derived from this log client-side, and `REVIEWED` is excluded from that resolver. A `REVIEWED` row carries **no** `approved_hours` and never moves `work.timesheets.status`.\n",
                "items": {
                  "$ref": "#/components/schemas/TimesheetApproval"
                }
              },
              "day_marks": {
                "type": "array",
                "description": "The per employee-day markers for THIS sheet's employee over THIS sheet's period (#1662, `WRK-S05`/`WRK-S10`): approved leave, tenant holidays, the employee's own week-off days and APPROVED overtime. **Only days carrying at least one marker are present** — a day absent from this array is an ordinary working day, not an unknown one.\n\nAn EMPTY array is a real answer and not always \"no markers\": the `attend` read models behind it are keyed on `people.employees.manager_id` while a timesheet is admitted on `work.timesheets.manager_id`, so a manager reading a sheet for somebody outside their direct reports sees the hours and no markers. The API fails closed in that direction deliberately — a marker must never become an existence oracle for an employee the caller may not read.\n",
                "items": {
                  "$ref": "#/components/schemas/TimesheetDayMark"
                }
              }
            }
          }
        ]
      },
      "TimesheetDayMark": {
        "type": "object",
        "description": "One employee-day's markers (#1662) — what a grid cell or a day view needs to tell an absence from a gap. Resolved from `attend.leave_day_marks` (falling back to `xc.approved_leave_projection` for a day the mark has not reached yet), the shared holiday resolver, `attend.schedules.weekly_off_days` with the legal entity's working week as the fallback, and `attend.overtime_requests` filtered to `APPROVED`.\n",
        "required": [
          "employee_id",
          "work_date",
          "weekly_off"
        ],
        "additionalProperties": false,
        "properties": {
          "employee_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "work_date": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "leave": {
            "anyOf": [
              {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "type": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "The tenant's own `org.leave_types.type_code`. NULL when only the approved-leave projection knows about the day — the day IS leave, its type is not yet projected. Nothing beyond the code and the half-day flag is carried: migration 0129 is explicit that copying a leave reason or approver into a consumer is a security decision needing its own review.\n"
                  },
                  "half": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "`FIRST_HALF` / `SECOND_HALF`; NULL on a full day."
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "holiday": {
            "anyOf": [
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Authored holiday name",
                    "most specific calendar wins.": null
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "weekly_off": {
            "type": "boolean",
            "description": "From the employee's own `attend.schedules.weekly_off_days`, falling back to the legal entity's pack-seeded `work_week.week_off_days`, then to nothing. Never a hardcoded Sat/Sun — a KSA entity runs Sun–Thu and a night-shift crew runs Wed/Thu.\n"
          },
          "ot_approved_hours": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DecimalHoursRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "`attend.overtime_requests.approved_hours` (falling back to the requested `ot_hours`) of the APPROVED request on this day; NULL when there is none. **Never derived from the hours on the sheet** — the `> 8h/day` client heuristic this replaced badged days where no request was ever raised and missed approved overtime on a short day.\n"
          }
        }
      },
      "TimesheetCreate": {
        "type": "object",
        "required": [
          "period_type",
          "period_start"
        ],
        "additionalProperties": false,
        "properties": {
          "period_type": {
            "$ref": "#/components/schemas/TimesheetPeriod"
          },
          "period_start": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "legal_entity_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "manager_id": {
            "deprecated": true,
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "Server-derived from `people.employees.manager_id` — approval routing is the org chart's decision, never the submitter's (#457). Tolerated for legacy clients only when it equals the derived value; a disagreeing value is a 422."
          }
        }
      },
      "TimesheetExportRequest": {
        "description": "The export's window and narrowing (#1661). `work_date_from`/`work_date_to` bound WORK DATES, not period starts — the export is per ENTRY, and a sheet whose period straddles a month boundary contributes only the days inside the window, which is what makes the file agree with the screen's own range. `status` is deliberately absent: an export filtered to APPROVED sheets would silently omit the unsubmitted days an approver is chasing.\n",
        "type": "object",
        "required": [
          "work_date_from",
          "work_date_to"
        ],
        "additionalProperties": false,
        "properties": {
          "work_date_from": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "work_date_to": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "format": {
            "type": "string",
            "enum": [
              "CSV",
              "XLSX"
            ],
            "default": "CSV",
            "description": "Rendered by the same writers the curated-report pipeline uses."
          },
          "project_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "employee_id": {
            "$ref": "#/components/schemas/UuidRef"
          }
        }
      },
      "TimesheetPage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Timesheet"
                }
              }
            }
          }
        ]
      },
      "TimesheetApproval": {
        "description": "work.timesheet_approvals *(Immutable, db 06 §2)* — the manager's sign-off decision. Frozen on write (`00 §8`): a reversal/re-decision is a new compensating row, never an edit.\n",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "timesheet_id",
              "approver_id",
              "decision",
              "decided_at"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "timesheet_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "approver_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "decision": {
                "$ref": "#/components/schemas/ApprovalDecision"
              },
              "approved_hours": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/DecimalHoursRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "comments": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "delegated_from": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "decided_at": {
                "$ref": "#/components/schemas/TimestampRef"
              }
            }
          },
          {
            "$ref": "#/components/schemas/AppendOnlyMetaRef"
          }
        ]
      },
      "TimesheetDetail": {
        "description": "WRK-S04/WRK-S05 — the caller's own timesheet and its decision history (`decision_log`, db 06 §2). Entry rows are served by the work-entries endpoints, not embedded here.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Timesheet"
          }
        ]
      },
      "TimesheetTeamDetail": {
        "description": "WRK-S07/WRK-S10/WRK-S11 — the team-readable timesheet and its decision history (`decision_log`, db 06 §2).",
        "allOf": [
          {
            "$ref": "#/components/schemas/Timesheet"
          }
        ]
      },
      "TimesheetApproveInput": {
        "type": "object",
        "additionalProperties": false,
        "description": "`approved_hours` is **optional**. Omitted, the server certifies the sheet's own `total_hours` — the same server-owned figure the batch path certifies. This is what lets the unified approval inbox (`xc.approval_inbox.decide`, which forwards only `action_input` + `note`) settle a timesheet; supplying a value explicitly still overrides it, so a partial certification stays possible on this direct path.\n",
        "properties": {
          "approved_hours": {
            "description": "Certified hours. Defaults to the timesheet's `total_hours` when omitted.",
            "allOf": [
              {
                "$ref": "#/components/schemas/DecimalHoursRef"
              }
            ]
          },
          "comments": {
            "type": "string"
          }
        }
      },
      "TimesheetRejectInput": {
        "type": "object",
        "required": [
          "comments"
        ],
        "additionalProperties": false,
        "properties": {
          "comments": {
            "type": "string",
            "minLength": 1
          }
        }
      },
      "TimesheetApproveBatchItem": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/TimesheetApproveBatchItemCurrent"
          },
          {
            "$ref": "#/components/schemas/TimesheetApproveBatchItemLegacy"
          }
        ]
      },
      "TimesheetApproveBatchItemCurrent": {
        "type": "object",
        "required": [
          "timesheet_id",
          "if_match"
        ],
        "additionalProperties": false,
        "properties": {
          "timesheet_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "if_match": {
            "type": "string",
            "description": "The timesheet's current ETag (row version) — the per-item equivalent of `If-Match`."
          }
        }
      },
      "TimesheetApproveBatchItemLegacy": {
        "type": "object",
        "deprecated": true,
        "description": "One-release compatibility shape. Prefer `TimesheetApproveBatchItemCurrent`.",
        "required": [
          "id",
          "approved_hours",
          "if_match"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "deprecated": true,
            "description": "Compatibility alias for `timesheet_id`; remove after one release.",
            "allOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              }
            ]
          },
          "approved_hours": {
            "deprecated": true,
            "description": "Compatibility-only and ignored; batch approval always certifies the server-owned `total_hours`.",
            "allOf": [
              {
                "$ref": "#/components/schemas/DecimalHoursRef"
              }
            ]
          },
          "if_match": {
            "type": "string",
            "description": "The timesheet's current ETag (row version) — the per-item equivalent of `If-Match`."
          }
        }
      },
      "TimesheetApproveBatchRequest": {
        "type": "object",
        "required": [
          "items"
        ],
        "additionalProperties": false,
        "properties": {
          "items": {
            "type": "array",
            "minItems": 1,
            "maxItems": 100,
            "items": {
              "$ref": "#/components/schemas/TimesheetApproveBatchItem"
            }
          }
        }
      },
      "TimesheetApproveBatchResult": {
        "type": "object",
        "readOnly": true,
        "required": [
          "timesheet_id",
          "outcome",
          "error_code",
          "message"
        ],
        "properties": {
          "timesheet_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "outcome": {
            "type": "string",
            "enum": [
              "APPROVED",
              "FAILED"
            ]
          },
          "error_code": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ErrorCode"
              },
              {
                "type": "null"
              }
            ]
          },
          "message": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why this row failed; null for an approved row."
          }
        }
      },
      "TimesheetApproveBatchResponse": {
        "type": "object",
        "required": [
          "results",
          "updated_count",
          "failed_count"
        ],
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TimesheetApproveBatchResult"
            }
          },
          "updated_count": {
            "type": "integer",
            "minimum": 0,
            "description": "Number of rows with outcome `APPROVED`."
          },
          "failed_count": {
            "type": "integer",
            "minimum": 0,
            "description": "Number of rows with outcome `FAILED`."
          }
        }
      },
      "WorkEntry": {
        "description": "work.work_entries — decimal hours logged on a date against a task and/or project (db 06 §2). Never float.",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "timesheet_id",
              "work_date",
              "hours",
              "activity_type",
              "is_billable"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "timesheet_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "task_id": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "project_id": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "work_date": {
                "$ref": "#/components/schemas/DateOnlyRef"
              },
              "start_time": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "time",
                "description": "HH:MM:SS, optional input convenience."
              },
              "end_time": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "time"
              },
              "hours": {
                "$ref": "#/components/schemas/DecimalHoursRef"
              },
              "description": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "activity_type": {
                "$ref": "#/components/schemas/EntryActivity"
              },
              "is_billable": {
                "type": "boolean"
              },
              "entered_at": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/TimestampRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          },
          {
            "type": "object",
            "readOnly": true,
            "properties": {
              "task": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/TaskRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "project": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/ProjectRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          }
        ]
      },
      "WorkEntryCreate": {
        "type": "object",
        "description": "`hours` may be omitted when both `start_time` and `end_time` are supplied — the server derives `hours` as their difference; `hours` remains the stored authoritative figure (fsd 05 WRK-S06). `is_billable=true` requires `project_id`.\n`description` is **required and non-empty** — it is the only thing an approver reads when certifying the hour, and both surfaces send it: the mobile timer and the web log-time modal prefill it from the task title (truncated to 200 characters) so one-click logging survives. Sending `null` is a `422` on `/description` with rule `required`, not a silent blank (#805).\n",
        "required": [
          "timesheet_id",
          "work_date",
          "description"
        ],
        "additionalProperties": false,
        "properties": {
          "timesheet_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "task_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "project_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "work_date": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "start_time": {
            "type": "string",
            "format": "time"
          },
          "end_time": {
            "type": "string",
            "format": "time"
          },
          "hours": {
            "$ref": "#/components/schemas/DecimalHoursRef"
          },
          "description": {
            "type": "string",
            "maxLength": 200
          },
          "activity_type": {
            "$ref": "#/components/schemas/EntryActivity"
          },
          "is_billable": {
            "type": "boolean",
            "default": false
          }
        }
      },
      "WorkEntryUpdate": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "task_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "project_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "start_time": {
            "type": "string",
            "format": "time"
          },
          "end_time": {
            "type": "string",
            "format": "time"
          },
          "hours": {
            "$ref": "#/components/schemas/DecimalHoursRef"
          },
          "description": {
            "type": "string",
            "maxLength": 200
          },
          "activity_type": {
            "$ref": "#/components/schemas/EntryActivity"
          },
          "is_billable": {
            "type": "boolean"
          }
        }
      },
      "WorkEntryPage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/WorkEntry"
                }
              }
            }
          }
        ]
      },
      "Project": {
        "description": "`work.projects` — the lightweight project register (db 06 §3).\nThe three commercial properties `client_name`, `budget_hours` and `budget_amount` are **scope-gated on `work.project.list`**: they are present only for a caller holding a `TENANT` or `PLATFORM` grant, and absent entirely (not null) for a picker-scoped caller such as a mobile `employee`. `work.project.get` is granted only to tenant-wide roles and always returns them. Consumers must treat all three as optional and must not infer \"no client / no budget\" from their absence.\n",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "project_no",
              "name",
              "billing_type",
              "status",
              "delivery_model"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "project_no": {
                "$ref": "#/components/schemas/BusinessNoRef"
              },
              "code": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "name": {
                "type": "string"
              },
              "legal_entity_id": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "department_id": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "owner_id": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "client_name": {
                "description": "Scope-gated — omitted on `work.project.list` for a caller without a tenant-wide grant.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "billing_type": {
                "$ref": "#/components/schemas/ProjectBillingType"
              },
              "status": {
                "$ref": "#/components/schemas/ProjectStatus"
              },
              "delivery_model": {
                "$ref": "#/components/schemas/ProjectDeliveryModel"
              },
              "start_date": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/DateOnlyRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "end_date": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/DateOnlyRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "budget_hours": {
                "description": "Scope-gated — omitted on `work.project.list` for a caller without a tenant-wide grant.",
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/DecimalHoursRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "budget_amount": {
                "description": "Scope-gated — omitted on `work.project.list` for a caller without a tenant-wide grant.",
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/MoneyRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "health": {
                "description": "The stored delivery verdict (`XC-F08` writes it; no read path recomputes it). `null` means **never computed** and must render as *not enough data* — never as `GREEN`. Deliberately not scope-gated: health is a delivery signal, not commercial information.\n",
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/ProjectHealthValue"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "health_override": {
                "type": "boolean",
                "description": "True when a human set `health` by hand."
              },
              "health_computed_at": {
                "description": "Dates the VERDICT only, never the inputs.",
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/TimestampRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "archived_at": {
                "description": "Set by close-out (`WRK-F17`). Non-null ⇒ the project is read-only.",
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/TimestampRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "task_count": {
                "type": "integer",
                "description": "Every undeleted task on the project — `CANCELLED` included, because cancelled scope was decided rather than delivered. Returned on `work.project.list` only (a server-side roll-up, #1429); consumers must not derive it from a capped `GET /tasks` read.\n"
              },
              "done_count": {
                "type": "integer",
                "description": "Tasks at `DONE`, excluding deleted. Paired with `task_count` rather than published as a percentage so that `task_count = 0` reads as *no tasks yet* instead of *0 % complete* — clients render `null`, never `0 %`, when the denominator is zero.\n"
              }
            }
          },
          {
            "$ref": "#/components/schemas/AuditMetaRef"
          },
          {
            "type": "object",
            "readOnly": true,
            "properties": {
              "legal_entity": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/LegalEntityRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "department": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/DepartmentRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "owner": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/EmployeeRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          }
        ]
      },
      "ProjectCreate": {
        "type": "object",
        "required": [
          "name",
          "billing_type",
          "status"
        ],
        "additionalProperties": false,
        "properties": {
          "code": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "minLength": 1
          },
          "legal_entity_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "department_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "owner_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "client_name": {
            "type": "string"
          },
          "billing_type": {
            "$ref": "#/components/schemas/ProjectBillingType"
          },
          "status": {
            "$ref": "#/components/schemas/ProjectStatus"
          },
          "delivery_model": {
            "description": "Defaults to `CONTINUOUS` when omitted (the column default, db 06 §3).",
            "$ref": "#/components/schemas/ProjectDeliveryModel"
          },
          "start_date": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "end_date": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "budget_hours": {
            "$ref": "#/components/schemas/DecimalHoursRef"
          },
          "budget_amount": {
            "$ref": "#/components/schemas/MoneyRef"
          }
        }
      },
      "ProjectUpdate": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "code": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "minLength": 1
          },
          "legal_entity_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "department_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "owner_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "client_name": {
            "type": "string"
          },
          "billing_type": {
            "$ref": "#/components/schemas/ProjectBillingType"
          },
          "status": {
            "type": "string",
            "enum": [
              "PLANNED",
              "ACTIVE",
              "ON_HOLD",
              "COMPLETED",
              "CANCELLED"
            ],
            "description": "The settable lifecycle states. **`ARCHIVED` is absent on purpose** — a project is archived by closing it out (`POST /projects/{id}/close`), which dispositions every open task, stamps the allocation windows, sets `archived_at`, records the close note on the audit plane and emits `work.project.closed` for `billing`. Setting the column alone would produce a project that looks archived and did none of that; the operation refuses it with `409 STATE_TRANSITION_INVALID` and `type: urn:groundit:problem:work:project-archive-via-close-only`.\n"
          },
          "delivery_model": {
            "description": "Switching a live project's model **invalidates nothing** — it gates no stored data, only which planning lens `WRK-S14` opens on and which milestone `kind`s the planner offers (db 06 §3). Existing milestones of any `kind` survive the switch.\n",
            "$ref": "#/components/schemas/ProjectDeliveryModel"
          },
          "start_date": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "end_date": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "budget_hours": {
            "$ref": "#/components/schemas/DecimalHoursRef"
          },
          "budget_amount": {
            "$ref": "#/components/schemas/MoneyRef"
          }
        }
      },
      "ProjectPage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          }
        ]
      },
      "ProjectTaskHours": {
        "type": "object",
        "readOnly": true,
        "properties": {
          "task": {
            "$ref": "#/components/schemas/TaskRef"
          },
          "hours": {
            "$ref": "#/components/schemas/DecimalHoursRef"
          }
        }
      },
      "ProjectEmployeeHours": {
        "type": "object",
        "readOnly": true,
        "properties": {
          "employee": {
            "$ref": "#/components/schemas/EmployeeRef"
          },
          "hours": {
            "$ref": "#/components/schemas/DecimalHoursRef"
          }
        }
      },
      "ProjectTimeRollup": {
        "type": "object",
        "description": "WRK-S13 — an aggregate over project-tagged work_entries only (project_id NOT NULL), never a stored table.",
        "readOnly": true,
        "required": [
          "project",
          "logged_hours",
          "billable_hours",
          "non_billable_hours"
        ],
        "properties": {
          "project": {
            "$ref": "#/components/schemas/ProjectRef"
          },
          "logged_hours": {
            "$ref": "#/components/schemas/DecimalHoursRef"
          },
          "budget_hours": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DecimalHoursRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "percent_consumed": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/RateRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "Derived: logged_hours ÷ budget_hours, as a fraction (e.g. \"0.780000\" = 78%)."
          },
          "billable_hours": {
            "$ref": "#/components/schemas/DecimalHoursRef"
          },
          "non_billable_hours": {
            "$ref": "#/components/schemas/DecimalHoursRef"
          },
          "by_task": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProjectTaskHours"
            }
          },
          "by_employee": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProjectEmployeeHours"
            }
          },
          "billing_type": {
            "$ref": "#/components/schemas/ProjectBillingType"
          },
          "client_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "budget_amount": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "ProjectHealthValue": {
        "type": "string",
        "enum": [
          "GREEN",
          "AMBER",
          "RED"
        ],
        "description": "work.project_health (db 06 §3 BOS addendum). Null until first computed; the API renders that absence as `NOT_ENOUGH_DATA`, never as green."
      },
      "ProjectDeliveryModel": {
        "type": "string",
        "enum": [
          "CONTINUOUS",
          "MILESTONE",
          "PHASED"
        ],
        "description": "`work.projects.delivery_model` (db 06 §3, migration 0131) — **enum-by-CHECK, not a Postgres enum type**: a methodology flag, not a lifecycle (`ProjectStatus` remains the only lifecycle on this resource). `CONTINUOUS` runs with no dated checkpoints and is the default for every project that predates the column; `MILESTONE` plans against dated checkpoints; `PHASED` plans in spanning stages. It selects the `WRK-S14` planning lens and the milestone `kind`s the planner offers — it constrains **no stored data**, so a live project may switch models without invalidating a single milestone.\n"
      },
      "MilestoneKind": {
        "type": "string",
        "enum": [
          "MILESTONE",
          "PHASE",
          "GATE"
        ],
        "description": "`work.milestones.kind` (db 06 §4, migration 0131) — enum-by-CHECK. **This is [ADR 0037](../../../architecture-docs/adr/0037-work-teams-methodology-boards.md)'s \"a phase is a milestone with a `kind`\"**: there is no `work.phases` table and no `tasks.phase_id`, and a phase is planned, tagged and rolled up through exactly the milestone machinery. `MILESTONE` is a dated checkpoint (the launch meaning, and the default every existing row carries); `PHASE` is a spanning stage and is the one kind that normally carries `start_date`; `GATE` is a decision point work must pass through. **Append-only vocabulary** — values are added, never renamed or removed, because a rendered plan would otherwise change meaning retroactively.\n"
      },
      "MilestoneStatus": {
        "type": "string",
        "enum": [
          "PLANNED",
          "ON_TRACK",
          "AT_RISK",
          "DONE",
          "DROPPED"
        ],
        "description": "work.milestone_status (db 06 §4). **Stored and PM-owned, not derived** — the workspace may *suggest* `AT_RISK`, but what is stored is what the PM committed to. `DONE` carries completion on its own (there is no `completed_at` column); `DROPPED` is kept for history and excluded from roll-ups.\n"
      },
      "DependencyType": {
        "type": "string",
        "enum": [
          "BLOCKS"
        ],
        "description": "work.dependency_type (db 06 §6). One value at launch; the enum exists so FINISH_START-style relations can be added append-only for WRK-F19."
      },
      "WatchSource": {
        "type": "string",
        "enum": [
          "EXPLICIT",
          "ASSIGNED",
          "MENTIONED"
        ],
        "description": "work.watch_source (db 06 §7). It carries the \"auto-adders never resurrect an un-watch\" rule — only an `EXPLICIT` watch may clear a tombstone — **and it is a display field**, which this description previously denied. `WRK-S09`'s Watchers panel renders the member as a per-row chip beside each watcher, because *mentioned into this thread* and *chose to follow it* are different answers to \"why am I getting these?\". The wire value stays the stable enum member in every locale; localizing it is the client's job, exactly as for every other enum in this spec.\n"
      },
      "TaskActivityType": {
        "type": "string",
        "enum": [
          "STATUS_CHANGED",
          "ASSIGNED",
          "UNASSIGNED",
          "COMMENTED",
          "ESTIMATE_CHANGED",
          "DUE_DATE_CHANGED",
          "MILESTONE_CHANGED",
          "DEPENDENCY_ADDED",
          "DEPENDENCY_REMOVED",
          "BLOCKED",
          "UNBLOCKED",
          "ATTACHMENT_ADDED",
          "TIME_LOGGED",
          "CONVERTED_FROM_TICKET"
        ],
        "description": "work.task_activity_type (db 06 §7). **Append-only vocabulary** — values are added, never renamed or removed, because rendered history would otherwise change meaning retroactively."
      },
      "TemplateKind": {
        "type": "string",
        "enum": [
          "PROJECT",
          "TASK"
        ],
        "description": "work.template_kind (db 06 §8)."
      },
      "TemplateStatus": {
        "type": "string",
        "enum": [
          "DRAFT",
          "PUBLISHED"
        ],
        "description": "work.template_status (db 06 §8). Only `PUBLISHED` templates are offered in the instantiate picker."
      },
      "RecurrenceSourceType": {
        "type": "string",
        "enum": [
          "TEMPLATE",
          "TASK"
        ],
        "description": "work.recurrence_source (db 06 §8) — polymorphic, no FK (db-docs/00 §13)."
      },
      "ViewSurface": {
        "type": "string",
        "enum": [
          "BOARD",
          "LIST",
          "CALENDAR"
        ],
        "description": "work.view_surface (db 06 §9). A view binds to ONE surface; the group/sort keys are not portable between them."
      },
      "ImportSourceType": {
        "type": "string",
        "enum": [
          "TRELLO",
          "JIRA",
          "CSV"
        ],
        "description": "WRK-S26 source systems (proposed entity — db 06 §10)."
      },
      "ImportBatchStatus": {
        "type": "string",
        "enum": [
          "DRAFT",
          "DRY_RUN_QUEUED",
          "DRY_RUN_COMPLETE",
          "DRY_RUN_FAILED",
          "RUNNING",
          "COMPLETED",
          "PARTIAL",
          "FAILED"
        ],
        "description": "WRK-S26 batch lifecycle (proposed entity — db 06 §10). `PARTIAL` reports exactly what landed."
      },
      "TaskOriginRef": {
        "type": "object",
        "description": "`work.tasks.origin_ref` (jsonb, db 06 §1 BOS addendum) — provenance for a task created from another record. Rendered as the *Converted-from* note on `WRK-S09` (\"Created from desk ticket TKT-1180 · 14 Jul 2026\"). **Displayed, never joined** (db-docs/00 §6.2/§7).\n",
        "readOnly": true,
        "properties": {
          "source_module": {
            "type": "string",
            "description": "e.g. `desk`, `work`."
          },
          "source_type": {
            "type": "string",
            "description": "e.g. `desk.tickets`, `work.task_recurrences`, `work.import_batches`."
          },
          "source_no": {
            "type": [
              "string",
              "null"
            ],
            "description": "The source record's human-readable number, when it has one."
          },
          "source_id": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "converted_at": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/TimestampRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "period_key": {
            "type": [
              "string",
              "null"
            ],
            "description": "Present only on a task minted by the recurrence sweep (#439): **which run** produced it (`2026-W31`, `2026-08`, …), which is what makes a board card traceable back to a row of `TaskRecurrence.history`. A recurrence has no human-readable business number, so `source_no` stays null rather than being redefined to carry this.\n"
          }
        }
      },
      "MilestoneRef": {
        "type": "object",
        "description": "Minimal `work.milestones` projection for embedding on tasks, calendars and breadcrumbs.",
        "readOnly": true,
        "properties": {
          "id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "name": {
            "type": "string"
          },
          "due_date": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DateOnlyRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "status": {
            "$ref": "#/components/schemas/MilestoneStatus"
          }
        }
      },
      "MilestoneRollup": {
        "type": "object",
        "description": "Derived per-milestone roll-up — task counts by status and estimate vs **approved** actual over the milestone's tasks. **Nothing here is stored** (db 06 §4), which is what keeps a milestone from becoming a second, stale copy of the truth.\n",
        "readOnly": true,
        "properties": {
          "task_count": {
            "type": "integer"
          },
          "done_count": {
            "type": "integer"
          },
          "overdue_count": {
            "type": "integer"
          },
          "blocked_count": {
            "type": "integer"
          },
          "estimated_hours": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DecimalHoursRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "approved_hours": {
            "$ref": "#/components/schemas/DecimalHoursRef"
          },
          "pending_hours": {
            "$ref": "#/components/schemas/DecimalHoursRef",
            "description": "Logged but not yet approved — stated so burn is never inflated by unsigned time (WRK-F09)."
          }
        }
      },
      "Milestone": {
        "description": "work.milestones — a dated checkpoint inside a project (db 06 §4). No business number: a milestone is cited inside its project, never standalone.",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "project_id",
              "name",
              "kind",
              "status",
              "sort_order"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "project_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "name": {
                "type": "string"
              },
              "kind": {
                "$ref": "#/components/schemas/MilestoneKind"
              },
              "start_date": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/DateOnlyRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Stage start — the second end a `PHASE` needs and a plain checkpoint does not have. Null for a `MILESTONE`/`GATE` unless the planner set one; **not required by `kind`**, so re-classifying a row is never a data migration. When both dates are present the database enforces `start_date <= due_date` (`work_milestones_window_check`).\n"
              },
              "due_date": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/DateOnlyRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Target date; null = an undated, ordered placeholder. Gregorian is canonical — Hijri is a render-time concern."
              },
              "status": {
                "$ref": "#/components/schemas/MilestoneStatus"
              },
              "sort_order": {
                "type": "integer",
                "minimum": 0
              },
              "description": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          {
            "$ref": "#/components/schemas/AuditMetaRef"
          },
          {
            "type": "object",
            "readOnly": true,
            "properties": {
              "rollup": {
                "$ref": "#/components/schemas/MilestoneRollup"
              },
              "warnings": {
                "type": "array",
                "description": "Non-blocking service warnings, e.g. a `due_date` outside the parent project's window (`WRK-S14`).",
                "items": {
                  "$ref": "#/components/schemas/WorkWarning"
                }
              }
            }
          }
        ]
      },
      "MilestoneCreate": {
        "type": "object",
        "required": [
          "name"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1
          },
          "kind": {
            "description": "Defaults to `MILESTONE` when omitted (the column default, db 06 §4).",
            "$ref": "#/components/schemas/MilestoneKind"
          },
          "start_date": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "due_date": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "status": {
            "$ref": "#/components/schemas/MilestoneStatus"
          },
          "sort_order": {
            "type": "integer",
            "minimum": 0
          },
          "description": {
            "type": "string"
          }
        }
      },
      "MilestoneUpdate": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1
          },
          "kind": {
            "description": "Re-classifying a row is a plain update — no date becomes required or forbidden by `kind`, so a `MILESTONE` may become a `PHASE` (and back) without a data migration (db 06 §4).\n",
            "$ref": "#/components/schemas/MilestoneKind"
          },
          "start_date": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "due_date": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "status": {
            "$ref": "#/components/schemas/MilestoneStatus"
          },
          "sort_order": {
            "type": "integer",
            "minimum": 0
          },
          "description": {
            "type": "string"
          }
        }
      },
      "MilestonePage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Milestone"
                }
              }
            }
          }
        ]
      },
      "WorkWarning": {
        "type": "object",
        "description": "A **non-blocking** advisory returned alongside a successful write or read. The BOS surfaces lean on this deliberately: overbooking, leave collisions and out-of-window milestone dates are surfaced with their underlying numbers and the write proceeds — a hard block would make a real, temporary over-allocation unrecordable (db 06 §5, fsd 05 `WRK-S19`). Clients must render these as advisories, **not** as errors. **Which codes a client can actually expect today (#437).** `LEAVE_PROJECTION_UNAVAILABLE` is attached to every allocation response — create, update and per row on the list — so `warnings[]` is **never empty** on that path and a client must classify by `code`, never by array length: amber-on-non-empty would pin a permanent warning to every row and read as an overbooked workspace. `LEAVE_COLLISION` — the per-window advisory that should **replace** that standing caveat now that the approved-leave projection is fed — is **still not emitted by the allocation writer**; on that path it is specified here and handled by the client, and the server side is tracked as [#916](https://github.com/Sysmeadac/GroundIT/issues/916). Until that lands, an allocation that overlaps approved leave is written with no collision advisory even though `work.assignment_context.get` can see the overlap. **Since 2026-08-24 (#801) the TASK doors DO emit it, and the allocation writer is unchanged** — read those as two separate facts. `work.task.create`, `work.task.update`, `work.task_assignment.create`, `work.task.bulk_update` (per successful item) and `work.assignment_context.get` emit `LEAVE_COLLISION` and `NON_WORKING_DAY` against the assignee; `work.project_allocation.create`/`.update` emit neither and still attach `LEAVE_PROJECTION_UNAVAILABLE` to every response. The two shapes:\n`LEAVE_COLLISION` — the assignee has approved leave overlapping the task's date window. `detail` is `{ employee_id, employee_name, due_date, start_date, on_due_date, windows: [{ from, to, is_half_day }] }`, and `message` already reads as a sentence: *\"Asha Menon is on approved leave 2026-09-12 – 2026-09-16, which covers this due date.\"* — *\"… which overlaps this task's dates.\"* when `on_due_date` is `false`, with `\" (half day)\"` appended for a half-day window. **`windows[]` is the one place a half-day is legible on this module's wire** — `ApprovedLeaveWindow` still cannot express one (#437). The source is `xc.approved_leave_projection` and **never** a cross-schema read into `leave` (`WRK-F13`); only `start_date`, `end_date` and `is_half_day` leave that projection, so no reason, type or approver exists here to leak — widening the projection is a security decision, not a field addition.\n`NON_WORKING_DAY` — the due date is a holiday for the assignee. `detail` is `{ date, reason: 'HOLIDAY', holiday_name, holiday_type, calendar_code, employee_id }`, e.g. *\"2026-09-14 is a holiday (Onam) for Asha Menon.\"* Resolved from `org.holiday_calendars` by the assignee's `legal_entity_id` + `work_location_id`, entity-wide and location-specific calendars **additive**, `status = 'ACTIVE'`, and `is_optional` / `type = 'RESTRICTED'` days excluded — the same rule the attendance materializer applies, so one date cannot be a holiday on a board and a working day on an attendance record. **`reason` is in the detail so `'WEEKLY_OFF'` can join it later without a contract change; weekly-off is deliberately NOT in this slice** (outside #801's acceptance criteria, and the week-off precedence chain is its own work), so an absent `NON_WORKING_DAY` must never be read as \"this is a working day\".\n`LEAVE_PROJECTION_UNAVAILABLE` **means something narrower on the task doors than on the allocation path.** There it is attached to every response (see above); on the task doors it is emitted **only** when the tenant's projection holds no live rows at all, and only when no `LEAVE_COLLISION` was found — so on those doors `warnings[]` really is empty for a clean write, and a client may treat non-empty as \"there is something to show\". The difference is deliberate and is stated here because the same code carries different weight depending on which door returned it. `PREDECESSOR_WINDOW_CLOSED` (#1663) — this write NARROWED another row, and says which one. Emitted by `work.project_allocation.create` when a new phase starts after an existing open-ended window (the taper), and always by `work.project_allocation.split`. `detail` is `{ allocation_id, allocation_pct, window_from, window_to }`, where `window_to` is the date the incumbent now ends on, and `message` reads *\"The open-ended 80.00 % window that started 2026-09-01 now ends 2026-09-21, so this phase can start 2026-09-22.\"* It is an advisory rather than an error because the narrowing is what the caller asked for, but it is never silent: a write that changes a row the caller did not name must say so.\n",
        "readOnly": true,
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "OVERBOOKED",
              "LEAVE_COLLISION",
              "MILESTONE_OUTSIDE_PROJECT_WINDOW",
              "NO_ESTIMATE",
              "LEAVE_PROJECTION_UNAVAILABLE",
              "NON_WORKING_DAY",
              "PREDECESSOR_WINDOW_CLOSED"
            ]
          },
          "message": {
            "type": "string",
            "description": "Human-readable, already carrying the numbers (e.g. \"Priya is at 140 % from 01 Aug\")."
          },
          "detail": {
            "type": "object",
            "additionalProperties": true,
            "description": "The arithmetic behind the warning, so the UI can show it rather than assert it. Deliberately open (`additionalProperties: true`) and **keyed by `code`** — the per-code shapes are documented in this schema's own description, not as a discriminated union, so a new code cannot break an existing client's parse. `LEAVE_COLLISION` and `NON_WORKING_DAY` carry the two shapes named there.\n"
          }
        }
      },
      "ProjectHealth": {
        "type": "object",
        "description": "The health chip **with its formula inputs**, so a chip is never an unexplained verdict (`WRK-F06`). `health_score` is deliberately **not** a column and not returned — the inputs are (db 06 §3).\n",
        "readOnly": true,
        "required": [
          "value",
          "health_override"
        ],
        "properties": {
          "value": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ProjectHealthValue"
              },
              {
                "type": "null"
              }
            ],
            "description": "Null when never computed — render as *Not enough data*, never as green."
          },
          "health_override": {
            "type": "boolean",
            "description": "`true` = the stored value was set by a human, is **labelled visibly**, and the recompute job does not overwrite it."
          },
          "health_computed_at": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/TimestampRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "overridden_by": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/EmployeeRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "override_reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "**Not a `work.projects` column** — read back from the audit trail (`XC-F06`) where the override action captured it, and rendered as \"Set by <name> · <reason>\" beside the computed value.\n"
          },
          "computed_value": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ProjectHealthValue"
              },
              {
                "type": "null"
              }
            ],
            "description": "What the formula says right now. Stays visible alongside an override — an override never silently replaces it."
          },
          "inputs_are_live": {
            "type": "boolean",
            "description": "`true` while `inputs` are recomputed at read time rather than read back from the snapshot the persisted `value` was derived from — which is the case today. Flips to `false` when issue **#919** persists the inputs. **Read the flag; do not assume either behaviour**, and do not present `value` and `inputs` as one coherent verdict while it is `true`.\n"
          },
          "inputs_as_of": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/TimestampRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "When `inputs` were computed — request time while `inputs_are_live` is `true`. Compare against `health_computed_at`: any gap is the window in which the chip and its breakdown describe different moments, and is what the UI should label.\n"
          },
          "inputs": {
            "type": "object",
            "description": "The formula's inputs, each rendered beside the chip on hover (\"overdue 18 % · burn 112 % · 4 stale · 2 blocked · computed 09:15\").\n**LIVE-COMPUTED, and NOT the values `value` was derived from — read this before drawing anything.** `value` is the enum `work.project_health_recompute` persisted at `health_computed_at`; these inputs are recomputed **at read time**, so the two can legitimately disagree. A client that renders them as one coherent verdict will eventually draw a `GREEN` chip beside \"burn 112 %\", explaining a verdict with numbers that did not produce it.\nTwo fields exist so that divergence is **labelled rather than discovered**: `inputs_are_live` (`true` today, always) and `inputs_as_of` (when these numbers were computed, which is request time). Compare `inputs_as_of` against `health_computed_at`: a gap means the chip and its breakdown describe different moments, and the UI should say so — *\"verdict as at 09:15, figures live\"* — instead of implying they are one snapshot. Persisting the inputs alongside the verdict so they become a single snapshot is the tracked follow-up, issue **#919**; when it lands `inputs_are_live` becomes `false` and `inputs_as_of` equals `health_computed_at`, which is why clients must read the flag rather than assume either behaviour.\n`null` inputs mean **not computable** — no tasks, no estimates — the same condition that makes `value` null and the chip *Not enough data*. It never means zero.\n**Known gap — `stale_task_count` currently has two definitions**, and which one you get depends on the path that served the response: the portfolio aggregate recomputes it live on a 3-day `tasks.updated_at` rule, while the project-health path uses an activity-based rule over `COALESCE(last_activity_at, created_at)` with a configurable threshold. Both land in this one field with **no marker distinguishing them**, so a client cannot compare the two, version-switch on them, or tell which rule produced the integer it holds. Reconciling them is tracked as issue **#915** and is deliberately out of scope here — the portfolio computation is a committed surface another slice consumes. Named rather than hidden, per api-docs `00 §6` / AGENTS.md §6.3: a spec that silently declares one integer where the system produces two invites an external consumer to build on a guarantee we do not provide.\n",
            "properties": {
              "overdue_task_pct": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/RateRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "burn_vs_estimate": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/RateRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Approved hours ÷ Σ estimated_hours. Approved time only (WRK-F09)."
              },
              "stale_task_count": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "**One field, three fillers — a known divergence (#437).** The portfolio roll-up (`work.portfolio.read`, `WRK-S18`) counts open tasks whose `tasks.updated_at` is older than **3 days**; the project-health sweep and the project-360 read count open tasks with no `work.task_activity` row newer than the shared `STALE_TASK_DAYS` threshold (**7 days**), falling back to the task's own `created_at` so a freshly created task is not born stale. Two screens can therefore state different staleness for the same project. Tracked as [#915](https://github.com/Sysmeadac/GroundIT/issues/915) and deliberately not resolved inside either slice that exposed it — one rule has to be chosen before the readers converge on it. Consumers should treat this as an *indicator*, not a figure to reconcile across screens.\n"
              },
              "blocked_count": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            }
          }
        }
      },
      "ProjectHealthOverrideInput": {
        "type": "object",
        "description": "`reason` is required when overriding; `clear: true` hands the project back to the formula and needs no value.",
        "additionalProperties": false,
        "properties": {
          "clear": {
            "type": "boolean",
            "default": false
          },
          "value": {
            "$ref": "#/components/schemas/ProjectHealthValue"
          },
          "reason": {
            "type": "string",
            "minLength": 1,
            "description": "Required on override. Captured to the audit trail (XC-F06), not to a `work.projects` column. **Known length-validation asymmetry, stated rather than papered over (2026-08-13, #429):** the running service caps this at **2000 characters on the `clear: true` path and does not cap it on the set path**, so the two halves of one endpoint validate the same field differently. No `maxLength` is declared here **deliberately** — declaring 2000 would make this spec claim a constraint the set path does not enforce, trading a code inconsistency for a spec-vs-code divergence, which is the worse of the two for a document external teams build against. A security review examined and **discarded** it as non-security (unbounded content is resource exhaustion, out of scope). Resolve by capping both paths in the service **and** adding `maxLength` here in the same change; until then, a client should not assume any upper bound on the set path.\n"
          }
        }
      },
      "ProjectTaskStatusCount": {
        "type": "object",
        "readOnly": true,
        "properties": {
          "status": {
            "$ref": "#/components/schemas/TaskStatus"
          },
          "count": {
            "type": "integer"
          }
        }
      },
      "ProjectAssigneeLoad": {
        "type": "object",
        "readOnly": true,
        "properties": {
          "employee": {
            "$ref": "#/components/schemas/EmployeeRef"
          },
          "open_count": {
            "type": "integer"
          },
          "overdue_count": {
            "type": "integer"
          },
          "blocked_count": {
            "type": "integer"
          }
        }
      },
      "ProjectBurn": {
        "type": "object",
        "description": "Estimate-vs-actual burn. **Actuals count approved `work_entries` only**; `pending_hours` states what is excluded, so burn is never inflated by unsigned time (WRK-F09).",
        "readOnly": true,
        "properties": {
          "estimated_hours": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DecimalHoursRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "approved_hours": {
            "$ref": "#/components/schemas/DecimalHoursRef"
          },
          "pending_hours": {
            "$ref": "#/components/schemas/DecimalHoursRef"
          },
          "logged_hours": {
            "$ref": "#/components/schemas/DecimalHoursRef"
          },
          "budget_hours": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DecimalHoursRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "budget_consumed_pct": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/RateRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "Null — **not 0** — when no budget is set (`WRK-S18`)."
          },
          "billable_hours": {
            "$ref": "#/components/schemas/DecimalHoursRef"
          },
          "non_billable_hours": {
            "$ref": "#/components/schemas/DecimalHoursRef"
          }
        }
      },
      "ProjectOverview": {
        "type": "object",
        "description": "WRK-S14 — the project 360 aggregate. Read-only; nothing here is a stored roll-up table.",
        "readOnly": true,
        "required": [
          "project",
          "health"
        ],
        "properties": {
          "project": {
            "$ref": "#/components/schemas/Project"
          },
          "archived_at": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/TimestampRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "Set by close-out (WRK-F17). Non-null ⇒ the project is read-only and every write surface must say so **before** the attempt, never fail at submit."
          },
          "health": {
            "$ref": "#/components/schemas/ProjectHealth"
          },
          "milestones": {
            "type": "array",
            "description": "The milestone roll-up strip (ordered by `sort_order`, then `due_date`).",
            "items": {
              "$ref": "#/components/schemas/Milestone"
            }
          },
          "tasks_by_status": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProjectTaskStatusCount"
            }
          },
          "tasks_by_assignee": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProjectAssigneeLoad"
            }
          },
          "overdue_task_count": {
            "type": "integer"
          },
          "task_count": {
            "type": "integer",
            "description": "Every undeleted task on the project (`CANCELLED` included) — the completion denominator (#1429)."
          },
          "done_count": {
            "type": "integer",
            "description": "Tasks at `DONE`. `null`, never `0 %`, is what a client renders when `task_count` is 0."
          },
          "blocked_work": {
            "type": "object",
            "description": "Tasks with an unresolved `BLOCKS` dependency, rendered as *blocked by* (WRK-F10).",
            "properties": {
              "count": {
                "type": "integer"
              },
              "tasks": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TaskRef"
                }
              }
            }
          },
          "burn": {
            "$ref": "#/components/schemas/ProjectBurn"
          },
          "allocated_member_count": {
            "type": "integer"
          }
        }
      },
      "ProjectMemberUtilization": {
        "type": "object",
        "readOnly": true,
        "properties": {
          "employee": {
            "$ref": "#/components/schemas/EmployeeRef"
          },
          "allocation_pct": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d+(\\.\\d{1,2})?$",
            "description": "numeric(5,2) **percentage** as a string (\"50.00\" = half a person) — the deliberate db 06 §5 deviation from fraction-typed ratios."
          },
          "capacity_hours": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DecimalHoursRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "approved_hours": {
            "$ref": "#/components/schemas/DecimalHoursRef"
          },
          "utilization_pct": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/RateRef"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "ProjectUtilization": {
        "type": "object",
        "description": "WRK-S14 *Time & utilization* / WRK-S18 drill — allocations × **approved** hours. Cost and margin are deliberately absent (BIL-F03, fsd 05 §1.3).",
        "readOnly": true,
        "required": [
          "project",
          "burn"
        ],
        "properties": {
          "project": {
            "$ref": "#/components/schemas/ProjectRef"
          },
          "window_from": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DateOnlyRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "window_to": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DateOnlyRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "burn": {
            "$ref": "#/components/schemas/ProjectBurn"
          },
          "by_member": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProjectMemberUtilization"
            }
          },
          "by_task": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProjectTaskHours"
            }
          },
          "work_week": {
            "type": [
              "string",
              "null"
            ],
            "description": "The market work week capacity was computed against (XC-F01, via `legal_entity_id`) — e.g. \"MON_FRI\" / \"SUN_THU\". Never hard-coded."
          },
          "billing_type": {
            "$ref": "#/components/schemas/ProjectBillingType"
          },
          "budget_amount": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "expenditure": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ProjectExpenditure"
              },
              {
                "type": "null"
              }
            ],
            "description": "Spend to date in MONEY (issue #1431) — recurring + one-time cost lines from `work.project_costs` plus salary derived from `work.project_allocations` × `pay.employee_compensation`. **Present only for a caller who also holds `work.project_cost.list`**: this is a commercial read and the block is withheld by column omission, never zeroed. `null` for a caller who holds the token but whose project has no ledger, no priceable allocation and no money budget — there is nothing to state.\n"
          },
          "cost_unavailable_reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "**Narrowed by #1431 to what still cannot be answered.** It no longer claims cost is absent — `expenditure` above states it. What remains missing is MARGIN and BILL RATES, which need `billing`'s rate model (`BIL-F03`): *\"Margin and bill rates need the billing rate model (BIL-F03); not available yet. Spend to date is expenditure only — no contract value, no invoicing.\"* Rendered as a sentence beside the expenditure figures rather than as a zero-valued margin tile (fsd 05 §1.3).\n"
          }
        }
      },
      "ProjectCostKind": {
        "type": "string",
        "enum": [
          "RECURRING",
          "ONE_TIME"
        ],
        "description": "`RECURRING` — priced per `cadence` over `[effective_from, effective_to]` (rent, laptop rental, electricity). `ONE_TIME` — a single charge on `incurred_on`. Enum-by-CHECK on `text` in the database, so the vocabulary is append-only (db 00 §9).\n"
      },
      "ProjectCostCadence": {
        "type": "string",
        "enum": [
          "MONTHLY",
          "QUARTERLY",
          "ANNUAL"
        ],
        "description": "How often a `RECURRING` line charges. Required for `RECURRING`, absent for `ONE_TIME`."
      },
      "ProjectCashbookOverview": {
        "type": "object",
        "required": [
          "project_id",
          "configured",
          "today",
          "from",
          "to",
          "opening_balance",
          "cash_balance",
          "total_received",
          "cash_spent",
          "expense_total",
          "salary_total",
          "salary_withheld",
          "total_expenditure",
          "expenditure_by_currency",
          "unpriced_allocation_count",
          "employee_dues",
          "dues_by_employee"
        ],
        "properties": {
          "project_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "configured": {
            "type": "boolean"
          },
          "currency_code": {
            "type": [
              "string",
              "null"
            ]
          },
          "today": {
            "$ref": "#/components/schemas/DateOnlyRef",
            "description": "Civil today in the tenant timezone; clients use this to bound cashbook date controls."
          },
          "from": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "to": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "opening_balance_id": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "opening_balance_version": {
            "type": [
              "integer",
              "null"
            ]
          },
          "opening_balance_effective_on": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DateOnlyRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "opening_balance": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "cash_balance": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "total_received": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "cash_spent": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "employee_advances": {
            "type": "string",
            "description": "Sum of repayments beyond what each employee was owed (advances), as of `to`."
          },
          "employee_balances": {
            "description": "Every employee with an employee-paid expense or a repayment up to `to`, owed-first (owed desc, then advance asc, then name). A repayment may exceed what was owed or go to someone owed nothing; the surplus is an advance that later employee-paid expenses absorb.",
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "employee_id",
                "employee_name",
                "owed_amount",
                "advance_amount",
                "paid_total",
                "repaid_total"
              ],
              "properties": {
                "employee_id": {
                  "$ref": "#/components/schemas/UuidRef"
                },
                "employee_name": {
                  "type": "string"
                },
                "owed_amount": {
                  "$ref": "#/components/schemas/MoneyRef"
                },
                "advance_amount": {
                  "$ref": "#/components/schemas/MoneyRef"
                },
                "paid_total": {
                  "$ref": "#/components/schemas/MoneyRef"
                },
                "repaid_total": {
                  "$ref": "#/components/schemas/MoneyRef"
                }
              }
            }
          },
          "cash_adjustment": {
            "type": "string",
            "description": "Net cash-count difference posted in the range (counted - book)."
          },
          "last_count": {
            "description": "Most recent cash count on or before `to`.",
            "anyOf": [
              {
                "type": "object",
                "required": [
                  "date",
                  "counted_amount",
                  "difference"
                ],
                "properties": {
                  "date": {
                    "$ref": "#/components/schemas/DateOnlyRef"
                  },
                  "counted_amount": {
                    "$ref": "#/components/schemas/MoneyRef"
                  },
                  "difference": {
                    "type": "string"
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "expense_total": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "Recurring plus one-time expenses, excluding salary; null when expenses span multiple currencies."
          },
          "salary_total": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "salary_withheld": {
            "type": "boolean",
            "description": "True when the existing salary visibility guard withholds compensation totals."
          },
          "total_expenditure": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "Expenses plus salary when permitted; null when currencies are mixed or salary is withheld."
          },
          "expenditure_by_currency": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProjectExpenditureByCurrency"
            }
          },
          "unpriced_allocation_count": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Allocations whose salary cannot be priced; null when salary visibility is withheld."
          },
          "employee_dues": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "dues_by_employee": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "employee_id",
                "employee_name",
                "amount"
              ],
              "properties": {
                "employee_id": {
                  "$ref": "#/components/schemas/UuidRef"
                },
                "employee_name": {
                  "type": "string"
                },
                "amount": {
                  "$ref": "#/components/schemas/MoneyRef"
                }
              }
            }
          }
        }
      },
      "ProjectCashbookDay": {
        "type": "object",
        "required": [
          "project_id",
          "date",
          "currency_code",
          "opening_balance",
          "received",
          "cash_spent",
          "employee_repayments",
          "closing_balance",
          "worker_count",
          "worker_count_version",
          "entries"
        ],
        "properties": {
          "project_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "date": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "currency_code": {
            "type": "string"
          },
          "opening_balance": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "received": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "cash_spent": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "employee_repayments": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "closing_balance": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "worker_count": {
            "type": [
              "integer",
              "null"
            ]
          },
          "worker_count_version": {
            "type": [
              "integer",
              "null"
            ]
          },
          "entries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProjectCashbookEntry"
            }
          },
          "cash_count": {
            "description": "The day's live cash count, when one was recorded; its difference is already in closing_balance.",
            "anyOf": [
              {
                "type": "object",
                "required": [
                  "id",
                  "counted_amount",
                  "book_amount",
                  "difference",
                  "notes",
                  "version"
                ],
                "properties": {
                  "id": {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  "counted_amount": {
                    "$ref": "#/components/schemas/MoneyRef"
                  },
                  "book_amount": {
                    "$ref": "#/components/schemas/MoneyRef"
                  },
                  "difference": {
                    "type": "string",
                    "description": "counted - book; negative is a shortage."
                  },
                  "notes": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "version": {
                    "type": "integer"
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "ProjectCashbookCashCount": {
        "type": "object",
        "required": [
          "id",
          "project_id",
          "kind",
          "date",
          "counted_amount",
          "book_amount",
          "difference",
          "currency_code",
          "notes",
          "version"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "project_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "kind": {
            "const": "CASH_COUNT"
          },
          "date": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "counted_amount": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "book_amount": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "difference": {
            "type": "string",
            "description": "counted - book; negative is a shortage."
          },
          "currency_code": {
            "type": "string"
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "version": {
            "type": "integer"
          }
        }
      },
      "ProjectCashbookEntry": {
        "type": "object",
        "required": [
          "id",
          "type",
          "date",
          "amount",
          "currency_code",
          "label",
          "paid_from",
          "employee_id",
          "employee_name",
          "bill_status",
          "notes",
          "version"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "type": {
            "type": "string",
            "enum": [
              "EXPENSE",
              "RECEIPT",
              "REPAYMENT"
            ]
          },
          "date": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "amount": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "currency_code": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "paid_from": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "PROJECT_BALANCE",
              "EMPLOYEE",
              null
            ]
          },
          "employee_id": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "employee_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "bill_status": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "WITH_BILL",
              "WITHOUT_BILL",
              null
            ]
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "version": {
            "type": "integer",
            "description": "ETag is \"v<version>\"."
          }
        }
      },
      "ProjectCashbookOpening": {
        "type": "object",
        "required": [
          "id",
          "project_id",
          "kind",
          "effective_on",
          "amount",
          "currency_code",
          "version"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "project_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "kind": {
            "const": "OPENING_BALANCE"
          },
          "effective_on": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "amount": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "currency_code": {
            "type": "string"
          },
          "version": {
            "type": "integer"
          }
        }
      },
      "ProjectCashbookOpeningCreate": {
        "type": "object",
        "required": [
          "effective_on",
          "amount",
          "currency_code"
        ],
        "additionalProperties": false,
        "properties": {
          "effective_on": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "amount": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "currency_code": {
            "type": "string",
            "minLength": 3,
            "maxLength": 3
          }
        }
      },
      "ProjectCashbookOpeningUpdate": {
        "type": "object",
        "minProperties": 1,
        "additionalProperties": false,
        "properties": {
          "effective_on": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "amount": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "currency_code": {
            "type": "string",
            "minLength": 3,
            "maxLength": 3
          }
        }
      },
      "ProjectCashbookWorkerCount": {
        "type": "object",
        "required": [
          "worker_count"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "project_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "date": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "worker_count": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100000
          },
          "version": {
            "type": "integer"
          }
        }
      },
      "ProjectCashbookMovementCreate": {
        "type": "object",
        "required": [
          "date",
          "amount"
        ],
        "additionalProperties": false,
        "properties": {
          "date": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "amount": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "notes": {
            "type": "string",
            "maxLength": 2000
          }
        }
      },
      "ProjectCashbookRepaymentCreate": {
        "type": "object",
        "required": [
          "date",
          "amount",
          "employee_id"
        ],
        "additionalProperties": false,
        "properties": {
          "date": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "amount": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "employee_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "notes": {
            "type": "string",
            "maxLength": 2000
          }
        }
      },
      "ProjectCashbookEntryUpdate": {
        "type": "object",
        "minProperties": 1,
        "additionalProperties": false,
        "properties": {
          "date": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "amount": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "employee_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 2000
          }
        }
      },
      "ProjectCost": {
        "type": "object",
        "description": "One `work.project_costs` row plus its derived spend to date. Amounts are decimal STRINGS (db 00 §6) — never a JSON number.",
        "required": [
          "id",
          "project_id",
          "kind",
          "label",
          "amount",
          "currency_code",
          "version"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "project_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "kind": {
            "$ref": "#/components/schemas/ProjectCostKind"
          },
          "label": {
            "type": "string",
            "description": "What the money is for — \"Office rent (Bengaluru)\", \"Laptop rental ×4\". Non-blank."
          },
          "amount": {
            "$ref": "#/components/schemas/MoneyRef",
            "description": "For `RECURRING`, the amount PER `cadence` period. For `ONE_TIME`, the whole charge."
          },
          "currency_code": {
            "type": "string",
            "minLength": 3,
            "maxLength": 3,
            "description": "ISO-4217 of THIS line. Required, and not defaulted from the project (see the create operation)."
          },
          "cadence": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ProjectCostCadence"
              },
              {
                "type": "null"
              }
            ]
          },
          "effective_from": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DateOnlyRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "effective_to": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DateOnlyRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "Inclusive window end for a `RECURRING` line; **null = still running**. The closed window is the record of what the line cost while it ran."
          },
          "incurred_on": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DateOnlyRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "category_code": {
            "type": [
              "string",
              "null"
            ],
            "description": "A system code or one of the tenant's own (`GET /project-cost-categories`). Shape-checked in the database, membership-checked in the service — a system code needs no per-tenant row."
          },
          "payment_method": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ProjectCostPaymentMethod"
              },
              {
                "type": "null"
              }
            ]
          },
          "payee": {
            "type": [
              "string",
              "null"
            ],
            "description": "Who was paid — the vendor, shop or contractor. Non-blank when present."
          },
          "paid_by_employee_id": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "payment_source": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "PROJECT_BALANCE",
              "EMPLOYEE",
              null
            ],
            "description": "Classifies a one-time cost as cash-funded or employee-funded. Null means historical/non-cashbook."
          },
          "bill_status": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "WITH_BILL",
              "WITHOUT_BILL",
              null
            ],
            "description": "Manager-recorded status, independent of whether an attachment is present."
          },
          "paid_by": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ProjectCostPayer"
              },
              {
                "type": "null"
              }
            ],
            "description": "The `paid_by_employee_id` employee, resolved to a display name. Null when the line records no payer."
          },
          "quantity": {
            "type": [
              "string",
              "null"
            ],
            "description": "`numeric(12,3)` as a decimal STRING; strictly positive when present."
          },
          "unit": {
            "type": [
              "string",
              "null"
            ],
            "description": "Free text — \"litre\", \"each\", \"person-day\". Not a controlled vocabulary."
          },
          "invoice_no": {
            "type": [
              "string",
              "null"
            ]
          },
          "tax_amount": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "Tax **INCLUDED IN** `amount`, never added to it — so `tax_amount <= amount` is a CHECK, not a convention, and the net figure is `amount - tax_amount`. In the same `currency_code` as the line.\n"
          },
          "vendor_tax_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "GSTIN (India) / VAT number (KSA). Free text: the format lives in the market pack, never in a CHECK."
          },
          "counts_against_budget": {
            "type": "boolean",
            "description": "`false` = **spent, but not against this project''s budget** (a client-reimbursed courier, a line another cost centre carries). The line stays in `one_time_total` and `total` because the money did leave, and leaves `budget_consumed_pct` and `budget_applicable_total` because it is not what the budget was set aside for. Defaults to `true`.\n"
          },
          "asset_id": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "asset": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ProjectCostAsset"
              },
              {
                "type": "null"
              }
            ],
            "description": "The `assets.assets` row this line registered, if any. Minted in the same transaction as the line — an asset with no ledger row behind it is a reconciliation nobody can perform after the fact."
          },
          "attachments": {
            "type": "array",
            "description": "Receipt **metadata only** — never a URL and never the `storage_key`. A list that handed back live object URLs would make every ledger read a bulk disclosure of receipt images. Mint one with `GET …/attachments/{attachmentId}/download-url`.\n",
            "items": {
              "$ref": "#/components/schemas/ProjectCostAttachment"
            }
          },
          "voided_at": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/TimestampRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "voided_by": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "void_reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Mandatory whenever `voided_at` is set — the biconditional is a database CHECK. A voided line has `spend_to_date` `\"0.00\"` and is out of every total."
          },
          "created_by": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "updated_by": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "spend_to_date": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "What this line has accrued by `as_of`, in the line's own `currency_code` — `amount × elapsed cadence periods` for a `RECURRING` line whose window has started, `amount` for a `ONE_TIME` line already incurred, and `\"0.00\"` for a line whose window or date is still in the future. Never null for a stored line; the type admits null only so a future unpriceable shape does not have to send a fabricated zero.\n"
          },
          "periods_elapsed": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Whole cadence periods counted into `spend_to_date`; null for a `ONE_TIME` line, which has no cadence."
          },
          "created_at": {
            "$ref": "#/components/schemas/TimestampRef"
          },
          "updated_at": {
            "$ref": "#/components/schemas/TimestampRef"
          },
          "version": {
            "type": "integer",
            "description": "Optimistic-concurrency counter; the `ETag` is `\"v<version>\"`."
          }
        }
      },
      "ProjectCostCreate": {
        "type": "object",
        "required": [
          "kind",
          "label",
          "amount",
          "currency_code"
        ],
        "additionalProperties": false,
        "properties": {
          "kind": {
            "$ref": "#/components/schemas/ProjectCostKind"
          },
          "label": {
            "type": "string",
            "minLength": 1
          },
          "amount": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "currency_code": {
            "type": "string",
            "minLength": 3,
            "maxLength": 3
          },
          "cadence": {
            "$ref": "#/components/schemas/ProjectCostCadence"
          },
          "effective_from": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "effective_to": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "incurred_on": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "notes": {
            "type": "string"
          },
          "category_code": {
            "type": "string",
            "maxLength": 40,
            "pattern": "^[A-Z][A-Z0-9_]{1,39}$"
          },
          "payment_method": {
            "$ref": "#/components/schemas/ProjectCostPaymentMethod"
          },
          "payee": {
            "type": "string",
            "maxLength": 200
          },
          "paid_by_employee_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "payment_source": {
            "type": "string",
            "enum": [
              "PROJECT_BALANCE",
              "EMPLOYEE"
            ]
          },
          "bill_status": {
            "type": "string",
            "enum": [
              "WITH_BILL",
              "WITHOUT_BILL"
            ]
          },
          "quantity": {
            "type": "string",
            "description": "Positive decimal string, up to 3 decimals."
          },
          "unit": {
            "type": "string",
            "maxLength": 40
          },
          "invoice_no": {
            "type": "string",
            "maxLength": 100
          },
          "tax_amount": {
            "$ref": "#/components/schemas/MoneyRef",
            "description": "Included in `amount`; `422` when it exceeds it."
          },
          "vendor_tax_id": {
            "type": "string",
            "maxLength": 40
          },
          "counts_against_budget": {
            "type": "boolean",
            "default": true
          },
          "attachments": {
            "type": "array",
            "maxItems": 10,
            "description": "Receipts already uploaded through the file upload-url endpoint. Each `storage_key` must sit under this workspace''s own `<tenantId>/uploads/` prefix and the object must exist, or the call is refused — otherwise a caller could attach any object in their tenant (a payslip, someone else''s tax proof) to a cost line and have the API mint a download URL for it. **Only the new-file shape is legal here**: the line does not exist yet, so the update body''s `{ id }` retain form has nothing to refer to and is a `422`.\n",
            "items": {
              "$ref": "#/components/schemas/ProjectCostAttachmentInput"
            }
          },
          "register_as_asset": {
            "$ref": "#/components/schemas/ProjectCostAssetRegistration",
            "description": "Register what this line bought in `assets.assets`, in the SAME transaction. `ONE_TIME` only — a recurring line''s `amount` is a per-period charge, so there is no acquisition value, and a monthly laptop *rental* is exactly the case where the tenant does not own the thing. The asset takes its `legal_entity_id` from the **project**, its `acquisition_value_amount` from `amount`, and its `acquired_on` from `incurred_on`.\n"
          }
        }
      },
      "ProjectCostUpdate": {
        "type": "object",
        "description": "Partial edit. `kind` is deliberately absent — a stored `amount` means something different under each kind, so switching is a delete plus an add, never a silent reinterpretation.",
        "additionalProperties": false,
        "minProperties": 1,
        "properties": {
          "label": {
            "type": "string",
            "minLength": 1
          },
          "amount": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "currency_code": {
            "type": "string",
            "minLength": 3,
            "maxLength": 3
          },
          "cadence": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ProjectCostCadence"
              },
              {
                "type": "null"
              }
            ]
          },
          "effective_from": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DateOnlyRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "effective_to": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DateOnlyRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "incurred_on": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DateOnlyRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "category_code": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 40
          },
          "payment_method": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ProjectCostPaymentMethod"
              },
              {
                "type": "null"
              }
            ]
          },
          "payee": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200
          },
          "paid_by_employee_id": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "payment_source": {
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "PROJECT_BALANCE",
                  "EMPLOYEE"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "bill_status": {
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "WITH_BILL",
                  "WITHOUT_BILL"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "quantity": {
            "type": [
              "string",
              "null"
            ]
          },
          "unit": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 40
          },
          "invoice_no": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 100
          },
          "tax_amount": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "vendor_tax_id": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 40
          },
          "counts_against_budget": {
            "type": "boolean"
          },
          "attachments": {
            "type": "array",
            "maxItems": 10,
            "description": "**Present ⇒ replace the whole set**; absent ⇒ leave the receipts alone. The distinction matters: a client patching only `payee` must not silently drop receipts it never sent. Removed rows are soft-deleted on both planes (the attachment row and its `xc.object_refs` entry) — a receipt that was on a line and then was not is itself a fact.\n\nEach element is **either** `{ id }` — *keep this receipt, which is already on this line* — **or** the new-file shape. The retain form exists because the ledger read deliberately returns receipt METADATA and never the `storage_key`: without it a client adding one file could not re-send the others, so \"add a receipt\" would silently DELETE every receipt already there. Echo back the `id`s you were given, append the new files, and the set you mean is the set you get. A retained receipt is left completely untouched — not re-verified against the object store, and no second `xc.object_refs` row is minted for a file that already has one. An `id` that is unknown, already removed, or belongs to a different cost line is a `422` pointing at that array element; `{ id }` mixed with file fields in the same element is refused rather than silently resolved one way. A `storage_key` that is already attached to a DIFFERENT live cost line is a `422` at that element too: the object-key uniqueness is workspace-wide, and silently dropping the row would report success while filing nothing.\n",
            "items": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/ProjectCostAttachmentRetain"
                },
                {
                  "$ref": "#/components/schemas/ProjectCostAttachmentInput"
                }
              ]
            }
          },
          "register_as_asset": {
            "$ref": "#/components/schemas/ProjectCostAssetRegistration",
            "description": "Allowed **only while `asset_id` is still null** (`409` otherwise). Re-running it would mint a second `assets.assets` row for the same purchase and orphan the first in the register — a duplicate a physical audit then has to chase down.\n"
          }
        }
      },
      "ProjectSalaryCostLine": {
        "type": "object",
        "description": "A **derived, read-only** salary cost for one live allocation — the owner's rule exactly: a member allocated 100 % to a project costs the project 100 % of their salary. Not a `work.project_costs` row and never written; recomputing it on read is what keeps a pay revision from silently ceasing to be reflected.\n",
        "readOnly": true,
        "required": [
          "employee",
          "allocation_pct",
          "window_from",
          "window_to"
        ],
        "properties": {
          "employee": {
            "$ref": "#/components/schemas/EmployeeRef"
          },
          "allocation_pct": {
            "type": "string",
            "description": "The allocation percentage as a decimal string (\"100.00\" = the whole person)."
          },
          "window_from": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "window_to": {
            "$ref": "#/components/schemas/DateOnlyRef",
            "description": "The allocation window clipped to `min(effective_to, as_of)` — cost accrues for elapsed time only, never for a window still to come."
          },
          "months": {
            "type": [
              "string",
              "null"
            ],
            "description": "Elapsed months of the clipped window, as a decimal string (a part-month is a fraction of the month's length). Null when the window has not started."
          },
          "monthly_cost": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "`ctc_amount / 12 × allocation_pct / 100`. Null when the employee has no priceable compensation row."
          },
          "cost": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "`monthly_cost × months`. **Null, never zero**, when the salary cannot be priced — see `unpriced_reason`."
          },
          "currency_code": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO-4217 of the compensation row, which follows the employee's legal entity (db 07 §1.1). Null when unpriced."
          },
          "unpriced_reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why `cost` is null. `NO_COMPENSATION_ROW` — nothing effective for this employee in the window. `NON_SALARIED_PAY_MODEL` — a `DAILY_WAGE` or `PIECE_RATE` worker, priced from a rate card or piece-rate catalogue and carrying no CTC (ADR 0033); pricing them needs the attendance/muster record and is out of scope here. `NO_CTC_AMOUNT` — a salaried row whose `ctc_amount` is absent.\n",
            "enum": [
              "NO_COMPENSATION_ROW",
              "NON_SALARIED_PAY_MODEL",
              "NO_CTC_AMOUNT",
              null
            ]
          }
        }
      },
      "ProjectExpenditureByCurrency": {
        "type": "object",
        "description": "Totals for ONE currency. The only place a sum is ever stated when a project mixes currencies.",
        "required": [
          "currency_code"
        ],
        "properties": {
          "currency_code": {
            "type": "string",
            "minLength": 3,
            "maxLength": 3
          },
          "recurring_total": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "one_time_total": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "expense_total": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "salary_total": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "total": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "budget_applicable_total": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "`total` minus the off-budget lines — the numerator `budget_consumed_pct` divides. Salary is never off-budget: a person allocated to a project costs that project, and there is no per-allocation flag to say otherwise (#1597)."
          },
          "off_budget_total": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "Σ over lines flagged `counts_against_budget: false`. **Spent, but not against this budget** — stated rather than hidden, so the surface can say which is which (#1597)."
          }
        }
      },
      "ProjectExpenditure": {
        "type": "object",
        "description": "Spend to date against the project's money budget. Every scalar is **null when unknown, never zero** — \"no cost lines and no priceable allocations\" and \"₹0 spent\" are different facts and a zero-valued tile asserts the second.\n",
        "readOnly": true,
        "required": [
          "as_of",
          "by_currency",
          "mixed_currency"
        ],
        "properties": {
          "salary_withheld": {
            "type": "boolean",
            "description": "True for operational project roles: salary lines and salary-inclusive totals are withheld; recurring and one-time expenses remain visible. Never interpret omitted compensation as zero salary."
          },
          "as_of": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "recurring_total": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "Σ `spend_to_date` over `RECURRING` lines. Null when the project has none, or when its lines span more than one currency."
          },
          "one_time_total": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "salary_total": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "Σ `cost` over the derived salary lines. Unpriced allocations are EXCLUDED, not zeroed — `unpriced_allocation_count` says how many."
          },
          "total": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "The three totals added. **Null when `mixed_currency` is true** — read `by_currency` instead. Voided lines are excluded (#1597)."
          },
          "budget_applicable_total": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "`total` minus `off_budget_total` — what `budget_consumed_pct` is computed from (#1597)."
          },
          "off_budget_total": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "Σ over lines flagged `counts_against_budget: false`. Render it beside \"Spent\" rather than folding it away — a spend the budget does not carry is still a spend (#1597)."
          },
          "currency_code": {
            "type": [
              "string",
              "null"
            ],
            "description": "The single currency `total` is stated in; null when there is nothing to total or the project mixes currencies."
          },
          "budget_amount": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "budget_currency_code": {
            "type": [
              "string",
              "null"
            ]
          },
          "budget_consumed_pct": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/RateRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "**MONEY** consumption — `budget_applicable_total ÷ budget_amount` as a `Rate` fraction, the sibling of `ProjectBurn.budget_consumed_pct` which is HOURS. May exceed 1. Null when there is no money budget, nothing to total, or the spend currency differs from the budget currency — comparing two currencies without a stated rate is the fabrication this refuses. **Since #1597 the numerator is the budget-APPLICABLE total, not `total`**: a line the operator marked `counts_against_budget: false` moves \"spent\" and does not move this percentage. Voided lines move neither.\n"
          },
          "mixed_currency": {
            "type": "boolean",
            "description": "True when the contributing lines carry more than one `currency_code`. The scalar totals are then null and `by_currency` is the answer."
          },
          "by_currency": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProjectExpenditureByCurrency"
            }
          },
          "unpriced_allocation_count": {
            "type": "integer",
            "description": "Live allocations whose salary could not be priced. A non-zero value means `salary_total` understates the truth, and the UI must say so rather than present the figure as complete."
          },
          "cost_line_count": {
            "type": "integer",
            "description": "Live (non-voided, non-deleted) cost lines on the project."
          }
        }
      },
      "ProjectCostLedger": {
        "type": "object",
        "description": "The *Costs* tab payload — stored lines, derived salary lines, and the roll-up.",
        "readOnly": true,
        "required": [
          "project",
          "entries",
          "salary_lines",
          "expenditure"
        ],
        "properties": {
          "project": {
            "$ref": "#/components/schemas/ProjectRef"
          },
          "as_of": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "entries": {
            "type": "array",
            "description": "The lines matching the query filters. **Filtered**, unlike `expenditure`.",
            "items": {
              "$ref": "#/components/schemas/ProjectCost"
            }
          },
          "salary_lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProjectSalaryCostLine"
            }
          },
          "expenditure": {
            "$ref": "#/components/schemas/ProjectExpenditure",
            "description": "Always the WHOLE project, never the filtered set — a total that silently answered a filtered question is the defect class this feature exists to avoid. Read `summary.filtered` for the filtered numbers."
          },
          "summary": {
            "type": "object",
            "description": "Totals for exactly what `entries` contains (#1597).",
            "required": [
              "filtered"
            ],
            "properties": {
              "filtered": {
                "$ref": "#/components/schemas/ProjectCostFilteredSummary"
              }
            }
          }
        }
      },
      "ProjectCostPaymentMethod": {
        "type": "string",
        "enum": [
          "PETTY_CASH",
          "BANK_TRANSFER",
          "UPI",
          "CARD",
          "CHEQUE",
          "CREDIT",
          "OTHER"
        ],
        "description": "How the spend was paid. Enum-by-CHECK on `text` in the database, so the vocabulary is append-only (db 00 §9) and a later rail can join it without an enum rewrite. `UPI` is India-shaped and simply unused in KSA — the vocabulary is market-neutral by construction.\n"
      },
      "ProjectCostPayer": {
        "type": "object",
        "description": "Whoever laid out the money — usually a site supervisor out of the tin.",
        "readOnly": true,
        "required": [
          "employee_id"
        ],
        "properties": {
          "employee_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "display_name": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "ProjectCostAsset": {
        "type": "object",
        "description": "The `assets.assets` row a `ONE_TIME` line registered. Read-only here — edit it in the asset register.",
        "readOnly": true,
        "required": [
          "id"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "asset_no": {
            "type": [
              "string",
              "null"
            ],
            "description": "Server-assigned `AST-<n>` from the register's own sequence."
          }
        }
      },
      "ProjectCostAttachment": {
        "type": "object",
        "description": "Receipt METADATA. Deliberately carries neither a URL nor the `storage_key` — see the download-url operation.",
        "readOnly": true,
        "required": [
          "id",
          "file_name",
          "content_type",
          "size_bytes"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "file_name": {
            "type": "string"
          },
          "content_type": {
            "type": "string"
          },
          "size_bytes": {
            "type": "integer",
            "description": "Strictly positive — a present row means a present file, which is what makes the \"missing receipt\" marker trustworthy."
          },
          "created_at": {
            "$ref": "#/components/schemas/TimestampRef"
          }
        }
      },
      "ProjectCostAttachmentRetain": {
        "type": "object",
        "description": "**Keep a receipt that is already on this cost line.** Update-body only — on create there is no line yet and so nothing to keep, and an `id` there would be a reference to somebody else's row (`422`). `id` is the value the ledger read returned in `ProjectCost.attachments[].id`.\n",
        "required": [
          "id"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "$ref": "#/components/schemas/UuidRef"
          }
        }
      },
      "ProjectCostAttachmentInput": {
        "type": "object",
        "description": "A receipt already uploaded through the file upload-url endpoint. The bytes never transit this API.",
        "required": [
          "storage_key",
          "file_name",
          "content_type",
          "size_bytes"
        ],
        "additionalProperties": false,
        "properties": {
          "storage_key": {
            "type": "string",
            "description": "Must start with `<tenantId>/uploads/` and must already exist in the object store."
          },
          "file_name": {
            "type": "string"
          },
          "content_type": {
            "type": "string"
          },
          "size_bytes": {
            "type": "integer",
            "minimum": 1
          },
          "content_hash": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{64}$",
            "description": "sha256 hex. Its presence decides the `xc.object_refs` lifecycle tier — a `RETAINED` object must be content-addressable, so an unhashed upload stays `TEMPORARY`."
          }
        }
      },
      "ProjectCostAssetRegistration": {
        "type": "object",
        "description": "Register what a `ONE_TIME` line bought, in `assets.assets`, in the same transaction as the line.",
        "required": [
          "name"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "category": {
            "type": "string",
            "enum": [
              "EQUIPMENT",
              "TOOL",
              "LAPTOP",
              "PHONE",
              "SIM",
              "ACCESS_CARD",
              "VEHICLE",
              "MONITOR",
              "PERIPHERAL",
              "OTHER"
            ],
            "default": "EQUIPMENT",
            "description": "`assets.asset_category`. `EQUIPMENT` and `TOOL` were added for this feature (migration `0220`) — the register was authored for the office estate and had no member for site plant."
          },
          "serial_no": {
            "type": "string",
            "maxLength": 120
          },
          "asset_tag": {
            "type": "string",
            "maxLength": 120
          }
        }
      },
      "ProjectCostVoid": {
        "type": "object",
        "description": "The mandatory reason a void carries. Stored on the row; `(voided_at IS NULL) = (void_reason IS NULL)` is a database CHECK.",
        "required": [
          "reason"
        ],
        "additionalProperties": false,
        "properties": {
          "reason": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500
          }
        }
      },
      "ProjectCostFilteredSummary": {
        "type": "object",
        "description": "Totals for exactly the lines `entries` contains. `total` sums **spend to date** — the same figure each row shows, so a footer and its rows can never disagree — while `tax_total` sums `tax_amount` **as entered**, once per line and never multiplied by a recurring line''s elapsed periods: the tax on an invoice is a fact about that invoice, and accruing it would invent tax nobody was charged. There is no scalar total across currencies, by design.\n",
        "readOnly": true,
        "required": [
          "count",
          "by_currency",
          "by_category",
          "missing_receipt_count"
        ],
        "properties": {
          "count": {
            "type": "integer"
          },
          "by_currency": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "currency_code",
                "total"
              ],
              "properties": {
                "currency_code": {
                  "type": "string",
                  "minLength": 3,
                  "maxLength": 3
                },
                "total": {
                  "$ref": "#/components/schemas/MoneyRef"
                },
                "tax_total": {
                  "$ref": "#/components/schemas/MoneyRef"
                }
              }
            }
          },
          "by_category": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "category_code",
                "currency_code",
                "total",
                "count"
              ],
              "properties": {
                "category_code": {
                  "type": "string",
                  "description": "Lines with no category are reported under `UNCATEGORISED` rather than dropped — otherwise the breakdown would silently fail to add up to the currency total."
                },
                "currency_code": {
                  "type": "string",
                  "minLength": 3,
                  "maxLength": 3
                },
                "total": {
                  "$ref": "#/components/schemas/MoneyRef"
                },
                "count": {
                  "type": "integer"
                }
              }
            }
          },
          "missing_receipt_count": {
            "type": "integer",
            "description": "Lines in the filtered set carrying no live receipt — the amber marker's own number."
          }
        }
      },
      "ProjectCostExportRequest": {
        "type": "object",
        "description": "The list's filter vocabulary, as a body. One builder serves the grid and its export, so the two can never describe different sets.",
        "additionalProperties": false,
        "properties": {
          "kind": {
            "$ref": "#/components/schemas/ProjectCostKind"
          },
          "category": {
            "type": "string",
            "maxLength": 40
          },
          "payment_method": {
            "$ref": "#/components/schemas/ProjectCostPaymentMethod"
          },
          "paid_by": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "incurred_from": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "incurred_to": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "has_receipt": {
            "type": "boolean"
          },
          "include_voided": {
            "type": "boolean",
            "default": false
          },
          "q": {
            "type": "string",
            "maxLength": 200
          }
        }
      },
      "ProjectExpenditureReportExportRequest": {
        "type": "object",
        "description": "The report's filters, as a body. `group_by` is absent: the export is line-level, not folded.",
        "additionalProperties": false,
        "properties": {
          "incurred_from": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "incurred_to": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "project_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "category": {
            "type": "string",
            "maxLength": 40
          },
          "payment_method": {
            "$ref": "#/components/schemas/ProjectCostPaymentMethod"
          },
          "include_voided": {
            "type": "boolean",
            "default": false
          }
        }
      },
      "ProjectCostExportHandle": {
        "type": "object",
        "description": "A generated CSV, handed back as a **short-lived presigned GET**. The bytes never come back through the API (db 00 §14) and the URL is never persisted: the idempotency ledger stores the object key and the link is minted after it on both the fresh and the replayed path, so a retry gets the same artifact through a fresh link rather than one that expired while the caller was retrying.\n",
        "readOnly": true,
        "required": [
          "url",
          "file_name",
          "expires_at"
        ],
        "properties": {
          "file_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "file_name": {
            "type": "string"
          },
          "mime_type": {
            "type": "string"
          },
          "size_bytes": {
            "type": "integer"
          },
          "content_hash": {
            "type": "string",
            "description": "sha256 of the generated bytes — the same digest recorded on the audit row."
          },
          "expires_at": {
            "$ref": "#/components/schemas/TimestampRef"
          }
        }
      },
      "AttachmentDownloadUrl": {
        "type": "object",
        "description": "A freshly minted, five-minute presigned GET for one receipt. Long enough to open the file, short enough that a copied link is not a share.",
        "readOnly": true,
        "required": [
          "url",
          "file_name",
          "expires_at"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri"
          },
          "file_name": {
            "type": "string"
          },
          "expires_at": {
            "$ref": "#/components/schemas/TimestampRef"
          }
        }
      },
      "ProjectCostCategory": {
        "type": "object",
        "description": "One expenditure category. `source` tells the console which rows it may rename — a `SYSTEM` row has no `id` and is never editable.",
        "required": [
          "code",
          "name",
          "source",
          "active"
        ],
        "properties": {
          "id": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "Null for a `SYSTEM` code, which is a constant in the API rather than a row."
          },
          "code": {
            "type": "string",
            "pattern": "^[A-Z][A-Z0-9_]{1,39}$"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "source": {
            "type": "string",
            "enum": [
              "SYSTEM",
              "TENANT"
            ]
          },
          "active": {
            "type": "boolean",
            "description": "A `SYSTEM` code is always active — there is deliberately no per-tenant \"disable EQUIPMENT\" switch, because the fixed half of the list is what makes the cross-project report comparable."
          },
          "sort_order": {
            "type": "integer"
          },
          "created_at": {
            "$ref": "#/components/schemas/TimestampRef"
          },
          "updated_at": {
            "$ref": "#/components/schemas/TimestampRef"
          },
          "version": {
            "type": "integer"
          }
        }
      },
      "ProjectCostCategoryCreate": {
        "type": "object",
        "required": [
          "code",
          "name"
        ],
        "additionalProperties": false,
        "properties": {
          "code": {
            "type": "string",
            "maxLength": 40,
            "description": "Uppercased on the way in. `422` when it collides with a system code; `409` when this workspace already has it."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "description": {
            "type": "string",
            "maxLength": 500
          },
          "sort_order": {
            "type": "integer",
            "minimum": 0,
            "default": 100
          }
        }
      },
      "ProjectCostCategoryUpdate": {
        "type": "object",
        "description": "`code` is deliberately absent — it is stored on every line that ever used the category, so editing it would orphan those lines or silently re-label history. Deactivating is the supported retirement.",
        "additionalProperties": false,
        "minProperties": 1,
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 500
          },
          "active": {
            "type": "boolean"
          },
          "sort_order": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "ProjectExpenditureReport": {
        "type": "object",
        "description": "The tenant-wide expenditure page's payload. `ONE_TIME` lines only — a recurring line is a run rate, not a logged expenditure.",
        "readOnly": true,
        "required": [
          "range",
          "group_by",
          "groups",
          "totals_by_currency",
          "projects"
        ],
        "properties": {
          "range": {
            "type": "object",
            "required": [
              "from",
              "to"
            ],
            "properties": {
              "from": {
                "$ref": "#/components/schemas/DateOnlyRef"
              },
              "to": {
                "$ref": "#/components/schemas/DateOnlyRef"
              }
            }
          },
          "group_by": {
            "type": "string",
            "enum": [
              "project",
              "category",
              "month",
              "payment_method"
            ]
          },
          "groups": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProjectExpenditureReportGroup"
            }
          },
          "totals_by_currency": {
            "type": "array",
            "description": "The footer — one row per currency, never a scalar across them.",
            "items": {
              "type": "object",
              "required": [
                "currency_code",
                "total",
                "count"
              ],
              "properties": {
                "currency_code": {
                  "type": "string",
                  "minLength": 3,
                  "maxLength": 3
                },
                "total": {
                  "$ref": "#/components/schemas/MoneyRef"
                },
                "tax_total": {
                  "$ref": "#/components/schemas/MoneyRef"
                },
                "count": {
                  "type": "integer"
                },
                "missing_receipt_count": {
                  "type": "integer"
                }
              }
            }
          },
          "projects": {
            "type": "array",
            "description": "The filter bar's project picker, confined exactly the way the report is — so it can never offer a project whose rows the report would then withhold.",
            "items": {
              "$ref": "#/components/schemas/ProjectRef"
            }
          }
        }
      },
      "ProjectExpenditureReportGroup": {
        "type": "object",
        "description": "One fold of the report. **Currency is part of the key**, so a project that spent in two currencies appears as two rows.",
        "readOnly": true,
        "required": [
          "key",
          "label",
          "currency_code",
          "total",
          "count"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "The project `id`, the category code, the `YYYY-MM` month, or the payment method — per `group_by`. `UNCATEGORISED` / `UNSPECIFIED` for rows carrying neither."
          },
          "label": {
            "type": "string",
            "description": "Display string: `CODE · Name` for a project, the category's resolved name (system or tenant), otherwise the key."
          },
          "currency_code": {
            "type": "string",
            "minLength": 3,
            "maxLength": 3
          },
          "total": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "tax_total": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "count": {
            "type": "integer"
          },
          "missing_receipt_count": {
            "type": "integer"
          }
        }
      },
      "TaskDisposition": {
        "type": "object",
        "description": "One open task's close-out decision. **Every non-terminal task must carry one** — that gate is the whole point of `WRK-S23` and is not skippable.",
        "required": [
          "task_id",
          "disposition"
        ],
        "additionalProperties": false,
        "properties": {
          "task_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "disposition": {
            "type": "string",
            "enum": [
              "MOVE",
              "CANCEL"
            ]
          },
          "target_project_id": {
            "$ref": "#/components/schemas/UuidRef",
            "description": "Required when `disposition = MOVE`; the receiving project must be `ACTIVE`. `board_id` is re-resolved and `milestone_id` cleared."
          }
        }
      },
      "ProjectCloseRequest": {
        "type": "object",
        "required": [
          "dispositions"
        ],
        "additionalProperties": false,
        "properties": {
          "dispositions": {
            "type": "array",
            "description": "One entry per open task. A `409` is returned when any non-terminal task is missing, naming them.",
            "items": {
              "$ref": "#/components/schemas/TaskDisposition"
            }
          },
          "close_note": {
            "type": "string",
            "description": "What closing meant. **Not a `work.projects` column** (db 06 §3) — captured to the audit trail and to an `ARCHIVED`-adjacent `work.task_activity` project row.\n"
          },
          "acknowledge_unapproved_time": {
            "type": "boolean",
            "default": false,
            "description": "Confirms the operator saw the *\"N timesheets covering this project are not approved — their hours are not in these totals\"* warning. Close is still allowed; the number is never quietly folded into the totals (`WRK-S23` stage 1).\n"
          }
        }
      },
      "ProjectCloseResult": {
        "type": "object",
        "readOnly": true,
        "required": [
          "project",
          "moved_task_count",
          "cancelled_task_count",
          "closed_allocation_count"
        ],
        "properties": {
          "project": {
            "$ref": "#/components/schemas/Project"
          },
          "archived_at": {
            "$ref": "#/components/schemas/TimestampRef"
          },
          "moved_task_count": {
            "type": "integer"
          },
          "cancelled_task_count": {
            "type": "integer"
          },
          "closed_allocation_count": {
            "type": "integer",
            "description": "Open allocation windows stamped with `effective_to` — never deleted, so historical utilization stays reconstructable (db 06 §5)."
          },
          "unapproved_timesheet_count": {
            "type": "integer",
            "description": "Timesheets in `DRAFT`/`SUBMITTED` with entries on this project at close time — recorded, not folded in."
          }
        }
      },
      "ProjectAllocation": {
        "description": "work.project_allocations — an effective-dated allocation of an employee's capacity to a project (db 06 §5).",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "employee_id",
              "project_id",
              "allocation_pct",
              "effective_from"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "employee_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "project_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "allocation_pct": {
                "type": "string",
                "pattern": "^\\d+(\\.\\d{1,2})?$",
                "description": "numeric(5,2) **percentage** as a string, `0 < pct <= 100` per row (\"50.00\" = half a person). A deliberate db 06 §5 deviation from fraction-typed ratios: it is the number the operator typed and edits, and never an operand of money arithmetic. A member's *summed* percentage across projects may exceed 100 — that is a `warnings[]` entry, not a validation failure.\n"
              },
              "effective_from": {
                "$ref": "#/components/schemas/DateOnlyRef"
              },
              "effective_to": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/DateOnlyRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Inclusive window end; **null = open-ended (the live row)**. Ending an allocation stamps this — never a hard delete."
              },
              "role_note": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "A single free-text label — role on the project (\"tech lead\", \"QA\") and/or why the allocation exists. Explicitly **not** an RBAC role or a `people` designation."
              }
            }
          },
          {
            "$ref": "#/components/schemas/AuditMetaRef"
          },
          {
            "type": "object",
            "readOnly": true,
            "properties": {
              "employee": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/EmployeeRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "project": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/ProjectRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "open_task_load": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Derived: count of the member's tasks with `status ∉ {DONE, CANCELLED}` — context beside the percentage."
              },
              "warnings": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/WorkWarning"
                }
              }
            }
          }
        ]
      },
      "ProjectAllocationCreate": {
        "type": "object",
        "required": [
          "employee_id",
          "project_id",
          "allocation_pct",
          "effective_from"
        ],
        "additionalProperties": false,
        "properties": {
          "employee_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "project_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "allocation_pct": {
            "type": "string",
            "pattern": "^\\d+(\\.\\d{1,2})?$"
          },
          "effective_from": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "effective_to": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "role_note": {
            "type": "string"
          }
        }
      },
      "ProjectAllocationUpdate": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "allocation_pct": {
            "type": "string",
            "pattern": "^\\d+(\\.\\d{1,2})?$"
          },
          "effective_from": {
            "$ref": "#/components/schemas/DateOnlyRef",
            "description": "Writable since #1663 — a window whose start was mistyped is corrected here rather than ended and re-created under a new id. The resulting window must not overlap any sibling window of the same member on the same project, in EITHER direction; a crossing is `422` `urn:groundit:problem:work:allocation-window-overlap`. To move the boundary between two adjacent phases, move BOTH — `effective_to` on the earlier and `effective_from` on the later — ordering the two PATCHes so the windows never transiently overlap, or re-split instead.\n"
          },
          "effective_to": {
            "$ref": "#/components/schemas/DateOnlyRef",
            "description": "Setting this **ends** the allocation (`WRK-S19` *End allocation*); the closed window is the utilization record."
          },
          "role_note": {
            "type": "string"
          }
        }
      },
      "ProjectAllocationSplit": {
        "type": "object",
        "description": "The body of `POST /project-allocations/{id}/split` (#1663). Only the split date is required; the successor inherits everything else from the window being split, including its `effective_to`, which is what makes the two windows adjacent with no gap and no overlap.\n",
        "required": [
          "effective_from"
        ],
        "additionalProperties": false,
        "properties": {
          "effective_from": {
            "$ref": "#/components/schemas/DateOnlyRef",
            "description": "The first day of the NEW phase. Must fall strictly after the window's `effective_from` and, when the window is closed, on or before its `effective_to`. The incumbent is stamped `effective_from = this date - 1 day`.\n"
          },
          "allocation_pct": {
            "type": "string",
            "pattern": "^\\d+(\\.\\d{1,2})?$",
            "description": "The new phase's percentage. Omitted → the incumbent's, which makes this a pure date split."
          },
          "role_note": {
            "type": [
              "string",
              "null"
            ],
            "description": "Omitted → the incumbent's role note is carried forward. Explicit `null` clears it on the new phase only."
          }
        }
      },
      "ProjectAllocationSplitResult": {
        "description": "The successor phase, plus the window it superseded. `warnings[]` always leads with `PREDECESSOR_WINDOW_CLOSED` naming the incumbent's new end date — a split is never silent about the row it rewrote.\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/ProjectAllocation"
          },
          {
            "type": "object",
            "required": [
              "superseded"
            ],
            "properties": {
              "superseded": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/ProjectAllocation"
                  }
                ],
                "description": "The incumbent as it now stands — same `id`, `effective_to` stamped to the day before the split, `version` bumped."
              }
            }
          }
        ]
      },
      "ProjectAllocationPage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ProjectAllocation"
                }
              }
            }
          }
        ]
      },
      "ApprovedLeaveWindow": {
        "type": "object",
        "description": "One approved-leave window from the **event-fed projection** (`XC-F09` pattern) — **never a cross-schema read into `leave`**. Exposes dates and status only: **never a leave reason, type detail or medical information** (`WRK-F13`, fsd 05 `WRK-S19`). **Known spec gap (#437) — this shape cannot express a half-day.** The projection behind it carries `is_half_day`, and the `WRK-S20` capacity arithmetic honours it (half a working day), but the three fields here reduce a half-day to `from == to`, which is indistinguishable from a full single-day absence. The picker therefore says *\"away on 14 Aug\"* for a person who is available for half of it. Recorded rather than papered over: adding a field is a contract change for the mobile and portal consumers and belongs in a round that can version it, and inferring \"half\" from equal dates would be wrong for every genuine one-day absence.\n",
        "readOnly": true,
        "required": [
          "from",
          "to"
        ],
        "properties": {
          "from": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "to": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "status": {
            "type": "string",
            "enum": [
              "APPROVED"
            ],
            "description": "Only approved leave is projected here."
          }
        }
      },
      "AssignmentContext": {
        "type": "object",
        "description": "WRK-S19 candidate panel / the leave-aware picker reused by WRK-S08, WRK-S09 and WRK-S16 — one answer to \"who can take this?\" everywhere. `approved_leave` is **live** since 2026-08-13 (#437): the windows come from `xc.approved_leave_projection` (db 14 §8), the same read model whose hours the `WRK-S20` heatmap nets off capacity, so the picker and the heatmap cannot disagree about who is away. The projection is the ONLY leave source on this path — never a cross-schema read into `leave` — and the SELECT list is narrowed to the two dates, so no reason, type or approver exists to leak here even in principle (`WRK-F13`, fsd 05 `WRK-S19` Access line).\n",
        "readOnly": true,
        "required": [
          "employee",
          "open_task_load",
          "leave_available"
        ],
        "properties": {
          "employee": {
            "$ref": "#/components/schemas/EmployeeRef"
          },
          "open_task_load": {
            "type": "integer",
            "description": "Tasks assigned with `status ∉ {DONE, CANCELLED}`."
          },
          "current_allocation_pct": {
            "anyOf": [
              {
                "type": "string",
                "pattern": "^\\d+(\\.\\d{1,2})?$"
              },
              {
                "type": "null"
              }
            ],
            "description": "Σ live `allocation_pct` effective today, across projects. May exceed 100 — that is the warning, not an error."
          },
          "allocations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProjectAllocation"
            }
          },
          "approved_leave": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApprovedLeaveWindow"
            }
          },
          "leave_available": {
            "type": "boolean",
            "description": "`false` when the leave projection is unavailable — the picker must then say *\"Leave data unavailable — showing load and allocation only\"* rather than implying the candidate is free.\n"
          },
          "warnings": {
            "type": "array",
            "description": "Overbooking and leave-collision advisories, shown **at the moment of choosing**, not after saving. Never blocking. Since #801 the two leave-aware codes join this array on the same terms the task write doors emit them: `LEAVE_COLLISION` whenever the candidate's approved leave overlaps the requested window, and — **only when the optional `due_date` query parameter is supplied** — `NON_WORKING_DAY` when that date is a holiday for the candidate. Without `due_date` there is no date to test a holiday against and the code cannot appear, which is why the picker sends the due date it is about to write.\n",
            "items": {
              "$ref": "#/components/schemas/WorkWarning"
            }
          }
        }
      },
      "WorkloadHeatmapCell": {
        "type": "object",
        "readOnly": true,
        "required": [
          "week_start",
          "state"
        ],
        "properties": {
          "week_start": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "week_end": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "committed_hours": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DecimalHoursRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "Σ `tasks.estimated_hours` spread over each task's `start_date`→`due_date` window — the estimate-based demand."
          },
          "capacity_hours": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DecimalHoursRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "`allocation_pct` × work-week hours − approved leave in that week. **Leave is really subtracted** since 2026-08-13 (#437), from `xc.approved_leave_projection` (db 14 §8) and never from `leave`. Four properties of the subtraction: a leave day that is a **rest day** under the market pack week costs nothing (an India Sunday, a KSA Friday); a **half-day** costs half a working day; a date covered by two projected windows is subtracted **once**, so a duplicate projection row cannot manufacture an overload; and the result is **floored at zero** — a negative capacity would invert `load_pct` and paint an absent week green. `state: NO_DATA` is judged on the *gross* capacity, so an allocated member who is merely away is not relabelled \"no data\". **Known question against the spec (#437, db 06 §5):** the formula is literal, and its leave term is therefore **not proportional to `allocation_pct`** — a 50 %-allocated person absent 2 of 5 days reads `20 − 16 = 4 h`, where the proportional reading (`50 % × (40 − 16)`) gives `12 h`. The server implements the text as written rather than reinterpreting it; deciding which reading is meant is a product ruling, after which this description, fsd 05 `WRK-S20` and the service change together.\n"
          },
          "load_pct": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/RateRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "state": {
            "type": "string",
            "enum": [
              "UNDER",
              "NEAR",
              "OVER",
              "NO_DATA"
            ],
            "description": "`< 80` · `80–100` · `> 100` · **`NO_DATA`** where the member has neither estimates nor allocation — grey is never drawn as green (`WRK-S20`)."
          }
        }
      },
      "WorkloadHeatmapRow": {
        "type": "object",
        "readOnly": true,
        "properties": {
          "employee": {
            "$ref": "#/components/schemas/EmployeeRef"
          },
          "cells": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WorkloadHeatmapCell"
            }
          },
          "unestimated_open_task_count": {
            "type": "integer",
            "description": "Open tasks with `estimated_hours IS NULL` — rendered as \"3 tasks without an estimate aren''t counted\", so the heatmap **states what it cannot see** rather than under-reporting silently.\n"
          }
        }
      },
      "WorkloadHeatmap": {
        "type": "object",
        "description": "WRK-S20 — assignee × week, with its formula stated so a red cell can be argued with.",
        "readOnly": true,
        "required": [
          "rows",
          "leave_included"
        ],
        "properties": {
          "from": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "to": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "work_week": {
            "type": [
              "string",
              "null"
            ],
            "description": "The pack-driven week boundary the columns use (XC-F01) — Mon–Fri India / Sun–Thu KSA, never hard-coded."
          },
          "formula": {
            "type": "string",
            "description": "The legend text: committed ÷ capacity, with each term defined. Returned so the UI cannot drift from the arithmetic."
          },
          "leave_included": {
            "type": "boolean",
            "description": "`true` when approved leave was subtracted from `capacity_hours`; `false` when it was not, and the header must then read **\"leave not included\"** rather than quietly overstating capacity. **Read the `false` case precisely.** The server sets it from *\"the projection returned at least one window overlapping this request\"*, so `false` means **either** the projection is unavailable or unfed **or** nobody in scope has approved leave in the window — the two are the same signal on the wire. That is a genuine limitation, not a shortcut: distinguishing them needs a **projector watermark** (\"the projection is complete through date D\"), and `xc.approved_leave_projection` carries only a per-row `projected_at`, which an absent row does not have (db 14 §8). Clients must therefore render `false` as *\"leave not included\"* and never as *\"nobody is on leave\"*.\n"
          },
          "omitted_employee_count": {
            "type": "integer",
            "description": "How many ACTIVE employees inside the same scope and the same filters have **no row** here because they carry neither a task assignment nor a project allocation (#1432). The grid only ever admits people who carry one or the other, so on a real tenant it is routinely shorter than the roster; without this count a reader has no way to tell a short grid from an idle team. `0` means the grid shows everyone in scope. Additive — clients that predate it read a missing value as *unknown* and must not print it as zero.\n"
          },
          "rows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WorkloadHeatmapRow"
            }
          }
        }
      },
      "PortfolioKpi": {
        "type": "object",
        "description": "One KPI tile. `value` is `null` with a `unavailable_reason` when the aggregate failed — the tile reads \"Couldn't compute\" rather than showing a wrong number (`WRK-S18`).",
        "readOnly": true,
        "required": [
          "key"
        ],
        "properties": {
          "key": {
            "type": "string",
            "enum": [
              "ACTIVE_PROJECTS",
              "AT_RISK",
              "UTILIZATION",
              "OVERDUE_TASKS"
            ]
          },
          "value": {
            "type": [
              "string",
              "null"
            ],
            "description": "Integer count or a `Rate` fraction, as a string — never a float."
          },
          "unavailable_reason": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "PortfolioProjectRow": {
        "type": "object",
        "readOnly": true,
        "properties": {
          "project": {
            "$ref": "#/components/schemas/ProjectRef"
          },
          "status": {
            "$ref": "#/components/schemas/ProjectStatus"
          },
          "owner": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/EmployeeRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "client_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "end_date": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DateOnlyRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "health": {
            "$ref": "#/components/schemas/ProjectHealth"
          },
          "burn": {
            "$ref": "#/components/schemas/ProjectBurn"
          },
          "team_size": {
            "type": "integer"
          },
          "utilization_pct": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/RateRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "open_task_count": {
            "type": "integer"
          },
          "overdue_task_count": {
            "type": "integer"
          },
          "task_count": {
            "type": "integer",
            "description": "Every undeleted task on the project (`CANCELLED` included) — the completion denominator (#1429)."
          },
          "done_count": {
            "type": "integer",
            "description": "Tasks at `DONE`. The grid renders `null`, never `0 %`, when `task_count` is 0."
          },
          "budget_amount": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "The project's money budget (#1431). Present only for a caller holding `work.project_cost.list`; withheld by column omission otherwise, like every other commercial column on this module."
          },
          "spent_to_date": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "Recurring + one-time cost lines accrued to today, plus salary derived from live allocations (#1431). **Null, not zero**, when the project has no ledger and no priceable allocation, and also when its cost lines span more than one currency — the portfolio grid is one column wide and cannot state two currencies honestly, so it states none and the project 360's *Costs* tab carries the per-currency breakdown.\n"
          },
          "spend_currency_code": {
            "type": [
              "string",
              "null"
            ]
          },
          "money_budget_consumed_pct": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/RateRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "`spent_to_date ÷ budget_amount` as a `Rate` fraction — **money**, and deliberately named apart from `burn.budget_consumed_pct`, which is HOURS. The two are different questions and the register conflating them in one cell is the defect #1431 was raised for. Null when there is no money budget, nothing spent, or the two currencies differ.\n"
          }
        }
      },
      "EscalationEntry": {
        "type": "object",
        "description": "One escalation-feed entry (`WRK-F16`) — the published projection of one `work.task_escalations` row. These are **notifications, not approvals** — deliberately **not** routed to the approvals inbox (ADR 0027 §(e)). Since issue #440 a jobs-tier sweep raises them for real: every field below is a value that sweep decided when it wrote the row, the raise is audited under `XC-F06` (`actor_type = 'JOB'`) and its notifications are written to `xc.outbox` in the same transaction, so `notified` finally answers \"did anyone hear about this?\" with something other than silence (`GAP-57`, closed; before it, this feed was computed live from overdue tasks with `escalation_level` fixed at `1`, `notified` empty and `employee` null). **The values are stamped, not live** — `raised_at` is when the escalation was raised (**not** when the breach began; the pre-#440 placeholder put the task's `due_date` here) and `breach_duration_hours` is the breach's age at that instant, so neither moves between reads. **`threshold` is server-authored English describing the rule in force at raise time**, so lowering a threshold later re-explains new entries without rewriting old ones. **`escalation_level` climbs with repeated breaches and is capped**, while the ledger's own breach-window ordinal behind it is uncapped — a capped level is a rendered urgency, not a count. **Which `kind` values can reach you depends on the operation:** all three exist and are raised, but `work.portfolio.read` is project-scoped and `IDLE_ASSIGNEE` rows carry no project, so that endpoint returns the two task kinds only — see its description. `notified` may legitimately be empty (the raise reached nobody) and may carry an `EmployeeRef` with `id` alone when the caller cannot read that employee; the array's length is the routing decision and is never shortened to match what the caller can see.\n",
        "readOnly": true,
        "required": [
          "kind",
          "raised_at"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "TASK_OVERDUE",
              "STALE_BLOCKED",
              "IDLE_ASSIGNEE"
            ]
          },
          "raised_at": {
            "$ref": "#/components/schemas/TimestampRef"
          },
          "threshold": {
            "type": "string",
            "description": "The breached threshold, in words (e.g. \"blocked > 5 days\", \"no active assignment ≥ 3 days\")."
          },
          "breach_duration_hours": {
            "type": [
              "integer",
              "null"
            ]
          },
          "escalation_level": {
            "type": "integer",
            "minimum": 1
          },
          "notified": {
            "type": "array",
            "description": "Who was notified, so the feed answers \"did anyone hear about this?\".",
            "items": {
              "$ref": "#/components/schemas/EmployeeRef"
            }
          },
          "task": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/TaskRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "project": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ProjectRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "employee": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/EmployeeRef"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "PortfolioSummary": {
        "type": "object",
        "description": "WRK-S18 — read-only. The KPI strip is computed over the **same scoped row set** as the rows, so a KPI never counts what the grid may not show.",
        "readOnly": true,
        "required": [
          "kpis",
          "projects",
          "scope_description"
        ],
        "properties": {
          "scope_description": {
            "type": "string",
            "description": "The scope in words (\"Across your 12 projects\") — so two people comparing screens can tell why their numbers differ."
          },
          "kpis": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PortfolioKpi"
            }
          },
          "projects": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PortfolioProjectRow"
            }
          },
          "escalations": {
            "type": "array",
            "description": "The escalation feed. An empty array is a real, good answer (\"Nothing has breached in this window\") and must be shown as such.",
            "items": {
              "$ref": "#/components/schemas/EscalationEntry"
            }
          },
          "expenditure_rollup": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PortfolioExpenditureRollup"
              },
              {
                "type": "null"
              }
            ],
            "description": "The tenant roll-up tile (#1431). Present only for a caller holding `work.project_cost.list`; null when nothing in the scoped row set has a priceable cost."
          }
        }
      },
      "PortfolioExpenditureRollup": {
        "type": "object",
        "description": "Spend and budget summed across the SAME scoped row set the grid shows (#1431) — so the tile never counts a project the grid may not display. Summed per currency and never across: `total_spent` and `total_budget` are populated only when every contributing project shares one currency, and `mixed_currency` says when they do not.\n",
        "readOnly": true,
        "required": [
          "mixed_currency",
          "by_currency"
        ],
        "properties": {
          "total_spent": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "total_budget": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "currency_code": {
            "type": [
              "string",
              "null"
            ]
          },
          "budget_consumed_pct": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/RateRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "mixed_currency": {
            "type": "boolean"
          },
          "by_currency": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "currency_code"
              ],
              "properties": {
                "currency_code": {
                  "type": "string",
                  "minLength": 3,
                  "maxLength": 3
                },
                "spent": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MoneyRef"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "budget": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MoneyRef"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "project_count": {
                  "type": "integer"
                }
              }
            }
          },
          "priced_project_count": {
            "type": "integer",
            "description": "Projects in scope contributing a spend figure. The tile states this beside the total so a reader knows the denominator."
          },
          "unpriced_allocation_count": {
            "type": "integer",
            "description": "Live allocations across the scope whose salary could not be priced (non-salaried pay model, or no compensation row). Non-zero means the total understates."
          }
        }
      },
      "MetricTile": {
        "type": "object",
        "description": "One velocity tile. `denominator` is returned because **a rate with a hidden denominator is not a metric** (`WRK-S22`).",
        "readOnly": true,
        "required": [
          "key"
        ],
        "properties": {
          "key": {
            "type": "string",
            "enum": [
              "THROUGHPUT",
              "COMPLETION_RATE",
              "ON_TIME_PCT",
              "OPEN_OVERDUE"
            ]
          },
          "value": {
            "type": [
              "string",
              "null"
            ]
          },
          "numerator": {
            "type": [
              "integer",
              "null"
            ]
          },
          "denominator": {
            "type": [
              "integer",
              "null"
            ]
          },
          "excludes": {
            "type": [
              "string",
              "null"
            ],
            "description": "What the metric leaves out, stated on the tile (e.g. \"tasks with no due date\")."
          },
          "series": {
            "type": "array",
            "description": "Per-week sparkline points over the stated window.",
            "items": {
              "type": "object",
              "properties": {
                "week_start": {
                  "$ref": "#/components/schemas/DateOnlyRef"
                },
                "value": {
                  "type": "integer"
                }
              }
            }
          }
        }
      },
      "MetricCohort": {
        "type": "object",
        "description": "**Which set of people every figure in this response was computed over.** `work` uses the word \"team\" for two unrelated sets — the `TEAM` RLS overlay (a manager''s reportee closure) and `work.teams` (an explicit squad roster) — and ADR 0037 makes disambiguating them a standing obligation on everything that touches the module. This object is that obligation discharged on the wire: the reader is told the cohort rather than left to infer it from a query parameter they may not have sent.\n",
        "readOnly": true,
        "required": [
          "kind",
          "count"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "REPORTS",
              "TEAM_MEMBERSHIP"
            ],
            "description": "`REPORTS` — the caller''s direct and indirect reports via `people.employees.manager_id` (`team_id` absent; the operation''s original and default behaviour). `TEAM_MEMBERSHIP` — the live roster of the `work.teams` squad named by `team_id` (`work.team_members`, `left_at IS NULL`).\n"
          },
          "team_id": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "The squad in context; `null` for `REPORTS`."
          },
          "count": {
            "type": "integer",
            "description": "The cohort size — **the membership denominator**, and what `report_count` mirrors. Computed inside the caller''s own RLS scope: `work.team_members` is RESTRICTIVE-policied (migration `0109` §9b), so a `TEAM`-scoped manager who can see a squad only because they manage its lead counts only the members who are their own reports. It is what this caller can see, never a privileged total.\n"
          }
        }
      },
      "TeamMetrics": {
        "type": "object",
        "description": "WRK-S22 — the velocity strip over the cohort named by `cohort`: the caller''s direct and indirect reports by default (`team` overlay; no new scope machinery, no `team_lead` role — ADR 0026 §(a)), or a `work.teams` squad''s live membership when `team_id` is supplied (ADR 0037) — optionally narrowed to one project's tasks by `project_id`, which is echoed back on the response.\n",
        "readOnly": true,
        "required": [
          "window_weeks",
          "tiles",
          "insufficient_history",
          "cohort"
        ],
        "properties": {
          "window_weeks": {
            "type": "integer",
            "description": "Stated on every tile — the window is never implicit."
          },
          "project_id": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "The project axis the tiles were computed over, echoed back for the same reason `window_weeks` and `cohort.kind` are: a tile whose scope is not stated is a tile a reader will scope wrong (#1420). `null` ⇒ every project. It sits BESIDE `cohort` rather than inside it because it changes WHICH tasks are counted, never WHO — `cohort.count` is the whole roster either way. `idle_report_count` and `pending_review_count` ignore it.\n"
          },
          "insufficient_history": {
            "type": "boolean",
            "description": "`true` ⇒ tiles read \"Not enough history yet\" rather than a 0 % or 100 % computed from two data points."
          },
          "cohort": {
            "$ref": "#/components/schemas/MetricCohort"
          },
          "report_count": {
            "type": "integer",
            "description": "The cohort size, mirroring `cohort.count`. Retained under its original name so existing clients keep working; **read `cohort.kind` before labelling it.** 0 with `kind=REPORTS` is the empty-team state (\"You have no direct reports\"), not someone else''s team; 0 with `kind=TEAM_MEMBERSHIP` is a squad with no live members this caller can see.\n"
          },
          "tiles": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MetricTile"
            }
          },
          "pending_review_count": {
            "type": "integer",
            "description": "Submitted timesheets awaiting **the caller''s own** first-level review (`work.timesheet.review`, keyed on `work.timesheets.manager_id`). Deliberately **not** cohort-derived in either mode — it is an inbox, not a squad statistic, and re-pointing it at `team_id` would make one field mean two things depending on a query parameter. A squad-wide approval figure would be a new field.\n"
          },
          "blocked_task_count": {
            "type": "integer"
          },
          "idle_report_count": {
            "type": "integer",
            "description": "Cohort members with 0 `ACTIVE` assignments for ≥ 3 days — the WRK-F02 signal on the lead''s own surface. **Read it as a floor, not a count.** `work.task_assignments` carries no `TEAM` arm in its RESTRICTIVE policy (migration `0031`: TENANT scope ∨ own-assignee ∨ own-assigner), so at this operation''s `team` scope a caller sees assignment rows only for work they personally assigned — every other cohort member therefore reads as idle. True for both `cohort.kind` values, and more visible over a squad than over a reportee closure. Do not escalate off this number alone until `work.task_assignments` gains a board-follows arm.'\n"
          }
        }
      },
      "TimesheetReviewInput": {
        "type": "object",
        "description": "Body for `work.timesheet.review`. There is deliberately **no `approved_hours` field** — a `REVIEWED` row never carries one (db 06 §2 addendum).",
        "additionalProperties": false,
        "properties": {
          "comments": {
            "type": "string"
          }
        }
      },
      "TimesheetReviewConflictProblem": {
        "description": "The `409 STATE_TRANSITION_INVALID` `work.timesheet.review` raises when the sheet already carries a `REVIEWED` row — from ANY reviewer, not just this one (#1421). `type` is `urn:groundit:problem:work:timesheet-already-reviewed`. Extends `Problem` exactly the way `xc`'s `ApprovalDecisionConflictProblem` does (`03-errors-pagination.md` §1 — a typed member on a specific occurrence, never a new `code`), and reuses the SAME two member names on purpose: a `REVIEWED` row IS a `work.timesheet_approvals` decision row, and a client that already reads `decided_by` off the decide-race reads this one with no new code. Both are OPTIONAL — populated only when the incumbent reviewer resolves to a live `people.employees` row visible to this caller; otherwise the `detail` text stands alone rather than adding a second failure on top of the 409. The concurrent-double-click arm (the partial unique index, migration `0207`) also carries neither, because the losing transaction cannot read the winner — its `detail` asks the client to refresh instead. **Disclosure boundary:** this 409 is reachable only after the four-eyes and `team`-scope guards, and the same name is already on the caller's screen in the *Reviewed by …* chip the row renders from `decision_log`.\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/Problem"
          },
          {
            "type": "object",
            "properties": {
              "decided_by": {
                "type": "string",
                "description": "The existing reviewer's display name (`people.employees.full_name`)."
              },
              "decided_by_id": {
                "type": "string",
                "format": "uuid",
                "description": "The existing reviewer's employee id."
              }
            }
          }
        ]
      },
      "TaskDependency": {
        "description": "work.task_dependencies — `task_id` is blocked by `depends_on_task_id` (db 06 §6). Who added the edge is the standard `created_by` audit column.",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "task_id",
              "depends_on_task_id",
              "dep_type"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "task_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "depends_on_task_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "dep_type": {
                "$ref": "#/components/schemas/DependencyType"
              }
            }
          },
          {
            "$ref": "#/components/schemas/AuditMetaRef"
          },
          {
            "type": "object",
            "readOnly": true,
            "properties": {
              "task": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/TaskRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "depends_on_task": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/TaskRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "is_live": {
                "type": "boolean",
                "description": "`true` while the blocker has not reached `DONE`/`CANCELLED` — the derived **blocked badge**, distinct from the assignee-declared `tasks.status = BLOCKED` (db 06 §6)."
              },
              "blocked_since": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/TimestampRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "How long it has waited — the stale-blocked input to the WRK-F16 SLA scan."
              }
            }
          }
        ]
      },
      "TaskDependencyView": {
        "type": "object",
        "description": "WRK-S09 *Dependencies* panel — the same stored `BLOCKS` edges read in both directions, as two labelled groups. Not two stored types.",
        "readOnly": true,
        "required": [
          "blocked_by",
          "blocks"
        ],
        "properties": {
          "blocked_by": {
            "type": "array",
            "description": "Edges where this task is `task_id` — the things it waits on.",
            "items": {
              "$ref": "#/components/schemas/TaskDependency"
            }
          },
          "blocks": {
            "type": "array",
            "description": "Edges where this task is `depends_on_task_id` — the things finishing it would unblock.",
            "items": {
              "$ref": "#/components/schemas/TaskDependency"
            }
          }
        }
      },
      "TaskDependencyCreate": {
        "type": "object",
        "description": "Exactly one of `depends_on_task_id` (this task is blocked by it) or `blocks_task_id` (this task blocks it), so the panel's two groups both have an add affordance.",
        "additionalProperties": false,
        "properties": {
          "depends_on_task_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "blocks_task_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "dep_type": {
            "$ref": "#/components/schemas/DependencyType"
          }
        }
      },
      "CommentMention": {
        "type": "object",
        "description": "One resolved @mention, captured **at post time** (db 06 §7) so the fan-out and the watcher auto-add never re-parse the body and a later rename does not rewrite history. Not queried or joined.\n",
        "properties": {
          "employee_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "display_name": {
            "type": "string"
          },
          "offset": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "TaskComment": {
        "description": "work.task_comments — one comment in a task's thread (db 06 §7). Mutable by its author: a comment is collaboration, not evidence.",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "task_id",
              "author_id"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "task_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "author_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "body": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Omitted (null) on a tombstoned comment — the row still renders, in position, as \"comment deleted\"."
              },
              "mentions": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/CommentMention"
                }
              },
              "edited_at": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/TimestampRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Null = never edited; the thread shows \"edited\" when set."
              }
            }
          },
          {
            "$ref": "#/components/schemas/AuditMetaRef"
          },
          {
            "type": "object",
            "readOnly": true,
            "properties": {
              "author": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/EmployeeRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "is_deleted": {
                "type": "boolean",
                "description": "Soft-deleted tombstone; rendered in place so reply context keeps its shape (db 06 §7)."
              },
              "attachments": {
                "type": "array",
                "description": "The files posted with this comment (#1418), oldest first — **metadata only, no presigned `download`**. Deliberately the cheaper projection: this is one `LEFT JOIN LATERAL` over the `(tenant_id, comment_id)` partial index and **zero** storage round-trips, where embedding a download handle would cost one presign per file per thread page. Every one of these ids also appears in `work.task_attachment.list` for the same task, which is where the client resolves the expiring URL — the thread renders the link, the attachment list resolves it, and the two never disagree because they are the same rows. The array is RLS-filtered like the rest of the query, so a caller who may read the thread but not the task's files sees it empty rather than a leak. It stays populated on a `is_deleted` tombstone: a deleted comment keeps its evidence (db 06 §1).\n",
                "items": {
                  "type": "object",
                  "required": [
                    "id",
                    "file_name",
                    "mime_type"
                  ],
                  "properties": {
                    "id": {
                      "$ref": "#/components/schemas/UuidRef"
                    },
                    "file_name": {
                      "type": "string"
                    },
                    "mime_type": {
                      "type": "string"
                    },
                    "byte_size": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "CommentMentionInput": {
        "description": "One @mention on the way in. Either a bare employee **uuid** or a `{ employee_id }` **object** — the two carry the same information and the server accepts both, having previously rejected the bare-uuid form (which the web composer sends) as a malformed body (#1412). `display_name` and `offset` are ignored on input: mentions are re-resolved server-side and stored as resolved.\n",
        "oneOf": [
          {
            "$ref": "#/components/schemas/UuidRef"
          },
          {
            "$ref": "#/components/schemas/CommentMention"
          }
        ]
      },
      "TaskCommentCreate": {
        "type": "object",
        "required": [
          "body"
        ],
        "additionalProperties": false,
        "properties": {
          "body": {
            "type": "string",
            "minLength": 1
          },
          "mentions": {
            "type": "array",
            "description": "Client-resolved @mentions; the server re-validates each against the caller's scope before storing and notifying. Each element may be a bare employee uuid or a `{ employee_id }` object.",
            "items": {
              "$ref": "#/components/schemas/CommentMentionInput"
            }
          },
          "attachment_ids": {
            "type": "array",
            "maxItems": 20,
            "uniqueItems": true,
            "description": "Ids of attachments **already registered** by `work.task_attachment.create` on this same task, bound to the new comment in the comment's own transaction (#1418) — the comment and its evidence land together or not at all. Each id must name an attachment on **this** task that is **still unbound** (`comment_id IS NULL`); anything else — another task, another tenant, or a row that already answers a different comment — is a `422` on `/attachment_ids`, and no part of the post is applied. This is the composer's flow: upload first, bind on post, so abandoning a draft leaves task-level attachments (visible and reusable) rather than orphaned bytes. There is no attachment delete operation, so staged-then-abandoned files are not cleaned up automatically.\n",
            "items": {
              "$ref": "#/components/schemas/UuidRef"
            }
          }
        }
      },
      "TaskCommentUpdate": {
        "type": "object",
        "required": [
          "body"
        ],
        "additionalProperties": false,
        "properties": {
          "body": {
            "type": "string",
            "minLength": 1
          },
          "mentions": {
            "type": "array",
            "description": "Re-resolved on edit and stored; editing in a mention auto-adds that employee as a `MENTIONED` watcher but notifies nobody (see the operation description). Same element shapes as on create.",
            "items": {
              "$ref": "#/components/schemas/CommentMentionInput"
            }
          }
        }
      },
      "TaskCommentPage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TaskComment"
                }
              }
            }
          }
        ]
      },
      "TaskChecklistItem": {
        "description": "work.task_checklist_items — one checked/unchecked line on a task (db 06 §7). **Not a subtask:** `work.tasks.parent_task_id` children stay full tasks and both regions ship on `WRK-S09`.\n",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "task_id",
              "label",
              "is_done",
              "rank"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "task_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "label": {
                "type": "string",
                "maxLength": 500
              },
              "is_done": {
                "type": "boolean"
              },
              "rank": {
                "type": "string",
                "description": "`numeric(9,6)` as a decimal STRING (`00 §6` — decimals never cross the wire as floats). `0 … 999.999999`, six fractional digits: an insert-between is a midpoint, so a reorder never renumbers the tail. Same convention as `work.tasks.rank`.\n",
                "example": "3.500000"
              },
              "done_at": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/TimestampRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Set with `is_done=true` and CLEARED with `is_done=false`, in the same statement — never stale on an open line."
              },
              "done_by": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "The employee who ticked it. Null on an open line, and null on a line ticked with no acting employee (a system tick)."
              }
            }
          },
          {
            "$ref": "#/components/schemas/AuditMetaRef"
          },
          {
            "type": "object",
            "readOnly": true,
            "properties": {
              "done_by_employee": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/EmployeeRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          }
        ]
      },
      "TaskChecklistItemCreate": {
        "type": "object",
        "required": [
          "label"
        ],
        "additionalProperties": false,
        "properties": {
          "label": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500
          },
          "rank": {
            "type": "string",
            "description": "Optional. Omitted, the item is APPENDED (`max(rank) + 1`). Sent, it is the midpoint the client computed between two neighbours.",
            "example": "3.500000"
          }
        }
      },
      "TaskChecklistItemUpdate": {
        "type": "object",
        "minProperties": 1,
        "additionalProperties": false,
        "description": "At least one field. `is_done` carries the done stamp with it — the server writes/clears `done_at` and `done_by` in the same statement, and neither is settable from the wire.\n",
        "properties": {
          "label": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500
          },
          "is_done": {
            "type": "boolean"
          },
          "rank": {
            "type": "string",
            "example": "3.500000"
          }
        }
      },
      "TaskChecklistItemPage": {
        "description": "Live items only — a removed line is absent, not tombstoned (unlike a comment, which keeps reply context).",
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TaskChecklistItem"
                }
              }
            }
          }
        ]
      },
      "TaskWatcher": {
        "description": "work.task_watchers — the explicit watch list (db 06 §7). One row per (task, employee) **ever**; `deleted_at` carries whether they are watching now.",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "task_id",
              "employee_id",
              "source"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "task_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "employee_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "source": {
                "$ref": "#/components/schemas/WatchSource"
              }
            }
          },
          {
            "$ref": "#/components/schemas/AuditMetaRef"
          },
          {
            "type": "object",
            "readOnly": true,
            "properties": {
              "employee": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/EmployeeRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          }
        ]
      },
      "TaskWatcherCreate": {
        "type": "object",
        "description": "Defaults to the caller. A create always writes `source = EXPLICIT` — only an explicit watch clears a prior un-watch tombstone (db 06 §7).",
        "additionalProperties": false,
        "properties": {
          "employee_id": {
            "$ref": "#/components/schemas/UuidRef"
          }
        }
      },
      "TaskWatcherPage": {
        "description": "Live watchers only — an un-watch tombstone is absent, not flagged (db 06 §7).",
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TaskWatcher"
                }
              }
            }
          }
        ]
      },
      "TaskActivity": {
        "description": "work.task_activity *(Immutable, db 06 §7)* — the product-facing trail `WRK-S09` renders. **Not the audit log**: `audit.audit_log` is the tenant-invisible, hash-chained evidence plane; this is the task''s story, visible to anyone who can see the task. Frozen on write — a mistaken entry is superseded by a later row, never corrected.\n",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "task_id",
              "activity_type"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "task_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "actor_id": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "**Null for system/jobs** actions — recurrence materialization, the SLA scan."
              },
              "activity_type": {
                "$ref": "#/components/schemas/TaskActivityType"
              },
              "payload": {
                "type": "object",
                "additionalProperties": true,
                "description": "Small typed diff whose shape **varies by `activity_type`** — `{ field, from, to, ref_id, note }` (`COMMENTED` carries the comment id in `ref_id`; `TIME_LOGGED` the hours and the work-entry id; `CONVERTED_FROM_TICKET` the source ticket number). Render input only: not queried, not joined (db-docs/00 §7).\n"
              }
            }
          },
          {
            "$ref": "#/components/schemas/AppendOnlyMetaRef"
          },
          {
            "type": "object",
            "readOnly": true,
            "properties": {
              "actor": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/EmployeeRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          }
        ]
      },
      "TaskActivityPage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TaskActivity"
                }
              }
            }
          }
        ]
      },
      "ProjectActivityScope": {
        "type": "string",
        "enum": [
          "TASK",
          "PROJECT"
        ],
        "description": "The **subject** of the event — a task inside the project, or the project itself. It is *not* the storage origin: a client must not infer that `TASK` means `work.task_activity` or that `PROJECT` means `audit.audit_log`. Storage is not a contract here and may change.\n"
      },
      "ProjectActivityKind": {
        "type": "string",
        "enum": [
          "TASK_STATUS_CHANGED",
          "TASK_ASSIGNED",
          "TASK_UNASSIGNED",
          "TASK_MILESTONE_CHANGED",
          "TASK_DUE_DATE_CHANGED",
          "TASK_BLOCKED",
          "TASK_UNBLOCKED",
          "TASK_TIME_LOGGED",
          "PROJECT_STATUS_CHANGED",
          "PROJECT_DELIVERY_MODEL_CHANGED",
          "PROJECT_OWNER_CHANGED",
          "PROJECT_DATES_CHANGED",
          "PROJECT_BUDGET_CHANGED",
          "PROJECT_HEALTH_OVERRIDDEN",
          "PROJECT_HEALTH_OVERRIDE_CLEARED",
          "PROJECT_CLOSED",
          "MILESTONE_ADDED",
          "MILESTONE_STATUS_CHANGED",
          "MILESTONE_DATES_CHANGED",
          "MILESTONE_REMOVED",
          "ALLOCATION_ADDED",
          "ALLOCATION_CHANGED",
          "ALLOCATION_ENDED"
        ],
        "description": "**The allow-list.** A CLOSED set: an event whose kind is not named here is not projected into this feed at all, whatever its origin. That closure is what lets the feed read `audit.audit_log` — the tenant-invisible evidence plane — without becoming a window onto it.\nThe set admits exactly the events that change the **delivery picture**. Deliberately absent, each for a stated reason: `COMMENTED` and `ATTACHMENT_ADDED` (their bodies and filenames are the classic PII carriers, and a caller who may read a thread reads it under `work.task_comment.list`); `ESTIMATE_CHANGED`, `DEPENDENCY_ADDED`/`_REMOVED` and `CONVERTED_FROM_TICKET` (task-desk detail that belongs on `WRK-S09`, not on the project roll-up); and **every** authentication, permission-grant, export and impersonation event in `audit.audit_log`, which has no business on a product surface.\n`PROJECT_BUDGET_CHANGED` is the worked example of the money rule: the kind states that the budget moved; `ProjectActivityDetail` has no field that can carry what it moved to.\n**`MILESTONE_REMOVED` is RESERVED — no producer in this slice.** There is no milestone delete or soft-delete operation on the work surface and nothing writes a corresponding audit action, so the mapping cannot emit this value: it is unreachable, not wrong. A consumer must still **handle** it (it is a declared member and the enum is append-only, so a client that throws on an unknown value would break when a producer lands), but must **not** build a UI branch that waits for it — no such event will arrive until a milestone-removal operation exists. `MILESTONE_STATUS_CHANGED` and `MILESTONE_DATES_CHANGED` both have producers today.\n**Append-only** — a kind is added, never renamed or removed, because rendered history would otherwise change meaning retroactively. Adding one is an api-docs change first and is reviewed as a **disclosure** change, since it widens what this endpoint projects out of `audit`.\n"
      },
      "ProjectActivityDetail": {
        "type": "object",
        "additionalProperties": false,
        "description": "What changed — **by name only**. There is deliberately no `from`, no `to`, no amount and no copied payload text: the endpoint's money/PII guarantee is enforced by the *absence of a field to put a value in*, not by a service-layer filter that a later refactor could drop. Compare `TaskActivity.payload`, which is an open `{ field, from, to, … }` diff — that shape is available on `work.task_activity.list` under its own token and is **not** re-exposed here.\n",
        "properties": {
          "field": {
            "type": [
              "string",
              "null"
            ],
            "description": "The NAME of the column or attribute that changed, e.g. `budget_amount`, `status`, `allocation_pct`, `owner_id`. Never its value. Null where the kind is self-describing (`PROJECT_CLOSED`, `TASK_BLOCKED`).\n"
          },
          "note": {
            "type": [
              "string",
              "null"
            ],
            "description": "A short operator-authored note where the action requires one — today only the **reason** on `PROJECT_HEALTH_OVERRIDDEN`, which `WRK-S14` renders as *\"Set by &lt;name&gt; · &lt;reason&gt;\"*. It is the one free-text field in this schema and it is populated for that kind alone; it is never a channel for a diff value, a comment body or an audit payload string.\n"
          }
        }
      },
      "ProjectActivity": {
        "description": "One item in the project's activity feed. Immutable — superseded by a later row, never corrected in place.\n",
        "type": "object",
        "required": [
          "id",
          "occurred_at",
          "scope",
          "kind"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "An **opaque, stable** identifier for this item, used for de-duplication and as the keyset cursor anchor. Deliberately not a `UuidRef`: publishing either origin's primary key would leak the storage split this shape exists to hide, and an `audit.audit_log` id is not a product-facing handle. Do not parse it and do not address any other endpoint with it.\n"
          },
          "occurred_at": {
            "$ref": "#/components/schemas/TimestampRef"
          },
          "scope": {
            "$ref": "#/components/schemas/ProjectActivityScope"
          },
          "kind": {
            "$ref": "#/components/schemas/ProjectActivityKind"
          },
          "actor_id": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "**Null for system/jobs** actions — the health recompute (`XC-F08`), recurrence materialization, the SLA scan."
          },
          "detail": {
            "$ref": "#/components/schemas/ProjectActivityDetail"
          },
          "task": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/TaskRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "The task the event is about. Present when `scope` is `TASK`, null otherwise."
          },
          "milestone": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MilestoneRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "Set on the `MILESTONE_*` kinds, and on `TASK_MILESTONE_CHANGED` for the milestone the task now sits under."
          },
          "actor": {
            "readOnly": true,
            "anyOf": [
              {
                "$ref": "#/components/schemas/EmployeeRef"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "ProjectActivityPage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ProjectActivity"
                }
              }
            }
          }
        ]
      },
      "TaskBulkUpdateItem": {
        "type": "object",
        "required": [
          "task_id",
          "if_match"
        ],
        "additionalProperties": false,
        "properties": {
          "task_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "if_match": {
            "type": "string",
            "description": "The task's current ETag (row version) — the per-item equivalent of `If-Match`, since a bulk call cannot carry one header."
          }
        }
      },
      "TaskBulkUpdateRequest": {
        "type": "object",
        "description": "At least one of the change fields must be present; the same change is applied to every listed task, each still evaluated against the per-row tokens.\n**`iteration_id` and `estimate_points` are here on purpose, and it is a deliberate REUSE, not an oversight** (leg B5, #871). They were added when `PATCH /tasks/{id}` — the single-row task edit — did not yet exist, so this was the only write door onto the two columns migration 0109 landed, and it is exactly the door the sprint lens needs: dragging cards into a sprint, back to the backlog, or sizing a selection are all many-cards-at-once gestures. **#874 has since landed** and `PATCH /tasks/{id}` now takes the single-row case while this stays the bulk one, exactly as that note predicted; neither supersedes the other. Note the two operations carry DIFFERENT field sets — `iteration_id` and `estimate_points` are bulk-only, and `title` and `description` are single-row-only.\n**`estimated_hours` joined this body with leg L2 (#874)** and pulls in `work.task.update` exactly as `priority`, `milestone_id` and `due_date` already do. It is HOURS — the burn denominator — and a different quantity from `estimate_points`, which is story points.\n**NULL MEANS TWO DIFFERENT THINGS ON THIS BODY, and the difference is presence.** For the pre-existing fields (`status`, `assignee_id`, `priority`, `milestone_id`, `due_date`) an omitted or `null` value means *leave unchanged* — the server applies `COALESCE`, so those fields have never been clearable here and this revision does not change that. For the two new fields, **an explicitly present `null` CLEARS the value** (`iteration_id: null` sends the cards back to the backlog; `estimate_points: null` un-sizes them) and only ABSENCE means leave-unchanged. Sprint planning is not possible otherwise: a lens that can drag a card into a sprint but never out of one is not a lens.\n**Cross-board assignment is refused.** An iteration belongs to exactly one board, so naming an iteration from a different board than a task's own is a `422` on that row, never an incoherent stored row. The named iteration must also exist and be readable, or the whole request is a `422` under `/iteration_id` — a bad reference is a field error, not a `404` (the `#805` convention this module already applies to `work_entries.project_id`).\n",
        "required": [
          "items"
        ],
        "additionalProperties": false,
        "properties": {
          "items": {
            "type": "array",
            "minItems": 1,
            "maxItems": 200,
            "items": {
              "$ref": "#/components/schemas/TaskBulkUpdateItem"
            }
          },
          "status": {
            "$ref": "#/components/schemas/TaskStatus"
          },
          "assignee_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "priority": {
            "$ref": "#/components/schemas/TaskPriority"
          },
          "milestone_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "due_date": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "estimated_hours": {
            "$ref": "#/components/schemas/DecimalHoursRef"
          },
          "iteration_id": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "Commit the selected cards to this sprint, or `null` to send them back to the backlog. Must belong to each task's own board (`422` per row otherwise). Requires `work.task.update`, the same per-row token the single-row edit will require.\n"
          },
          "estimate_points": {
            "anyOf": [
              {
                "type": "string",
                "pattern": "^(?:0|[1-9]\\d{0,3})(?:\\.\\d{1,2})?$"
              },
              {
                "type": "null"
              }
            ],
            "description": "Story points for the selected cards (0–9999.99, two decimals), or `null` to un-size them. **Never hours** — see `Task.estimate_points`. Requires `work.task.update`.\n"
          }
        }
      },
      "TaskBulkUpdateResult": {
        "type": "object",
        "readOnly": true,
        "properties": {
          "task_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "outcome": {
            "type": "string",
            "enum": [
              "UPDATED",
              "FAILED"
            ]
          },
          "error_code": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ErrorCode"
              },
              {
                "type": "null"
              }
            ]
          },
          "message": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why this row failed — surfaced per row, never collapsed into a single \"some items failed\"."
          },
          "warnings": {
            "type": "array",
            "description": "Leave/holiday advisories for **this row's** assignee against **this row's** dates (#801) — per item, because a bulk edit spans many people and many due dates and one collapsed banner would name none of them. Present on `outcome: UPDATED` rows only; a `FAILED` row is unchanged and carries its `error_code`/`message` instead. Never blocking — a warned row still updated.\n",
            "items": {
              "$ref": "#/components/schemas/WorkWarning"
            }
          }
        }
      },
      "TaskBulkUpdateResponse": {
        "type": "object",
        "required": [
          "results"
        ],
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TaskBulkUpdateResult"
            }
          },
          "updated_count": {
            "type": "integer"
          },
          "failed_count": {
            "type": "integer"
          }
        }
      },
      "SavedViewFilters": {
        "type": "object",
        "description": "`work.saved_views.filters` — an **opaque preference blob, never joined** (db 06 §9). The ids inside are not FKs and are not validated on read, so a view naming a deleted project simply returns nothing instead of erroring. `group_by` and `sort` live **inside** it (fsd 05 §1.7) so a shared view reproduces the grouping its author saw.\n",
        "additionalProperties": true,
        "properties": {
          "project_ids": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          "board_ids": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          "assignee_ids": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          "milestone_ids": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          "statuses": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TaskStatus"
            }
          },
          "priorities": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TaskPriority"
            }
          },
          "due": {
            "description": "Either an explicit range or one of the named windows.",
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "from": {
                    "$ref": "#/components/schemas/DateOnlyRef"
                  },
                  "to": {
                    "$ref": "#/components/schemas/DateOnlyRef"
                  }
                }
              },
              {
                "type": "string",
                "enum": [
                  "OVERDUE",
                  "THIS_WEEK"
                ]
              }
            ]
          },
          "watched_by_me": {
            "type": "boolean"
          },
          "group_by": {
            "type": "string",
            "description": "e.g. `status`, `assignee`, `project`, `milestone`, `priority`, `due_week`, `none` (WRK-S16)."
          },
          "sort": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "field": {
                  "type": "string"
                },
                "dir": {
                  "type": "string",
                  "enum": [
                    "ASC",
                    "DESC"
                  ]
                }
              }
            }
          }
        }
      },
      "SavedView": {
        "description": "work.saved_views — a named filter/group/sort set over one work surface (db 06 §9).",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "owner_id",
              "surface",
              "name",
              "is_shared"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "owner_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "surface": {
                "$ref": "#/components/schemas/ViewSurface"
              },
              "name": {
                "type": "string"
              },
              "filters": {
                "$ref": "#/components/schemas/SavedViewFilters"
              },
              "is_shared": {
                "type": "boolean",
                "description": "`false` = private to the owner · `true` = visible tenant-wide. **There is no `TEAM` tier** (db 06 §9). Tenant-visible is **not access-granting**: a shared view never widens the rows a viewer may see.\n"
              }
            }
          },
          {
            "$ref": "#/components/schemas/AuditMetaRef"
          }
        ]
      },
      "SavedViewCreate": {
        "type": "object",
        "required": [
          "surface",
          "name"
        ],
        "additionalProperties": false,
        "properties": {
          "surface": {
            "$ref": "#/components/schemas/ViewSurface"
          },
          "name": {
            "type": "string",
            "minLength": 1
          },
          "filters": {
            "$ref": "#/components/schemas/SavedViewFilters"
          }
        }
      },
      "SavedViewUpdate": {
        "type": "object",
        "description": "`is_shared` is deliberately absent — visibility is the separate `work.saved_view.share` gate.",
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1
          },
          "filters": {
            "$ref": "#/components/schemas/SavedViewFilters"
          }
        }
      },
      "SavedViewShareInput": {
        "type": "object",
        "required": [
          "is_shared"
        ],
        "additionalProperties": false,
        "properties": {
          "is_shared": {
            "type": "boolean"
          }
        }
      },
      "SavedViewPage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/SavedView"
                }
              }
            }
          }
        ]
      },
      "WorkTemplatePayload": {
        "type": "object",
        "description": "`work.work_templates.payload` (db 06 §8). Dates are **relative `offset_days`, never absolute**, so a template ages well; `milestone_ref`/`parent_ref` are **template-local string keys** resolved at instantiation, not uuids. Nothing inside is an FK and none of it is queried.\n",
        "additionalProperties": true,
        "properties": {
          "milestones": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "offset_days": {
                  "type": "integer"
                },
                "sort_order": {
                  "type": "integer"
                }
              }
            }
          },
          "boards": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "board_type": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/BoardType"
                    }
                  ],
                  "deprecated": true,
                  "description": "Accepted and validated for backward compatibility with templates already stored, then **ignored** — `0177` dropped the column it used to write (#873). Do not send it in new templates.\n"
                },
                "columns": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/TaskBoardColumn"
                  }
                }
              }
            }
          },
          "tasks": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "title": {
                  "type": "string"
                },
                "description": {
                  "type": "string"
                },
                "priority": {
                  "$ref": "#/components/schemas/TaskPriority"
                },
                "estimated_hours": {
                  "$ref": "#/components/schemas/DecimalHoursRef"
                },
                "offset_days": {
                  "type": "integer"
                },
                "milestone_ref": {
                  "type": "string"
                },
                "parent_ref": {
                  "type": "string"
                },
                "role_note": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "WorkTemplate": {
        "description": "work.work_templates — a reusable project or task structure (db 06 §8). Instantiation is a **copy, not a link**: editing a template never mutates what it already created.",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "template_kind",
              "name",
              "status"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "template_kind": {
                "$ref": "#/components/schemas/TemplateKind"
              },
              "name": {
                "type": "string"
              },
              "description": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "payload": {
                "$ref": "#/components/schemas/WorkTemplatePayload"
              },
              "status": {
                "$ref": "#/components/schemas/TemplateStatus",
                "description": "**Versionless publishing** — republishing overwrites `payload`, and there is no `published_at` column, so *\"which template version created this project?\"* is deliberately not answerable until a tenant asks for it (db 06 §8). The standard `updated_at` carries the timing.\n"
              }
            }
          },
          {
            "$ref": "#/components/schemas/AuditMetaRef"
          }
        ]
      },
      "WorkTemplateCreate": {
        "type": "object",
        "required": [
          "template_kind",
          "name"
        ],
        "additionalProperties": false,
        "properties": {
          "template_kind": {
            "$ref": "#/components/schemas/TemplateKind"
          },
          "name": {
            "type": "string",
            "minLength": 1
          },
          "description": {
            "type": "string"
          },
          "payload": {
            "$ref": "#/components/schemas/WorkTemplatePayload"
          }
        }
      },
      "WorkTemplateUpdate": {
        "type": "object",
        "description": "`status` is deliberately absent — publishing is the separate `work.template.publish` gate.",
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1
          },
          "description": {
            "type": "string"
          },
          "payload": {
            "$ref": "#/components/schemas/WorkTemplatePayload"
          }
        }
      },
      "WorkTemplatePage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/WorkTemplate"
                }
              }
            }
          }
        ]
      },
      "WorkTemplateInstantiateInput": {
        "type": "object",
        "description": "A `PROJECT` template creates its own project; a `TASK` template needs `target_project_id` (or a `board_id`).",
        "additionalProperties": false,
        "properties": {
          "dry_run": {
            "type": "boolean",
            "default": false,
            "description": "`true` returns the same `preview` the apply modal shows and writes **nothing**."
          },
          "start_date": {
            "$ref": "#/components/schemas/DateOnlyRef",
            "description": "The anchor `offset_days` resolve against. Defaults to today."
          },
          "project_name": {
            "type": "string",
            "description": "Overrides the created project's name (`PROJECT` templates)."
          },
          "target_project_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "board_id": {
            "$ref": "#/components/schemas/UuidRef"
          }
        }
      },
      "WorkTemplateInstantiateResult": {
        "type": "object",
        "description": "What was (or, for `dry_run`, would be) created — the preview a PM sees before committing. On failure nothing is created; it is one transaction.",
        "readOnly": true,
        "required": [
          "dry_run",
          "created_task_count"
        ],
        "properties": {
          "dry_run": {
            "type": "boolean"
          },
          "project": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ProjectRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "created_milestone_count": {
            "type": "integer"
          },
          "created_board_count": {
            "type": "integer"
          },
          "created_task_count": {
            "type": "integer"
          },
          "preview": {
            "type": "array",
            "description": "Every milestone and task to be created, in order, with its resolved dates.",
            "items": {
              "type": "object",
              "properties": {
                "kind": {
                  "type": "string",
                  "enum": [
                    "MILESTONE",
                    "BOARD",
                    "TASK"
                  ]
                },
                "title": {
                  "type": "string"
                },
                "resolved_date": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/DateOnlyRef"
                    },
                    {
                      "type": "null"
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "RecurrenceRule": {
        "type": "object",
        "description": "`work.task_recurrences.recurrence` — an **RFC-5545 RRULE subset**, deliberately not the full spec (no `BYSETPOS`, no `EXDATE` at launch). **Unsupported keys are rejected at write, not silently ignored** (db 06 §8): quietly dropping a rule the operator typed is the failure mode that erodes trust in a scheduler. The week boundary and timezone resolve from the legal entity's market work week (`XC-F01`), never a hard-coded Monday.\n",
        "additionalProperties": false,
        "required": [
          "freq"
        ],
        "properties": {
          "freq": {
            "type": "string",
            "enum": [
              "DAILY",
              "WEEKLY",
              "MONTHLY",
              "YEARLY"
            ]
          },
          "interval": {
            "type": "integer",
            "minimum": 1,
            "default": 1
          },
          "byday": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "MO",
                "TU",
                "WE",
                "TH",
                "FR",
                "SA",
                "SU"
              ]
            }
          },
          "bymonthday": {
            "type": "array",
            "items": {
              "type": "integer",
              "minimum": -31,
              "maximum": 31
            }
          },
          "timezone": {
            "type": "string"
          },
          "until": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DateOnlyRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "The rule's end. There are no separate start/end columns — a rule's start is when it is created."
          },
          "count": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1
          }
        }
      },
      "TaskRecurrence": {
        "description": "work.task_recurrences — a recurrence rule attached to a published template or a seed task (db 06 §8). Materialized by the jobs tier, idempotent per rule per period.",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "source_type",
              "source_id",
              "recurrence",
              "is_active"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "source_type": {
                "$ref": "#/components/schemas/RecurrenceSourceType"
              },
              "source_id": {
                "$ref": "#/components/schemas/UuidRef",
                "description": "The `work_templates` row (`TEMPLATE`) or the seed `tasks` row (`TASK`) — **polymorphic, no FK** (db-docs/00 §13), write-time service-validated. **There is no `name` column**: the source''s own name is the rule''s display label.\n"
              },
              "recurrence": {
                "$ref": "#/components/schemas/RecurrenceRule"
              },
              "next_run_at": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/TimestampRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "**Advisory scheduling state, not the guard** — `last_materialized_period` is the guard."
              },
              "last_materialized_period": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Period key of the last successful materialization (`2026-W31`, `2026-08`, …) — **the idempotency key**, written in the same transaction as the created task(s), so a retried or double-scheduled job cannot double-create work.\n"
              },
              "is_active": {
                "type": "boolean",
                "description": "Pausing stops materialization **without deleting the history**."
              }
            }
          },
          {
            "$ref": "#/components/schemas/AuditMetaRef"
          },
          {
            "type": "object",
            "readOnly": true,
            "properties": {
              "source_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Resolved display label from the source template or seed task."
              },
              "history": {
                "type": "array",
                "description": "Materialization history — per run, the period key, what it created, and **failures with their reason**. A skipped or failed run is listed, never omitted. A missed window materializes the **current** period on recovery and does **not** backfill.\n\n**Source (#439):** projected from `xc.job_runs` rows written by the jobs-tier sweep `work.task_recurrence.materialize`, one per (rule, period), keyed `idempotency_key = '<recurrence id>:<period key>'` — that table's unique index is a second, database-level guard behind `last_materialized_period`. Filtered to this rule via `scope_ref->>'recurrence_id'`, **newest first, capped at 20** (an embedded field on a paged read, not a page of its own; a two-year-old daily rule has ~730 runs).\n\nA run SKIPPED by the period-key guard writes **no new row** — the period's existing row *is* its record, which keeps \"listed, never omitted\" true without minting a row per no-op tick. There is therefore no `SKIPPED` status on the wire.\n",
                "items": {
                  "type": "object",
                  "required": [
                    "period_key",
                    "status",
                    "ran_at",
                    "created_task_count",
                    "created_task_ids"
                  ],
                  "properties": {
                    "period_key": {
                      "type": "string",
                      "description": "The run's period key (`2026-08-24` · `2026-W31` · `2026-08` · `2026`) — the idempotency key."
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "SUCCEEDED",
                        "FAILED"
                      ],
                      "description": "`xc.job_run_status`, verbatim. `FAILED` always carries `error`."
                    },
                    "ran_at": {
                      "$ref": "#/components/schemas/TimestampRef",
                      "description": "When the run settled (`finished_at`, falling back to `started_at` then `created_at` so this is never blank)."
                    },
                    "created_task_count": {
                      "type": "integer",
                      "description": "`0` on a failure."
                    },
                    "created_task_ids": {
                      "type": "array",
                      "description": "The `work.tasks` this run created. Ids only — the cards are fetched from the task endpoints.",
                      "items": {
                        "$ref": "#/components/schemas/UuidRef"
                      }
                    },
                    "error": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The reason a run failed — a soft-deleted source, an unpublished template, no default board. Non-null exactly when `status = FAILED`."
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "TaskRecurrenceCreate": {
        "type": "object",
        "required": [
          "source_type",
          "source_id",
          "recurrence"
        ],
        "additionalProperties": false,
        "properties": {
          "source_type": {
            "$ref": "#/components/schemas/RecurrenceSourceType"
          },
          "source_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "recurrence": {
            "$ref": "#/components/schemas/RecurrenceRule"
          },
          "is_active": {
            "type": "boolean",
            "default": true
          }
        }
      },
      "TaskRecurrenceUpdate": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "recurrence": {
            "$ref": "#/components/schemas/RecurrenceRule"
          },
          "is_active": {
            "type": "boolean"
          }
        }
      },
      "TaskRecurrencePage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TaskRecurrence"
                }
              }
            }
          }
        ]
      },
      "MyWorkRow": {
        "type": "object",
        "readOnly": true,
        "properties": {
          "task": {
            "$ref": "#/components/schemas/Task"
          },
          "project": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ProjectRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "milestone": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MilestoneRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "blocked_by": {
            "type": "array",
            "description": "What blocks it — shown as who/what, not just a badge (`WRK-F10`).",
            "items": {
              "$ref": "#/components/schemas/TaskRef"
            }
          },
          "comment_excerpt": {
            "type": [
              "string",
              "null"
            ],
            "description": "For the *Recently mentioned* section — the excerpt, with the row deep-linking to the comment anchor on `WRK-S09`."
          },
          "mentioned_at": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/TimestampRef"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "MyWork": {
        "type": "object",
        "description": "WRK-S15 — the self-scoped personal queue. **Empty sections are omitted, not returned empty**, so the page never renders five \"no items\" panels; an entirely empty payload is the *\"Nothing assigned to you right now\"* state. Rows come from the same `XC-F09` projections as the Work tab — no second aggregation path.\n",
        "readOnly": true,
        "properties": {
          "today": {
            "type": "object",
            "description": "The today strip.",
            "properties": {
              "hours_logged_today": {
                "$ref": "#/components/schemas/DecimalHoursRef"
              },
              "timesheet": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/MyWorkTimesheet"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          },
          "overdue": {
            "type": "array",
            "description": "`due_date < today AND status ∉ {DONE, CANCELLED}`, oldest first. Omitted when empty.",
            "items": {
              "$ref": "#/components/schemas/MyWorkRow"
            }
          },
          "due_soon": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MyWorkRow"
            }
          },
          "assigned": {
            "type": "array",
            "description": "Everything else open, from `ACTIVE` `work.task_assignments`, grouped by project on screen.",
            "items": {
              "$ref": "#/components/schemas/MyWorkRow"
            }
          },
          "blocked": {
            "type": "array",
            "description": "Tasks with an unresolved `BLOCKS` dependency, or `status = BLOCKED`.",
            "items": {
              "$ref": "#/components/schemas/MyWorkRow"
            }
          },
          "mentioned": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MyWorkRow"
            }
          }
        }
      },
      "MyWorkTimesheet": {
        "type": "object",
        "readOnly": true,
        "description": "The caller's current-period timesheet projection in the My Work today strip. Its `decision_log` uses the exact `TimesheetApproval` shape and newest-first ordering as the general timesheet reads; a currently `REJECTED` sheet surfaces the first rejection comment.\n",
        "required": [
          "id",
          "timesheet_no",
          "period_type",
          "period_start",
          "period_end",
          "status",
          "total_hours",
          "approved_hours",
          "version",
          "decision_log"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "timesheet_no": {
            "$ref": "#/components/schemas/BusinessNoRef"
          },
          "period_type": {
            "$ref": "#/components/schemas/TimesheetPeriod"
          },
          "period_start": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "period_end": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "status": {
            "$ref": "#/components/schemas/TimesheetStatus"
          },
          "total_hours": {
            "$ref": "#/components/schemas/DecimalHoursRef"
          },
          "approved_hours": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DecimalHoursRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "version": {
            "type": "integer",
            "minimum": 0
          },
          "decision_log": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TimesheetApproval"
            }
          }
        }
      },
      "ProjectShare": {
        "type": "object",
        "description": "A curated, expiring client share link (`WRK-S25`, `WRK-F20`). **`work.project_shares` / `work.project_share_access` are PROPOSED names, not entities in db 06** — the security round owes the signed-link threat model first, and its columns *are* that threat model (db 06 §10). This shape specifies the **behaviour** the threat model has to make safe, deliberately, so there is something concrete to attack.\n",
        "readOnly": true,
        "required": [
          "id",
          "project_id",
          "field_allowlist",
          "expires_at"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "project_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "The signed link, returned **once, at mint time** and never again on a list read. Whether the token is ever stored in the clear is a threat-model decision, not one this contract forecloses.\n"
          },
          "field_allowlist": {
            "type": "array",
            "minItems": 1,
            "description": "Explicit opt-in per field group — **nothing is on by default**. May **never** include assignee names, comments, task detail, hours or rates (`WRK-F20`).\n",
            "items": {
              "type": "string",
              "enum": [
                "NAME",
                "STATUS",
                "PERCENT_COMPLETE",
                "MILESTONES",
                "CURATED_UPDATES"
              ]
            }
          },
          "expires_at": {
            "$ref": "#/components/schemas/TimestampRef"
          },
          "revoked_at": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/TimestampRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "Revocation is **immediate**, and a revoked link then answers exactly like an unknown or expired one — no oracle."
          },
          "last_accessed_at": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/TimestampRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "access_count": {
            "type": "integer",
            "description": "Every access is logged and visible to the curator (`WRK-F20`)."
          }
        }
      },
      "ProjectShareCreate": {
        "type": "object",
        "required": [
          "field_allowlist",
          "expires_at"
        ],
        "additionalProperties": false,
        "properties": {
          "field_allowlist": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string",
              "enum": [
                "NAME",
                "STATUS",
                "PERCENT_COMPLETE",
                "MILESTONES",
                "CURATED_UPDATES"
              ]
            }
          },
          "expires_at": {
            "$ref": "#/components/schemas/TimestampRef",
            "description": "**Required** — a share with no expiry is not offered (`WRK-S25`)."
          },
          "updates": {
            "type": "array",
            "description": "Curated updates written **for** the client — not a republished internal feed.",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "ProjectSharePage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ProjectShare"
                }
              }
            }
          }
        ]
      },
      "ImportRowIssue": {
        "type": "object",
        "readOnly": true,
        "properties": {
          "row_number": {
            "type": "integer",
            "description": "The source row — errors are always reported **by row number**, never in aggregate."
          },
          "severity": {
            "type": "string",
            "enum": [
              "ERROR",
              "WARNING"
            ]
          },
          "message": {
            "type": "string"
          }
        }
      },
      "ImportBatch": {
        "type": "object",
        "description": "A work-import batch (`WRK-S26`, `WRK-F21`). **`work.import_batches` is a PROPOSED entity** — db 06 §10 defers the staging model (a batch plus a per-row staging table) to the import round, which is what makes the dry-run preview and the idempotent-per-batch re-run possible without touching live `work.tasks`.\n",
        "readOnly": true,
        "required": [
          "id",
          "source_type",
          "status"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "source_type": {
            "$ref": "#/components/schemas/ImportSourceType"
          },
          "status": {
            "$ref": "#/components/schemas/ImportBatchStatus"
          },
          "target_project_id": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "include_attachments": {
            "type": "boolean",
            "description": "**Off by default** — fetching remote files is explicit opt-in (`WRK-F21`)."
          },
          "mapping": {
            "type": "object",
            "additionalProperties": true,
            "description": "Source column → `work.tasks` field, plus the status/user value maps. Unmatched source users are **left unassigned, never guessed**."
          },
          "ignored_source_columns": {
            "type": "array",
            "description": "Listed explicitly as \"will be ignored\" — never dropped silently (`WRK-S26`).",
            "items": {
              "type": "string"
            }
          },
          "dry_run_result": {
            "type": "object",
            "description": "Counts and the **full** issue list. Nothing is written by a dry run.",
            "properties": {
              "to_create": {
                "type": "integer"
              },
              "to_update": {
                "type": "integer"
              },
              "to_skip": {
                "type": "integer"
              },
              "issues": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ImportRowIssue"
                }
              },
              "sample": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TaskRef"
                }
              }
            }
          },
          "result": {
            "type": "object",
            "description": "The run outcome. A partial import reports **exactly** what landed, and the batch can be re-run idempotently.",
            "properties": {
              "created": {
                "type": "integer"
              },
              "skipped": {
                "type": "integer"
              },
              "failed": {
                "type": "integer"
              },
              "issues": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ImportRowIssue"
                }
              }
            }
          },
          "job_id": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "The jobs-tier run to poll (`xc.job_run.get`, XC-F08)."
          }
        }
      },
      "ImportBatchCreate": {
        "type": "object",
        "required": [
          "source_type",
          "storage_key",
          "mapping"
        ],
        "additionalProperties": false,
        "properties": {
          "source_type": {
            "$ref": "#/components/schemas/ImportSourceType"
          },
          "storage_key": {
            "type": "string",
            "description": "The uploaded file's reference from the presigned-upload flow (`XC-F07`); bytes never transit this API."
          },
          "target_project_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "create_project": {
            "type": "boolean",
            "default": false,
            "description": "Create a new project to receive the rows instead of targeting an existing one."
          },
          "mapping": {
            "type": "object",
            "additionalProperties": true
          },
          "include_attachments": {
            "type": "boolean",
            "default": false
          }
        }
      },
      "TeamStatus": {
        "type": "string",
        "enum": [
          "ACTIVE",
          "ARCHIVED"
        ],
        "description": "`work.team_status` (db 06 §12). `ACTIVE` a live squad — the only state the Teams-nav progressive disclosure counts · `ARCHIVED` retired, kept for history. Archiving also closes the roster; the membership resolver reads `team_members` alone and never checks this column.\n"
      },
      "TeamMemberRole": {
        "type": "string",
        "enum": [
          "LEAD",
          "MEMBER",
          "GUEST"
        ],
        "description": "`work.team_member_role` (db 06 §12) — the member's role ON the roster. Distinct from `teams.lead_employee_id`, which is the squad's WRITE AUTHORITY; the two are mirrored by convention, not by constraint.\n"
      },
      "TeamMemberSource": {
        "type": "string",
        "enum": [
          "EXPLICIT",
          "ORG_UNIT_SYNC"
        ],
        "description": "`work.team_member_source` (db 06 §12) — how the row got there. `EXPLICIT` hand-added · `ORG_UNIT_SYNC` written by the one-shot, re-runnable org-unit seed. Keeping the two distinguishable is what lets a re-seed report drift instead of silently reconciling somebody's deliberate edit.\n"
      },
      "Team": {
        "description": "work.teams — a delivery squad (db 06 §12, ADR 0037).",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "team_key",
              "name",
              "status"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "team_key": {
                "type": "string",
                "description": "Short handle, unique per tenant case-insensitively; `^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$`. A team is cited by this, not by a business number."
              },
              "name": {
                "type": "string"
              },
              "description": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "lead_employee_id": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "org_unit_id": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Nullable SOFT anchor to the org tree — no FK, so archiving a team never touches the org tree and moving an org unit never re-shapes a squad."
              },
              "status": {
                "$ref": "#/components/schemas/TeamStatus"
              },
              "settings": {
                "type": "object",
                "additionalProperties": true
              }
            }
          },
          {
            "$ref": "#/components/schemas/AuditMetaRef"
          }
        ]
      },
      "TeamCreate": {
        "type": "object",
        "required": [
          "team_key",
          "name"
        ],
        "additionalProperties": false,
        "properties": {
          "team_key": {
            "type": "string",
            "minLength": 1,
            "maxLength": 32
          },
          "name": {
            "type": "string",
            "minLength": 1
          },
          "description": {
            "type": "string"
          },
          "lead_employee_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "org_unit_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "settings": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "TeamUpdate": {
        "type": "object",
        "additionalProperties": false,
        "description": "`status` is intentionally absent — retiring a squad is `work.team.archive`, which also closes the roster.",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "lead_employee_id": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "org_unit_id": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "settings": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "TeamPage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Team"
                }
              }
            }
          }
        ]
      },
      "MyTeam": {
        "description": "A team the caller is a live member of, carrying the caller's own membership facts.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Team"
          },
          {
            "type": "object",
            "required": [
              "membership_id",
              "member_role",
              "joined_at"
            ],
            "properties": {
              "membership_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "member_role": {
                "$ref": "#/components/schemas/TeamMemberRole"
              },
              "joined_at": {
                "$ref": "#/components/schemas/TimestampRef"
              }
            }
          }
        ]
      },
      "MyTeamPage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/MyTeam"
                }
              }
            }
          }
        ]
      },
      "TeamMember": {
        "description": "work.team_members — one membership row; `left_at = null` is the live one (db 06 §12).",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "team_id",
              "employee_id",
              "member_role",
              "source",
              "joined_at"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "team_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "employee_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "member_role": {
                "$ref": "#/components/schemas/TeamMemberRole"
              },
              "source": {
                "$ref": "#/components/schemas/TeamMemberSource"
              },
              "joined_at": {
                "$ref": "#/components/schemas/TimestampRef"
              },
              "left_at": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/TimestampRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          },
          {
            "$ref": "#/components/schemas/AuditMetaRef"
          },
          {
            "type": "object",
            "readOnly": true,
            "properties": {
              "employee": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/EmployeeRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          }
        ]
      },
      "TeamMemberCreate": {
        "type": "object",
        "required": [
          "employee_id"
        ],
        "additionalProperties": false,
        "properties": {
          "employee_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "member_role": {
            "$ref": "#/components/schemas/TeamMemberRole"
          }
        }
      },
      "TeamMemberUpdate": {
        "type": "object",
        "required": [
          "member_role"
        ],
        "additionalProperties": false,
        "properties": {
          "member_role": {
            "$ref": "#/components/schemas/TeamMemberRole"
          }
        }
      },
      "IterationStatus": {
        "type": "string",
        "enum": [
          "PLANNED",
          "ACTIVE",
          "COMPLETED"
        ],
        "description": "`work.iteration_status` (db 06 §13). `PLANNED` created, not started — the backlog-grooming target · `ACTIVE` in flight, **at most one per board** (`work_iterations_board_active_key`) · `COMPLETED` closed with `closed_at` stamped, its unfinished tasks moved by `work.iteration.close` and never by a cascade. The enum is a lifecycle, not a filter vocabulary: there is no writable `status` field anywhere — `activate` and `close` are the only two transitions, and both are refused from `COMPLETED`.\n"
      },
      "Iteration": {
        "description": "work.iterations — a board-owned sprint (db 06 §13, ADR 0037). Board-owned rather than project-owned because a stable team's cadence outlives any one engagement. It owns no time and no estimate of its own: velocity is an aggregate over its tasks' `estimate_points` and their approved work entries, so nothing rolled-up is stored or projected here.\n",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "board_id",
              "name",
              "status",
              "sort_order"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "board_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "name": {
                "type": "string",
                "description": "Iteration title, e.g. \"Sprint 14\". Unique per board among live rows; non-empty after trim."
              },
              "goal": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The sprint goal the team committed to. The column is `goal`, not `description` — 0109 named it after what a sprint actually carries."
              },
              "start_date": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/DateOnlyRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "end_date": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/DateOnlyRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Must not precede `start_date` when both are present (`work_iterations_dates_check`). Either may be null — an undated placeholder is a legitimate plan."
              },
              "status": {
                "$ref": "#/components/schemas/IterationStatus"
              },
              "sort_order": {
                "type": "integer",
                "minimum": 0,
                "description": "Display order within the board; the planning list's default sort."
              },
              "closed_at": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/TimestampRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Stamped by `work.iteration.close`. Required whenever `status = COMPLETED` (`work_iterations_closed_check`) — the pair is inseparable in the schema, so a closed sprint always says when."
              }
            }
          },
          {
            "$ref": "#/components/schemas/AuditMetaRef"
          }
        ]
      },
      "IterationCreate": {
        "type": "object",
        "required": [
          "name"
        ],
        "additionalProperties": false,
        "description": "`board_id` comes from the path, not the body. `status` is not accepted at all — a sprint is always created `PLANNED` and started by `work.iteration.activate`, so that the one-`ACTIVE` partial unique is never raced by a create.\n",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "goal": {
            "type": "string",
            "maxLength": 4000
          },
          "start_date": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "end_date": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "sort_order": {
            "type": "integer",
            "minimum": 0,
            "default": 0
          }
        }
      },
      "IterationUpdate": {
        "type": "object",
        "additionalProperties": false,
        "description": "`status` and `board_id` are intentionally absent — see `work.iteration.update`. At least one field is required.\n",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "goal": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 4000
          },
          "start_date": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DateOnlyRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "end_date": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DateOnlyRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "sort_order": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "IterationPage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Iteration"
                }
              }
            }
          }
        ]
      },
      "IterationCloseRequest": {
        "type": "object",
        "additionalProperties": false,
        "description": "Where the sprint's unfinished work goes. An absent body is equivalent to `{\"rollover_to_iteration_id\": null}` — the backlog.\n",
        "properties": {
          "rollover_to_iteration_id": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "The receiving sprint, which must be on the **same board** and must not itself be `COMPLETED` (both `422`). `null` — the default — means the backlog, i.e. `tasks.iteration_id = NULL`.\n"
          }
        }
      },
      "IterationCloseResult": {
        "type": "object",
        "required": [
          "iteration",
          "moved",
          "not_moved"
        ],
        "description": "**What actually happened, not what was intended.** The rollover `UPDATE` runs under the caller's own task-write arms, which are narrower than the arms that admitted the close, so a partial rollover is a normal outcome and is reported rather than hidden. `moved` + `not_moved` together account for every incomplete task the caller could SEE in the sprint; `retained_count` is the finished work deliberately left attributed to it.\n",
        "properties": {
          "iteration": {
            "$ref": "#/components/schemas/Iteration"
          },
          "rollover_to_iteration_id": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "Echo of the destination — `null` = the backlog."
          },
          "moved": {
            "$ref": "#/components/schemas/IterationRolloverGroup"
          },
          "not_moved": {
            "allOf": [
              {
                "$ref": "#/components/schemas/IterationRolloverGroup"
              }
            ],
            "description": "Incomplete cards the close could NOT move, because the caller is not the assignee, not on the task's assignment ledger, and not (at `TEAM` scope) the assignee's manager. They stay attributed to the now-`COMPLETED` sprint — nothing is deleted and no link is cleared — and remain reachable by filtering tasks on this iteration. A non-empty list here is the signal to hand the remainder to someone with the reach, not an error to retry.\n"
          },
          "retained_count": {
            "type": "integer",
            "description": "`DONE` / `CANCELLED` cards left where they are. **Not a failure** — finished work is what the sprint delivered, and moving it would erase the only record velocity can be computed from. Ids are deliberately not returned: this is a completed-work count, and the caller already has the sprint to query.\n"
          }
        }
      },
      "IterationRolloverGroup": {
        "type": "object",
        "required": [
          "count",
          "task_ids"
        ],
        "properties": {
          "count": {
            "type": "integer"
          },
          "task_ids": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UuidRef"
            },
            "description": "Bounded by the sprint's own size — a sprint is a planning window, not a backlog dump."
          }
        }
      },
      "TeamMemberPage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TeamMember"
                }
              }
            }
          }
        ]
      },
      "TeamMemberSyncRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "org_unit_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "member_role": {
            "allOf": [
              {
                "$ref": "#/components/schemas/TeamMemberRole"
              }
            ],
            "description": "Role to stamp on the rows this run creates. Defaults to `MEMBER`; existing rows are never re-roled by a seed."
          }
        }
      },
      "TeamMemberSyncResult": {
        "type": "object",
        "required": [
          "team_id",
          "org_unit_id",
          "source_visible",
          "added",
          "already_member",
          "drift"
        ],
        "properties": {
          "team_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "org_unit_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "source_visible": {
            "type": "integer",
            "description": "ACTIVE employees of the unit the CALLER could read. A `TEAM`-scoped lead sees their own scope of it; a tenant-granted caller sees the whole unit."
          },
          "added": {
            "type": "array",
            "description": "Memberships created by this run, each stamped `source=ORG_UNIT_SYNC`.",
            "items": {
              "$ref": "#/components/schemas/TeamMember"
            }
          },
          "already_member": {
            "type": "array",
            "description": "Employee ids in the unit that already had a live membership — the reason a re-run is a no-op.",
            "items": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          "drift": {
            "type": "array",
            "description": "Live members who are NOT in the unit. Reported, never removed — a seed is not a sync.",
            "items": {
              "$ref": "#/components/schemas/UuidRef"
            }
          }
        }
      },
      "ProjectTeam": {
        "description": "work.project_teams — one project ↔ team edge (db 06 §12).",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "project_id",
              "team_id"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "project_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "team_id": {
                "$ref": "#/components/schemas/UuidRef"
              }
            }
          },
          {
            "$ref": "#/components/schemas/AuditMetaRef"
          },
          {
            "type": "object",
            "readOnly": true,
            "properties": {
              "team": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/TeamRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          }
        ]
      },
      "TeamRef": {
        "type": "object",
        "description": "Minimal `work.teams` projection for embedding on a project ↔ team edge.",
        "readOnly": true,
        "properties": {
          "id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "team_key": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/TeamStatus"
          }
        }
      },
      "ProjectTeamCreate": {
        "type": "object",
        "required": [
          "team_id"
        ],
        "additionalProperties": false,
        "properties": {
          "team_id": {
            "$ref": "#/components/schemas/UuidRef"
          }
        }
      },
      "ProjectTeamPage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ProjectTeam"
                }
              }
            }
          }
        ]
      },
      "TeamSummary": {
        "type": "object",
        "description": "Team-home header counts. Every figure is computed inside the CALLER's own RLS scope — it is what this caller may read, not a privileged total.\n",
        "required": [
          "team_id",
          "members",
          "boards",
          "projects",
          "tasks"
        ],
        "properties": {
          "team_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "members": {
            "type": "object",
            "required": [
              "total",
              "lead",
              "member",
              "guest"
            ],
            "properties": {
              "total": {
                "type": "integer"
              },
              "lead": {
                "type": "integer"
              },
              "member": {
                "type": "integer"
              },
              "guest": {
                "type": "integer"
              }
            }
          },
          "boards": {
            "type": "object",
            "required": [
              "total",
              "active"
            ],
            "properties": {
              "total": {
                "type": "integer"
              },
              "active": {
                "type": "integer"
              }
            }
          },
          "projects": {
            "type": "object",
            "required": [
              "total"
            ],
            "properties": {
              "total": {
                "type": "integer"
              }
            }
          },
          "tasks": {
            "type": "object",
            "description": "Tasks on the squad's boards, by lifecycle position. `open` excludes DONE and CANCELLED.",
            "required": [
              "total",
              "open",
              "done",
              "overdue"
            ],
            "properties": {
              "total": {
                "type": "integer"
              },
              "open": {
                "type": "integer"
              },
              "done": {
                "type": "integer"
              },
              "overdue": {
                "type": "integer"
              }
            }
          }
        }
      },
      "ValidationProblem": {
        "description": "422 field-level validation failure; extends Problem with a per-field error array. `detail` is ALWAYS present on a 422 (#1251) and is the human summary of `errors[]`: one offending field renders as `\"<field>: <its message>\"` (`withholding_amount: is required for an India entity`), several as `\"N fields were refused: a, b, c.\"`, capped at five names. It is display copy derived from members already in the same body — clients keep branching on `code` and mapping `errors[].pointer` back to a control, never parsing this sentence.\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/Problem"
          },
          {
            "type": "object",
            "required": [
              "detail",
              "errors"
            ],
            "properties": {
              "detail": {
                "type": "string",
                "description": "Human summary of `errors[]`, always populated on a 422 so a client never has to fall back to generic copy for the one status that names a fixable field.\n",
                "example": "withholding_amount: is required for an India entity"
              },
              "errors": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "pointer",
                    "rule"
                  ],
                  "properties": {
                    "pointer": {
                      "type": "string",
                      "description": "JSON Pointer to the offending field, e.g. /claim_amount"
                    },
                    "rule": {
                      "type": "string",
                      "enum": [
                        "required",
                        "format",
                        "length",
                        "range",
                        "cross-field",
                        "async-server",
                        "consent-gated",
                        "uniqueness-business",
                        "not_found"
                      ],
                      "description": "FSD validation taxonomy rule (fsd-docs/00 §8.2). `not_found` is the server-side-lookup arm: a body field that REFERENCES another resource (e.g. `project_id` on a work entry) and did not resolve for this caller. It is reported here, under the field's pointer, and NOT as a 404 — the request addresses its own resource, so the failure belongs on the form field the client can actually fix (#805).\n"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem detail (application/problem+json). The platform-wide error envelope (03 §1).",
        "required": [
          "type",
          "title",
          "status"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "default": "about:blank",
            "description": "Problem-type URI."
          },
          "title": {
            "type": "string",
            "description": "Short, human-readable summary (stable per type)."
          },
          "status": {
            "type": "integer",
            "description": "HTTP status code, duplicated for convenience."
          },
          "detail": {
            "type": "string",
            "description": "Human-readable explanation specific to this occurrence."
          },
          "instance": {
            "type": "string",
            "description": "URI reference for this specific occurrence."
          },
          "code": {
            "$ref": "#/components/schemas/ErrorCode"
          },
          "correlation_id": {
            "$ref": "#/components/schemas/Uuid"
          }
        }
      },
      "Uuid": {
        "type": "string",
        "format": "uuid",
        "description": "UUIDv7 surrogate primary key (db-docs/00 §3). Never the business identifier."
      },
      "DateOnly": {
        "type": "string",
        "format": "date",
        "description": "Calendar-only value (pay-period date, leave date, due date)."
      },
      "Timestamp": {
        "type": "string",
        "format": "date-time",
        "description": "timestamptz, serialized UTC ISO-8601. Presentation timezone is a client concern."
      },
      "DecimalHours": {
        "type": "string",
        "pattern": "^-?\\d+(\\.\\d{1,2})?$",
        "description": "numeric(9,2) decimal hours/days as a string (OT hours, leave days). Never a float."
      },
      "Rate": {
        "type": "string",
        "pattern": "^-?\\d+(\\.\\d{1,6})?$",
        "description": "numeric(9,6) fraction as a string, e.g. \"0.120000\" for the 12% EPF rate. Never a float; percentages are stored as fractions."
      },
      "Money": {
        "type": "object",
        "description": "Exact decimal money (db-docs/00 §6). `amount` is a STRING so float never enters the wire.",
        "required": [
          "amount",
          "currency_code"
        ],
        "additionalProperties": false,
        "properties": {
          "amount": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d{1,2})?$",
            "description": "numeric(18,2) as a string, e.g. \"45000.00\"."
          },
          "currency_code": {
            "$ref": "#/components/schemas/CurrencyCode"
          }
        }
      },
      "BusinessNo": {
        "type": "string",
        "description": "Tenant-unique, prefixed human reference (`employee_no`, `requisition_no`, `offer_no`, `payslip_no`, `claim_no`, `ticket_no`, `asset_no`, `case_no`). Read-model field; never a path key.\n"
      },
      "FileDownload": {
        "type": "object",
        "description": "Authorized file handle (db-docs/00 §14, xc.files). Bytes never transit the API — the backend mints a time-limited presigned URL after authorization. Clients never see storage keys or hold storage credentials; the presigned URL is never persisted.\n",
        "required": [
          "file_id",
          "file_name",
          "url",
          "expires_at"
        ],
        "properties": {
          "file_id": {
            "$ref": "#/components/schemas/Uuid"
          },
          "file_name": {
            "type": "string"
          },
          "mime_type": {
            "type": "string"
          },
          "size_bytes": {
            "type": "integer"
          },
          "content_hash": {
            "type": [
              "string",
              "null"
            ],
            "description": "SHA-256",
            "present where tamper-evidence matters (payslips": null,
            "letters": null,
            "e-sign artifacts).": null
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Time-limited presigned URL."
          },
          "expires_at": {
            "$ref": "#/components/schemas/Timestamp"
          }
        }
      },
      "AuditMeta": {
        "type": "object",
        "description": "Standard mutable-entity columns (db-docs/00 §5). Read-only; present on every mutable read-model.",
        "readOnly": true,
        "properties": {
          "created_at": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "created_by": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Uuid"
              },
              {
                "type": "null"
              }
            ],
            "description": "null = system/jobs"
          },
          "updated_at": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "updated_by": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Uuid"
              },
              {
                "type": "null"
              }
            ]
          },
          "deleted_at": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Timestamp"
              },
              {
                "type": "null"
              }
            ],
            "description": "soft-delete marker; live rows are null. Deleted rows are excluded by default scope."
          },
          "version": {
            "type": "integer",
            "description": "Optimistic-lock counter (where present); surfaces as the ETag."
          }
        }
      },
      "AppendOnlyMeta": {
        "type": "object",
        "description": "Standard append-only/immutable columns (db-docs/00 §5/§8). No update/version/delete; corrections are new compensating rows.",
        "readOnly": true,
        "properties": {
          "created_at": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "created_by": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Uuid"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "CursorPage": {
        "type": "object",
        "description": "Generic cursor-pagination envelope. List operations compose it via allOf to type `data`, e.g. `allOf: [ {$ref CursorPage}, { properties: { data: { items: {$ref Employee} } } } ]`.\n",
        "required": [
          "data",
          "page"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {}
          },
          "page": {
            "type": "object",
            "required": [
              "has_more"
            ],
            "properties": {
              "next_cursor": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "prev_cursor": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "has_more": {
                "type": "boolean"
              },
              "total_est": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Optional, capped, APPROXIMATE row estimate for grid \"X of Z\" display only — never an exact COUNT(*) on large tables (attend.attendance_records, xc.notifications, audit.*).\n"
              }
            }
          }
        }
      },
      "CurrencyCode": {
        "type": "string",
        "enum": [
          "INR",
          "SAR"
        ],
        "description": "ISO-4217. A legal entity operates in a single currency (db-docs/00 §6)."
      },
      "ErrorCode": {
        "type": "string",
        "description": "Machine-stable error code (paired with the HTTP status; drives client handling, not display copy).",
        "enum": [
          "VALIDATION_FAILED",
          "IDEMPOTENCY_KEY_REUSE",
          "VERSION_CONFLICT",
          "PRECONDITION_REQUIRED",
          "MAKER_EQUALS_CHECKER",
          "STEP_UP_REQUIRED",
          "CONSENT_REQUIRED",
          "TOKEN_DENIED",
          "SCOPE_DENIED",
          "OWNERSHIP_DENIED",
          "STATE_TRANSITION_INVALID",
          "FACE_MISMATCH",
          "FACE_VERIFICATION_REQUIRED",
          "WITHHOLDING_REVIEW_REQUIRED",
          "FEATURE_NOT_IN_PLAN",
          "PLAN_LIMIT_EXCEEDED",
          "TENANT_PAST_DUE",
          "TENANT_SUSPENDED",
          "TENANT_CANCELLED",
          "RATE_LIMITED",
          "NOT_FOUND"
        ]
      }
    },
    "parameters": {
      "PathId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "UUIDv7 surrogate key of the target resource. Business numbers (`employee_no`, `claim_no`, …) are read-model fields, never path keys.",
        "schema": {
          "$ref": "#/components/schemas/Uuid"
        }
      },
      "PageSize": {
        "name": "page[size]",
        "in": "query",
        "required": false,
        "description": "Max items per page. Cursor pagination only (03 §2); offset pagination is rejected (ADR 0015).",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 200,
          "default": 50
        }
      },
      "PageAfter": {
        "name": "page[after]",
        "in": "query",
        "required": false,
        "description": "Opaque forward keyset cursor (from a prior page's `page.next_cursor`).",
        "schema": {
          "type": "string"
        }
      },
      "PageBefore": {
        "name": "page[before]",
        "in": "query",
        "required": false,
        "description": "Opaque backward keyset cursor (from a prior page's `page.prev_cursor`).",
        "schema": {
          "type": "string"
        }
      },
      "SortParam": {
        "name": "sort",
        "in": "query",
        "required": false,
        "description": "Comma-separated sort keys; leading `-` = descending. Each key MUST be in the operation's documented sort whitelist (free-form sort is rejected so the keyset cursor stays stable).\n",
        "schema": {
          "type": "string"
        }
      },
      "AcceptLanguage": {
        "name": "Accept-Language",
        "in": "header",
        "required": false,
        "description": "Locale for server-rendered/localized text (LocalizedText resolution, letters, notifications). Active locale set comes from the legal entity's compliance pack; KSA tenants default `ar`.\n",
        "schema": {
          "type": "string",
          "enum": [
            "en",
            "ar"
          ],
          "default": "en"
        }
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "description": "Client-generated unique key. REQUIRED on every mutation (this round tightens ADR 0015's \"platform + retryable mutations\" floor to ALL mutations for uniformity — offline punch/leave sync depends on it). Scoped (tenant, principal, route, key); a replay within the ~24h window returns the stored response with `Idempotency-Replayed: true`; the same key with a different body → 409 IDEMPOTENCY_KEY_REUSE (04 §1).\n",
        "schema": {
          "type": "string",
          "minLength": 8,
          "maxLength": 255
        }
      },
      "IfMatch": {
        "name": "If-Match",
        "in": "header",
        "required": true,
        "description": "Optimistic-concurrency precondition for mutating a VERSIONED mutable entity (db-docs/00 §5 applies `version` where concurrent edits are likely). Value is the entity's current ETag (the row `version`). Absent → 428; stale → 412 (04 §2). N/A for append-only entities and for unversioned low-contention entities (their update ops simply omit this parameter).\n",
        "schema": {
          "type": "string"
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing, expired, or invalid session token (no authenticated principal).",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Authenticated but denied — permission token not granted, out of scope (self/team/branch), not the owner, tenant suspended, or an MC-2 operation without a fresh step-up challenge. `code` ∈ TOKEN_DENIED | SCOPE_DENIED | OWNERSHIP_DENIED | MAKER_EQUALS_CHECKER | STEP_UP_REQUIRED | CONSENT_REQUIRED | TENANT_SUSPENDED. A plan feature-flag being off is 402 FEATURE_NOT_IN_PLAN, not 403 (see PaymentRequired).\n",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource does not exist OR is masked by RLS (tenant/self/team/branch scope) — the API does not distinguish, so existence is never confirmed across a scope boundary (02 §4 disclosure posture).\n",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Conflict": {
        "description": "Idempotency-key reuse with a different body, an invalid state transition, or a uniqueness/business-rule violation (fsd 00 §8.2). For database-backed uniqueness rules, detail names the exact violated rule; unmapped constraints are not collapsed into a generic 409.\n",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "UnprocessableEntity": {
        "description": "Field-level validation failed. The body carries both `errors[]` (per-field, pointer-addressed) and a `detail` summarising them — never an empty `detail` (#1251).\n",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/ValidationProblem"
            },
            "examples": {
              "singleField": {
                "summary": "One refused field — `detail` names it",
                "value": {
                  "type": "about:blank",
                  "title": "Unprocessable Entity",
                  "status": 422,
                  "code": "VALIDATION_FAILED",
                  "detail": "withholding_amount: is required for an India entity",
                  "errors": [
                    {
                      "pointer": "/withholding_amount",
                      "rule": "required",
                      "message": "is required for an India entity"
                    }
                  ]
                }
              },
              "severalFields": {
                "summary": "Several refused fields — `detail` counts and names them",
                "value": {
                  "type": "about:blank",
                  "title": "Unprocessable Entity",
                  "status": 422,
                  "code": "VALIDATION_FAILED",
                  "detail": "2 fields were refused: effective_from, cap_amount.amount.",
                  "errors": [
                    {
                      "pointer": "/effective_from",
                      "rule": "cross-field",
                      "message": "Overlaps an existing version."
                    },
                    {
                      "pointer": "/cap_amount/amount",
                      "rule": "range",
                      "message": "Must be a non-negative decimal string."
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "Locked": {
        "description": "Tenant subscription `past_due` (ADR 0009): WRITES are blocked (423), reads still succeed. `code` = TENANT_PAST_DUE. Mutations return this; list/get operations do not.\n",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Per-principal/route rate limit exceeded.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "PreconditionRequired": {
        "description": "If-Match header absent on a mutation of a versioned mutable entity (428).",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "PreconditionFailed": {
        "description": "If-Match / ETag mismatch — the row changed since it was read (412, VERSION_CONFLICT).",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      }
    },
    "headers": {
      "ETag": {
        "description": "Strong validator of the row `version` (e.g. `\"v7\"`). Use as `If-Match` on the next mutation.",
        "schema": {
          "type": "string"
        }
      },
      "Location": {
        "description": "URI of the created/affected resource.",
        "schema": {
          "type": "string",
          "format": "uri-reference"
        }
      },
      "IdempotencyReplayed": {
        "description": "`true` when a stored idempotent response was replayed rather than freshly computed.",
        "schema": {
          "type": "boolean"
        }
      },
      "RetryAfter": {
        "description": "Seconds to wait before retrying (on 429 / 503).",
        "schema": {
          "type": "integer"
        }
      }
    }
  }
}