openapi: 3.1.0 info: title: ItWorx Knowledge Hub API version: 1.0.0-rc.1 description: | Versioned API for the ItWorx Knowledge Hub web client and scoped automation. This is not an MCP transport. The future MCP service will consume this API. servers: - url: /api/v1 tags: - name: System - name: Setup - name: Auth - name: Admin - name: Search - name: Rag - name: Pages - name: Drafts - name: Templates - name: Attachments - name: Imports - name: Source batch purge - name: Projects - name: Decisions - name: Runbooks - name: Service accounts - name: Backups - name: Infrastructure - name: Activity - name: Workspace - name: Ingestion proposals - name: Preferences - name: DataSources - name: Jobs security: - cookieAuth: [] - bearerAuth: [] paths: /ingestion-proposals: get: tags: [Ingestion proposals] operationId: listIngestionProposals description: Human review queue only. Page/pageSize and all filters are evaluated server-side; cursor/limit remain supported for existing clients. Project and risk are accepted but return no matches until this version persists those attributes, rather than fabricating filter results. parameters: - name: status in: query schema: { type: string } - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/Limit" - name: page in: query schema: { type: integer, minimum: 1, maximum: 10000, default: 1 } - name: pageSize in: query schema: { type: integer, minimum: 1, maximum: 100, default: 50 } - name: projectId in: query schema: { type: string, format: uuid } - name: submitterId in: query schema: { type: string, format: uuid } - name: minAgeDays in: query description: Minimum proposal age, calculated from the first immutable proposal revision. schema: { type: integer, minimum: 0, maximum: 3650 } - name: risk in: query schema: { type: string, enum: [low, medium, high] } - name: documentType in: query schema: { type: string, maxLength: 64 } responses: "200": { description: Review queue, content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalListResponse" } } } } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } post: tags: [Ingestion proposals] operationId: createIngestionProposal description: >- Requires an Idempotency-Key and an editor cookie or an ingestion:write service token. Repeating the same request with the same caller and key replays the original 201 response. Reusing that key with a different request body returns a 409 problem with type https://itworx.dev/problems/idempotency-key-conflict. parameters: - $ref: "#/components/parameters/RequiredIdempotencyKey" requestBody: required: true content: { application/json: { schema: { $ref: "#/components/schemas/CreateIngestionProposalRequest" } } } responses: "201": { description: Proposal created, headers: { ETag: { $ref: "#/components/headers/ETag" } }, content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalDetail" } } } } "400": { description: Idempotency-Key is missing or invalid, content: { application/problem+json: { schema: { $ref: "#/components/schemas/ProblemDetails" } } } } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "409": description: Idempotency-Key was already used with a different request body. content: application/problem+json: schema: { $ref: "#/components/schemas/ProblemDetails" } examples: idempotencyKeyConflict: value: type: https://itworx.dev/problems/idempotency-key-conflict title: Idempotency-Key hergebruikt met een andere aanvraag. status: 409 correlationId: pilot-contract detail: Deze Idempotency-Key is eerder gebruikt voor een andere aanvraag-inhoud. "422": { $ref: "#/components/responses/BadRequest" } /ingestion-proposals/{proposalId}: parameters: - name: proposalId in: path required: true schema: { type: string, format: uuid } get: tags: [Ingestion proposals] operationId: getIngestionProposal description: A human reviewer may read any workspace proposal. An ingestion:write token may read only the proposal it submitted. responses: "200": { description: Complete metadata-only review/status dossier, headers: { ETag: { $ref: "#/components/headers/ETag" } }, content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalDetail" } } } } "404": { $ref: "#/components/responses/NotFound" } patch: tags: [Ingestion proposals] operationId: patchIngestionProposal description: >- Requires If-Match. Human editors may change manifest/rationale; an edit to an Approved proposal clears its approval and returns it to ReadyForReview. Only the submitting ingestion:write account may submit analysis, which advances the proposal to ReadyForReview. A stale proposal ETag returns 412 with type https://itworx.local/problems/ingestion-concurrency-conflict. parameters: - $ref: "#/components/parameters/IfMatch" requestBody: required: true content: { application/json: { schema: { $ref: "#/components/schemas/PatchIngestionProposalRequest" } } } responses: "200": { description: Updated proposal, headers: { ETag: { $ref: "#/components/headers/ETag" } }, content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalDetail" } } } } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "412": description: Proposal ETag does not match the current version. content: application/problem+json: schema: { $ref: "#/components/schemas/ProblemDetails" } examples: ingestionConcurrencyConflict: value: type: https://itworx.local/problems/ingestion-concurrency-conflict title: Versieconflict. status: 412 correlationId: pilot-contract /ingestion-proposals/{proposalId}/sources: post: tags: [Ingestion proposals] operationId: stageIngestionProposalSource description: Requires If-Match and Idempotency-Key. Upload is quarantined, signature-validated and stored only as immutable metadata plus a content-addressed attachment reference; neither bytes nor storage paths are returned. parameters: - name: proposalId in: path required: true schema: { type: string, format: uuid } - $ref: "#/components/parameters/IfMatch" - $ref: "#/components/parameters/RequiredIdempotencyKey" requestBody: required: true content: multipart/form-data: schema: type: object required: [file] properties: { file: { type: string, format: binary } } responses: "200": { description: Source staged, headers: { ETag: { $ref: "#/components/headers/ETag" } }, content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalDetail" } } } } "413": { description: Source exceeds the configured size limit } "422": { $ref: "#/components/responses/BadRequest" } /ingestion-proposals/{proposalId}/submit: post: tags: [Ingestion proposals] operationId: submitManualIngestionProposal description: >- Human submitter only. Moves a proposal with publication details and at least one staged source into ReadyForReview without claiming automated analysis. Requires If-Match. A separate human review and approval remain mandatory. parameters: - name: proposalId in: path required: true schema: { type: string, format: uuid } - $ref: "#/components/parameters/IfMatch" responses: "200": { description: Submitted for review, headers: { ETag: { $ref: "#/components/headers/ETag" } }, content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalDetail" } } } } "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" } "428": { $ref: "#/components/responses/PreconditionRequired" } /ingestion-proposals/{proposalId}/request-changes: post: tags: [Ingestion proposals] operationId: requestIngestionProposalChanges description: >- Human reviewer only. Requires If-Match and returns the updated metadata-only dossier. Stale ETags return 412 with type https://itworx.local/problems/ingestion-concurrency-conflict. parameters: - name: proposalId in: path required: true schema: { type: string, format: uuid } - $ref: "#/components/parameters/IfMatch" requestBody: required: true content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalDecisionRequest" } } } responses: "200": { description: Change request recorded, headers: { ETag: { $ref: "#/components/headers/ETag" } }, content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalDetail" } } } } "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" } "428": { $ref: "#/components/responses/PreconditionRequired" } /ingestion-proposals/{proposalId}/approve: post: tags: [Ingestion proposals] operationId: approveIngestionProposal description: >- Human reviewer only. Service accounts cannot approve. Requires If-Match and approves a ReadyForReview proposal without publishing it. Stale ETags return 412 with type https://itworx.local/problems/ingestion-concurrency-conflict. parameters: - name: proposalId in: path required: true schema: { type: string, format: uuid } - $ref: "#/components/parameters/IfMatch" requestBody: required: true content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalDecisionRequest" } } } responses: "200": { description: Proposal approved for later publication, headers: { ETag: { $ref: "#/components/headers/ETag" } }, content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalDetail" } } } } "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" } "428": { $ref: "#/components/responses/PreconditionRequired" } /ingestion-proposals/{proposalId}/reject: post: tags: [Ingestion proposals] operationId: rejectIngestionProposal description: >- Human reviewer only. Service accounts cannot reject. Requires If-Match and records the rejection comment on the metadata-only proposal dossier. Stale ETags return 412 with type https://itworx.local/problems/ingestion-concurrency-conflict. parameters: - name: proposalId in: path required: true schema: { type: string, format: uuid } - $ref: "#/components/parameters/IfMatch" requestBody: required: true content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalDecisionRequest" } } } responses: "200": { description: Proposal rejected, headers: { ETag: { $ref: "#/components/headers/ETag" } }, content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalDetail" } } } } "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" } "428": { $ref: "#/components/responses/PreconditionRequired" } /ingestion-proposals/{proposalId}/retry: post: tags: [Ingestion proposals] operationId: retryIngestionProposal description: Requires If-Match. An ingestion:write caller can retry only its own eligible failed proposal; a human editor may retry an eligible proposal. An optional Idempotency-Key safely replays a successful retry response. parameters: - name: proposalId in: path required: true schema: { type: string, format: uuid } - $ref: "#/components/parameters/IfMatch" - $ref: "#/components/parameters/IdempotencyKey" responses: "200": { description: Eligible proposal reset for re-analysis, headers: { ETag: { $ref: "#/components/headers/ETag" } }, content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalDetail" } } } } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } "412": { $ref: "#/components/responses/PreconditionFailed" } "428": { $ref: "#/components/responses/PreconditionRequired" } /ingestion-proposals/{proposalId}/publish: post: tags: [Ingestion proposals] operationId: publishIngestionProposal description: >- Human review only. Requires a human editor cookie session, If-Match and Idempotency-Key; service accounts are forbidden from publishing. A replay with the same key and request body returns the original success response even when it carries the original now-stale If-Match value, and does not create a second page revision. Reusing the key with a different body returns a typed idempotency conflict. A stale target page ETag returns 409 with type https://itworx.local/problems/ingestion-target-stale and moves the proposal back to ReadyForReview. parameters: - name: proposalId in: path required: true schema: { type: string, format: uuid } - $ref: "#/components/parameters/IfMatch" - $ref: "#/components/parameters/RequiredIdempotencyKey" responses: "200": { description: Approved proposal published, headers: { ETag: { $ref: "#/components/headers/ETag" } }, content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalDetail" } } } } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": description: >- Invalid state transition, stale target, or idempotency-key conflict. Typed problem types are https://itworx.local/problems/ingestion-invalid-transition, https://itworx.local/problems/ingestion-target-stale, and https://itworx.dev/problems/idempotency-key-conflict. content: application/problem+json: schema: { $ref: "#/components/schemas/ProblemDetails" } examples: staleTarget: value: type: https://itworx.local/problems/ingestion-target-stale title: Publicatiedoel is gewijzigd. status: 409 correlationId: pilot-contract detail: Het voorstel is teruggezet naar de reviewwachtrij. invalidTransition: value: type: https://itworx.local/problems/ingestion-invalid-transition title: Conflict. status: 409 correlationId: pilot-contract idempotencyKeyConflict: value: type: https://itworx.dev/problems/idempotency-key-conflict title: Idempotency-Key hergebruikt met een andere aanvraag. status: 409 correlationId: pilot-contract detail: Deze Idempotency-Key is eerder gebruikt voor een andere aanvraag-inhoud. "412": description: Proposal ETag is stale. content: application/problem+json: schema: { $ref: "#/components/schemas/ProblemDetails" } examples: ingestionConcurrencyConflict: value: type: https://itworx.local/problems/ingestion-concurrency-conflict title: Versieconflict. status: 412 correlationId: pilot-contract "428": { $ref: "#/components/responses/PreconditionRequired" } /system/info: get: tags: [System] operationId: getSystemInfo responses: "200": description: Permission-filtered system information content: application/json: schema: $ref: "#/components/schemas/SystemInfo" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /setup/status: get: tags: [Setup] operationId: getSetupStatus security: [] description: >- Reveals only whether first-run setup is still required. Never reveals any other installation detail to an unauthenticated caller. responses: "200": description: Setup status content: application/json: schema: $ref: "#/components/schemas/SetupStatus" /setup/complete: post: tags: [Setup] operationId: completeSetup security: [] description: >- Single-use first-run owner setup. Requires CSRF protection and is rate limited. Permanently fails once setup has completed. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CompleteSetupRequest" responses: "200": description: Installation completed; caller is signed in content: application/json: schema: $ref: "#/components/schemas/CompleteSetupResponse" "409": description: Setup already completed content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "422": description: Validation failed (email/password/workspace name) content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "429": $ref: "#/components/responses/RateLimited" /auth/csrf: get: tags: [Auth] operationId: getCsrfToken security: [] description: Issues the double-submit anti-forgery token required on cookie-authenticated mutations. responses: "200": description: Anti-forgery token content: application/json: schema: $ref: "#/components/schemas/CsrfToken" /auth/session: get: tags: [Auth] operationId: getSession security: [] description: >- Always returns 200. An unauthenticated caller only ever sees `authenticated: false` plus whether setup is required — never whether a specific account exists. responses: "200": description: Current session state content: application/json: schema: $ref: "#/components/schemas/SessionResponse" /auth/login: post: tags: [Auth] operationId: login security: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/LoginRequest" responses: "200": description: Signed in content: application/json: schema: $ref: "#/components/schemas/SessionResponse" "401": description: Invalid credentials (also returned for an unknown account and a disabled account) content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "429": $ref: "#/components/responses/RateLimited" /auth/logout: post: tags: [Auth] operationId: logout responses: "200": description: Signed out "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" /auth/recovery/redeem: post: tags: [Auth] operationId: redeemRecoveryToken security: [] description: Consumes a single-use operator-issued recovery token and sets a new password. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RecoveryRedeemRequest" responses: "200": description: Password reset; all prior sessions for this account are invalidated "400": description: Invalid or expired token content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "422": description: New password rejected content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "429": $ref: "#/components/responses/RateLimited" /admin/users: get: tags: [Admin] operationId: listUsers description: Administrator only. responses: "200": description: Workspace members content: application/json: schema: type: array items: $ref: "#/components/schemas/UserSummary" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" post: tags: [Admin] operationId: createUser description: >- Administrator only. There is no invite email; the administrator sets an initial password and communicates it out of band. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateUserRequest" responses: "200": description: User created content: application/json: schema: $ref: "#/components/schemas/UserSummary" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "422": $ref: "#/components/responses/BadRequest" /admin/users/{userId}: patch: tags: [Admin] operationId: updateUser description: >- Administrator only. Changes role and/or enabled state. Refuses a change that would leave zero enabled administrators. parameters: - $ref: "#/components/parameters/UserId" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateUserRequest" responses: "200": description: Updated "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": description: Would remove the last enabled administrator content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /admin/users/{userId}/recovery-tokens: post: tags: [Admin] operationId: issueRecoveryToken description: >- Administrator only. Issues a single-use, one-hour password-recovery token for another account. The raw token is returned exactly once, in this response. parameters: - $ref: "#/components/parameters/UserId" responses: "200": description: Token issued content: application/json: schema: $ref: "#/components/schemas/IssueRecoveryTokenResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /admin/trash/purge: post: tags: [Admin] operationId: purgeTrash description: >- Administrator only. The explicit administrative purge (docs/09-data-safety: never a permanent delete through a normal UI flow). Permanently removes every trashed page in the workspace whose retention window has already elapsed; never a page still within retention, never a page that was never trashed. responses: "200": description: Purge completed content: application/json: schema: $ref: "#/components/schemas/TrashPurgeResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /admin/staging-cleanup/status: get: tags: [Admin] operationId: getStagingCleanupStatus description: >- Administrator only (docs/adr/ADR-029-v2-r5-final-hardening.md). Live counts of currently-open and expired-pending-cleanup page-creation staging sessions, plus the most recent cleanup run (scheduled or manual) and its result. The "last run" fields are in-memory only and reset on application restart — an accepted, documented limitation; the cleanup guarantee itself does not depend on this history. responses: "200": description: Current staging-cleanup status content: application/json: schema: $ref: "#/components/schemas/StagingCleanupStatusResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /admin/staging-cleanup/run: post: tags: [Admin] operationId: runStagingCleanup description: >- Administrator only, CSRF-protected (docs/adr/ADR-029-v2-r5-final-hardening.md). Runs the exact same reclamation logic as the periodic background worker, once, synchronously. `dryRun: true` only counts currently-expired, claimable sessions — it claims nothing, deletes nothing and is safe to call as often as wanted; never expose a destructive "run now" action without this dry-run mode. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RunStagingCleanupRequest" responses: "200": description: Cleanup run (or dry-run preview) result content: application/json: schema: $ref: "#/components/schemas/RunStagingCleanupResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /activity: get: tags: [Activity] operationId: listActivity description: >- Requires a reader cookie session or an `audit:read` service-token scope. The most recent audit/activity events across the workspace (page/project/decision mutations, imports, backups, staging-session cleanup, etc.), newest first — the same durable `audit_events` trail every mutation writes to (docs/12-auth-security.md "Logging and privacy": never a password, raw token or session identifier). parameters: - name: limit in: query required: false description: Defaults to 100. schema: type: integer minimum: 1 maximum: 500 responses: "200": description: Recent activity, newest first content: application/json: schema: $ref: "#/components/schemas/ActivityListResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /workspace/members: get: tags: [Workspace] operationId: searchWorkspaceMembers description: >- Requires a reader cookie session — deliberately not reachable by a service-account bearer token (no automation use case has been named for this yet). Backs the accessible owner picker (docs/adr/ADR-029-v2-r5-final-hardening.md, replacing manual `OwnerId`/GUID entry): only enabled members of the current workspace, matched case-insensitively against display name or email, capped at 25 results. Never discloses accounts outside the workspace or disabled accounts. An empty/omitted `q` returns the first 25 members alphabetically (the picker's initial open state, before typing). parameters: - name: q in: query required: false schema: type: string responses: "200": description: Matching workspace members content: application/json: schema: $ref: "#/components/schemas/WorkspaceMemberListResponse" "401": $ref: "#/components/responses/Unauthorized" /search: get: tags: [Search] operationId: searchKnowledge description: >- PostgreSQL full-text (weighted title/tags/body) plus trigram typo-tolerant title matching. Authorization is entirely SQL-level: always scoped to the caller's workspace, always excludes trashed pages (docs/10-search-and-indexing.md). An empty or absent `q` returns all permitted, non-trashed pages (browsing by tag/type filter alone). parameters: - $ref: "#/components/parameters/CorrelationId" - name: q in: query schema: type: string maxLength: 500 - name: type in: query description: A single DocumentType value. schema: $ref: "#/components/schemas/DocumentType" - name: tag in: query description: A single exact tag match. schema: type: string - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/Limit" responses: "200": description: Permission-filtered search results content: application/json: schema: $ref: "#/components/schemas/SearchResponse" "401": $ref: "#/components/responses/Unauthorized" /unified-search: get: tags: [Search] operationId: searchWorkspaceContent description: >- Human reader session only. Searches visible knowledge, projects, reviewed services, runbooks and decisions in the current workspace. Trashed content and hidden or archived services are excluded. Counts cover all five kinds; `kind` filters the returned page. A `tag` filters exact, case-insensitive page tags on knowledge, runbook and decision results and excludes projects and services. An empty query returns no results unless `tag` is supplied. The endpoint shares the search rate limit of 60 requests per effective remote IP per minute. An invalid `kind` returns 422. parameters: - name: q in: query schema: type: string maxLength: 500 - name: kind in: query schema: type: string enum: [knowledge, projects, services, runbooks, decisions] - name: tag in: query description: Exact, case-insensitive page tag; applies to knowledge, runbooks and decisions. schema: type: string maxLength: 64 - $ref: "#/components/parameters/Cursor" - name: limit in: query schema: type: integer minimum: 1 maximum: 100 default: 25 responses: "200": description: Permission-filtered, ranked results and complete grouped counts content: application/json: schema: $ref: "#/components/schemas/UnifiedSearchResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "422": description: Invalid kind "429": description: Search request budget exhausted /rag/publication-manifest: get: tags: [Rag] operationId: getRagPublicationManifest description: >- A narrow, read-only view of every page tagged `rag:publish` (docs/32-rag-publication-convention.md), built for RAGcore's n8n sync — the one reliable way to discover the full curated publication set, since this API has no `GET /pages` enumeration (only `/pages/roots` plus a tree walk). Reuses the existing `pages:read` scope; no new scope was added. A page that still carries `rag:publish` but is trashed, still a draft, awaiting review, archived, or deprecated without the `rag:allow-deprecated` tag comes back with `retired: true` and a `retiredReason`, and its `markdown`/`checksum` are `null` — so a sync consumer can safely withdraw previously ingested content instead of only ever learning about new content. parameters: - $ref: "#/components/parameters/CorrelationId" - name: space in: query description: >- A single `rag:space:` tag suffix. Case-sensitive lowercase slug; an invalid shape is rejected with 422 rather than passed into the array- containment filter. schema: type: string maxLength: 64 - $ref: "#/components/parameters/Cursor" - name: limit in: query required: false schema: type: integer minimum: 1 maximum: 200 default: 100 responses: "200": description: Publication manifest page content: application/json: schema: $ref: "#/components/schemas/RagPublicationManifestResponse" "401": $ref: "#/components/responses/Unauthorized" "422": description: Invalid `space` parameter content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /pages/roots: get: tags: [Pages] operationId: listRootPages description: Top-level pages (no parent), cursor-paginated. parameters: - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/Limit" responses: "200": description: Root pages content: application/json: schema: $ref: "#/components/schemas/PageChildrenResponse" "401": $ref: "#/components/responses/Unauthorized" /pages/preview: post: tags: [Pages] operationId: previewMarkdown description: >- Requires a reader cookie session or a `pages:read` service-token scope. Renders arbitrary Markdown through the exact same server-side pipeline (Markdig with `.DisableHtml()` plus the allowlist `HtmlSanitizer` pass) used for a published page revision — safe to call with unsaved editor content, never persists anything. The same safe pipeline is reused for project free-text Markdown fields (`currentStateMarkdown`, `summaryMarkdown`, etc. — docs/adr/ADR-029-v2-r5-final-hardening.md). requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PreviewMarkdownRequest" responses: "200": description: Rendered, sanitized HTML content: application/json: schema: $ref: "#/components/schemas/PreviewMarkdownResponse" "401": $ref: "#/components/responses/Unauthorized" /pages: post: tags: [Pages] operationId: createPage parameters: - $ref: "#/components/parameters/CorrelationId" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreatePageRequest" responses: "200": description: Page created and published as revision 1 headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/PageDetail" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "422": description: Validation failed, or an unknown templateKey was supplied content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /pages/{pageId}: parameters: - $ref: "#/components/parameters/PageId" - $ref: "#/components/parameters/CorrelationId" get: tags: [Pages] operationId: getPage responses: "200": description: Current page, with server-sanitized rendered HTML headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/PageDetail" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" put: tags: [Pages] operationId: updatePage description: Publishes a new revision. Requires If-Match with the page's current ETag. parameters: - $ref: "#/components/parameters/IfMatch" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdatePageRequest" responses: "200": description: Updated page headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/PageDetail" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": description: Page is trashed and must be restored before it can be edited content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "412": $ref: "#/components/responses/PreconditionFailed" "422": $ref: "#/components/responses/BadRequest" "428": $ref: "#/components/responses/PreconditionRequired" /pages/{pageId}/children: get: tags: [Pages] operationId: listPageChildren parameters: - $ref: "#/components/parameters/PageId" - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/Limit" responses: "200": description: Direct children, cursor-paginated (proven at 10,000-child scale, docs/27-acceptance-matrix.md M2-A07) content: application/json: schema: $ref: "#/components/schemas/PageChildrenResponse" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" /pages/{pageId}/move: post: tags: [Pages] operationId: movePage description: >- Reparents and/or reorders a page among its siblings. Rejects a move that would make a page its own descendant. Requires If-Match with the page's current ETag. parameters: - $ref: "#/components/parameters/PageId" - $ref: "#/components/parameters/CorrelationId" - $ref: "#/components/parameters/IfMatch" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MovePageRequest" responses: "200": description: Moved page headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/PageDetail" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": description: The requested move would make the page its own descendant (a cycle) content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "412": $ref: "#/components/responses/PreconditionFailed" "428": $ref: "#/components/responses/PreconditionRequired" /pages/{pageId}/trash: post: tags: [Pages] operationId: trashPage description: >- Moves a page to the trash/retention model. Never a permanent delete (docs/09-data-safety). Requires If-Match with the page's current ETag and accepts either an editor cookie session or a `pages:write` service token, enabling bounded import rollback without permanent deletion. parameters: - $ref: "#/components/parameters/PageId" - $ref: "#/components/parameters/CorrelationId" - $ref: "#/components/parameters/IfMatch" responses: "200": description: Page trashed "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "412": $ref: "#/components/responses/PreconditionFailed" "428": $ref: "#/components/responses/PreconditionRequired" /pages/{pageId}/restore: post: tags: [Pages] operationId: restorePage description: Restores a trashed page. Requires If-Match with the page's current ETag. parameters: - $ref: "#/components/parameters/PageId" - $ref: "#/components/parameters/CorrelationId" - $ref: "#/components/parameters/IfMatch" responses: "200": description: Page restored "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "412": $ref: "#/components/responses/PreconditionFailed" "428": $ref: "#/components/responses/PreconditionRequired" /pages/{pageId}/revisions: get: tags: [Pages] operationId: listPageRevisions description: Immutable revision history, most recent first (M3-A01/M3-A02). parameters: - $ref: "#/components/parameters/PageId" - $ref: "#/components/parameters/CorrelationId" responses: "200": description: Revisions content: application/json: schema: type: array items: $ref: "#/components/schemas/PageRevisionSummary" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" /pages/{pageId}/revisions/{revisionNumber}: get: tags: [Pages] operationId: getPageRevision parameters: - $ref: "#/components/parameters/PageId" - $ref: "#/components/parameters/RevisionNumber" - $ref: "#/components/parameters/CorrelationId" responses: "200": description: One revision's full content, server-rendered content: application/json: schema: $ref: "#/components/schemas/PageRevisionDetailResponse" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" /pages/{pageId}/revisions/{revisionNumber}/restore: post: tags: [Pages] operationId: restorePageRevision description: >- Publishes a new current revision whose content matches the chosen past snapshot. Never rewrites history: the state right before this call is itself preserved as its own revision. Requires If-Match with the page's current ETag. parameters: - $ref: "#/components/parameters/PageId" - $ref: "#/components/parameters/RevisionNumber" - $ref: "#/components/parameters/CorrelationId" - $ref: "#/components/parameters/IfMatch" responses: "200": description: Restored page headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/PageDetail" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": description: Page is trashed and must be restored before it can be edited content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "412": $ref: "#/components/responses/PreconditionFailed" "428": $ref: "#/components/responses/PreconditionRequired" /pages/{pageId}/backlinks: get: tags: [Pages] operationId: getPageBacklinks description: >- "What links here": pages whose most recently published content contains a resolved internal link to this page (M3-A03). parameters: - $ref: "#/components/parameters/PageId" - $ref: "#/components/parameters/CorrelationId" responses: "200": description: Backlinks content: application/json: schema: $ref: "#/components/schemas/BacklinksResponse" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" /drafts/{draftKey}: parameters: - $ref: "#/components/parameters/DraftKey" - $ref: "#/components/parameters/CorrelationId" get: tags: [Drafts] operationId: getDraft description: >- Private per-user autosaved draft. A draft never overwrites the published revision (docs/27-acceptance-matrix.md, M2-A04). responses: "200": description: Draft content: application/json: schema: $ref: "#/components/schemas/DraftResponse" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" put: tags: [Drafts] operationId: saveDraft requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SaveDraftRequest" responses: "200": description: >- Draft saved. `baseRevisionIsStale` is true when the page has been published again since this draft's base revision, without blocking the save. content: application/json: schema: $ref: "#/components/schemas/DraftResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" delete: tags: [Drafts] operationId: discardDraft responses: "200": description: Draft discarded "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" /templates: get: tags: [Templates] operationId: listTemplates description: Starter document templates available when creating a new page. responses: "200": description: Available templates content: application/json: schema: type: array items: $ref: "#/components/schemas/TemplateSummary" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /pages/{pageId}/attachments: parameters: - $ref: "#/components/parameters/PageId" - $ref: "#/components/parameters/CorrelationId" get: tags: [Attachments] operationId: listPageAttachments description: Requires a reader cookie session or an `attachments:read` service-token scope. responses: "200": description: Attachments referenced by this page content: application/json: schema: $ref: "#/components/schemas/AttachmentListResponse" "401": $ref: "#/components/responses/Unauthorized" post: tags: [Attachments] operationId: uploadAttachment description: >- Requires an editor cookie session or an `attachments:write` service-token scope. The upload pipeline from docs/13-attachments.md: streams once to quarantine while hashing, verifies size, detects the real signature (never trusting the declared type/extension alone), applies delivery policy, then commits. Identical content deduplicates to one physical blob while still creating an independent logical reference. requestBody: required: true content: multipart/form-data: schema: type: object required: [file] properties: file: type: string format: binary responses: "200": description: Uploaded content: application/json: schema: $ref: "#/components/schemas/AttachmentSummary" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "413": description: File exceeds the configured maximum upload size content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "422": description: Rejected content (unsafe signature) or an invalid filename content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /pages/staged: post: tags: [Pages] operationId: beginPageCreationSession description: >- Opens a staging session for creating a new page that may need attachments inserted before the page is ever visible to a reader (docs/adr/ADR-025-v2-r1-data-integrity-remediation.md). Requires an editor cookie session or a `pages:write` service-token scope. parameters: - $ref: "#/components/parameters/CorrelationId" - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/BeginPageCreationSessionRequest" responses: "200": description: Session opened content: application/json: schema: $ref: "#/components/schemas/PageCreationSessionResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "422": description: The supplied parentPageId does not exist content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /pages/staged/{sessionId}/attachments: parameters: - $ref: "#/components/parameters/SessionId" - $ref: "#/components/parameters/CorrelationId" post: tags: [Attachments] operationId: uploadStagedAttachment description: >- The same real, immediately-addressable attachment as `POST /pages/{pageId}/attachments` — owned by an open page-creation session instead of an existing page until that session's commit re-points ownership atomically. Requires an editor cookie session or an `attachments:write` service-token scope. requestBody: required: true content: multipart/form-data: schema: type: object required: [file] properties: file: type: string format: binary responses: "200": description: Uploaded content: application/json: schema: $ref: "#/components/schemas/AttachmentSummary" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": description: The session does not exist, is no longer open, or has expired content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "413": description: File exceeds the configured maximum upload size content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "422": description: Rejected content (unsafe signature) or an invalid filename content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /pages/staged/{sessionId}/commit: parameters: - $ref: "#/components/parameters/SessionId" - $ref: "#/components/parameters/CorrelationId" post: tags: [Pages] operationId: commitPageCreationSession description: >- The single explicit publication action: atomically creates the page and re-points every attachment already uploaded into this session to it, in one transaction — no half-published page with broken attachment links can ever become visible (docs/adr/ADR-025-v2-r1-data-integrity-remediation.md). Requires an editor cookie session or a `pages:write` service-token scope. parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CommitPageCreationSessionRequest" responses: "200": description: Page created and published, with every staged attachment now owned by it headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/PageDetail" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": description: The session does not exist content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "409": description: The session is no longer open (already committed, discarded, or expired) content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "422": $ref: "#/components/responses/BadRequest" /pages/staged/{sessionId}/discard: parameters: - $ref: "#/components/parameters/SessionId" - $ref: "#/components/parameters/CorrelationId" post: tags: [Pages] operationId: discardPageCreationSession description: >- Explicit compensation for an abandoned session: removes every attachment uploaded into it that is not used elsewhere, then marks the session discarded. Idempotent — discarding an already-discarded session is a safe no-op. Requires an editor cookie session or a `pages:write` service-token scope. parameters: - $ref: "#/components/parameters/IdempotencyKey" responses: "200": description: Session discarded (or already was) content: application/json: schema: $ref: "#/components/schemas/DiscardPageCreationSessionResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": description: The session does not exist content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "409": description: The session was already committed and can no longer be discarded content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /infrastructure/overview: get: tags: [Infrastructure] operationId: getInfrastructureOverview description: >- Requires a reader cookie session or an `infrastructure:read` service-token scope. The dashboard summary: the most recent inventory snapshot (if any), every known host, and aggregate service counts (total, proposed/unreviewed, missing, unhealthy, unprotected-by-backup). Returns an all-zero/empty response rather than an error when no workspace exists yet. responses: "200": description: Infrastructure overview content: application/json: schema: $ref: "#/components/schemas/InfrastructureOverviewResponse" "401": $ref: "#/components/responses/Unauthorized" /infrastructure/services: get: tags: [Infrastructure] operationId: listInfrastructureServices description: >- Requires a reader cookie session or an `infrastructure:read` service-token scope. Cursor-paginated services catalog (docs/adr/ADR-026-v2-r2-infrastructure-scale-and-sync-durability.md) — proven at real production scale (InfrastructureCatalogReleaseScaleTests.cs, 50,000 services) with an index-order scan, never a full in-memory load. `projectId`, `search` and `status` are applied server-side on the same paginated query; filter results never depend on pages already loaded by the client. parameters: - name: includeHidden in: query required: false description: Defaults to false — hidden/archived services are excluded from the default catalog view. schema: type: boolean - name: projectId in: query required: false description: Only services whose editorial profile links this project. schema: type: string format: uuid - name: search in: query required: false description: Case-insensitive match against detected name, image reference, and editorial display name/category. schema: type: string - name: status in: query required: false description: Server-side operational status filter. schema: type: string enum: [attention, healthy, missing] - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/Limit" responses: "200": description: A page of services, newest-`(DetectedName, Id)`-ordered content: application/json: schema: $ref: "#/components/schemas/InfrastructureServiceListResponse" "401": $ref: "#/components/responses/Unauthorized" /infrastructure/storage: get: tags: [Infrastructure] operationId: listInfrastructureStorage description: >- Requires a reader cookie session or an `infrastructure:read` service-token scope. Cursor-paginated storage listing (V2-R5, docs/adr/ADR-029-v2-r5-final-hardening.md) — every known volume across every service, each row carrying its owning service's ID/name for display. parameters: - name: search in: query required: false description: Case-insensitive match against the owning service's detected name and the volume's own name/mount path, applied server-side. schema: type: string - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/Limit" responses: "200": description: A page of storage volumes content: application/json: schema: $ref: "#/components/schemas/InfrastructureStorageListResponse" "401": $ref: "#/components/responses/Unauthorized" /infrastructure/services/{serviceId}: get: tags: [Infrastructure] operationId: getInfrastructureService description: >- Requires a reader cookie session or an `infrastructure:read` service-token scope. The full fact graph for one service: editorial profile, manual overrides, components, endpoints, networks, volumes, databases, dependencies, health observations, deployment references and backup coverage, each fact carrying its own provenance (source key, confidence, last-seen snapshot, observed-at). The `ETag` response header is the editorial profile's optimistic-concurrency token, required as `If-Match` on `PUT .../editorial-profile`. parameters: - name: serviceId in: path required: true schema: type: string format: uuid responses: "200": description: Full service detail headers: ETag: schema: type: string content: application/json: schema: $ref: "#/components/schemas/InfrastructureServiceDetail" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" /infrastructure/services/{serviceId}/editorial-profile: put: tags: [Infrastructure] operationId: updateInfrastructureEditorialProfile description: >- Editor role or `infrastructure:write` service-token scope required, CSRF- and `If-Match`-protected. Updates the human-authored editorial layer for a service (display name, summary, category, purpose, owning project, procedures, tags, favorite/hidden/archived flags) — automatically-detected facts are never touched by this endpoint. A stale `If-Match` is a 412, never a silent overwrite. parameters: - name: serviceId in: path required: true schema: type: string format: uuid - $ref: "#/components/parameters/IfMatch" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateEditorialProfileRequest" responses: "200": description: Updated service detail headers: ETag: schema: type: string content: application/json: schema: $ref: "#/components/schemas/InfrastructureServiceDetail" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "412": description: If-Match did not match the current editorial profile ETag content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "422": $ref: "#/components/responses/ValidationFailed" "428": description: If-Match header is required and was missing content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /infrastructure/services/{serviceId}/overrides: post: tags: [Infrastructure] operationId: setManualOverride description: >- Editor role or `infrastructure:write` service-token scope required, CSRF-protected. Sets (creates or replaces) one field-level manual correction over an automatically-detected fact, validated against the exact allowlist `GET /infrastructure/overrides/catalog` returns for this object kind — an inventory sync never overwrites or removes an existing override (docs/adr/ADR-025-v2-r1-data-integrity-remediation.md, audit finding H2). parameters: - name: serviceId in: path required: true schema: type: string format: uuid requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SetManualOverrideRequest" responses: "200": description: The created/updated override's ID content: application/json: schema: type: object properties: id: type: string format: uuid required: [id] "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "422": $ref: "#/components/responses/ValidationFailed" /infrastructure/overrides: post: tags: [Infrastructure] operationId: setInfrastructureOverride description: >- Editor role or `infrastructure:write` service-token scope required, CSRF-protected. The generic sibling of `POST /infrastructure/services/{serviceId}/overrides` (V2-R5, docs/adr/ADR-029-v2-r5-final-hardening.md): sets a field-level manual correction for any `InfrastructureObjectKind` (Host, Network, Volume, Database, Endpoint, Component, Deployment, Backup), not only a Service reached through its own nested route — validated against the exact allowlist `GET /infrastructure/overrides/catalog?targetKind=...` returns for that kind. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SetInfrastructureOverrideRequest" responses: "200": description: The created/updated override's ID content: application/json: schema: type: object properties: id: type: string format: uuid required: [id] "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "422": $ref: "#/components/responses/ValidationFailed" /infrastructure/overrides/{overrideId}: delete: tags: [Infrastructure] operationId: removeManualOverride description: >- Editor role or `infrastructure:write` service-token scope required, CSRF-protected. Removes one manual override, reverting that field to its automatically-detected value (if any). parameters: - name: overrideId in: path required: true schema: type: string format: uuid responses: "204": description: Override removed "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /infrastructure/services/{serviceId}/review: post: tags: [Infrastructure] operationId: reviewInfrastructureService description: >- Administrator only, CSRF-protected. Accepts or rejects a `Proposed` (newly detected, unreviewed) service — the deliberate human-in-the-loop gate before an automatically-detected container becomes part of the confirmed catalog. A name match alone is never treated as a high-confidence relationship (docs/adr/ADR-029-v2-r5-final-hardening.md). parameters: - name: serviceId in: path required: true schema: type: string format: uuid requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ReviewInfrastructureServiceRequest" responses: "200": description: The service's resulting presence state content: application/json: schema: $ref: "#/components/schemas/ReviewInfrastructureServiceResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "422": $ref: "#/components/responses/ValidationFailed" /infrastructure/inventory/snapshots: post: tags: [Infrastructure] operationId: synchronizeInfrastructureInventory description: >- Administrator only, CSRF- and `Idempotency-Key`-protected. Imports one inventory snapshot (hosts + services + every nested fact) captured by the external, read-only inventory tooling — never destructive: existing editorial profiles and manual overrides are always preserved, a previously-seen service that is missing from this snapshot is marked missing (not deleted), and replaying the same `sourceKey`/`sourceHash` is a safe no-op (`alreadyImported: true`), not a duplicate import. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SynchronizeInventoryRequest" responses: "200": description: Synchronization result content: application/json: schema: $ref: "#/components/schemas/InventorySyncResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "409": description: Idempotency-Key reused with a different request body content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "422": $ref: "#/components/responses/ValidationFailed" /infrastructure/inventory/curated-package: post: tags: [Infrastructure] operationId: importCuratedInventoryPackage description: >- Administrator only, CSRF- and `Idempotency-Key`-protected. A single bounded call that both synchronizes a raw inventory snapshot and applies a curated set of editorial profiles/review decisions on top of it in one step — the mechanism the curated-infrastructure-wiki publication used (docs/deployments/2026-08-02-curated-infrastructure-wiki.md). Never overwrites an editorial profile a human has since hand-edited. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ImportCuratedInventoryRequest" responses: "200": description: Combined synchronization + curation result content: application/json: schema: $ref: "#/components/schemas/ImportCuratedInventoryResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "409": description: Idempotency-Key reused with a different request body content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "422": $ref: "#/components/responses/ValidationFailed" /infrastructure/changes: get: tags: [Infrastructure] operationId: listInfrastructureChanges description: >- Requires a reader cookie session or an `infrastructure:read` service-token scope. The most recent inventory-sync change log entries (new/changed/missing objects), newest first, each naming its target kind/ID, source key, change kind and the specific fields that changed. parameters: - name: limit in: query required: false description: Defaults to 100. schema: type: integer minimum: 1 maximum: 500 responses: "200": description: Recent inventory changes, newest first content: application/json: schema: type: array items: $ref: "#/components/schemas/InventoryChangeResponse" "401": $ref: "#/components/responses/Unauthorized" /infrastructure/overrides/catalog: get: tags: [Infrastructure] operationId: getManualOverrideFieldCatalog description: >- The exact, server-authoritative set of fields a ManualOverride may target for the given InfrastructureObjectKind, with each field's JSON value kind, max length and (for enum fields) allowed values (docs/adr/ADR-025-v2-r1-data-integrity-remediation.md, audit finding H3). The frontend fetches this instead of keeping its own list, so the two can never independently drift. Requires a reader cookie session or an `infrastructure:read` service-token scope. parameters: - name: targetKind in: query required: false description: Defaults to Service, the only kind reachable through the current UI/API. schema: type: string enum: [Host, Service, Component, Endpoint, Network, Volume, Database, Deployment, Backup] responses: "200": description: Field catalog for the requested target kind content: application/json: schema: $ref: "#/components/schemas/ManualOverrideFieldCatalogResponse" "401": $ref: "#/components/responses/Unauthorized" "422": description: Unknown targetKind content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /attachments/{attachmentId}/content: get: tags: [Attachments] operationId: getAttachmentContent description: >- Requires a reader cookie session or an `attachments:read` service-token scope. Every request re-authorizes the logical reference (docs/13-attachments.md) — a content hash alone is never sufficient permission to retrieve a blob. Served with a safe Content-Disposition (inline only for a known-safe raster image, attachment otherwise) and X-Content-Type-Options: nosniff. parameters: - name: attachmentId in: path required: true schema: type: string format: uuid responses: "200": description: The raw file content content: application/octet-stream: {} "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" /admin/imports/preflight: post: tags: [Imports] operationId: preflightImport description: >- Administrator only. Discovers and fingerprints a read-only source beneath the operator-mounted imports directory, selects an adapter, and builds a plan — no target mutation occurs (docs/17-leafwiki-migration.md). requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PreflightImportRequest" responses: "200": description: Plan ready (or an idempotent repeat of an already-preflighted request) content: application/json: schema: $ref: "#/components/schemas/ImportPreflightResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "422": description: Invalid source path, or no adapter can interpret this source content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /admin/imports/{jobId}: get: tags: [Imports] operationId: getImportJob parameters: - $ref: "#/components/parameters/ImportJobId" responses: "200": description: Import job status content: application/json: schema: $ref: "#/components/schemas/ImportJobSummaryResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /admin/imports/{jobId}/execute: post: tags: [Imports] operationId: executeImport description: >- Administrator only. Re-verifies the source fingerprint and plan hash have not changed since preflight, then creates each planned page — parents before children — with full provenance for rollback. Never overwrites an existing page; an item already imported with unchanged content is skipped idempotently. parameters: - $ref: "#/components/parameters/ImportJobId" - name: sourceRelativePath in: query required: true description: The same relative path supplied to preflight, re-verified against its recorded fingerprint. schema: type: string responses: "200": description: Execution result content: application/json: schema: $ref: "#/components/schemas/ExecuteImportResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": description: The source or plan changed since preflight, or the job is not in a plan-ready state content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /admin/imports/{jobId}/rollback: post: tags: [Imports] operationId: rollbackImport description: >- Administrator only. Removes exactly the pages/attachments this job's own provenance created — never a page a user has since edited, which is preserved instead (docs/17-leafwiki-migration.md). parameters: - $ref: "#/components/parameters/ImportJobId" responses: "200": description: Rollback result content: application/json: schema: $ref: "#/components/schemas/RollbackImportResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": description: There is nothing to roll back for this job content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /admin/source-batches/purge/dry-run: post: tags: [Source batch purge] operationId: dryRunSourceBatchPurge description: >- Administrator only. Inspects an explicitly supplied, independently recorded page-ID list for one source batch. It blocks on missing or unlisted source pages, derived content outside the list, retained backlinks or project links. This endpoint never writes data. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SourceBatchPurgeRequest" responses: "200": description: Immutable-scope dry-run and confirmation hash content: application/json: schema: $ref: "#/components/schemas/SourceBatchPurgeDryRunResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /admin/source-batches/purge/execute: post: tags: [Source batch purge] operationId: executeSourceBatchPurge description: >- Administrator only. Permanently removes only the pages and attachment references proven by the preceding dry-run. The confirmation hash binds the request to the current page hashes and attachment graph; any change requires a new dry-run. Physical blobs are hash-verified and removed only after their database references have been committed as absent. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ExecuteSourceBatchPurgeRequest" responses: "200": description: Completed source-batch purge content: application/json: schema: $ref: "#/components/schemas/SourceBatchPurgeResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "409": description: The dry-run changed or the source scope is ambiguous content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /projects: get: tags: [Projects] operationId: listProjects parameters: - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/Limit" - name: status in: query schema: $ref: "#/components/schemas/ProjectStatus" - name: priority in: query schema: $ref: "#/components/schemas/ProjectPriority" - name: ownerId in: query schema: type: string format: uuid - name: search in: query description: Case-insensitive match against project name or summary, applied before cursor pagination. schema: type: string - name: updatedAfter in: query description: Only projects updated at or after this ISO-8601 timestamp. schema: type: string format: date-time - name: sort in: query schema: type: string enum: [name, updatedAt] responses: "200": description: Project collection content: application/json: schema: $ref: "#/components/schemas/ProjectListResponse" "401": $ref: "#/components/responses/Unauthorized" post: tags: [Projects] operationId: createProject requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateProjectRequest" responses: "200": description: Created project (full bundle) headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/ProjectDetail" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /projects/totals: get: tags: [Projects] operationId: getProjectCatalogTotals description: Authoritative workspace totals for dashboard KPI rendering; independent of cursor-paginated catalog pages. responses: "200": description: Total and active project counts content: application/json: schema: $ref: "#/components/schemas/ProjectCatalogTotalsResponse" "401": $ref: "#/components/responses/Unauthorized" /projects/{projectId}: parameters: - $ref: "#/components/parameters/ProjectId" get: tags: [Projects] operationId: getProject description: >- The project's bounded context bundle — structured data plus stable citations into linked pages, never a copy of their content (docs/15-project-hub.md "Project context bundle", M5-A05). responses: "200": description: Project detail bundle (documents, recent updates, milestones, decisions) headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/ProjectDetail" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" put: tags: [Projects] operationId: updateProject parameters: - $ref: "#/components/parameters/IfMatch" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateProjectRequest" responses: "200": description: Updated project headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/ProjectDetail" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "412": $ref: "#/components/responses/PreconditionFailed" "428": $ref: "#/components/responses/PreconditionRequired" /projects/{projectId}/context: parameters: - $ref: "#/components/parameters/ProjectId" get: tags: [Projects] operationId: getProjectContext description: >- A bounded, single-call context bundle for automation and future MCP-server consumers (docs/09-api-contract.md "Project context", docs/24-mcp-readiness.md, M6-A04). Update/decision/document counts are clamped; the response reports what was truncated instead of ever growing unbounded. Distinct from the unbounded `GET /projects/{projectId}` detail bundle. responses: "200": description: Bounded project context bundle with stable citations content: application/json: schema: $ref: "#/components/schemas/ProjectContextResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /projects/{projectId}/icon: parameters: - $ref: "#/components/parameters/ProjectId" post: tags: [Projects] operationId: uploadProjectIcon description: >- Replaces the current project icon using the attachment quarantine and byte-signature validation pipeline. Accepts only PNG, JPEG, GIF or WebP up to 2 MiB. The response points to same-origin, workspace-authorized attachment content. Requires the project's current ETag. parameters: - $ref: "#/components/parameters/IfMatch" requestBody: required: true content: multipart/form-data: schema: type: object required: [file] properties: file: type: string format: binary responses: "200": description: Project icon replaced headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/ProjectIconResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "412": $ref: "#/components/responses/PreconditionFailed" "413": description: Icon file exceeds 2 MiB content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "422": description: Unsupported or invalid image content content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "428": $ref: "#/components/responses/PreconditionRequired" /projects/{projectId}/status: parameters: - $ref: "#/components/parameters/ProjectId" post: tags: [Projects] operationId: changeProjectStatus description: Only the documented lifecycle transitions are accepted (docs/15-project-hub.md). parameters: - $ref: "#/components/parameters/IfMatch" requestBody: required: true content: application/json: schema: type: object required: [status] properties: status: $ref: "#/components/schemas/ProjectStatus" responses: "200": description: Updated project headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/ProjectDetail" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": description: The requested transition is not allowed from the project's current status content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "412": $ref: "#/components/responses/PreconditionFailed" "428": $ref: "#/components/responses/PreconditionRequired" /projects/{projectId}/documents: parameters: - $ref: "#/components/parameters/ProjectId" post: tags: [Projects] operationId: linkProjectDocument description: >- Links a page to the project with a role; the page remains canonical, only the relationship is stored (docs/15-project-hub.md). requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/LinkProjectDocumentRequest" responses: "200": description: Created link content: application/json: schema: $ref: "#/components/schemas/ProjectDocumentSummary" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /projects/documents/{documentId}: parameters: - name: documentId in: path required: true schema: type: string format: uuid delete: tags: [Projects] operationId: unlinkProjectDocument description: Removes only the relationship — the linked page is never touched or deleted. responses: "200": description: Unlinked "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /projects/{projectId}/updates: parameters: - $ref: "#/components/parameters/ProjectId" get: tags: [Projects] operationId: getProjectUpdates description: Ordered, attributable, linkable timeline (M5-A02). parameters: - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/Limit" responses: "200": description: Update timeline page content: application/json: schema: $ref: "#/components/schemas/ProjectUpdatesResponse" "401": $ref: "#/components/responses/Unauthorized" post: tags: [Projects] operationId: createProjectUpdate description: >- Immutable, append-only entry. Setting supersedesUpdateId marks the earlier update Superseded without rewriting it (docs/15-project-hub.md). requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateProjectUpdateRequest" responses: "200": description: Created update content: application/json: schema: $ref: "#/components/schemas/ProjectUpdateSummary" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /projects/{projectId}/milestones: parameters: - $ref: "#/components/parameters/ProjectId" get: tags: [Projects] operationId: getProjectMilestones responses: "200": description: Milestones, in their stable sort order content: application/json: schema: type: array items: $ref: "#/components/schemas/ProjectMilestoneSummary" "401": $ref: "#/components/responses/Unauthorized" post: tags: [Projects] operationId: createProjectMilestone requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateProjectMilestoneRequest" responses: "200": description: Created milestone content: application/json: schema: $ref: "#/components/schemas/ProjectMilestoneSummary" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /projects/milestones/{milestoneId}: parameters: - name: milestoneId in: path required: true schema: type: string format: uuid put: tags: [Projects] operationId: updateProjectMilestone parameters: - $ref: "#/components/parameters/IfMatch" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateProjectMilestoneRequest" responses: "200": description: Updated milestone content: application/json: schema: $ref: "#/components/schemas/ProjectMilestoneSummary" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "412": $ref: "#/components/responses/PreconditionFailed" "428": $ref: "#/components/responses/PreconditionRequired" /decisions: get: tags: [Decisions] operationId: listDecisions parameters: - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/Limit" - name: projectId in: query schema: type: string format: uuid - name: status in: query schema: $ref: "#/components/schemas/DecisionStatus" responses: "200": description: Decision collection content: application/json: schema: $ref: "#/components/schemas/DecisionListResponse" "401": $ref: "#/components/responses/Unauthorized" post: tags: [Decisions] operationId: createDecision description: >- Creates one ArchitectureDecision page plus its lifecycle profile in one step (docs/16-runbooks-and-adrs.md "unique page profile"). requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateDecisionRequest" responses: "200": description: Created decision (full detail, including rendered content) headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/DecisionDetail" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /decisions/{decisionId}: parameters: - name: decisionId in: path required: true schema: type: string format: uuid get: tags: [Decisions] operationId: getDecision description: Includes lineage (supersedes/supersededBy) — links are bidirectional and cycle-free. responses: "200": description: Decision detail with lineage headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/DecisionDetail" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" /decisions/{decisionId}/accept: parameters: - name: decisionId in: path required: true schema: type: string format: uuid post: tags: [Decisions] operationId: acceptDecision description: >- If this decision supersedes another, that decision must currently be Accepted and is atomically flipped to Superseded with bidirectional links (docs/16-runbooks-and-adrs.md). parameters: - $ref: "#/components/parameters/IfMatch" responses: "200": description: Accepted decision headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/DecisionDetail" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": description: Only a Proposed decision can be accepted content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "412": $ref: "#/components/responses/PreconditionFailed" "428": $ref: "#/components/responses/PreconditionRequired" /decisions/{decisionId}/reject: parameters: - name: decisionId in: path required: true schema: type: string format: uuid post: tags: [Decisions] operationId: rejectDecision parameters: - $ref: "#/components/parameters/IfMatch" responses: "200": description: Rejected decision content: application/json: schema: $ref: "#/components/schemas/DecisionDetail" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": description: Only a Proposed decision can be rejected content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "412": $ref: "#/components/responses/PreconditionFailed" "428": $ref: "#/components/responses/PreconditionRequired" /decisions/{decisionId}/withdraw: parameters: - name: decisionId in: path required: true schema: type: string format: uuid post: tags: [Decisions] operationId: withdrawDecision parameters: - $ref: "#/components/parameters/IfMatch" responses: "200": description: Withdrawn decision content: application/json: schema: $ref: "#/components/schemas/DecisionDetail" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": description: Only a Proposed or Accepted decision can be withdrawn content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "412": $ref: "#/components/responses/PreconditionFailed" "428": $ref: "#/components/responses/PreconditionRequired" /runbooks: get: tags: [Runbooks] operationId: listRunbooks parameters: - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/Limit" - name: riskLevel in: query schema: $ref: "#/components/schemas/RunbookRiskLevel" - name: status in: query schema: $ref: "#/components/schemas/RunbookStatus" responses: "200": description: Runbook collection content: application/json: schema: $ref: "#/components/schemas/RunbookListResponse" "401": $ref: "#/components/responses/Unauthorized" post: tags: [Runbooks] operationId: createRunbook description: >- Creates one Runbook page plus its validation/risk profile in one step (docs/16-runbooks-and-adrs.md "unique page profile"). requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateRunbookRequest" responses: "200": description: Created runbook (full detail, including rendered content) headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/RunbookDetail" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /runbooks/{runbookId}: parameters: - name: runbookId in: path required: true schema: type: string format: uuid get: tags: [Runbooks] operationId: getRunbook description: >- isStale is true whenever the linked page has a newer revision than the last validation, or was never validated (docs/16-runbooks-and-adrs.md). responses: "200": description: Runbook detail headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/RunbookDetail" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" /runbooks/{runbookId}/validate: parameters: - name: runbookId in: path required: true schema: type: string format: uuid post: tags: [Runbooks] operationId: validateRunbook description: >- Records that a human reviewed the current revision — the platform never executes any step (docs/16-runbooks-and-adrs.md). parameters: - $ref: "#/components/parameters/IfMatch" responses: "200": description: Validated runbook headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/RunbookDetail" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "412": $ref: "#/components/responses/PreconditionFailed" "428": $ref: "#/components/responses/PreconditionRequired" /runbooks/{runbookId}/profile: parameters: - name: runbookId in: path required: true schema: type: string format: uuid put: tags: [Runbooks] operationId: updateRunbookProfile parameters: - $ref: "#/components/parameters/IfMatch" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateRunbookProfileRequest" responses: "200": description: Updated runbook headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/RunbookDetail" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "412": $ref: "#/components/responses/PreconditionFailed" "428": $ref: "#/components/responses/PreconditionRequired" /service-accounts: get: tags: [Service accounts] operationId: listServiceAccounts description: Administrator-only, cookie-authenticated — never reachable by a service token itself (docs/09-api-contract.md). responses: "200": description: Service accounts and their (secret-free) tokens content: application/json: schema: $ref: "#/components/schemas/ServiceAccountListResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" post: tags: [Service accounts] operationId: createServiceAccount requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateServiceAccountRequest" responses: "200": description: Service account and its one-time raw token — never retrievable again content: application/json: schema: $ref: "#/components/schemas/CreatedServiceAccountToken" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "422": description: One or more requested scopes are not recognized content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /service-accounts/{serviceAccountId}/tokens: parameters: - name: serviceAccountId in: path required: true schema: type: string format: uuid post: tags: [Service accounts] operationId: issueServiceAccountToken description: >- Rotation pattern: issue a new token, update the client, then revoke the old one — a credential is never briefly unusable. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/IssueServiceAccountTokenRequest" responses: "200": description: New token — its one-time raw value content: application/json: schema: $ref: "#/components/schemas/CreatedServiceAccountToken" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /service-accounts/tokens/{tokenId}/revoke: parameters: - name: tokenId in: path required: true schema: type: string format: uuid post: tags: [Service accounts] operationId: revokeServiceAccountToken description: Takes effect on the very next request using this token — tokens are verified live against the database, never cached as valid. responses: "200": description: Revoked "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /admin/backups: get: tags: [Backups] operationId: listBackups description: >- Administrator-only (docs/18-backup-restore.md). Never includes a filesystem path or connection string — only what an administrator needs to judge backup health. responses: "200": description: Completed backups, newest first content: application/json: schema: $ref: "#/components/schemas/BackupListResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" post: tags: [Backups] operationId: createBackup description: >- Synchronously creates and verifies a full backup (every table plus every content-addressed attachment blob), then applies retention. A failed backup never replaces the last known-good one. Restore is intentionally not exposed here — it runs as a separate maintenance-mode invocation of the same deployable image (docs/18-backup-restore.md; ADR-016 decision 4). responses: "200": description: The newly created, verified backup content: application/json: schema: $ref: "#/components/schemas/BackupSummaryResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "409": description: A backup is already in progress content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /me/preferences: get: tags: [Preferences] operationId: getMyPreferences security: [{ cookieAuth: [] }] responses: "200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/UserPreferences" } } } } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } put: tags: [Preferences] operationId: updateMyPreferences security: [{ cookieAuth: [] }] requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/UserPreferencesRequest" } } } } responses: "200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/UserPreferences" } } } } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "412": { $ref: "#/components/responses/PreconditionFailed" } "428": { $ref: "#/components/responses/PreconditionRequired" } /admin/platform-settings: get: tags: [Admin] operationId: getPlatformSettings security: [{ cookieAuth: [] }] responses: "200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/PlatformSettings" } } } } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } put: tags: [Admin] operationId: updatePlatformSettings security: [{ cookieAuth: [] }] requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/PlatformSettingsRequest" } } } } responses: "200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/PlatformSettings" } } } } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "412": { $ref: "#/components/responses/PreconditionFailed" } "428": { $ref: "#/components/responses/PreconditionRequired" } /admin/data-sources: get: tags: [DataSources] operationId: listDataSources security: [{ cookieAuth: [] }] responses: "200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/DataSourceList" } } } } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } post: tags: [DataSources] operationId: registerDataSource security: [{ cookieAuth: [] }] requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/DataSourceRegistration" } } } } responses: "200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/DataSource" } } } } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /admin/data-sources/{id}: put: tags: [DataSources] operationId: configureDataSource security: [{ cookieAuth: [] }] parameters: [{ name: id, in: path, required: true, schema: { type: string, format: uuid } }] requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/DataSourceConfiguration" } } } } responses: "200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/DataSource" } } } } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "412": { $ref: "#/components/responses/PreconditionFailed" } "428": { $ref: "#/components/responses/PreconditionRequired" } /admin/data-sources/{id}/observations: post: tags: [DataSources] operationId: reportDataSourceObservation security: [{ cookieAuth: [] }] parameters: [{ name: id, in: path, required: true, schema: { type: string, format: uuid } }] requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/DataSourceObservation" } } } } responses: "200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/DataSource" } } } } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /admin/backups/status: get: tags: [Backups] operationId: getBackupStatus security: [{ cookieAuth: [] }] responses: "200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/BackupStatus" } } } } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /admin/backups/{backupId}/protection: put: tags: [Backups] operationId: setBackupProtection security: [{ cookieAuth: [] }] parameters: [{ name: backupId, in: path, required: true, schema: { type: string } }] requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/BackupProtectionRequest" } } } } responses: "200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/BackupEvidence" } } } } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "412": { $ref: "#/components/responses/PreconditionFailed" } "428": { $ref: "#/components/responses/PreconditionRequired" } /admin/audit-log: get: tags: [Admin] operationId: listAdminAuditLog security: [{ cookieAuth: [] }] responses: "200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/AdminAuditList" } } } } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /jobs: get: tags: [Jobs] operationId: listBackgroundJobs security: [{ cookieAuth: [] }] responses: "200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/BackgroundJobList" } } } } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /jobs/course-tagging: post: tags: [Jobs] operationId: queueCourseTaggingJob security: [{ cookieAuth: [] }] parameters: [{ $ref: "#/components/parameters/RequiredIdempotencyKey" }] requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/QueueCourseTaggingJobRequest" } } } } responses: "202": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/BackgroundJob" } } } } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /jobs/backups: post: tags: [Jobs] operationId: queueBackupJob security: [{ cookieAuth: [] }] parameters: [{ $ref: "#/components/parameters/RequiredIdempotencyKey" }] requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/QueueBackupJobRequest" } } } } responses: "202": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/BackgroundJob" } } } } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /jobs/{jobId}: get: tags: [Jobs] operationId: getBackgroundJob security: [{ cookieAuth: [] }] parameters: [{ name: jobId, in: path, required: true, schema: { type: string, format: uuid } }] responses: "200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/BackgroundJob" } } } } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /jobs/{jobId}/cancel: post: tags: [Jobs] operationId: cancelBackgroundJob security: [{ cookieAuth: [] }] parameters: [{ name: jobId, in: path, required: true, schema: { type: string, format: uuid } }] responses: "200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/BackgroundJob" } } } } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /jobs/{jobId}/events: get: tags: [Jobs] operationId: streamBackgroundJobEvents security: [{ cookieAuth: [] }] parameters: [{ name: jobId, in: path, required: true, schema: { type: string, format: uuid } }] responses: "200": { description: Job and progress Server-Sent Events with heartbeat and terminal close, content: { text/event-stream: { schema: { type: string } } } } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } components: securitySchemes: cookieAuth: type: apiKey in: cookie name: ikh_auth bearerAuth: type: http scheme: bearer bearerFormat: IKH service token parameters: CorrelationId: name: X-Correlation-ID in: header required: false schema: type: string maxLength: 100 IdempotencyKey: name: Idempotency-Key in: header required: false schema: type: string minLength: 16 maxLength: 200 RequiredIdempotencyKey: name: Idempotency-Key in: header required: true schema: type: string minLength: 1 maxLength: 200 Cursor: name: cursor in: query required: false schema: type: string maxLength: 2048 Limit: name: limit in: query required: false schema: type: integer minimum: 1 maximum: 100 default: 25 PageId: name: pageId in: path required: true schema: type: string format: uuid SessionId: name: sessionId in: path required: true schema: type: string format: uuid RevisionNumber: name: revisionNumber in: path required: true schema: type: integer format: int64 minimum: 1 ImportJobId: name: jobId in: path required: true schema: type: string format: uuid ProjectId: name: projectId in: path required: true schema: type: string format: uuid UserId: name: userId in: path required: true schema: type: string format: uuid DraftKey: name: draftKey in: path required: true description: The page ID in edit mode, or a client-generated key for an unpublished new page. schema: type: string IfMatch: name: If-Match in: header required: true schema: type: string headers: ETag: description: Strong entity version tag schema: type: string responses: BadRequest: description: Invalid request content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" Unauthorized: description: Authentication required content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" Forbidden: description: Insufficient permission content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" NotFound: description: Resource not found or not disclosed content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" Conflict: description: Idempotency or domain conflict content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" PreconditionFailed: description: ETag does not match the current resource content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" PreconditionRequired: description: If-Match is required content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" ValidationFailed: description: Request failed validation content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" RateLimited: description: Request rate limited headers: Retry-After: schema: type: integer content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" schemas: UserPreferencesRequest: type: object required: [theme, density, primaryNavigationCollapsed, contextPanelOpen, navigationOrder, dashboardWidgetOrder, hiddenDashboardWidgets, defaultLandingPage, catalogView, contextPanelWidth, navigationWidth] properties: theme: { type: string, enum: [system, light, dark] } density: { type: string, enum: [comfortable, compact] } primaryNavigationCollapsed: { type: boolean } contextPanelOpen: { type: boolean } navigationOrder: { type: array, items: { type: string } } dashboardWidgetOrder: { type: array, items: { type: string } } hiddenDashboardWidgets: { type: array, items: { type: string } } defaultLandingPage: { type: string } catalogView: { type: string, enum: [grid, list] } contextPanelWidth: { type: integer, minimum: 256, maximum: 440 } navigationWidth: { type: integer, minimum: 192, maximum: 320 } UserPreferences: allOf: - $ref: "#/components/schemas/UserPreferencesRequest" - type: object required: [schemaVersion, eTag, isDefault, updatedAt] properties: schemaVersion: { type: integer, enum: [1] } eTag: { type: string } isDefault: { type: boolean } updatedAt: { type: [string, "null"], format: date-time } PlatformSettingsRequest: type: object required: [displayName, primaryLocale, timeZone, ingestionProposals, backupPolicyExpectation, inventoryFreshnessPolicy] properties: displayName: { type: string, minLength: 1, maxLength: 200 } primaryLocale: { type: string } timeZone: { type: string } ingestionProposals: { type: object, required: [enabled, maximumSourceCount, maximumSourceBytes, failedRetryLimit], properties: { enabled: { type: boolean }, maximumSourceCount: { type: integer, minimum: 1, maximum: 20 }, maximumSourceBytes: { type: integer, format: int64 }, failedRetryLimit: { type: integer, minimum: 1, maximum: 3 } } } backupPolicyExpectation: { type: object, required: [expectedIntervalHours, scheduleDescription, dailyRetentionCount, weeklyRetentionCount, monthlyRetentionCount, protectedBackupCount, restoreVerificationMaxAgeDays], properties: { expectedIntervalHours: { type: integer }, scheduleDescription: { type: string }, dailyRetentionCount: { type: integer }, weeklyRetentionCount: { type: integer }, monthlyRetentionCount: { type: integer }, protectedBackupCount: { type: integer }, restoreVerificationMaxAgeDays: { type: integer } } } inventoryFreshnessPolicy: { type: object, required: [warningAfterHours], properties: { warningAfterHours: { type: integer } } } PlatformSettings: allOf: - $ref: "#/components/schemas/PlatformSettingsRequest" - type: object required: [schemaVersion, eTag, isDefault, updatedAt] properties: schemaVersion: { type: integer, enum: [1] } eTag: { type: string } isDefault: { type: boolean } updatedAt: { type: [string, "null"], format: date-time } backupPolicyExpectation: { type: object, description: Includes enforcement External; configured schedule is an expectation, not a scheduler run. } DataSourceRegistration: type: object required: [sourceKey, displayName, kind, expectedIntervalHours] properties: sourceKey: { type: string, maxLength: 200 } displayName: { type: string, maxLength: 120 } kind: { type: string, enum: [Inventory, Gitea, KnowledgeClient, ManualIntake, LeafWiki, CuratedPackage] } expectedIntervalHours: { type: integer, minimum: 1, maximum: 8760 } DataSourceConfiguration: type: object required: [displayName, enabled, expectedIntervalHours] properties: displayName: { type: string, maxLength: 120 } enabled: { type: boolean } expectedIntervalHours: { type: integer, minimum: 1, maximum: 8760 } DataSourceObservation: type: object required: [success] properties: success: { type: boolean } recordCount: { type: [integer, "null"], format: int64 } errorCode: { type: [string, "null"] } correlationId: { type: [string, "null"], maxLength: 100 } DataSource: type: object required: [id, sourceKey, displayName, kind, enabled, state, configurationState, syncAuthority, expectedIntervalHours, eTag, actions] properties: id: { type: string, format: uuid } sourceKey: { type: string } displayName: { type: string } kind: { type: string } enabled: { type: boolean } state: { type: string, enum: [Unknown, Current, Stale, Failed, Disabled] } configurationState: { type: string } syncAuthority: { type: string } expectedIntervalHours: { type: integer } lastAttemptAt: { type: [string, "null"], format: date-time } lastSuccessAt: { type: [string, "null"], format: date-time } lastFailureAt: { type: [string, "null"], format: date-time } dataObservedAt: { type: [string, "null"], format: date-time } recordCount: { type: [integer, "null"], format: int64 } lastErrorCode: { type: [string, "null"] } lastCorrelationId: { type: [string, "null"] } eTag: { type: string } actions: { type: array, items: { type: string } } DataSourceList: type: object required: [items, nextCursor] properties: items: { type: array, items: { $ref: "#/components/schemas/DataSource" } } nextCursor: { type: [string, "null"] } BackupProtectionRequest: type: object required: [protected] properties: protected: { type: boolean } BackupEvidence: type: object required: [protected, protectionVersion] properties: protected: { type: boolean } protectionVersion: { type: integer } integrityVerifiedAt: { type: [string, "null"], format: date-time } restoreVerifiedAt: { type: [string, "null"], format: date-time } lastRestoreSucceeded: { type: [boolean, "null"] } BackupStatus: type: object required: [state, scheduleAuthority, scheduleDescription, restoreConfidence, storedBackupBytes, protectedCount, expectedIntervalHours, applicationRetentionCount, dailyRetentionExpectation, weeklyRetentionExpectation, monthlyRetentionExpectation, restoreVerificationMaxAgeDays] properties: state: { type: string, enum: [Unknown, Current, Stale] } scheduleAuthority: { type: string, enum: [External] } scheduleDescription: { type: string } nextScheduledRun: { type: [string, "null"], format: date-time } nextExpectedBy: { type: [string, "null"], format: date-time } lastSuccessfulBackupAt: { type: [string, "null"], format: date-time } lastIntegrityVerifiedAt: { type: [string, "null"], format: date-time } lastVerifiedRestoreAt: { type: [string, "null"], format: date-time } restoreConfidence: { type: string, enum: [Unknown, Stale, Verified] } storedBackupBytes: { type: integer, format: int64, description: Sum of stored backup manifest sizes; not filesystem capacity. } protectedCount: { type: integer } expectedIntervalHours: { type: integer } applicationRetentionCount: { type: integer } dailyRetentionExpectation: { type: integer } weeklyRetentionExpectation: { type: integer } monthlyRetentionExpectation: { type: integer } restoreVerificationMaxAgeDays: { type: integer } AdminAuditList: type: object required: [items, nextCursor] properties: items: { type: array, items: { $ref: "#/components/schemas/AdminAuditItem" } } nextCursor: { type: [integer, "null"], format: int64 } AdminAuditItem: type: object required: [id, actorKind, source, action, summary, occurredAt] properties: id: { type: integer, format: int64 } actorKind: { type: string } actorId: { type: [string, "null"], format: uuid } source: { type: string } action: { type: string } targetType: { type: [string, "null"] } targetId: { type: [string, "null"] } summary: { type: string } occurredAt: { type: string, format: date-time } correlationId: { type: [string, "null"] } QueueCourseTaggingJobRequest: type: object required: [rootPageId, tag] properties: rootPageId: { type: string, format: uuid } tag: { type: string } QueueBackupJobRequest: { type: object, description: Empty object; the job snapshots the current server-side backup policy. } BackgroundJob: type: object required: [id, workspaceId, actorId, type, status, createdAt, cancelRequested, progress, total, processed, succeeded, skipped, failed, errors, version] properties: id: { type: string, format: uuid } workspaceId: { type: string, format: uuid } actorId: { type: string, format: uuid } type: { type: string } status: { type: string, enum: [queued, running, succeeded, partially_succeeded, failed, cancelled] } createdAt: { type: string, format: date-time } startedAt: { type: [string, "null"], format: date-time } completedAt: { type: [string, "null"], format: date-time } cancelRequestedAt: { type: [string, "null"], format: date-time } cancelRequested: { type: boolean } progress: { type: integer, minimum: 0, maximum: 100 } total: { type: integer } processed: { type: integer } succeeded: { type: integer } skipped: { type: integer } failed: { type: integer } errors: { type: array, items: { type: object, properties: { code: { type: string }, message: { type: string }, itemId: { type: [string, "null"], format: uuid } } } } result: { type: [object, "null"] } correlationId: { type: [string, "null"] } version: { type: integer, format: int64 } BackgroundJobList: type: object required: [items] properties: items: { type: array, items: { $ref: "#/components/schemas/BackgroundJob" } } CreateIngestionProposalRequest: type: object required: [manifest] properties: manifest: { type: string, minLength: 1, maxLength: 100000 } correlationId: { type: [string, "null"], maxLength: 200 } publication: { oneOf: [{ $ref: "#/components/schemas/IngestionPublicationRequest" }, { type: "null" }] } IngestionPublicationRequest: type: object required: [title, markdown, documentType, documentStatus, mutationKind] properties: title: { type: string, minLength: 1, maxLength: 200 } markdown: { type: string, minLength: 1, maxLength: 100000 } documentType: { $ref: "#/components/schemas/DocumentType" } documentStatus: { $ref: "#/components/schemas/DocumentStatus" } mutationKind: { type: string, enum: [CreatePage, UpdatePage] } parentPageId: { type: [string, "null"], format: uuid } targetPageId: { type: [string, "null"], format: uuid } expectedTargetETag: { type: [string, "null"], maxLength: 32 } PatchIngestionProposalRequest: type: object description: Human callers use manifest/rationale; the submitting service account uses analysis only. properties: manifest: { type: [string, "null"], maxLength: 100000 } rationale: { type: [string, "null"], maxLength: 2000 } analysis: { type: [string, "null"], maxLength: 100000 } IngestionProposalDecisionRequest: type: object properties: comment: { type: [string, "null"], maxLength: 2000 } IngestionProposalSourceDetail: type: object required: [id, fileName, mimeType, byteSize, digestSha256, scanResult, attachmentReferenceId] properties: id: { type: string, format: uuid } fileName: { type: string } mimeType: { type: string } byteSize: { type: integer, format: int64 } digestSha256: { type: string, pattern: '^[a-f0-9]{64}$' } scanResult: { type: string } attachmentReferenceId: { type: string, format: uuid } IngestionProposalRevisionDetail: type: object required: [revisionNumber, status, operation, actorId, occurredAt] properties: revisionNumber: { type: integer, format: int64 } status: { type: string } operation: { type: string } actorId: { type: string, format: uuid } occurredAt: { type: string, format: date-time } IngestionProposalSummary: type: object required: [id, correlationId, status, revisionNumber, sourceCount, submitterId, createdAt, destination, metadata, mutationKind] properties: id: { type: string, format: uuid } correlationId: { type: string } status: { type: string } revisionNumber: { type: integer, format: int64 } sourceCount: { type: integer } submitterId: { type: string, format: uuid } createdAt: { type: string, format: date-time } destination: { oneOf: [{ $ref: "#/components/schemas/IngestionProposalDestination" }, { type: "null" }] } metadata: { $ref: "#/components/schemas/IngestionProposalMetadata" } mutationKind: { type: [string, "null"], enum: [CreatePage, UpdatePage, null] } IngestionProposalListResponse: type: object required: [items, nextCursor, page, pageSize, total, correlationId] properties: items: { type: array, items: { $ref: "#/components/schemas/IngestionProposalSummary" } } nextCursor: { type: [string, "null"] } page: { type: integer, minimum: 1 } pageSize: { type: integer, minimum: 1, maximum: 100 } total: { type: integer, format: int64, minimum: 0 } correlationId: { type: string } IngestionProposalDestination: type: object required: [pageId, parentPageId, title] properties: pageId: { type: [string, "null"], format: uuid } parentPageId: { type: [string, "null"], format: uuid } title: { type: [string, "null"] } IngestionProposalMetadata: type: object description: Project and risk are currently unavailable persisted attributes and are therefore explicitly null. required: [documentType, documentStatus, projectId, risk] properties: documentType: { type: [string, "null"] } documentStatus: { type: [string, "null"] } projectId: { type: [string, "null"], format: uuid } risk: { type: [string, "null"], enum: [low, medium, high, null] } IngestionProposalCitation: type: object required: [label, pageId, excerpt, confidence] properties: label: { type: string } pageId: { type: [string, "null"], format: uuid } excerpt: { type: [string, "null"] } confidence: { type: [number, "null"] } IngestionProposalWarning: type: object required: [code, message, severity] properties: code: { type: string } message: { type: string } severity: { type: [string, "null"], enum: [low, medium, high, null] } IngestionProposalCandidate: type: object required: [pageId, title, kind] properties: pageId: { type: [string, "null"], format: uuid } title: { type: [string, "null"] } kind: { type: [string, "null"] } IngestionProposalDiff: type: object required: [before, after, summary] properties: before: { type: [string, "null"] } after: { type: [string, "null"] } summary: { type: [string, "null"] } IngestionProposalPermissions: type: object required: [canEdit, canDecide, canPublish] properties: canEdit: { type: boolean } canDecide: { type: boolean } canPublish: { type: boolean } IngestionProposalDetail: type: object description: Safe dossier metadata only; it never contains source bytes, paths, URLs, credentials, or tokens. required: [id, correlationId, status, revisionNumber, manifest, retryCount, maxPublishRetries, isRetryEligible, sources, revisions, etag, submitterId, createdAt, destination, metadata, mutationKind, attachments, expectedTargetETag, citations, warnings, duplicateCandidates, conflictCandidates, diff, permissions] properties: id: { type: string, format: uuid } correlationId: { type: string } status: { type: string } revisionNumber: { type: integer, format: int64 } manifest: { type: string } analysis: { type: [string, "null"] } reviewerComment: { type: [string, "null"] } retryCount: { type: integer } maxPublishRetries: { type: integer } isRetryEligible: { type: boolean } failedLifecycleStatus: { type: [string, "null"] } failedProcessingStage: { type: [string, "null"] } failureErrorType: { type: [string, "null"] } failureMessage: { type: [string, "null"] } sources: { type: array, items: { $ref: "#/components/schemas/IngestionProposalSourceDetail" } } revisions: { type: array, items: { $ref: "#/components/schemas/IngestionProposalRevisionDetail" } } etag: { type: string } publishedPageId: { type: [string, "null"], format: uuid } publishedPageRevisionNumber: { type: [integer, "null"], format: int64 } submitterId: { type: string, format: uuid } createdAt: { type: string, format: date-time } destination: { oneOf: [{ $ref: "#/components/schemas/IngestionProposalDestination" }, { type: "null" }] } metadata: { $ref: "#/components/schemas/IngestionProposalMetadata" } mutationKind: { type: [string, "null"], enum: [CreatePage, UpdatePage, null] } attachments: { type: array, items: { $ref: "#/components/schemas/IngestionProposalSourceDetail" } } expectedTargetETag: { type: [string, "null"] } citations: { type: array, items: { $ref: "#/components/schemas/IngestionProposalCitation" } } warnings: { type: array, items: { $ref: "#/components/schemas/IngestionProposalWarning" } } duplicateCandidates: { type: array, items: { $ref: "#/components/schemas/IngestionProposalCandidate" } } conflictCandidates: { type: array, items: { $ref: "#/components/schemas/IngestionProposalCandidate" } } diff: description: after contains the proposed publication Markdown. before is null when no current target snapshot is available. oneOf: [{ $ref: "#/components/schemas/IngestionProposalDiff" }, { type: "null" }] permissions: { $ref: "#/components/schemas/IngestionProposalPermissions" } SystemInfo: type: object required: [product, version, commit, schemaVersion, environment, primaryLocale, timeZone, uptimeSeconds, release] properties: product: type: string const: ItWorx Knowledge Hub version: type: string commit: type: string schemaVersion: type: string environment: type: string primaryLocale: type: string timeZone: type: string uptimeSeconds: type: integer release: $ref: "#/components/schemas/ReleaseIdentity" ReleaseIdentity: type: object description: >- The canonical, machine-readable release identity (docs/adr/ADR-029-v2-r5-final-hardening.md). Every field is honestly nullable — a value not actually known at build or deploy time is never fabricated. required: [semanticVersion, releaseTag, sourceCommit, imageRepository, imageTag, imageId, imageDigest, buildTimestamp, deploymentTimestamp, openApiVersion, openApiHash, frontendBuildHash, databaseMigrationRange, inventorySnapshotId, backupId] properties: semanticVersion: type: [string, "null"] releaseTag: type: [string, "null"] sourceCommit: type: [string, "null"] imageRepository: type: [string, "null"] imageTag: type: [string, "null"] imageId: type: [string, "null"] imageDigest: type: [string, "null"] buildTimestamp: type: [string, "null"] format: date-time deploymentTimestamp: type: [string, "null"] format: date-time openApiVersion: type: [string, "null"] openApiHash: type: [string, "null"] frontendBuildHash: type: [string, "null"] databaseMigrationRange: type: [string, "null"] description: "The first and last applied EF Core migration for the database this instance is actually connected to, formatted as 'first..last' (or a single migration name if only one has been applied)." inventorySnapshotId: type: [string, "null"] backupId: type: [string, "null"] format: int64 minimum: 0 DocumentType: type: string enum: - WikiPage - StudyNote - CourseModule - ProjectDocument - ArchitectureDecision - Runbook - Troubleshooting - Reference - ChangeLog - SessionNote - Checklist DocumentStatus: type: string enum: [Draft, Active, NeedsReview, Deprecated, Archived] PageSummary: type: object required: [id, parentPageId, title, slug, documentType, documentStatus, hasChildren, updatedAt] properties: id: type: string format: uuid parentPageId: type: [string, "null"] format: uuid title: type: string slug: type: string documentType: $ref: "#/components/schemas/DocumentType" documentStatus: $ref: "#/components/schemas/DocumentStatus" hasChildren: type: boolean updatedAt: type: string format: date-time PageChildrenResponse: type: object required: [items, nextCursor] properties: items: type: array items: $ref: "#/components/schemas/PageSummary" nextCursor: type: [string, "null"] BreadcrumbEntry: type: object required: [id, title] properties: id: type: string format: uuid title: type: string PageDetail: type: object required: - id - workspaceId - parentPageId - title - slug - markdown - renderedHtml - documentType - documentStatus - currentRevisionNumber - tags - breadcrumb - createdBy - updatedBy - createdAt - updatedAt - publishedAt - trashedAt - eTag properties: id: type: string format: uuid workspaceId: type: string format: uuid parentPageId: type: [string, "null"] format: uuid title: type: string slug: type: string markdown: type: string renderedHtml: type: string description: Server-rendered and sanitized. Never render untrusted Markdown as HTML client-side. documentType: $ref: "#/components/schemas/DocumentType" documentStatus: $ref: "#/components/schemas/DocumentStatus" currentRevisionNumber: type: integer format: int64 tags: type: array items: type: string breadcrumb: type: array items: $ref: "#/components/schemas/BreadcrumbEntry" createdBy: type: [string, "null"] format: uuid updatedBy: type: [string, "null"] format: uuid createdAt: type: string format: date-time updatedAt: type: string format: date-time publishedAt: type: [string, "null"] format: date-time trashedAt: type: [string, "null"] format: date-time description: Null unless the page is in the trash/retention model. eTag: type: string description: Also present as the response ETag header; included in the body so clients need not parse headers. CreatePageRequest: type: object required: [parentPageId, title, documentType, markdown, templateKey] description: >- Either supply documentType and markdown directly, or supply templateKey to seed the page from a starter template (in which case documentType/markdown are ignored and may be null). properties: parentPageId: type: [string, "null"] format: uuid title: type: string minLength: 1 maxLength: 300 documentType: oneOf: - $ref: "#/components/schemas/DocumentType" - type: "null" markdown: type: [string, "null"] maxLength: 2097152 templateKey: type: [string, "null"] tags: type: array items: type: string maxItems: 25 BeginPageCreationSessionRequest: type: object properties: parentPageId: type: [string, "null"] format: uuid PageCreationSessionResponse: type: object required: [sessionId, expiresAt] properties: sessionId: type: string format: uuid expiresAt: type: string format: date-time CommitPageCreationSessionRequest: type: object required: [title] description: >- No parentPageId (fixed at session-begin time) or templateKey (a template and pre-publish attachments are mutually exclusive, matching the existing product behavior). properties: title: type: string minLength: 1 maxLength: 300 documentType: oneOf: - $ref: "#/components/schemas/DocumentType" - type: "null" markdown: type: [string, "null"] maxLength: 2097152 tags: type: array items: type: string maxItems: 25 DiscardPageCreationSessionResponse: type: object required: [attachmentsRemoved] properties: attachmentsRemoved: type: integer UpdatePageRequest: type: object required: [title, markdown, documentType, documentStatus, changeSummary] properties: title: type: string minLength: 1 maxLength: 300 markdown: type: string maxLength: 2097152 documentType: $ref: "#/components/schemas/DocumentType" documentStatus: $ref: "#/components/schemas/DocumentStatus" changeSummary: type: [string, "null"] maxLength: 500 tags: type: array items: type: string maxItems: 25 description: Omitted or null leaves the page's tags unchanged. MovePageRequest: type: object required: [newParentPageId, afterSiblingId] properties: newParentPageId: type: [string, "null"] format: uuid afterSiblingId: type: [string, "null"] format: uuid description: Null means "first among the new siblings". TemplateSummary: type: object required: [key, name, documentType, description] properties: key: type: string name: type: string documentType: $ref: "#/components/schemas/DocumentType" description: type: string DraftResponse: type: object required: [draftKey, pageId, baseRevisionNumber, title, markdown, updatedAt, expiresAt, baseRevisionIsStale] properties: draftKey: type: string pageId: type: [string, "null"] format: uuid baseRevisionNumber: type: [integer, "null"] format: int64 title: type: string markdown: type: string updatedAt: type: string format: date-time expiresAt: type: string format: date-time baseRevisionIsStale: type: boolean description: True when the page has published a newer revision since this draft's base. SaveDraftRequest: type: object required: [pageId, baseRevisionNumber, title, markdown] properties: pageId: type: [string, "null"] format: uuid baseRevisionNumber: type: [integer, "null"] format: int64 title: type: string markdown: type: string PageRevisionSummary: type: object required: [revisionNumber, title, documentType, documentStatus, authoredBy, changeSummary, createdAt] properties: revisionNumber: type: integer format: int64 title: type: string documentType: $ref: "#/components/schemas/DocumentType" documentStatus: $ref: "#/components/schemas/DocumentStatus" authoredBy: type: [string, "null"] format: uuid changeSummary: type: [string, "null"] createdAt: type: string format: date-time PageRevisionDetailResponse: type: object required: [revisionNumber, title, markdown, renderedHtml, documentType, documentStatus, authoredBy, changeSummary, createdAt] properties: revisionNumber: type: integer format: int64 title: type: string markdown: type: string renderedHtml: type: string description: >- Server-rendered and sanitized without re-resolving internal links against today's link table (a historical snapshot's links are not live). documentType: $ref: "#/components/schemas/DocumentType" documentStatus: $ref: "#/components/schemas/DocumentStatus" authoredBy: type: [string, "null"] format: uuid changeSummary: type: [string, "null"] createdAt: type: string format: date-time BacklinkEntry: type: object required: [id, title, slug, documentType] properties: id: type: string format: uuid title: type: string slug: type: string documentType: $ref: "#/components/schemas/DocumentType" BacklinksResponse: type: object required: [items] properties: items: type: array items: $ref: "#/components/schemas/BacklinkEntry" TrashPurgeResponse: type: object required: [purgedCount] properties: purgedCount: type: integer AttachmentDeliveryPolicy: type: string enum: [InlinePreviewEligible, DownloadOnly, Unknown] AttachmentSummary: type: object required: [id, filename, mediaType, deliveryPolicy, byteLength, description, altText, createdAt] properties: id: type: string format: uuid filename: type: string description: The sanitized, stored display filename — never necessarily identical to what the client declared. mediaType: type: string description: The detected media type from the byte signature, not the caller-declared Content-Type. deliveryPolicy: $ref: "#/components/schemas/AttachmentDeliveryPolicy" byteLength: type: integer format: int64 description: type: [string, "null"] altText: type: [string, "null"] createdAt: type: string format: date-time AttachmentListResponse: type: object required: [items] properties: items: type: array items: $ref: "#/components/schemas/AttachmentSummary" ManualOverrideFieldCatalogEntry: type: object required: [fieldName, displayLabel, valueKind, maxStringLength, allowedEnumValues] properties: fieldName: type: string displayLabel: type: string valueKind: type: string enum: [Text, Enum] maxStringLength: type: integer description: 0 for an Enum-kind field, where length is governed by allowedEnumValues instead. allowedEnumValues: type: [array, "null"] items: type: string ManualOverrideFieldCatalogResponse: type: object required: [targetKind, fields] properties: targetKind: type: string fields: type: array items: $ref: "#/components/schemas/ManualOverrideFieldCatalogEntry" FactProvenance: type: object required: [sourceKey, confidence, lastSnapshotId, observedAt] properties: sourceKey: type: string confidence: type: string enum: [Observed, Inferred, Declared, Unknown] lastSnapshotId: type: string format: uuid observedAt: type: string format: date-time InventorySnapshotSummary: type: object required: [id, sourceKey, sourceHash, capturedAt, importedAt, hostCount, serviceCount] properties: id: type: string format: uuid sourceKey: type: string sourceHash: type: string capturedAt: type: string format: date-time importedAt: type: string format: date-time hostCount: type: integer serviceCount: type: integer InfrastructureHostSummary: type: object required: [id, sourceKey, hostname, operatingSystem, kernelVersion, architecture, logicalCpuCount, totalMemoryBytes, presence, lastSeenAt] properties: id: type: string format: uuid sourceKey: type: string hostname: type: string operatingSystem: type: string kernelVersion: type: string architecture: type: string logicalCpuCount: type: integer totalMemoryBytes: type: integer format: int64 presence: type: string enum: [Present, Missing] lastSeenAt: type: string format: date-time InfrastructureOverviewResponse: type: object required: [latestSnapshot, hosts, serviceCount, proposedServiceCount, missingServiceCount, unhealthyServiceCount, unprotectedServiceCount] properties: latestSnapshot: type: [object, "null"] allOf: - $ref: "#/components/schemas/InventorySnapshotSummary" hosts: type: array items: $ref: "#/components/schemas/InfrastructureHostSummary" serviceCount: type: integer proposedServiceCount: type: integer missingServiceCount: type: integer unhealthyServiceCount: type: integer unprotectedServiceCount: type: integer InfrastructureServiceSummary: type: object required: [id, sourceKey, name, detectedName, category, summary, runtimeKind, runtimeStatus, health, presence, confidence, imageReference, projectId, projectName, backupStatus, primaryEndpointUrl, isFavorite, isHidden, componentCount, endpointCount, volumeCount, databaseCount, lastSeenAt] properties: id: type: string format: uuid sourceKey: type: string name: type: string description: Editorial display name if set, otherwise the detected name. detectedName: type: string category: type: string summary: type: string runtimeKind: type: string runtimeStatus: type: string health: type: string enum: [Healthy, Degraded, Unhealthy, Unknown] presence: type: string enum: [Present, Proposed, Missing] confidence: type: string enum: [Observed, Inferred, Declared, Unknown] imageReference: type: string projectId: type: [string, "null"] format: uuid projectName: type: [string, "null"] backupStatus: type: string primaryEndpointUrl: type: [string, "null"] isFavorite: type: boolean isHidden: type: boolean componentCount: type: integer endpointCount: type: integer volumeCount: type: integer databaseCount: type: integer lastSeenAt: type: string format: date-time InfrastructureServiceListResponse: type: object required: [items, nextCursor, totalCount] properties: items: type: array items: $ref: "#/components/schemas/InfrastructureServiceSummary" nextCursor: type: [string, "null"] totalCount: type: integer description: >- How many services match the current `search`/`status`/`projectId`/ `includeHidden` filters in total (OUX-4). Counted server-side before cursor pagination, so it is the size of the filtered catalog rather than of the returned page, and stays stable while paging through it. InfrastructureStorageRow: type: object required: [id, serviceId, serviceName, name, mountPath, storageClass, readOnly, sizeBytes, provenance] properties: id: type: string format: uuid serviceId: type: string format: uuid serviceName: type: string name: type: string mountPath: type: string storageClass: type: string readOnly: type: boolean sizeBytes: type: [integer, "null"] format: int64 provenance: $ref: "#/components/schemas/FactProvenance" InfrastructureStorageListResponse: type: object required: [items, nextCursor] properties: items: type: array items: $ref: "#/components/schemas/InfrastructureStorageRow" nextCursor: type: [string, "null"] EditorialProfileResponse: type: object required: [id, displayName, summary, category, purpose, ownerId, projectId, repositoryUrl, liveUrl, backupStatus, monitoringUrl, maintenanceNotes, knownRisks, updateProcedure, rollbackProcedure, tags, isFavorite, isHidden, isArchived, updatedAt, etag] properties: id: type: string format: uuid displayName: type: string summary: type: string category: type: string purpose: type: string ownerId: type: [string, "null"] format: uuid projectId: type: [string, "null"] format: uuid repositoryUrl: type: string liveUrl: type: string backupStatus: type: string monitoringUrl: type: string maintenanceNotes: type: string knownRisks: type: string updateProcedure: type: string rollbackProcedure: type: string tags: type: array items: type: string isFavorite: type: boolean isHidden: type: boolean isArchived: type: boolean updatedAt: type: string format: date-time etag: type: string ManualOverrideResponse: type: object required: [id, fieldName, value, reason, createdBy, updatedAt] properties: id: type: string format: uuid fieldName: type: string value: description: The overridden value — any JSON scalar, array or object, matching the field's ManualOverrideFieldCatalogEntry.valueKind. reason: type: string createdBy: type: string format: uuid updatedAt: type: string format: date-time ServiceComponentResponse: type: object required: [id, name, kind, runtimeId, status, imageReference, provenance] properties: id: type: string format: uuid name: type: string kind: type: string runtimeId: type: string status: type: string imageReference: type: string provenance: $ref: "#/components/schemas/FactProvenance" InfrastructureEndpointResponse: type: object required: [id, scheme, host, publishedPort, targetPort, url, isPublic, provenance] properties: id: type: string format: uuid scheme: type: string host: type: string publishedPort: type: [integer, "null"] targetPort: type: [integer, "null"] url: type: string isPublic: type: boolean provenance: $ref: "#/components/schemas/FactProvenance" InfrastructureNetworkResponse: type: object required: [id, name, driver, scope, internal, provenance] properties: id: type: string format: uuid name: type: string driver: type: string scope: type: string internal: type: boolean provenance: $ref: "#/components/schemas/FactProvenance" InfrastructureVolumeResponse: type: object required: [id, name, mountPath, storageClass, readOnly, sizeBytes, provenance] properties: id: type: string format: uuid name: type: string mountPath: type: string storageClass: type: string readOnly: type: boolean sizeBytes: type: [integer, "null"] format: int64 provenance: $ref: "#/components/schemas/FactProvenance" InfrastructureDatabaseResponse: type: object required: [id, name, engine, version, host, port, hostPortPublished, provenance] properties: id: type: string format: uuid name: type: string engine: type: string version: type: string host: type: string port: type: [integer, "null"] hostPortPublished: type: boolean provenance: $ref: "#/components/schemas/FactProvenance" ServiceDependencyResponse: type: object required: [id, dependsOnServiceId, dependsOnSourceKey, kind, provenance] properties: id: type: string format: uuid dependsOnServiceId: type: [string, "null"] format: uuid dependsOnSourceKey: type: string kind: type: string provenance: $ref: "#/components/schemas/FactProvenance" HealthObservationResponse: type: object required: [id, state, checkKind, responseTimeMilliseconds, provenance] properties: id: type: string format: uuid state: type: string enum: [Healthy, Degraded, Unhealthy, Unknown] checkKind: type: string responseTimeMilliseconds: type: [integer, "null"] provenance: $ref: "#/components/schemas/FactProvenance" DeploymentReferenceResponse: type: object required: [id, repositoryUrl, commit, imageReference, imageDigest, deployedAt, provenance] properties: id: type: string format: uuid repositoryUrl: type: string commit: type: string imageReference: type: string imageDigest: type: string deployedAt: type: [string, "null"] format: date-time provenance: $ref: "#/components/schemas/FactProvenance" BackupCoverageResponse: type: object required: [id, method, schedule, lastSuccessfulAt, restoreTested, provenance] properties: id: type: string format: uuid method: type: string schedule: type: string lastSuccessfulAt: type: [string, "null"] format: date-time restoreTested: type: boolean provenance: $ref: "#/components/schemas/FactProvenance" InfrastructureServiceDetail: type: object required: [service, editorialProfile, editorialETag, overrides, components, endpoints, networks, volumes, databases, dependencies, healthObservations, deployments, backups] properties: service: $ref: "#/components/schemas/InfrastructureServiceSummary" editorialProfile: type: [object, "null"] allOf: - $ref: "#/components/schemas/EditorialProfileResponse" editorialETag: type: string overrides: type: array items: $ref: "#/components/schemas/ManualOverrideResponse" components: type: array items: $ref: "#/components/schemas/ServiceComponentResponse" endpoints: type: array items: $ref: "#/components/schemas/InfrastructureEndpointResponse" networks: type: array items: $ref: "#/components/schemas/InfrastructureNetworkResponse" volumes: type: array items: $ref: "#/components/schemas/InfrastructureVolumeResponse" databases: type: array items: $ref: "#/components/schemas/InfrastructureDatabaseResponse" dependencies: type: array items: $ref: "#/components/schemas/ServiceDependencyResponse" healthObservations: type: array items: $ref: "#/components/schemas/HealthObservationResponse" deployments: type: array items: $ref: "#/components/schemas/DeploymentReferenceResponse" backups: type: array items: $ref: "#/components/schemas/BackupCoverageResponse" UpdateEditorialProfileRequest: type: object required: [isFavorite, isHidden, isArchived] description: Every field besides the three booleans is optional — omit a field to leave it unchanged. properties: displayName: type: [string, "null"] summary: type: [string, "null"] category: type: [string, "null"] purpose: type: [string, "null"] ownerId: type: [string, "null"] format: uuid projectId: type: [string, "null"] format: uuid repositoryUrl: type: [string, "null"] liveUrl: type: [string, "null"] backupStatus: type: [string, "null"] monitoringUrl: type: [string, "null"] maintenanceNotes: type: [string, "null"] knownRisks: type: [string, "null"] updateProcedure: type: [string, "null"] rollbackProcedure: type: [string, "null"] tags: type: [array, "null"] items: type: string isFavorite: type: boolean isHidden: type: boolean isArchived: type: boolean SetManualOverrideRequest: type: object required: [fieldName, value] properties: fieldName: type: string description: Must be one of the field names GET /infrastructure/overrides/catalog returns for the target's object kind. value: description: Any JSON scalar, array or object, validated server-side against the field's catalog entry (valueKind, maxStringLength, allowedEnumValues). reason: type: [string, "null"] SetInfrastructureOverrideRequest: type: object required: [targetKind, targetId, fieldName, value] properties: targetKind: type: string enum: [Host, Service, Component, Endpoint, Network, Volume, Database, Deployment, Backup] targetId: type: string format: uuid fieldName: type: string description: Must be one of the field names GET /infrastructure/overrides/catalog?targetKind=... returns for this targetKind. value: description: Any JSON scalar, array or object, validated server-side against the field's catalog entry (valueKind, maxStringLength, allowedEnumValues). reason: type: [string, "null"] ReviewInfrastructureServiceRequest: type: object required: [decision] properties: decision: type: string enum: [Accept, Reject] reason: type: [string, "null"] ReviewInfrastructureServiceResponse: type: object required: [serviceId, presence] properties: serviceId: type: string format: uuid presence: type: string enum: [Present, Missing] InventoryChangeResponse: type: object required: [id, snapshotId, targetKind, targetId, sourceKey, changeKind, changedFields, recordedAt] properties: id: type: string format: uuid snapshotId: type: string format: uuid targetKind: type: string targetId: type: string format: uuid sourceKey: type: string changeKind: type: string enum: [Added, Updated, Missing, Unchanged] changedFields: type: array items: type: string recordedAt: type: string format: date-time InventoryHostRequest: type: object required: [sourceKey, hostname, logicalCpuCount, totalMemoryBytes] properties: sourceKey: type: string hostname: type: string operatingSystem: type: [string, "null"] kernelVersion: type: [string, "null"] architecture: type: [string, "null"] logicalCpuCount: type: integer totalMemoryBytes: type: integer format: int64 InventoryFactConfidence: type: string enum: [Observed, Inferred, Declared, Unknown] InventoryComponentRequest: type: object required: [sourceKey, name, confidence] properties: sourceKey: type: string name: type: string kind: type: [string, "null"] runtimeId: type: [string, "null"] status: type: [string, "null"] imageReference: type: [string, "null"] confidence: $ref: "#/components/schemas/InventoryFactConfidence" InventoryEndpointRequest: type: object required: [sourceKey, isPublic, confidence] properties: sourceKey: type: string scheme: type: [string, "null"] host: type: [string, "null"] publishedPort: type: [integer, "null"] targetPort: type: [integer, "null"] url: type: [string, "null"] isPublic: type: boolean confidence: $ref: "#/components/schemas/InventoryFactConfidence" InventoryNetworkRequest: type: object required: [sourceKey, name, internal, confidence] properties: sourceKey: type: string name: type: string driver: type: [string, "null"] scope: type: [string, "null"] internal: type: boolean confidence: $ref: "#/components/schemas/InventoryFactConfidence" InventoryVolumeRequest: type: object required: [sourceKey, name, readOnly, confidence] properties: sourceKey: type: string name: type: string mountPath: type: [string, "null"] storageClass: type: [string, "null"] readOnly: type: boolean sizeBytes: type: [integer, "null"] format: int64 confidence: $ref: "#/components/schemas/InventoryFactConfidence" InventoryDatabaseRequest: type: object required: [sourceKey, name, hostPortPublished, confidence] properties: sourceKey: type: string name: type: string engine: type: [string, "null"] version: type: [string, "null"] host: type: [string, "null"] port: type: [integer, "null"] hostPortPublished: type: boolean confidence: $ref: "#/components/schemas/InventoryFactConfidence" InventoryDependencyRequest: type: object required: [sourceKey, dependsOnSourceKey, confidence] properties: sourceKey: type: string dependsOnSourceKey: type: string kind: type: [string, "null"] confidence: $ref: "#/components/schemas/InventoryFactConfidence" InventoryHealthRequest: type: object required: [sourceKey, state, confidence] properties: sourceKey: type: string state: type: string enum: [Healthy, Degraded, Unhealthy, Unknown] checkKind: type: [string, "null"] responseTimeMilliseconds: type: [integer, "null"] confidence: $ref: "#/components/schemas/InventoryFactConfidence" InventoryDeploymentRequest: type: object required: [sourceKey, confidence] properties: sourceKey: type: string repositoryUrl: type: [string, "null"] commit: type: [string, "null"] imageReference: type: [string, "null"] imageDigest: type: [string, "null"] deployedAt: type: [string, "null"] format: date-time confidence: $ref: "#/components/schemas/InventoryFactConfidence" InventoryBackupRequest: type: object required: [sourceKey, restoreTested, confidence] properties: sourceKey: type: string method: type: [string, "null"] schedule: type: [string, "null"] lastSuccessfulAt: type: [string, "null"] format: date-time restoreTested: type: boolean confidence: $ref: "#/components/schemas/InventoryFactConfidence" InventoryServiceRequest: type: object required: [sourceKey, detectedName, health, readOnlyRootFilesystem, noNewPrivileges, privileged, confidence] properties: sourceKey: type: string hostSourceKey: type: [string, "null"] detectedName: type: string runtimeKind: type: [string, "null"] runtimeStatus: type: [string, "null"] health: type: string enum: [Healthy, Degraded, Unhealthy, Unknown] imageReference: type: [string, "null"] imageDigest: type: [string, "null"] runtimeUser: type: [string, "null"] readOnlyRootFilesystem: type: boolean noNewPrivileges: type: boolean privileged: type: boolean confidence: $ref: "#/components/schemas/InventoryFactConfidence" components: type: [array, "null"] items: $ref: "#/components/schemas/InventoryComponentRequest" endpoints: type: [array, "null"] items: $ref: "#/components/schemas/InventoryEndpointRequest" networks: type: [array, "null"] items: $ref: "#/components/schemas/InventoryNetworkRequest" volumes: type: [array, "null"] items: $ref: "#/components/schemas/InventoryVolumeRequest" databases: type: [array, "null"] items: $ref: "#/components/schemas/InventoryDatabaseRequest" dependencies: type: [array, "null"] items: $ref: "#/components/schemas/InventoryDependencyRequest" healthObservations: type: [array, "null"] items: $ref: "#/components/schemas/InventoryHealthRequest" deployments: type: [array, "null"] items: $ref: "#/components/schemas/InventoryDeploymentRequest" backups: type: [array, "null"] items: $ref: "#/components/schemas/InventoryBackupRequest" SynchronizeInventoryRequest: type: object required: [sourceKey, sourceHash, capturedAt] properties: sourceKey: type: string sourceHash: type: string capturedAt: type: string format: date-time hosts: type: [array, "null"] items: $ref: "#/components/schemas/InventoryHostRequest" services: type: [array, "null"] items: $ref: "#/components/schemas/InventoryServiceRequest" InventorySyncResponse: type: object required: [snapshotId, alreadyImported, addedServices, updatedServices, missingServices, preservedEditorialProfiles, preservedOverrides] properties: snapshotId: type: string format: uuid alreadyImported: type: boolean addedServices: type: integer updatedServices: type: integer missingServices: type: integer preservedEditorialProfiles: type: integer preservedOverrides: type: integer CuratedServiceProfileRequest: type: object required: [sourceKey, reviewed, archived] properties: sourceKey: type: string displayName: type: [string, "null"] summary: type: [string, "null"] category: type: [string, "null"] purpose: type: [string, "null"] repositoryUrl: type: [string, "null"] liveUrl: type: [string, "null"] tags: type: [array, "null"] items: type: string reviewed: type: boolean archived: type: boolean ImportCuratedInventoryRequest: type: object required: [inventory] properties: inventory: $ref: "#/components/schemas/SynchronizeInventoryRequest" services: type: [array, "null"] items: $ref: "#/components/schemas/CuratedServiceProfileRequest" ImportCuratedInventoryResponse: type: object required: [inventory, createdEditorialProfiles, confirmedServices, archivedServices] properties: inventory: $ref: "#/components/schemas/InventorySyncResponse" createdEditorialProfiles: type: integer confirmedServices: type: integer archivedServices: type: integer ActivityItemResponse: type: object required: [id, actorKind, actorId, source, action, targetType, targetId, summary, occurredAt] properties: id: type: integer format: int64 actorKind: type: string enum: [System, User, ServiceAccount] actorId: type: [string, "null"] format: uuid source: type: string action: type: string targetType: type: [string, "null"] targetId: type: [string, "null"] summary: type: string occurredAt: type: string format: date-time ActivityListResponse: type: object required: [items] properties: items: type: array items: $ref: "#/components/schemas/ActivityItemResponse" WorkspaceMemberSummary: type: object required: [id, displayName, email] properties: id: type: string format: uuid displayName: type: string email: type: string WorkspaceMemberListResponse: type: object required: [items] properties: items: type: array items: $ref: "#/components/schemas/WorkspaceMemberSummary" PreviewMarkdownRequest: type: object required: [markdown] properties: markdown: type: string PreviewMarkdownResponse: type: object required: [renderedHtml] properties: renderedHtml: type: string description: Sanitized HTML — safe to render directly, matching the same pipeline a published page revision uses. StagingCleanupRunSummary: type: object required: [sessionsExpired, sessionsClaimed, sessionsLostRace, attachmentReferencesRemoved, blobsRemoved, warnings] properties: sessionsExpired: type: integer sessionsClaimed: type: integer sessionsLostRace: type: integer description: Expired sessions this run observed but a concurrent run claimed first — never lost, just processed elsewhere. attachmentReferencesRemoved: type: integer blobsRemoved: type: integer warnings: type: array items: type: string StagingCleanupStatusResponse: type: object required: [activeSessionCount, expiredPendingCleanupCount, runInProgress, lastRunStartedAt, lastRunCompletedAt, lastRunTrigger, lastResult] properties: activeSessionCount: type: integer expiredPendingCleanupCount: type: integer runInProgress: type: boolean lastRunStartedAt: type: [string, "null"] format: date-time lastRunCompletedAt: type: [string, "null"] format: date-time lastRunTrigger: type: [string, "null"] enum: [scheduled, manual, null] lastResult: type: [object, "null"] allOf: - $ref: "#/components/schemas/StagingCleanupRunSummary" RunStagingCleanupRequest: type: object required: [dryRun] properties: dryRun: type: boolean RunStagingCleanupResponse: type: object required: [dryRun, result] properties: dryRun: type: boolean result: $ref: "#/components/schemas/StagingCleanupRunSummary" PreflightImportRequest: type: object required: [sourceRelativePath, idempotencyKey] properties: sourceRelativePath: type: string description: Resolved beneath the operator-mounted KnowledgeHub:Paths:Imports directory — never an arbitrary filesystem path. idempotencyKey: type: string ImportPlanItemResponse: type: object required: [sourceIdentity, kind, proposedTitle, proposedParentSourceIdentity, conflict, conflictDetail] properties: sourceIdentity: type: string kind: type: string enum: [Markdown, SqliteRow, Asset, Unsupported] proposedTitle: type: string proposedParentSourceIdentity: type: [string, "null"] conflict: type: string enum: [NoConflict, ExactRerunDuplicate, SamePathDifferentContent, ExistingUserPage, AmbiguousLink, Unsupported] conflictDetail: type: [string, "null"] ImportPreflightResponse: type: object required: [status, jobId, adapterName, discoveredCount, plannedCount, items, warnings, blockers] properties: status: type: string enum: [Success, NoApplicableAdapter, AlreadyPreflighted] jobId: type: [string, "null"] format: uuid adapterName: type: [string, "null"] discoveredCount: type: integer plannedCount: type: integer items: type: array items: $ref: "#/components/schemas/ImportPlanItemResponse" warnings: type: array items: type: string blockers: type: array items: type: string ExecuteImportResponse: type: object required: [status, imported, skipped, failed, sourceUnchangedAfterExecution, diagnostics] properties: status: type: string enum: [Success, JobNotFound, NotPlanReady, SourceChanged, PlanChanged] imported: type: integer skipped: type: integer failed: type: integer sourceUnchangedAfterExecution: type: boolean description: A re-fingerprint comparison proving the source was never mutated during import (M4-A08). diagnostics: type: array items: type: string RollbackImportResponse: type: object required: [status, removedPages, preservedPages, removedAttachments] properties: status: type: string enum: [Success, JobNotFound, NothingToRollBack] removedPages: type: integer preservedPages: type: integer description: Pages this import created but a user has since edited — never deleted by rollback. removedAttachments: type: integer ImportJobSummaryResponse: type: object required: [id, adapterName, status, discoveredCount, plannedCount, importedCount, skippedCount, failedCount, createdAt, startedAt, completedAt] properties: id: type: string format: uuid adapterName: type: string status: type: string enum: [Discovered, PlanReady, Executing, Completed, Failed, RolledBack] discoveredCount: type: integer plannedCount: type: integer importedCount: type: integer skippedCount: type: integer failedCount: type: integer createdAt: type: string format: date-time startedAt: type: [string, "null"] format: date-time completedAt: type: [string, "null"] format: date-time SourceBatchPurgeRequest: type: object required: [batchId, source, pages] properties: batchId: type: string minLength: 1 source: type: string pattern: "^[a-z0-9-]{1,64}$" pages: type: array minItems: 1 items: $ref: "#/components/schemas/SourceBatchPurgePageEvidence" SourceBatchPurgePageEvidence: type: object required: [pageId, contentHash] properties: pageId: type: string format: uuid contentHash: type: string pattern: "^[a-fA-F0-9]{64}$" ExecuteSourceBatchPurgeRequest: allOf: - $ref: "#/components/schemas/SourceBatchPurgeRequest" - type: object required: [confirmationHash] properties: confirmationHash: type: string pattern: "^[a-f0-9]{64}$" SourceBatchPurgeDryRunResponse: type: object required: [safe, confirmationHash, pages, descendantPages, revisions, drafts, links, attachmentUsages, attachmentReferences, orphanedBlobs, sharedBlobs, importJobs, importItems, provenanceRecords, sourceTagAssignments, searchIndexRecords, generatedSourceDerivedPages, retainedPages, blockers] properties: safe: type: boolean confirmationHash: type: string pages: type: integer descendantPages: type: integer revisions: type: integer drafts: type: integer links: type: integer attachmentUsages: type: integer attachmentReferences: type: integer orphanedBlobs: type: integer sharedBlobs: type: integer importJobs: type: integer importItems: type: integer provenanceRecords: type: integer sourceTagAssignments: type: integer searchIndexRecords: type: integer generatedSourceDerivedPages: type: integer retainedPages: type: integer blockers: type: array items: type: string SourceBatchPurgeResponse: type: object required: [status, dryRun, removedPages, removedRevisions, removedDrafts, removedLinks, removedAttachmentUsages, removedAttachmentReferences, removedImportJobs, removedImportItems, removedProvenanceRecords, removedBlobs] properties: status: type: string enum: [purged] dryRun: $ref: "#/components/schemas/SourceBatchPurgeDryRunResponse" removedPages: type: integer removedRevisions: type: integer removedDrafts: type: integer removedLinks: type: integer removedAttachmentUsages: type: integer removedAttachmentReferences: type: integer removedImportJobs: type: integer removedImportItems: type: integer removedProvenanceRecords: type: integer removedBlobs: type: integer SearchResultItem: type: object required: [id, parentPageId, title, slug, documentType, documentStatus, tags, updatedAt, snippet] properties: id: type: string format: uuid parentPageId: type: [string, "null"] format: uuid title: type: string slug: type: string documentType: $ref: "#/components/schemas/DocumentType" documentStatus: $ref: "#/components/schemas/DocumentStatus" tags: type: array items: type: string updatedAt: type: string format: date-time snippet: type: string description: >- May contain the safe server-generated highlight markers `**` around matched terms. Never raw HTML — clients must escape this text before rendering and only then interpret the `**` markers. SearchResponse: type: object required: [items, nextCursor] properties: items: type: array items: $ref: "#/components/schemas/SearchResultItem" nextCursor: type: [string, "null"] UnifiedSearchResponse: type: object required: [items, counts, nextCursor] properties: items: type: array items: $ref: "#/components/schemas/UnifiedSearchResultItem" counts: $ref: "#/components/schemas/UnifiedSearchCounts" nextCursor: type: [string, "null"] UnifiedSearchCounts: type: object required: [knowledge, projects, services, runbooks, decisions, total] properties: knowledge: { type: integer, minimum: 0 } projects: { type: integer, minimum: 0 } services: { type: integer, minimum: 0 } runbooks: { type: integer, minimum: 0 } decisions: { type: integer, minimum: 0 } total: { type: integer, minimum: 0 } UnifiedSearchResultItem: type: object required: [kind, id, title, summary, snippet, updatedAt, href] properties: kind: type: string enum: [knowledge, projects, services, runbooks, decisions] id: { type: string, format: uuid } title: { type: string } summary: { type: string } snippet: { type: string } updatedAt: { type: string, format: date-time } href: { type: string, description: Same-origin relative route to the result. } RagPublicationManifestItem: type: object description: >- docs/32-rag-publication-convention.md. `markdown` and `checksum` are always `null` when `retired` is `true`. required: - contentId - documentType - documentStatus - title - slug - canonicalUrl - markdown - revision - checksum - locale - space - updatedAt - publishedAt - retired - retiredReason properties: contentId: type: string format: uuid documentType: $ref: "#/components/schemas/DocumentType" documentStatus: $ref: "#/components/schemas/DocumentStatus" title: type: string slug: type: string canonicalUrl: type: string markdown: type: [string, "null"] revision: type: integer format: int64 checksum: type: [string, "null"] description: Lowercase-hex SHA-256 of `markdown` (Page.ContentHash). Null when retired. locale: type: string description: >- The page's own `locale:` tag, or the configured `KnowledgeHub:PrimaryLocale` when it declares none. Never silently `en`. space: type: [string, "null"] description: The page's own `rag:space:` tag suffix, if any. updatedAt: type: string format: date-time publishedAt: type: [string, "null"] format: date-time retired: type: boolean retiredReason: type: [string, "null"] enum: [null, trashed, draft, needs-review, deprecated, archived] RagPublicationManifestResponse: type: object required: [items, nextCursor] properties: items: type: array items: $ref: "#/components/schemas/RagPublicationManifestItem" nextCursor: type: [string, "null"] ProjectStatus: type: string description: docs/15-project-hub.md lifecycle diagram. Archived is terminal in v1 (ADR-014 decision 6). enum: [Concept, Planned, Active, Blocked, Maintenance, Paused, Completed, Archived] ProjectPriority: type: string enum: [Low, Medium, High, Critical] ProjectDocumentRole: type: string enum: - Overview - CurrentState - Architecture - Deployment - Infrastructure - Security - Testing - Troubleshooting - Release - UserDocumentation - Other ProjectUpdateCategory: type: string enum: [Progress, Feature, Fix, Deployment, Validation, Release, Decision, Risk, Blocker, Documentation, Maintenance] ProjectUpdateVerificationStatus: type: string description: The UI must distinguish reported claims from validated evidence (docs/15-project-hub.md). enum: [Reported, Verified, Superseded] ProjectMilestoneState: type: string enum: [Planned, InProgress, Completed, Cancelled] DecisionStatus: type: string enum: [Proposed, Accepted, Rejected, Superseded, Withdrawn] RunbookRiskLevel: type: string enum: [Low, Medium, High, Critical] RunbookStatus: type: string enum: [Draft, Active, Deprecated] ProjectSummary: type: object required: [id, name, slug, summary, status, priority, ownerId, repositoryUrl, liveUrl, updatedAt, iconAttachmentId] properties: id: { type: string, format: uuid } name: { type: string } slug: { type: string } summary: { type: string } status: { $ref: "#/components/schemas/ProjectStatus" } priority: { $ref: "#/components/schemas/ProjectPriority" } ownerId: { type: string, format: uuid } repositoryUrl: { type: [string, "null"], format: uri } liveUrl: { type: [string, "null"], format: uri } updatedAt: { type: string, format: date-time } iconAttachmentId: { type: [string, "null"], format: uuid, description: "Same-origin attachment content is served at /api/v1/attachments/{iconAttachmentId}/content." } ProjectListResponse: type: object required: [items, nextCursor, totalCount] properties: items: type: array items: $ref: "#/components/schemas/ProjectSummary" nextCursor: type: [string, "null"] totalCount: type: integer minimum: 0 description: >- How many projects match the current filters in total (OUX-4), counted server-side before cursor pagination. Not a substitute for `/projects/totals`, which answers the unfiltered dashboard question. ProjectCatalogTotalsResponse: type: object required: [totalCount, activeCount] properties: totalCount: { type: integer, minimum: 0 } activeCount: { type: integer, minimum: 0 } ProjectDocumentSummary: type: object required: [id, pageId, pageTitle, pageSlug, pageDocumentType, role] properties: id: { type: string, format: uuid } pageId: { type: string, format: uuid } pageTitle: { type: string } pageSlug: { type: string } pageDocumentType: { $ref: "#/components/schemas/DocumentType" } role: { $ref: "#/components/schemas/ProjectDocumentRole" } ProjectUpdateSummary: type: object required: [id, category, title, summaryMarkdown, verificationStatus, actorId, occurredAt] properties: id: { type: string, format: uuid } category: { $ref: "#/components/schemas/ProjectUpdateCategory" } title: { type: string } summaryMarkdown: { type: string } verificationStatus: { $ref: "#/components/schemas/ProjectUpdateVerificationStatus" } actorId: { type: string, format: uuid } occurredAt: { type: string, format: date-time } commitSha: { type: [string, "null"] } releaseTag: { type: [string, "null"] } issueIdentifier: { type: [string, "null"] } url: { type: [string, "null"] } pageId: { type: [string, "null"], format: uuid } decisionRecordId: { type: [string, "null"], format: uuid } milestoneId: { type: [string, "null"], format: uuid } attachmentReferenceId: { type: [string, "null"], format: uuid } supersedesUpdateId: { type: [string, "null"], format: uuid } ProjectUpdatesResponse: type: object required: [items, nextCursor] properties: items: type: array items: $ref: "#/components/schemas/ProjectUpdateSummary" nextCursor: type: [string, "null"] ProjectMilestoneSummary: type: object required: [id, name, state, summary, successCriteria, eTag] properties: id: { type: string, format: uuid } name: { type: string } state: { $ref: "#/components/schemas/ProjectMilestoneState" } targetDate: { type: [string, "null"], format: date } completionDate: { type: [string, "null"], format: date } summary: { type: string } successCriteria: { type: string } eTag: type: string description: Also present as the response ETag header. ProjectDecisionSummary: type: object required: [id, decisionNumber, status, summary] properties: id: { type: string, format: uuid } decisionNumber: { type: integer } status: { $ref: "#/components/schemas/DecisionStatus" } summary: { type: string } ProjectDetail: type: object required: - id - name - slug - summary - status - priority - currentStateMarkdown - ownerId - technologies - documents - recentUpdates - milestones - decisions - createdAt - updatedAt - eTag - iconAttachmentId properties: id: { type: string, format: uuid } name: { type: string } slug: { type: string } summary: { type: string } status: { $ref: "#/components/schemas/ProjectStatus" } priority: { $ref: "#/components/schemas/ProjectPriority" } currentStateMarkdown: { type: string } ownerId: { type: string, format: uuid } startDate: { type: [string, "null"], format: date } targetDate: { type: [string, "null"], format: date } completionDate: { type: [string, "null"], format: date } repositoryUrl: { type: [string, "null"] } liveUrl: { type: [string, "null"] } documentationUrl: { type: [string, "null"] } issueTrackerUrl: { type: [string, "null"] } technologies: type: array items: { type: string } documents: type: array items: $ref: "#/components/schemas/ProjectDocumentSummary" recentUpdates: type: array items: $ref: "#/components/schemas/ProjectUpdateSummary" milestones: type: array items: $ref: "#/components/schemas/ProjectMilestoneSummary" decisions: type: array items: $ref: "#/components/schemas/ProjectDecisionSummary" createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time } eTag: type: string description: Also present as the response ETag header; included in the body so clients need not parse headers. iconAttachmentId: { type: [string, "null"], format: uuid } ProjectIconResponse: type: object required: [iconAttachmentId, eTag] properties: iconAttachmentId: { type: string, format: uuid } eTag: { type: string, description: Current project ETag after the icon change. } CreateProjectRequest: type: object required: [name, summary, priority, currentStateMarkdown, ownerId] properties: name: { type: string, minLength: 1, maxLength: 200 } summary: { type: string } priority: { $ref: "#/components/schemas/ProjectPriority" } currentStateMarkdown: { type: string } ownerId: { type: string, format: uuid } UpdateProjectRequest: type: object required: [name, summary, priority, currentStateMarkdown, ownerId] properties: name: { type: string, minLength: 1, maxLength: 200 } summary: { type: string } priority: { $ref: "#/components/schemas/ProjectPriority" } currentStateMarkdown: { type: string } ownerId: { type: string, format: uuid } startDate: { type: [string, "null"], format: date } targetDate: { type: [string, "null"], format: date } completionDate: { type: [string, "null"], format: date } repositoryUrl: { type: [string, "null"] } liveUrl: { type: [string, "null"] } documentationUrl: { type: [string, "null"] } issueTrackerUrl: { type: [string, "null"] } technologies: type: [array, "null"] items: { type: string } LinkProjectDocumentRequest: type: object required: [pageId, role] properties: pageId: { type: string, format: uuid } role: { $ref: "#/components/schemas/ProjectDocumentRole" } CreateProjectUpdateRequest: type: object required: [category, title, summaryMarkdown, verificationStatus] properties: category: { $ref: "#/components/schemas/ProjectUpdateCategory" } title: { type: string, maxLength: 200 } summaryMarkdown: { type: string } verificationStatus: { $ref: "#/components/schemas/ProjectUpdateVerificationStatus" } occurredAt: { type: [string, "null"], format: date-time } commitSha: { type: [string, "null"] } releaseTag: { type: [string, "null"] } issueIdentifier: { type: [string, "null"] } url: { type: [string, "null"] } pageId: { type: [string, "null"], format: uuid } decisionRecordId: { type: [string, "null"], format: uuid } milestoneId: { type: [string, "null"], format: uuid } attachmentReferenceId: { type: [string, "null"], format: uuid } supersedesUpdateId: { type: [string, "null"], format: uuid } CreateProjectMilestoneRequest: type: object required: [name, summary, successCriteria] properties: name: { type: string, minLength: 1 } targetDate: { type: [string, "null"], format: date } summary: { type: string } successCriteria: { type: string } UpdateProjectMilestoneRequest: type: object required: [name, summary, successCriteria, state] properties: name: { type: string, minLength: 1 } targetDate: { type: [string, "null"], format: date } summary: { type: string } successCriteria: { type: string } state: { $ref: "#/components/schemas/ProjectMilestoneState" } DecisionLineageEntry: type: object required: [id, decisionNumber, status, summary] properties: id: { type: string, format: uuid } decisionNumber: { type: integer } status: { $ref: "#/components/schemas/DecisionStatus" } summary: { type: string } DecisionDetail: type: object required: - id - workspaceId - pageId - decisionNumber - status - decisionDate - title - markdown - renderedHtml - summary - createdAt - updatedAt - eTag properties: id: { type: string, format: uuid } workspaceId: { type: string, format: uuid } projectId: { type: [string, "null"], format: uuid } pageId: { type: string, format: uuid } decisionNumber: { type: integer } status: { $ref: "#/components/schemas/DecisionStatus" } decisionDate: { type: string, format: date } title: { type: string } markdown: { type: string } renderedHtml: type: string description: Server-sanitized HTML — the same rendering pipeline as an ordinary Page. summary: { type: string } supersedes: oneOf: - $ref: "#/components/schemas/DecisionLineageEntry" - type: "null" supersededBy: oneOf: - $ref: "#/components/schemas/DecisionLineageEntry" - type: "null" createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time } eTag: { type: string } DecisionSummary: type: object required: [id, decisionNumber, status, title, decisionDate, summary, updatedAt] properties: id: { type: string, format: uuid } projectId: { type: [string, "null"], format: uuid } decisionNumber: { type: integer } status: { $ref: "#/components/schemas/DecisionStatus" } title: { type: string } decisionDate: { type: string, format: date } summary: { type: string } updatedAt: { type: string, format: date-time } DecisionListResponse: type: object required: [items, nextCursor] properties: items: type: array items: $ref: "#/components/schemas/DecisionSummary" nextCursor: type: [string, "null"] CreateDecisionRequest: type: object required: [title, summary] properties: parentPageId: { type: [string, "null"], format: uuid } projectId: { type: [string, "null"], format: uuid } title: { type: string, minLength: 1 } markdown: { type: [string, "null"] } summary: { type: string } supersedesDecisionId: { type: [string, "null"], format: uuid } RunbookDetail: type: object required: - id - pageId - owner - riskLevel - status - requiresMaintenanceWindow - isStale - title - markdown - renderedHtml - currentRevisionNumber - createdAt - updatedAt - eTag properties: id: { type: string, format: uuid } pageId: { type: string, format: uuid } owner: { type: string, format: uuid } riskLevel: { $ref: "#/components/schemas/RunbookRiskLevel" } status: { $ref: "#/components/schemas/RunbookStatus" } requiresMaintenanceWindow: { type: boolean } estimatedDurationMinutes: { type: [integer, "null"] } reviewDueAt: { type: [string, "null"], format: date-time } lastValidatedAt: { type: [string, "null"], format: date-time } validatedBy: { type: [string, "null"], format: uuid } isStale: type: boolean description: True whenever the linked page has a newer revision than the last validation, or was never validated. title: { type: string } markdown: { type: string } renderedHtml: type: string description: Server-sanitized HTML — the same rendering pipeline as an ordinary Page. Never executed. currentRevisionNumber: { type: integer, format: int64 } createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time } eTag: { type: string } RunbookSummary: type: object required: [id, pageId, title, riskLevel, status, isStale, updatedAt] properties: id: { type: string, format: uuid } pageId: { type: string, format: uuid } title: { type: string } riskLevel: { $ref: "#/components/schemas/RunbookRiskLevel" } status: { $ref: "#/components/schemas/RunbookStatus" } isStale: { type: boolean } lastValidatedAt: { type: [string, "null"], format: date-time } updatedAt: { type: string, format: date-time } RunbookListResponse: type: object required: [items, nextCursor] properties: items: type: array items: $ref: "#/components/schemas/RunbookSummary" nextCursor: type: [string, "null"] CreateRunbookRequest: type: object required: [title, riskLevel] properties: parentPageId: { type: [string, "null"], format: uuid } title: { type: string, minLength: 1 } markdown: { type: [string, "null"] } templateKey: { type: [string, "null"] } riskLevel: { $ref: "#/components/schemas/RunbookRiskLevel" } requiresMaintenanceWindow: { type: boolean } estimatedDurationMinutes: { type: [integer, "null"] } UpdateRunbookProfileRequest: type: object required: [owner, riskLevel, status, requiresMaintenanceWindow] properties: owner: { type: string, format: uuid } riskLevel: { $ref: "#/components/schemas/RunbookRiskLevel" } status: { $ref: "#/components/schemas/RunbookStatus" } requiresMaintenanceWindow: { type: boolean } estimatedDurationMinutes: { type: [integer, "null"] } reviewDueAt: { type: [string, "null"], format: date-time } CreateServiceAccountRequest: type: object required: [name, description, scopes] properties: name: type: string minLength: 1 maxLength: 200 description: type: string maxLength: 1000 scopes: type: array minItems: 1 uniqueItems: true items: type: string description: "One of the known scope constants, e.g. pages:read, pages:write, projects:read, runbooks:write (see docs/09-api-contract.md)." IssueServiceAccountTokenRequest: type: object required: [scopes] properties: scopes: type: array minItems: 1 uniqueItems: true items: type: string CreatedServiceAccountToken: type: object description: The raw token is only ever present in this one response — it is never persisted in the clear and is not retrievable again. required: [serviceAccountId, tokenId, rawToken] properties: serviceAccountId: type: string format: uuid tokenId: type: string format: uuid rawToken: type: string description: "Format ikh__. Present exactly once." ServiceAccountTokenSummary: type: object required: [id, publicId, scopes, createdAt] properties: id: type: string format: uuid publicId: type: string description: Safe, non-secret identifier — never the raw secret. scopes: type: array items: type: string createdAt: type: string format: date-time lastUsedAt: type: [string, "null"] format: date-time expiresAt: type: [string, "null"] format: date-time revokedAt: type: [string, "null"] format: date-time ServiceAccountSummary: type: object required: [id, name, description, enabled, createdAt, tokens] properties: id: type: string format: uuid name: type: string description: type: string enabled: type: boolean createdAt: type: string format: date-time tokens: type: array items: $ref: "#/components/schemas/ServiceAccountTokenSummary" ServiceAccountListResponse: type: object required: [items] properties: items: type: array items: $ref: "#/components/schemas/ServiceAccountSummary" CitationResponse: type: object description: A stable, citable pointer to a document or decision included in a project context bundle. required: [kind, id, title, canonicalPath, updatedAt] properties: kind: type: string enum: [Page, DecisionRecord, RunbookProfile] id: type: string format: uuid revision: type: [integer, "null"] format: int64 title: type: string canonicalPath: type: string updatedAt: type: string format: date-time ProjectContextProjectSummary: type: object required: [id, name, status, summary, currentStateMarkdown, updatedAt] properties: id: type: string format: uuid name: type: string status: type: string summary: type: string currentStateMarkdown: type: string updatedAt: type: string format: date-time ProjectContextTruncation: type: object required: [truncated, omittedCounts] properties: truncated: type: boolean omittedCounts: type: object additionalProperties: type: integer description: "Counts of items omitted per collection (e.g. updates, decisions), keyed by collection name." ProjectContextResponse: type: object description: "A bounded, permission-filtered bundle of the current project state, suitable for a single-call context fetch (docs/09-api-contract.md 'Project context', M6-A04). Unlike the unbounded project overview, this response always reports what was truncated rather than growing unbounded." required: [project, recentUpdates, activeMilestones, decisions, runbooks, documents, citations, truncation] properties: project: $ref: "#/components/schemas/ProjectContextProjectSummary" recentUpdates: type: array items: $ref: "#/components/schemas/ProjectUpdateSummary" activeMilestones: type: array items: $ref: "#/components/schemas/ProjectMilestoneSummary" decisions: type: array items: $ref: "#/components/schemas/ProjectDecisionSummary" runbooks: type: array items: $ref: "#/components/schemas/ProjectDocumentSummary" documents: type: array items: $ref: "#/components/schemas/ProjectDocumentSummary" citations: type: array items: $ref: "#/components/schemas/CitationResponse" truncation: $ref: "#/components/schemas/ProjectContextTruncation" BackupSummaryResponse: type: object description: Never includes a filesystem path, connection string or other private detail. required: [backupId, createdAt, totalSizeBytes, tableCount, totalRowCount, attachmentCount, schemaVersion, productVersion] properties: backupId: type: string createdAt: type: string format: date-time totalSizeBytes: type: integer format: int64 tableCount: type: integer totalRowCount: type: integer format: int64 attachmentCount: type: integer schemaVersion: type: string productVersion: type: string BackupListResponse: type: object required: [items] properties: items: type: array items: $ref: "#/components/schemas/BackupSummaryResponse" ProblemDetails: type: object required: [type, title, status, correlationId] properties: type: type: string format: uri-reference title: type: string status: type: integer detail: type: string instance: type: string format: uri-reference correlationId: type: string errors: type: object additionalProperties: type: array items: type: string SetupStatus: type: object required: [setupRequired] properties: setupRequired: type: boolean CompleteSetupRequest: type: object required: [workspaceName, displayName, email, password] properties: workspaceName: type: string minLength: 1 maxLength: 200 displayName: type: string minLength: 1 maxLength: 200 email: type: string format: email password: type: string minLength: 12 CompleteSetupResponse: type: object required: [workspaceId, userId] properties: workspaceId: type: string format: uuid userId: type: string format: uuid CsrfToken: type: object required: [token, headerName] properties: token: type: string headerName: type: string WorkspaceRole: type: string enum: [Reader, Editor, Administrator] SessionResponse: type: object required: [authenticated, setupRequired] properties: authenticated: type: boolean setupRequired: type: boolean userId: type: [string, "null"] format: uuid displayName: type: [string, "null"] email: type: [string, "null"] workspaceId: type: [string, "null"] format: uuid role: oneOf: - $ref: "#/components/schemas/WorkspaceRole" - type: "null" LoginRequest: type: object required: [email, password] properties: email: type: string format: email password: type: string RecoveryRedeemRequest: type: object required: [token, newPassword] properties: token: type: string newPassword: type: string minLength: 12 UserSummary: type: object required: [id, displayName, email, role, enabled, createdAt] properties: id: type: string format: uuid displayName: type: string email: type: string role: $ref: "#/components/schemas/WorkspaceRole" enabled: type: boolean createdAt: type: string format: date-time lastLoginAt: type: [string, "null"] format: date-time CreateUserRequest: type: object required: [displayName, email, password, role] properties: displayName: type: string minLength: 1 maxLength: 200 email: type: string format: email password: type: string minLength: 12 role: $ref: "#/components/schemas/WorkspaceRole" UpdateUserRequest: type: object properties: role: oneOf: - $ref: "#/components/schemas/WorkspaceRole" - type: "null" enabled: type: [boolean, "null"] IssueRecoveryTokenResponse: type: object required: [token, expiresAt] properties: token: type: string description: Shown once, at issuance. Never retrievable again. expiresAt: type: string format: date-time