{
  "openapi": "3.2.0",
  "info": {
    "title": "Vulcan API",
    "version": "2.4.1",
    "description": "REST API for Vulcan — STIG-ready security guidance authoring platform.\nAll endpoints require session-based authentication via cookie unless noted otherwise.\nMutation endpoints return a canonical `{toast: {title, message, variant}}` object.\n",
    "contact": {
      "name": "MITRE SAF Team",
      "url": "https://github.com/mitre/vulcan"
    },
    "license": {
      "name": "Apache-2.0",
      "url": "https://www.apache.org/licenses/LICENSE-2.0"
    }
  },
  "servers": [
    {
      "url": "/",
      "description": "Same-origin (session cookie auth)"
    }
  ],
  "security": [
    {
      "cookieAuth": []
    },
    {
      "tokenAuth": []
    }
  ],
  "tags": [
    {
      "name": "Projects",
      "description": "Project CRUD, export, import, and member management"
    },
    {
      "name": "Components",
      "description": "Component CRUD, spreadsheet import, export, and locking"
    },
    {
      "name": "Rules",
      "description": "Rule CRUD, revert, section locks, and satisfaction relationships"
    },
    {
      "name": "Reviews",
      "description": "Review lifecycle — create, triage, adjudicate, withdraw, admin actions"
    },
    {
      "name": "Reactions",
      "description": "Thumbs up/down reactions on comment reviews"
    },
    {
      "name": "Memberships",
      "description": "Project and component membership management"
    },
    {
      "name": "Search",
      "description": "Global search across projects, components, rules, SRGs, and STIGs"
    },
    {
      "name": "Users",
      "description": "User management and admin operations"
    },
    {
      "name": "Benchmarks",
      "description": "SRG and STIG upload, listing, export, and deletion"
    },
    {
      "name": "Auth",
      "description": "Session authentication — login, logout, current user identity"
    },
    {
      "name": "Personal Access Tokens",
      "description": "Personal Access Token CRUD and admin revocation"
    },
    {
      "name": "System",
      "description": "Version and health check endpoints"
    }
  ],
  "paths": {
    "/api/auth/me": {
      "get": {
        "operationId": "getAuthMe",
        "tags": [
          "Auth"
        ],
        "summary": "Current authenticated user identity",
        "description": "Returns the authenticated user's identity, admin status, and provider. Used by the SPA on every page load to determine auth state, populate the navbar, and guard routes. Returns 401 when not authenticated.",
        "responses": {
          "200": {
            "description": "Authenticated user identity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CurrentUserResponse"
                },
                "examples": {
                  "regular_user": {
                    "summary": "Non-admin local user",
                    "value": {
                      "id": 42,
                      "name": "Jane Doe",
                      "email": "jane@example.com",
                      "admin": false,
                      "provider": null
                    }
                  },
                  "admin_user": {
                    "summary": "Admin user via OIDC",
                    "value": {
                      "id": 1,
                      "name": "Admin",
                      "email": "admin@example.com",
                      "admin": true,
                      "provider": "oidc"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/api/auth/login": {
      "post": {
        "operationId": "postAuthLogin",
        "tags": [
          "Auth"
        ],
        "security": [],
        "summary": "Authenticate with email and password",
        "description": "Creates a session for local (email/password) authentication. Returns the authenticated user identity on success. Sets a session cookie for subsequent requests. OIDC and LDAP providers use their own OAuth callback flows.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "password"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "User's login email address.",
                    "example": "jane@example.com"
                  },
                  "password": {
                    "type": "string",
                    "format": "password",
                    "description": "User's password.",
                    "example": "••••••••••••••••"
                  }
                }
              },
              "examples": {
                "login": {
                  "summary": "Local login",
                  "value": {
                    "email": "jane@example.com",
                    "password": "S3cure!#Pass001"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Authentication successful — session created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CurrentUserResponse"
                },
                "examples": {
                  "success": {
                    "summary": "Successful login",
                    "value": {
                      "id": 42,
                      "name": "Jane Doe",
                      "email": "jane@example.com",
                      "admin": false,
                      "provider": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid email or password, or the account is temporarily locked after too many failed attempts (RFC 9457 problem details).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "type",
                    "title",
                    "status",
                    "detail"
                  ],
                  "properties": {
                    "type": {
                      "type": "string",
                      "description": "Stable machine identifier for the error class.",
                      "example": "/docs/api/errors#invalid_credentials"
                    },
                    "title": {
                      "type": "string",
                      "description": "Short human summary of the error class.",
                      "example": "Invalid credentials"
                    },
                    "status": {
                      "type": "integer",
                      "description": "HTTP status code, repeated in the body.",
                      "example": 401
                    },
                    "detail": {
                      "type": "string",
                      "description": "Occurrence-specific human explanation.",
                      "example": "The email or password is incorrect."
                    }
                  }
                },
                "examples": {
                  "invalid_credentials": {
                    "summary": "Bad credentials",
                    "value": {
                      "type": "/docs/api/errors#invalid_credentials",
                      "title": "Invalid credentials",
                      "status": 401,
                      "detail": "The email or password is incorrect."
                    }
                  },
                  "account_locked": {
                    "summary": "Account locked after too many failed attempts",
                    "value": {
                      "type": "/docs/api/errors#account_locked",
                      "title": "Account locked",
                      "status": 401,
                      "detail": "This account is temporarily locked due to too many failed sign-in attempts. Please try again later or reset your password."
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/api/auth/logout": {
      "delete": {
        "operationId": "deleteAuthLogout",
        "tags": [
          "Auth"
        ],
        "summary": "Sign out and destroy session",
        "description": "Destroys the current session. Subsequent requests require re-authentication. Returns a confirmation message.",
        "responses": {
          "200": {
            "description": "Session destroyed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message"
                  ],
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "Confirmation message.",
                      "example": "Signed out successfully"
                    }
                  }
                },
                "examples": {
                  "success": {
                    "summary": "Signed out",
                    "value": {
                      "message": "Signed out successfully"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/api/navigation": {
      "get": {
        "operationId": "getNavigation",
        "tags": [
          "System"
        ],
        "summary": "App shell navigation data",
        "description": "Returns navbar links, pending access request notifications, and locked user alerts for the authenticated user. Used by the SPA app shell on every page load after authentication.",
        "responses": {
          "200": {
            "description": "Navigation data for app shell",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NavigationResponse"
                },
                "examples": {
                  "admin": {
                    "summary": "Admin user with pending requests",
                    "value": {
                      "nav_links": [
                        {
                          "icon": "folder2-open",
                          "name": "Projects",
                          "link": "/projects"
                        },
                        {
                          "icon": "patch-check-fill",
                          "name": "Released Components",
                          "link": "/components"
                        }
                      ],
                      "access_requests": [
                        {
                          "id": 1,
                          "user": {
                            "id": 42,
                            "name": "Jane Doe",
                            "email": "jane@example.com"
                          },
                          "project": {
                            "id": 7,
                            "name": "RHEL 9 STIG"
                          }
                        }
                      ],
                      "locked_users": [
                        {
                          "id": 99,
                          "name": "Locked User",
                          "email": "locked@example.com"
                        }
                      ]
                    }
                  },
                  "regular": {
                    "summary": "Non-admin user",
                    "value": {
                      "nav_links": [
                        {
                          "icon": "folder2-open",
                          "name": "Projects",
                          "link": "/projects"
                        }
                      ],
                      "access_requests": [],
                      "locked_users": []
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/api/settings": {
      "get": {
        "operationId": "getSettings",
        "tags": [
          "System"
        ],
        "security": [],
        "summary": "Public pre-auth UI configuration",
        "description": "Returns application settings needed before authentication: banner, consent modal, auth provider flags, password policy, and registration status. No authentication required — the login page and consent banner need this data before the user signs in.",
        "responses": {
          "200": {
            "description": "Public settings for SPA pre-auth UI",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SettingsResponse"
                },
                "examples": {
                  "default": {
                    "summary": "Typical deployment",
                    "value": {
                      "banner": {
                        "enabled": true,
                        "text": "UNCLASSIFIED",
                        "background_color": "#007a33",
                        "text_color": "#ffffff"
                      },
                      "consent": {
                        "enabled": false,
                        "version": 1,
                        "title": "Terms of Use",
                        "content": "",
                        "ttl": 0
                      },
                      "local_login": {
                        "enabled": true
                      },
                      "user_registration": {
                        "enabled": true
                      },
                      "ldap": {
                        "enabled": false,
                        "title": null
                      },
                      "oidc": {
                        "enabled": false,
                        "title": null
                      },
                      "smtp": {
                        "enabled": false
                      },
                      "password": {
                        "min_length": 15,
                        "min_uppercase": 2,
                        "min_lowercase": 2,
                        "min_number": 2,
                        "min_special": 2
                      },
                      "lockout": {
                        "enabled": true,
                        "maximum_attempts": 3,
                        "last_attempt_warning": true
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/api/version": {
      "get": {
        "operationId": "getVersion",
        "tags": [
          "System"
        ],
        "security": [],
        "summary": "Application version and metadata",
        "description": "Returns the application name, version, Rails version, Ruby version, and environment. No authentication required — used by monitoring tools, deployment verification scripts, and the frontend health check.",
        "responses": {
          "200": {
            "description": "Version and runtime metadata",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VersionResponse"
                },
                "examples": {
                  "production": {
                    "summary": "Typical production response",
                    "value": {
                      "name": "Vulcan",
                      "version": "2.4.1",
                      "rails": "8.1.3.1",
                      "ruby": "3.4.10",
                      "environment": "production"
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/api/search/global": {
      "get": {
        "operationId": "globalSearch",
        "tags": [
          "Search"
        ],
        "summary": "Search across all resource types",
        "description": "Searches projects, components, rules, SRGs, and STIGs by name or title. Results are grouped by type and limited per group. Project results cover everything the caller can discover (memberships plus discoverable projects); component and rule content is served only from the caller's memberships and released components; SRG/STIG results and the srg_rules requirement catalog are instance-global. The rules group covers both document kinds — stig rules and authored SRG requirements — and can be scoped to a specific component via the component_id parameter. Requires authentication — returns 401 if not signed in.",
        "parameters": [
          {
            "$ref": "#/components/parameters/SearchQuery"
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum results per resource type.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 20,
              "default": 5
            },
            "example": 5
          },
          {
            "name": "component_id",
            "in": "query",
            "description": "Scope rule search to a specific component.",
            "schema": {
              "type": "integer"
            },
            "example": 29
          }
        ],
        "responses": {
          "200": {
            "description": "Search results grouped by type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GlobalSearchResponse"
                },
                "examples": {
                  "results": {
                    "summary": "Search for \"container\"",
                    "value": {
                      "projects": [
                        {
                          "id": 4,
                          "name": "Container Platform"
                        }
                      ],
                      "components": [
                        {
                          "id": 29,
                          "name": "Container SRG"
                        }
                      ],
                      "rules": [],
                      "stig_rules": [],
                      "srg_rules": [],
                      "srgs": [
                        {
                          "id": 1,
                          "title": "Container Platform Security Requirements Guide"
                        }
                      ],
                      "stigs": []
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/api/users/search": {
      "get": {
        "operationId": "searchUsers",
        "tags": [
          "Search"
        ],
        "summary": "Search users by name or email",
        "description": "Searches user accounts by name or email address. Used by the membership assignment UI to find users to add to projects. Requires authentication.",
        "parameters": [
          {
            "$ref": "#/components/parameters/SearchQuery"
          },
          {
            "name": "membership_type",
            "in": "query",
            "required": true,
            "description": "Type of resource to search members for.",
            "schema": {
              "type": "string",
              "enum": [
                "Project",
                "Component"
              ]
            },
            "example": "Project"
          },
          {
            "name": "membership_id",
            "in": "query",
            "required": true,
            "description": "ID of the project or component to search members for.",
            "schema": {
              "type": "integer"
            },
            "example": 1
          },
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "description": "Search scope. Default searches non-members (for \"add member\" flow, admin only). \"members\" searches existing members (for PoC selection, any member).",
            "schema": {
              "type": "string",
              "enum": [
                "members"
              ]
            },
            "example": "members"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results (1-25, default 10).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 25
            },
            "example": 10
          }
        ],
        "responses": {
          "200": {
            "description": "Matching users",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "users"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "users": {
                      "type": "array",
                      "description": "Matching users (id, name, email only — minimal shape for dropdowns).",
                      "items": {
                        "type": "object",
                        "additionalProperties": false,
                        "required": [
                          "id",
                          "name",
                          "email"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer",
                            "description": "User ID.",
                            "example": 42
                          },
                          "name": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Display name.",
                            "example": "Jane Doe"
                          },
                          "email": {
                            "type": "string",
                            "format": "email",
                            "description": "Email address.",
                            "example": "jane.doe@example.org"
                          }
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "results": {
                    "summary": "Search for \"jane\"",
                    "value": {
                      "users": [
                        {
                          "id": 42,
                          "name": "Jane Doe",
                          "email": "jane.doe@example.org"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/api/projects": {
      "get": {
        "operationId": "listApiProjects",
        "tags": [
          "Projects"
        ],
        "summary": "List projects with pagination, search, and sorting",
        "description": "Paginated project listing for SPA consumption. Supports substring search over name and description, whitelist-validated sorting, and pagy-backed pagination (default 25 per page, maximum 100). Unknown sort fields are silently ignored. Requires authentication. Returns 400 when the requested page is out of range. Timestamps on this endpoint are ISO 8601 (render_as_json path), unlike the string-rendered project pages.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Case-insensitive substring filter matched against project name and description.",
            "schema": {
              "type": "string"
            },
            "example": "Photon"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort field. Unknown fields are silently ignored.",
            "schema": {
              "type": "string",
              "enum": [
                "name",
                "created_at",
                "updated_at"
              ]
            },
            "example": "name"
          },
          {
            "name": "order",
            "in": "query",
            "required": false,
            "description": "Sort direction. Defaults to asc when sort is applied.",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ]
            },
            "example": "asc"
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number (1-based). Out-of-range pages return 400.",
            "schema": {
              "type": "integer"
            },
            "example": 1
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Records per page (default 25, capped at 100).",
            "schema": {
              "type": "integer"
            },
            "example": 25
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated project rows",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "rows",
                    "pagination"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "rows": {
                      "type": "array",
                      "description": "Projects on the current page.",
                      "items": {
                        "$ref": "#/components/schemas/ProjectSummary"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/PaginationMeta"
                    }
                  }
                },
                "examples": {
                  "first_page": {
                    "summary": "First page of projects",
                    "value": {
                      "rows": [
                        {
                          "id": 34,
                          "name": "Photon 3",
                          "description": null,
                          "visibility": "discoverable",
                          "memberships_count": 2,
                          "admin_name": "Demo Admin",
                          "admin_email": "admin@example.org",
                          "created_at": "2026-05-30T14:06:03.797Z",
                          "updated_at": "2026-05-30T14:06:28.292Z"
                        }
                      ],
                      "pagination": {
                        "page": 1,
                        "per_page": 25,
                        "total": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Requested page is out of range (RFC 9457 problem details)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "type",
                    "title",
                    "status",
                    "detail"
                  ],
                  "properties": {
                    "type": {
                      "type": "string",
                      "description": "Stable machine identifier for the error class.",
                      "example": "/docs/api/errors#page_out_of_range"
                    },
                    "title": {
                      "type": "string",
                      "description": "Short human summary of the error class.",
                      "example": "Page out of range"
                    },
                    "status": {
                      "type": "integer",
                      "description": "HTTP status code, repeated in the body.",
                      "example": 400
                    },
                    "detail": {
                      "type": "string",
                      "description": "Error message naming the valid page range.",
                      "example": "Page 9999 is out of range (1..2)"
                    }
                  }
                },
                "examples": {
                  "page_out_of_range": {
                    "summary": "Page beyond the last available page",
                    "value": {
                      "type": "/docs/api/errors#page_out_of_range",
                      "title": "Page out of range",
                      "status": 400,
                      "detail": "Page 9999 is out of range (1..2)"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/srgs/latest": {
      "get": {
        "operationId": "latestSrgs",
        "tags": [
          "Benchmarks"
        ],
        "summary": "List the latest release of each SRG",
        "description": "Returns one row per SRG — its numerically highest V{major}R{minor} release — for dropdown population. Public reference data, no authentication required. The q parameter filters SRGs by case-insensitive substring with abbreviation expansion (GPOS matches General Purpose Operating System); queries shorter than 2 characters return no rows.",
        "security": [],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "SRG filter — case-insensitive substring matched against name, title, and srg_id, with abbreviation expansion (minimum 2 characters).",
            "schema": {
              "type": "string"
            },
            "example": "GPOS"
          }
        ],
        "responses": {
          "200": {
            "description": "Latest release of each SRG, ordered by title",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BenchmarkLatestResponse"
                },
                "examples": {
                  "latest": {
                    "summary": "Latest SRGs",
                    "value": {
                      "rows": [
                        {
                          "id": 42,
                          "srg_id": "General_Purpose_Operating_System",
                          "title": "General Purpose Operating System Security Requirements Guide",
                          "version": "V3R3",
                          "name": "General Purpose Operating System - Ver 3, Rel 3"
                        },
                        {
                          "id": 43,
                          "srg_id": "Web_Server_SRG",
                          "title": "Web Server Security Requirements Guide",
                          "version": "V4R4",
                          "name": "Web Server SRG - Ver 4, Rel 4"
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/srgs/{id}/stats": {
      "get": {
        "operationId": "srgStats",
        "tags": [
          "Benchmarks"
        ],
        "summary": "Get rule counts and component usage for an SRG",
        "description": "Returns the SRG's rule count, severity breakdown, and which components are based on it. Usage is scoped to components the caller can see (member projects or released) — the count and list use the same scope, so hidden usage is never revealed. Requires authentication.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric ID of the SRG.",
            "schema": {
              "type": "integer"
            },
            "example": 3
          }
        ],
        "responses": {
          "200": {
            "description": "SRG stats with caller-scoped usage",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BenchmarkStatsResponse"
                },
                "examples": {
                  "stats": {
                    "summary": "SRG stats",
                    "value": {
                      "rule_count": 250,
                      "severity_counts": {
                        "high": 30,
                        "medium": 200,
                        "low": 20
                      },
                      "usage": {
                        "count": 2,
                        "components": [
                          {
                            "id": 38,
                            "name": "RHEL 9 Hardened Baseline",
                            "project_id": 7,
                            "project_name": "RHEL Hardening"
                          },
                          {
                            "id": 41,
                            "name": "Photon OS 5 Baseline",
                            "project_id": 9,
                            "project_name": "Photon OS 5 Hardening"
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/stigs/latest": {
      "get": {
        "operationId": "latestStigs",
        "tags": [
          "Benchmarks"
        ],
        "summary": "List the latest release of each STIG",
        "description": "Returns one row per STIG — its numerically highest V{major}R{minor} release — for dropdown population. Public reference data, no authentication required. The q parameter filters STIGs by case-insensitive substring with abbreviation expansion (RHEL matches Red Hat Enterprise Linux); queries shorter than 2 characters return no rows.",
        "security": [],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "STIG filter — case-insensitive substring matched against name, title, and stig_id, with abbreviation expansion (minimum 2 characters).",
            "schema": {
              "type": "string"
            },
            "example": "RHEL"
          }
        ],
        "responses": {
          "200": {
            "description": "Latest release of each STIG, ordered by title",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BenchmarkLatestResponse"
                },
                "examples": {
                  "latest": {
                    "summary": "Latest STIGs",
                    "value": {
                      "rows": [
                        {
                          "id": 7,
                          "stig_id": "RHEL_9_STIG",
                          "title": "Red Hat Enterprise Linux 9 Security Technical Implementation Guide",
                          "version": "V2R7",
                          "name": "RHEL 9 STIG - Ver 2, Rel 7"
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/stigs/{id}/stats": {
      "get": {
        "operationId": "stigStats",
        "tags": [
          "Benchmarks"
        ],
        "summary": "Get rule counts for a STIG",
        "description": "Returns the STIG's rule count and severity breakdown. Pure reference data — no usage section (components are based on SRGs, not STIGs) — and public like the rest of the STIG catalog.",
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric ID of the STIG.",
            "schema": {
              "type": "integer"
            },
            "example": 4
          }
        ],
        "responses": {
          "200": {
            "description": "STIG rule counts and severity breakdown",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BenchmarkStatsResponse"
                },
                "examples": {
                  "stats": {
                    "summary": "STIG stats",
                    "value": {
                      "rule_count": 380,
                      "severity_counts": {
                        "high": 40,
                        "medium": 300,
                        "low": 40
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/components/latest": {
      "get": {
        "operationId": "latestComponents",
        "tags": [
          "Benchmarks"
        ],
        "summary": "List the latest released component per prefix",
        "description": "Returns one released component per prefix — the numerically highest version/release pair — for dropdown population. A component is a STIG in progress; released ones are instance-wide reference data for any authenticated user. Unreleased drafts never appear. The q parameter filters components by case-insensitive substring with abbreviation expansion (RHEL matches Red Hat Enterprise Linux); queries shorter than 2 characters return no rows.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Component filter — case-insensitive substring matched against name, prefix, and title, with abbreviation expansion (minimum 2 characters).",
            "schema": {
              "type": "string"
            },
            "example": "RHEL-09"
          }
        ],
        "responses": {
          "200": {
            "description": "Latest released component per prefix, ordered by prefix",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ComponentLatestResponse"
                },
                "examples": {
                  "latest": {
                    "summary": "Latest released components",
                    "value": {
                      "rows": [
                        {
                          "id": 38,
                          "prefix": "RHEL-09",
                          "name": "RHEL 9 Hardened Baseline",
                          "title": "Red Hat Enterprise Linux 9",
                          "version": 2,
                          "release": 1
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/components/{id}/summary": {
      "get": {
        "operationId": "componentSummary",
        "tags": [
          "Components"
        ],
        "summary": "Get a lightweight component summary",
        "description": "Returns the component header — identity, counts, SRG info, the caller's effective permissions, and the serialized comment-phase state machine — without the heavy rules/reviews/histories arrays. Access matches the component show rules: released components are readable by any authenticated user; unreleased ones require viewer permission.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric ID of the component.",
            "schema": {
              "type": "integer"
            },
            "example": 38
          }
        ],
        "responses": {
          "200": {
            "description": "Component summary with phase state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ComponentSummaryResponse"
                },
                "examples": {
                  "open_period": {
                    "summary": "Component in an open comment period",
                    "value": {
                      "id": 38,
                      "name": "RHEL 9 Hardened Baseline",
                      "prefix": "RHEL-09",
                      "document_type": "stig",
                      "title": "Red Hat Enterprise Linux 9",
                      "version": 2,
                      "release": 1,
                      "released": false,
                      "project_id": 7,
                      "component_id": null,
                      "security_requirements_guide_id": 3,
                      "based_on_title": "General Purpose Operating System Security Requirements Guide",
                      "based_on_version": "V3R3",
                      "rules_count": 203,
                      "memberships_count": 4,
                      "severity_counts": {
                        "high": 20,
                        "medium": 173,
                        "low": 10
                      },
                      "pending_comment_count": 5,
                      "effective_permissions": "viewer",
                      "updated_at": "2026-07-10T14:07:37.142Z",
                      "comment_phase": "open",
                      "closed_reason": null,
                      "comment_period_starts_at": "2026-07-01T00:00:00.000Z",
                      "comment_period_ends_at": "2026-07-15T00:00:00.000Z",
                      "accepting_new_comments": true,
                      "triaging_active": true,
                      "frozen_for_writes": false,
                      "comment_period_days_remaining": 5
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/components/{id}/stats": {
      "get": {
        "operationId": "componentStats",
        "tags": [
          "Components"
        ],
        "summary": "Get rule statistics for a component",
        "description": "Returns rule counts by status and severity plus completion and lock percentages, computed as SQL aggregates. Access matches the component show rules: released components are readable by any authenticated user; unreleased ones require viewer permission.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric ID of the component.",
            "schema": {
              "type": "integer"
            },
            "example": 38
          }
        ],
        "responses": {
          "200": {
            "description": "Component rule statistics",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ComponentStatsResponse"
                },
                "examples": {
                  "stats": {
                    "summary": "Component stats",
                    "value": {
                      "document_type": "stig",
                      "rules_by_status": {
                        "not_yet_determined": 50,
                        "applicable_configurable": 120,
                        "applicable_inherently_meets": 15,
                        "applicable_does_not_meet": 8,
                        "not_applicable": 10
                      },
                      "rules_by_severity": {
                        "high": 20,
                        "medium": 173,
                        "low": 10
                      },
                      "rule_count": 203,
                      "completion_pct": 75.4,
                      "lock_pct": 12.3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/components/{id}/workflow_state": {
      "get": {
        "operationId": "componentWorkflowState",
        "tags": [
          "Components"
        ],
        "summary": "Get workflow readiness for a component",
        "description": "Returns where the component stands across the authoring, lock, review, comment, triage, and export stages — SQL-aggregated counts plus the comment-phase write-guard booleans. Access matches the component show rules: released components are readable by any authenticated user; unreleased ones require viewer permission.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric ID of the component.",
            "schema": {
              "type": "integer"
            },
            "example": 38
          }
        ],
        "responses": {
          "200": {
            "description": "Component workflow state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ComponentWorkflowStateResponse"
                },
                "examples": {
                  "in_progress": {
                    "summary": "Component mid-authoring with an open comment period",
                    "value": {
                      "document_type": "stig",
                      "authoring": {
                        "rules_total": 203,
                        "rules_determined": 153
                      },
                      "locks": {
                        "locked": 25,
                        "total": 203,
                        "all_locked": false
                      },
                      "reviews": {
                        "under_review": 4
                      },
                      "comment": {
                        "phase": "open",
                        "accepting_new_comments": true,
                        "triaging_active": true,
                        "frozen_for_writes": false,
                        "pending_comments": 3
                      },
                      "triage": {
                        "pending": 3,
                        "awaiting_adjudication": 1
                      },
                      "export": {
                        "released": false,
                        "releasable": false
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/components/{id}/triage_summary": {
      "get": {
        "operationId": "componentTriageSummary",
        "tags": [
          "Components"
        ],
        "summary": "Get triage summary for a component",
        "description": "Returns top-level comment counts per triage status plus the adjudication percentage — rule-attached and component-attached comments both count. Access matches the component show rules: released components are readable by any authenticated user; unreleased ones require viewer permission.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric ID of the component.",
            "schema": {
              "type": "integer"
            },
            "example": 38
          }
        ],
        "responses": {
          "200": {
            "description": "Component triage summary",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TriageSummaryResponse"
                },
                "examples": {
                  "summary": {
                    "summary": "Component with comments in triage",
                    "value": {
                      "by_triage_status": {
                        "pending": 3,
                        "concur": 1,
                        "concur_with_comment": 0,
                        "non_concur": 0,
                        "duplicate": 0,
                        "informational": 1,
                        "needs_clarification": 0,
                        "withdrawn": 0,
                        "addressed_by": 0
                      },
                      "total": 5,
                      "adjudicated": 1,
                      "adjudication_pct": 20
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/projects/{id}/stats": {
      "get": {
        "operationId": "projectStats",
        "tags": [
          "Projects"
        ],
        "summary": "Get rule statistics for a project",
        "description": "Returns rule statistics aggregated across every component in the project plus a per-component breakdown, computed from grouped SQL queries. Requires viewer permission on the project.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric ID of the project.",
            "schema": {
              "type": "integer"
            },
            "example": 7
          }
        ],
        "responses": {
          "200": {
            "description": "Project rule statistics with per-component breakdown",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectStatsResponse"
                },
                "examples": {
                  "stats": {
                    "summary": "Project with two components",
                    "value": {
                      "aggregate": {
                        "rules_by_status_by_type": {
                          "stig": {
                            "not_yet_determined": 51,
                            "applicable_configurable": 121,
                            "applicable_inherently_meets": 15,
                            "applicable_does_not_meet": 8,
                            "not_applicable": 10
                          },
                          "srg": {
                            "not_yet_determined": 2,
                            "applicable": 1,
                            "not_applicable": 0
                          }
                        },
                        "rules_by_severity": {
                          "high": 21,
                          "medium": 176,
                          "low": 11
                        },
                        "rule_count": 208,
                        "completion_pct": 74.5,
                        "lock_pct": 12
                      },
                      "components": [
                        {
                          "id": 38,
                          "name": "RHEL 9 Hardened Baseline",
                          "prefix": "RHEL-09",
                          "document_type": "stig",
                          "rule_count": 203,
                          "completion_pct": 75.4,
                          "lock_pct": 12.3
                        },
                        {
                          "id": 41,
                          "name": "Photon OS 5 Baseline",
                          "prefix": "PHTN-50",
                          "document_type": "srg",
                          "rule_count": 5,
                          "completion_pct": 40,
                          "lock_pct": 0
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/projects/{id}/triage_summary": {
      "get": {
        "operationId": "projectTriageSummary",
        "tags": [
          "Projects"
        ],
        "summary": "Get triage summary for a project",
        "description": "Returns triage metrics aggregated across all of the project's components — top-level comment counts per triage status plus the adjudication percentage. Requires viewer permission on the project.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric ID of the project.",
            "schema": {
              "type": "integer"
            },
            "example": 7
          }
        ],
        "responses": {
          "200": {
            "description": "Project-wide triage summary",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TriageSummaryResponse"
                },
                "examples": {
                  "summary": {
                    "summary": "Project with comments in triage",
                    "value": {
                      "by_triage_status": {
                        "pending": 3,
                        "concur": 1,
                        "concur_with_comment": 0,
                        "non_concur": 0,
                        "duplicate": 0,
                        "informational": 1,
                        "needs_clarification": 0,
                        "withdrawn": 0,
                        "addressed_by": 0
                      },
                      "total": 5,
                      "adjudicated": 1,
                      "adjudication_pct": 20
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/search/rules": {
      "get": {
        "operationId": "searchRulesLegacy",
        "tags": [
          "Search"
        ],
        "summary": "Legacy requirement search by SRG version",
        "description": "Searches requirement rows of both document kinds — stig rules and authored SRG requirements — by version identifier. Scoped to components the user can access: project or component membership, or released components; admins are unrestricted. Returns compact tuples [id, rule_id, component_id, prefix]. Requires authentication. This is a legacy search endpoint — prefer /api/search/global for new integrations.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "SRG version identifier to search for.",
            "schema": {
              "type": "string"
            },
            "example": "SRG-OS-000001-GPOS-00001"
          }
        ],
        "responses": {
          "200": {
            "description": "Matching rules as compact tuples",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "rules"
                  ],
                  "properties": {
                    "rules": {
                      "type": "array",
                      "description": "Each item is a tuple [rule_id, rule_id_string, component_id, prefix].",
                      "items": {
                        "type": "array",
                        "items": {}
                      }
                    }
                  }
                },
                "examples": {
                  "results": {
                    "summary": "Rules matching SRG version",
                    "value": {
                      "rules": [
                        [
                          1786,
                          "000001",
                          1,
                          "PHOS-03"
                        ],
                        [
                          2500,
                          "000001",
                          5,
                          "CNTR-01"
                        ]
                      ]
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/search/components": {
      "get": {
        "operationId": "searchComponentsLegacy",
        "tags": [
          "Search"
        ],
        "summary": "Legacy component search by SRG ID",
        "description": "Searches components by the SRG identifier they are based on. Scoped to components the user can access: project or component membership, or released components; admins are unrestricted. Returns compact tuples [id, name]. This is a legacy search endpoint — prefer /api/search/global for new integrations.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "SRG identifier to search for.",
            "schema": {
              "type": "string"
            },
            "example": "Container_Platform_SRG"
          }
        ],
        "responses": {
          "200": {
            "description": "Matching components as compact tuples",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "components"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "components": {
                      "type": "array",
                      "description": "Each item is a tuple [id, name].",
                      "items": {
                        "type": "array",
                        "items": {}
                      }
                    }
                  }
                },
                "examples": {
                  "results": {
                    "summary": "Components matching SRG",
                    "value": {
                      "components": [
                        [
                          1,
                          "Photon OS 3"
                        ],
                        [
                          8,
                          "Container Platform"
                        ]
                      ]
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/search/projects": {
      "get": {
        "operationId": "searchProjectsLegacy",
        "tags": [
          "Search"
        ],
        "summary": "Legacy project search by SRG ID",
        "description": "Searches projects that have components based on a given SRG. Returns compact tuples [id, name]. This is a legacy search endpoint — prefer /api/search/global for new integrations.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "SRG identifier to search for.",
            "schema": {
              "type": "string"
            },
            "example": "Container_Platform_SRG"
          }
        ],
        "responses": {
          "200": {
            "description": "Matching projects as compact tuples",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "projects"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "projects": {
                      "type": "array",
                      "description": "Each item is a tuple [id, name].",
                      "items": {
                        "type": "array",
                        "items": {}
                      }
                    }
                  }
                },
                "examples": {
                  "results": {
                    "summary": "Projects with matching SRG components",
                    "value": {
                      "projects": [
                        [
                          4,
                          "Container Platform"
                        ]
                      ]
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/projects": {
      "get": {
        "operationId": "listProjects",
        "tags": [
          "Projects"
        ],
        "summary": "List accessible projects",
        "description": "Returns all projects the current user can access, including owned projects, member projects, and discoverable projects. Includes membership counts and pending comment counts per project. Requires authentication.",
        "responses": {
          "200": {
            "description": "Projects list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ProjectIndexResponse"
                  }
                },
                "examples": {
                  "projects": {
                    "summary": "Two accessible projects",
                    "value": [
                      {
                        "id": 4,
                        "name": "Container Platform",
                        "memberships_count": 14
                      },
                      {
                        "id": 1,
                        "name": "Photon 3",
                        "memberships_count": 14
                      }
                    ]
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      },
      "post": {
        "operationId": "createProject",
        "tags": [
          "Projects"
        ],
        "summary": "Create a new project",
        "description": "Creates a new project with the given name and description. The creating user is automatically added as an admin member. Requires authentication.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "project": {
                    "description": "The shared project input surface plus the create-only slack_channel_id convenience field, which merges into the metadata data hash as \"Slack Channel ID\" and wins over a same-key entry supplied in project_metadata_attributes.",
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/ProjectInput"
                      },
                      {
                        "type": "object",
                        "properties": {
                          "slack_channel_id": {
                            "type": "string",
                            "description": "Convenience field — stored as the metadata key \"Slack Channel ID\". Accepted at creation only; use project_metadata_attributes on update.",
                            "example": "C0123456789"
                          }
                        }
                      }
                    ]
                  }
                }
              },
              "examples": {
                "create": {
                  "summary": "Create a new project",
                  "value": {
                    "project": {
                      "name": "Red Hat Enterprise Linux 9",
                      "description": "STIG development for RHEL 9"
                    }
                  }
                },
                "create_with_metadata": {
                  "summary": "Create with metadata in the same call",
                  "value": {
                    "project": {
                      "name": "Red Hat Enterprise Linux 9",
                      "description": "STIG development for RHEL 9",
                      "visibility": "discoverable",
                      "project_metadata_attributes": {
                        "data": {
                          "POC Name": "Jane Doe",
                          "POC Email": "jane@example.com"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Project created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectCreateResponse"
                },
                "examples": {
                  "created": {
                    "summary": "Project created",
                    "value": {
                      "toast": {
                        "title": "Project created.",
                        "message": [
                          "Successfully created project Red Hat Enterprise Linux 9."
                        ],
                        "variant": "success"
                      },
                      "redirect_url": "/projects/5"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/projects/{projectId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ],
      "get": {
        "operationId": "getProject",
        "tags": [
          "Projects"
        ],
        "summary": "Project detail with component list and stats",
        "description": "Returns full project details including components, membership count, comment counts, and project metadata. Requires membership in the project or admin role.",
        "responses": {
          "200": {
            "description": "Project detail with components",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectShowResponse"
                },
                "examples": {
                  "project": {
                    "summary": "Photon 3 project detail",
                    "value": {
                      "id": 1,
                      "name": "Photon 3",
                      "memberships_count": 16,
                      "pending_comment_count": 1,
                      "components": [
                        {
                          "id": 1,
                          "name": "Photon OS 3",
                          "prefix": "PHOS-03"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      },
      "patch": {
        "operationId": "patchProject",
        "tags": [
          "Projects"
        ],
        "summary": "Partial update of project attributes",
        "description": "Partial update — send only changed fields. Updates the project name, description, or visibility. Requires admin role on the project. Returns a canonical toast response.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "project": {
                    "$ref": "#/components/schemas/ProjectInput"
                  }
                }
              },
              "examples": {
                "rename": {
                  "summary": "Rename a project",
                  "value": {
                    "project": {
                      "name": "Container Platform v2"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Project updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "updated": {
                    "summary": "Project renamed",
                    "value": {
                      "toast": {
                        "title": "Project updated.",
                        "message": [
                          "Successfully updated project."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      },
      "put": {
        "operationId": "updateProject",
        "tags": [
          "Projects"
        ],
        "summary": "Full replacement of project attributes",
        "description": "Full replacement — all fields required. Updates the project name, description, or visibility. Requires admin role on the project. Returns a canonical toast response.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "project": {
                    "$ref": "#/components/schemas/ProjectInput"
                  }
                }
              },
              "examples": {
                "rename": {
                  "summary": "Rename a project",
                  "value": {
                    "project": {
                      "name": "Container Platform v2"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Project updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "updated": {
                    "summary": "Project renamed",
                    "value": {
                      "toast": {
                        "title": "Project updated.",
                        "message": [
                          "Successfully updated project."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      },
      "delete": {
        "operationId": "deleteProject",
        "tags": [
          "Projects"
        ],
        "summary": "Delete a project and all its components",
        "description": "Permanently deletes the project, all its components, rules, reviews, and memberships. Requires admin role on the project. This action cannot be undone. Returns 403 for non-admin users.",
        "responses": {
          "200": {
            "description": "Project deleted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "deleted": {
                    "summary": "Project deleted",
                    "value": {
                      "toast": {
                        "title": "Project deleted.",
                        "message": [
                          "Successfully deleted project."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/projects/{projectId}/comments": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ],
      "get": {
        "operationId": "getProjectComments",
        "tags": [
          "Projects"
        ],
        "summary": "Aggregated comments across all project components",
        "description": "Returns paginated comments from all components in the project, with triage status counts. Different row shape from component comments — includes component_id/component_name but omits some triage attribution fields. Uses Project#paginated_comments (NOT CommentQueryService).",
        "parameters": [
          {
            "$ref": "#/components/parameters/TriageStatusFilter"
          },
          {
            "$ref": "#/components/parameters/PageParam"
          },
          {
            "$ref": "#/components/parameters/PerPageParam"
          },
          {
            "name": "section",
            "in": "query",
            "description": "Filter by requirement section (fixtext, check_content, etc.).",
            "schema": {
              "type": "string"
            },
            "example": "fixtext"
          },
          {
            "name": "component_id",
            "in": "query",
            "required": false,
            "description": "Filter to a specific component within the project.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "author_id",
            "in": "query",
            "required": false,
            "description": "Filter by comment author.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Text search within comment content.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "resolved",
            "in": "query",
            "required": false,
            "description": "Filter by resolved state (true/false/all).",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false",
                "all"
              ]
            },
            "example": "false"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated project comment rows with status counts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "rows",
                    "pagination",
                    "status_counts"
                  ],
                  "properties": {
                    "rows": {
                      "type": "array",
                      "description": "Comment rows with component context.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "rule_id": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "rule_displayed_name": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "commentable_type": {
                            "type": "string"
                          },
                          "component_id": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "component_name": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "section": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "author_name": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "comment": {
                            "type": "string"
                          },
                          "created_at": {
                            "type": "string"
                          },
                          "triage_status": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "triage_set_at": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "adjudicated_at": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "duplicate_of_review_id": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "triager_display_name": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "triager_imported": {
                            "type": "boolean"
                          },
                          "adjudicator_display_name": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "adjudicator_imported": {
                            "type": "boolean"
                          },
                          "responses_count": {
                            "type": "integer"
                          },
                          "reactions": {
                            "type": "object",
                            "properties": {
                              "up": {
                                "type": "integer"
                              },
                              "down": {
                                "type": "integer"
                              },
                              "mine": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              }
                            }
                          }
                        }
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "required": [
                        "page",
                        "per_page",
                        "total"
                      ],
                      "properties": {
                        "page": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        },
                        "total": {
                          "type": "integer"
                        }
                      }
                    },
                    "status_counts": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "integer"
                      }
                    }
                  }
                },
                "examples": {
                  "comments": {
                    "summary": "Project-wide comment queue",
                    "value": {
                      "rows": [
                        {
                          "id": 26,
                          "rule_displayed_name": "PHOS-03-000038",
                          "component_id": 1,
                          "component_name": "Photon OS 3",
                          "triage_status": "pending",
                          "comment": "This requirement needs clarification.",
                          "responses_count": 0,
                          "reactions": {
                            "up": 0,
                            "down": 0,
                            "mine": null
                          }
                        }
                      ],
                      "pagination": {
                        "page": 1,
                        "per_page": 25,
                        "total": 3
                      },
                      "status_counts": {
                        "pending": 1,
                        "concur": 1,
                        "informational": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/projects/{projectId}/export/{type}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        },
        {
          "name": "type",
          "in": "path",
          "required": true,
          "description": "Export format.",
          "schema": {
            "type": "string",
            "enum": [
              "csv",
              "xccdf",
              "inspec",
              "json_archive"
            ]
          },
          "example": "xccdf"
        }
      ],
      "get": {
        "operationId": "exportProject",
        "tags": [
          "Projects"
        ],
        "summary": "Export project data in the specified format",
        "description": "Exports selected components from the project as CSV, XCCDF XML, InSpec profile, or JSON archive. Supports mode selection (working copy, vendor submission, published STIG) and optional SRG/membership inclusion. Returns a binary file download. Requires project membership.",
        "parameters": [
          {
            "name": "component_ids",
            "in": "query",
            "required": true,
            "description": "Comma-separated IDs of components to include in the export.",
            "schema": {
              "type": "string"
            },
            "example": "29,30"
          },
          {
            "name": "mode",
            "in": "query",
            "description": "Export mode controlling which fields and rules are included.",
            "schema": {
              "type": "string",
              "enum": [
                "working_copy",
                "vendor_submission",
                "published_stig"
              ]
            },
            "example": "working_copy"
          },
          {
            "name": "include_srg",
            "in": "query",
            "description": "Include the source SRG in the export package.",
            "schema": {
              "type": "string",
              "enum": [
                "true"
              ]
            }
          },
          {
            "name": "include_memberships",
            "in": "query",
            "description": "Include project membership data in the export.",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            }
          },
          {
            "name": "exclude_satisfied_by",
            "in": "query",
            "description": "Exclude rules that are satisfied by another rule.",
            "schema": {
              "type": "string",
              "enum": [
                "true"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Binary file download (CSV, XML, ZIP, or JSON)",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/xml": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/zip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/json": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/projects/create_from_backup": {
      "post": {
        "operationId": "createFromBackup",
        "tags": [
          "Projects"
        ],
        "summary": "Create a new project from a JSON archive backup",
        "description": "Creates a new project by restoring from a JSON archive (.zip). The archive must have been created by the json_archive export. Optionally override the project name. The archived project metadata (project.json) is restored onto the new project. The creating user becomes the project admin. Requires authentication.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "JSON archive .zip file from a previous export."
                  },
                  "name": {
                    "type": "string",
                    "description": "Override project name. Uses the archived name if omitted.",
                    "example": "Restored Container Platform"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Dry-run returns preview (summary + warnings + project_defaults). Real create returns redirect URL + summary + toast.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/CreateFromBackupDryRunResponse"
                    },
                    {
                      "$ref": "#/components/schemas/CreateFromBackupResponse"
                    }
                  ]
                },
                "examples": {
                  "dry_run": {
                    "summary": "Preview of what would be imported",
                    "value": {
                      "summary": {
                        "dry_run": true,
                        "components_imported": 2,
                        "rules_imported": 264,
                        "component_details": [
                          {
                            "name": "Photon OS 3",
                            "rule_count": 132
                          }
                        ]
                      },
                      "warnings": [],
                      "project_defaults": {
                        "name": "My Original Project",
                        "description": "A test project",
                        "visibility": "discoverable"
                      }
                    }
                  },
                  "created": {
                    "summary": "Project created from archive",
                    "value": {
                      "redirect_url": "/projects/123",
                      "summary": {
                        "components_imported": 2,
                        "rules_imported": 264
                      },
                      "toast": {
                        "title": "Project imported.",
                        "message": [
                          "Project created from backup successfully."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Import or preview failed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "toast"
                  ],
                  "properties": {
                    "toast": {
                      "$ref": "#/components/schemas/ToastObject"
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "project_defaults": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "description": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "visibility": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/projects/{projectId}/import_backup": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ],
      "post": {
        "operationId": "importBackup",
        "tags": [
          "Projects"
        ],
        "summary": "Import a JSON archive backup",
        "description": "Imports components, rules, reviews, and memberships from a JSON archive (.zip) into the project, replacing or augmenting existing content. Returns 200 with a toast + summary. Requires admin role on the project. The archive must have been created by the json_archive export format.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "JSON archive .zip file from a previous export."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Backup imported (default mode) or dry-run preview completed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportBackupResponse"
                },
                "examples": {
                  "imported": {
                    "summary": "Archive imported successfully",
                    "value": {
                      "toast": {
                        "title": "Backup restored.",
                        "message": [
                          "Backup restored successfully."
                        ],
                        "variant": "success"
                      },
                      "summary": {
                        "components_imported": 2,
                        "rules_imported": 264,
                        "satisfactions_imported": 12,
                        "reviews_imported": 48,
                        "memberships_imported": 3,
                        "srgs_imported": 1,
                        "component_details": [
                          {
                            "name": "Photon OS 3",
                            "rule_count": 132,
                            "srg_title": "General Purpose Operating System SRG",
                            "srg_version": "V3R3"
                          }
                        ]
                      },
                      "warnings": []
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Import failed (validation errors)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "toast"
                  ],
                  "properties": {
                    "toast": {
                      "$ref": "#/components/schemas/ToastObject"
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/projects/{projectId}/histories": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ],
      "get": {
        "operationId": "getProjectHistories",
        "tags": [
          "Projects"
        ],
        "summary": "Audit history for the project",
        "description": "Returns the 50 most recent audit trail entries for the project, including component and rule changes. Used by the project history sidebar. Requires project membership.",
        "responses": {
          "200": {
            "description": "Recent audit entries",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/AuditEntry"
                  }
                },
                "examples": {
                  "history": {
                    "summary": "Recent project changes",
                    "value": [
                      {
                        "id": 500,
                        "auditable_type": "Component",
                        "auditable_id": 38,
                        "action": "update",
                        "name": "Demo Admin",
                        "created_at": "2026-05-28T15:00:00Z",
                        "audited_changes": [
                          {
                            "field": "released",
                            "prev_value": false,
                            "new_value": true
                          }
                        ]
                      }
                    ]
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/components": {
      "get": {
        "operationId": "listComponents",
        "tags": [
          "Components"
        ],
        "summary": "List released components",
        "description": "Returns all released (published) components visible to the current user. Released components are read-only snapshots that have been through the DISA review process. Requires authentication.",
        "responses": {
          "200": {
            "description": "Released components",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ComponentIndexResponse"
                  }
                },
                "examples": {
                  "components": {
                    "summary": "Two released components",
                    "value": [
                      {
                        "id": 29,
                        "name": "Container SRG",
                        "prefix": "CNTR",
                        "version": 1,
                        "release": 1,
                        "released": true
                      },
                      {
                        "id": 30,
                        "name": "Photon OS 3",
                        "prefix": "PHOS-03",
                        "version": 1,
                        "release": 1,
                        "released": true
                      }
                    ]
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/projects/{projectId}/components": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ProjectId"
        }
      ],
      "post": {
        "operationId": "createComponent",
        "tags": [
          "Components"
        ],
        "summary": "Create or duplicate a component in a project",
        "description": "Creates a new component in the project from one or more declared source SRGs, duplicates an existing component, or imports from an uploaded XCCDF/CSV file. Requires admin role on the project. The component is initialized with requirements from every declared source (full union by default, or the requirement_selections subset) — security_requirements_guide_id designates the primary.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "component": {
                    "$ref": "#/components/schemas/ComponentInput"
                  }
                }
              },
              "examples": {
                "from_srg": {
                  "summary": "Create from an SRG baseline",
                  "value": {
                    "component": {
                      "name": "Container SRG",
                      "prefix": "CNTR",
                      "title": "Container Platform Security Technical Implementation Guide",
                      "version": 1,
                      "release": 1,
                      "security_requirements_guide_id": 1
                    }
                  }
                },
                "multi_source_srg": {
                  "summary": "Create a dual-home SRG component from two core sources",
                  "value": {
                    "component": {
                      "name": "Container Platform SRG",
                      "prefix": "CNTR",
                      "title": "Container Platform Security Requirements Guide",
                      "version": 1,
                      "release": 1,
                      "document_type": "srg",
                      "security_requirements_guide_id": 1,
                      "declared_source_srg_ids": [
                        1,
                        2
                      ]
                    }
                  }
                },
                "one_call_with_extras": {
                  "summary": "Create complete in one call — metadata, questions, advanced fields",
                  "value": {
                    "component": {
                      "name": "Container SRG",
                      "prefix": "CNTR",
                      "title": "Container Platform Security Technical Implementation Guide",
                      "version": 1,
                      "release": 1,
                      "security_requirements_guide_id": 1,
                      "advanced_fields": true,
                      "component_metadata_attributes": {
                        "data": {
                          "Vendor": "Acme",
                          "POC": "Sam"
                        }
                      },
                      "additional_questions_attributes": [
                        {
                          "name": "Deployment environment",
                          "question_type": "dropdown",
                          "options": [
                            "Cloud",
                            "On-prem"
                          ]
                        }
                      ]
                    }
                  }
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "XCCDF XML or CSV file to import as a component."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Component created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "created": {
                    "summary": "Component created from SRG",
                    "value": {
                      "toast": {
                        "title": "Component created.",
                        "message": [
                          "Successfully created component Container SRG."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/components/{componentId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ComponentId"
        }
      ],
      "get": {
        "operationId": "getComponent",
        "tags": [
          "Components"
        ],
        "summary": "Component detail with rules",
        "description": "Returns full component details including all rules with their content fields, for either document kind. Members (any role) receive the editor view; a non-member receives the read-only show view for a RELEASED component (released components are never concealed, whatever the project's visibility). An unreleased component answers a non-member per the disclosure policy: 403 with the project admins to ask when the project is discoverable, or the concealment 404 when it is hidden.",
        "responses": {
          "200": {
            "description": "Component with rules — the editor view for members, the read-only show view otherwise. A null effective_permissions marks the show branch. The two shapes share their core fields, so they are documented as anyOf.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ComponentEditorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ComponentShowResponse"
                    }
                  ]
                },
                "examples": {
                  "component": {
                    "summary": "Component editor view (project member)",
                    "value": {
                      "id": 1,
                      "name": "Photon OS 3",
                      "prefix": "PHOS-03",
                      "title": "Photon OS 3 STIG Readiness Guide",
                      "rules_count": 203,
                      "comment_phase": "open",
                      "version": 1,
                      "release": 1,
                      "released": false
                    }
                  },
                  "non_member_show": {
                    "summary": "Read-only show view (non-member, released component)",
                    "value": {
                      "id": 1,
                      "name": "Photon OS 3",
                      "prefix": "PHOS-03",
                      "title": "Photon OS 3 STIG Readiness Guide",
                      "released": true,
                      "effective_permissions": null
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      },
      "put": {
        "operationId": "updateComponent",
        "tags": [
          "Components"
        ],
        "summary": "Update component attributes",
        "description": "Updates component metadata (name, prefix, version, release, description). Requires admin role on the parent project. Does not modify rules — use the rule endpoints for rule content changes.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "component": {
                    "$ref": "#/components/schemas/ComponentUpdateInput"
                  }
                }
              },
              "examples": {
                "update": {
                  "summary": "Update version and release",
                  "value": {
                    "component": {
                      "version": 2,
                      "release": 1
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Component updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "updated": {
                    "summary": "Component updated",
                    "value": {
                      "toast": {
                        "title": "Component updated.",
                        "message": [
                          "Successfully updated component."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      },
      "patch": {
        "operationId": "patchComponent",
        "tags": [
          "Components"
        ],
        "summary": "Partial update of component attributes",
        "description": "Partial update — same behavior as PUT but only supplied fields are changed. Requires admin role on the parent project.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "component": {
                    "$ref": "#/components/schemas/ComponentUpdateInput"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Component updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      },
      "delete": {
        "operationId": "deleteComponent",
        "tags": [
          "Components"
        ],
        "summary": "Delete a component and all its rules",
        "description": "Permanently deletes the component, all its rules, reviews, and associated data. Requires admin role on the parent project. This action cannot be undone.",
        "responses": {
          "200": {
            "description": "Component deleted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "deleted": {
                    "summary": "Component deleted",
                    "value": {
                      "toast": {
                        "title": "Component deleted.",
                        "message": [
                          "Successfully deleted component."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/components/{componentId}/comments": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ComponentId"
        }
      ],
      "get": {
        "operationId": "getComponentComments",
        "tags": [
          "Components"
        ],
        "summary": "Paginated triage table for component comments",
        "description": "Returns paginated public comments on this component's rules, with triage status counts and filtering. Powers the triage page table and split-pane views. Supports filtering by status, section, rule, author, text search, and resolution state. Requires project membership.",
        "parameters": [
          {
            "$ref": "#/components/parameters/TriageStatusFilter"
          },
          {
            "$ref": "#/components/parameters/PageParam"
          },
          {
            "$ref": "#/components/parameters/PerPageParam"
          },
          {
            "name": "section",
            "in": "query",
            "description": "Filter by requirement section (fixtext, check_content, etc.).",
            "schema": {
              "type": "string"
            },
            "example": "fixtext"
          },
          {
            "name": "rule_id",
            "in": "query",
            "description": "Filter to comments on a specific rule.",
            "schema": {
              "type": "integer"
            },
            "example": 100
          },
          {
            "name": "author_id",
            "in": "query",
            "description": "Filter to comments by a specific author.",
            "schema": {
              "type": "integer"
            },
            "example": 42
          },
          {
            "name": "q",
            "in": "query",
            "description": "Full-text search across comment content.",
            "schema": {
              "type": "string"
            },
            "example": "container image"
          },
          {
            "name": "resolved",
            "in": "query",
            "required": false,
            "description": "Filter by resolved state (true/false/all).",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false",
                "all"
              ]
            },
            "example": "false"
          },
          {
            "name": "commentable_type",
            "in": "query",
            "required": false,
            "description": "Restrict rows to comments on requirements (\"rule\") or on the component itself (\"component\"). Absent or any other value returns both.",
            "schema": {
              "type": "string",
              "enum": [
                "rule",
                "component"
              ]
            },
            "example": "rule"
          },
          {
            "name": "include_rule_content",
            "in": "query",
            "description": "Include rule content fields for split-pane triage view.",
            "schema": {
              "type": "string",
              "enum": [
                "true"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated comment rows with status counts",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaginatedComments"
                },
                "examples": {
                  "comments": {
                    "summary": "Pending comments on Container SRG",
                    "value": {
                      "rows": [
                        {
                          "id": 44,
                          "commentable_type": "BaseRule",
                          "rule_displayed_name": "CNTR-00-000050",
                          "section": "fixtext",
                          "author_name": "John Osborne",
                          "author_email": "josborne@example.org",
                          "comment": "This requirement needs clarification...",
                          "triage_status": "pending",
                          "created_at": "2026-05-19T16:15:00Z",
                          "responses_count": 0,
                          "reactions": {
                            "up": 1,
                            "down": 0
                          }
                        }
                      ],
                      "pagination": {
                        "page": 1,
                        "per_page": 25,
                        "total": 1,
                        "total_comments": 10
                      },
                      "status_counts": {
                        "pending": 10,
                        "concur": 2
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/components/{componentId}/export/{type}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ComponentId"
        },
        {
          "name": "type",
          "in": "path",
          "required": true,
          "description": "Export format.",
          "schema": {
            "type": "string",
            "enum": [
              "csv",
              "xccdf",
              "inspec",
              "json_archive",
              "disposition_csv"
            ]
          },
          "example": "xccdf"
        }
      ],
      "get": {
        "operationId": "exportComponent",
        "tags": [
          "Components"
        ],
        "summary": "Export component in the specified format",
        "description": "Downloads the component as CSV, XCCDF XML, InSpec profile, JSON archive, or disposition CSV. Supports mode selection (working_copy, vendor_submission, published_stig) via query param. The xccdf type is kind-routed by the component's document type: SRG components export their authored requirements through the published_srg mode (only Applicable requirements publish); STIG components use published_stig. disposition_csv is for DISA comment triage matrix export. Requires project membership.",
        "parameters": [
          {
            "name": "mode",
            "in": "query",
            "description": "Export mode controlling which fields are included. The xccdf type ignores this parameter — its mode is derived from the component's document type (published_srg for SRG kind, published_stig for STIG).",
            "schema": {
              "type": "string",
              "enum": [
                "working_copy",
                "vendor_submission",
                "published_stig"
              ]
            },
            "example": "working_copy"
          },
          {
            "name": "triage_status",
            "in": "query",
            "description": "Filter disposition CSV by triage status.",
            "schema": {
              "type": "string"
            },
            "example": "pending"
          }
        ],
        "responses": {
          "200": {
            "description": "Binary file download",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/xml": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/zip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/components/{componentId}/lock": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ComponentId"
        }
      ],
      "post": {
        "operationId": "lockComponent",
        "tags": [
          "Components"
        ],
        "summary": "Lock all unlocked rules in a component",
        "description": "Locks every currently-unlocked rule in the component, preventing further edits. Requires component-admin authority. Already-locked rules are unaffected.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "review"
                ],
                "properties": {
                  "review": {
                    "description": "The bulk-lock surface is action + comment only — one review row is generated per locked requirement carrying both, and the comment is required (each generated review validates comment presence). Other review keys (section, responding_to_review_id, component_id) are ignored.",
                    "type": "object",
                    "required": [
                      "action",
                      "comment"
                    ],
                    "properties": {
                      "action": {
                        "type": "string",
                        "description": "The review action recorded on every generated row.",
                        "enum": [
                          "lock_control"
                        ],
                        "example": "lock_control"
                      },
                      "comment": {
                        "type": "string",
                        "description": "Audit trail comment explaining why the component was locked.",
                        "example": "Locking for DISA submission review."
                      }
                    }
                  }
                }
              },
              "examples": {
                "lock": {
                  "summary": "Lock with audit comment",
                  "value": {
                    "review": {
                      "action": "lock_control",
                      "comment": "Locking for DISA submission review."
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Lockable rules locked. Rules with incomplete data are SKIPPED, not failed — Not Yet Determined without satisfactions, Does Not Meet without mitigations, Inherently Meets without an artifact description — and each skip is named in a warning line appended to the message (variant becomes warning).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "locked": {
                    "summary": "All candidates locked",
                    "value": {
                      "toast": {
                        "title": "Locked 2 controls.",
                        "message": [
                          "Locked: CNTR-00-000001, CNTR-00-000002"
                        ],
                        "variant": "success"
                      }
                    }
                  },
                  "locked_with_skips": {
                    "summary": "Some rules skipped for incomplete data",
                    "value": {
                      "toast": {
                        "title": "Locked 1 control.",
                        "message": [
                          "Locked: CNTR-00-000001\n\nNot Yet Determined (skipped): CNTR-00-000002"
                        ],
                        "variant": "warning"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Every candidate rule was skipped — nothing could be locked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "nothing_lockable": {
                    "summary": "All candidates skipped",
                    "value": {
                      "toast": {
                        "title": "No controls could be locked.",
                        "message": [
                          "Not Yet Determined (skipped): CNTR-00-000001, CNTR-00-000002"
                        ],
                        "variant": "warning"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/components/{componentId}/lock_sections": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ComponentId"
        }
      ],
      "patch": {
        "operationId": "lockSections",
        "tags": [
          "Components"
        ],
        "summary": "Lock or unlock sections on every unlocked rule",
        "description": "Locks (or unlocks) the named content sections across all unlocked requirement rows in the component — both document kinds. Section names come from the lockable-section vocabulary (Title, Severity, Status, Fix, Check, ...). Locked sections cannot be edited until unlocked. Requires reviewer role or higher on the component. Returns 422 when any section name is not in the vocabulary.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "sections",
                  "locked"
                ],
                "properties": {
                  "sections": {
                    "type": "array",
                    "description": "Section names to lock or unlock.",
                    "items": {
                      "type": "string",
                      "enum": [
                        "Title",
                        "Severity",
                        "Status",
                        "Fix",
                        "Check",
                        "Vulnerability Discussion",
                        "DISA Metadata",
                        "Vendor Comments",
                        "Artifact Description",
                        "XCCDF Metadata"
                      ]
                    },
                    "example": [
                      "Fix",
                      "Check"
                    ]
                  },
                  "locked": {
                    "type": "boolean",
                    "description": "True locks the named sections; false unlocks them.",
                    "example": true
                  },
                  "comment": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Optional audit comment recorded on each changed rule. When blank, a generated comment naming the action and sections is recorded instead.",
                    "example": "Content freeze for the review window."
                  }
                }
              },
              "examples": {
                "lock_fix_check": {
                  "summary": "Lock the Fix and Check sections",
                  "value": {
                    "sections": [
                      "Fix",
                      "Check"
                    ],
                    "locked": true,
                    "comment": "Content freeze for the review window."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Section lock applied across all unlocked rules",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "locked": {
                    "summary": "Sections locked",
                    "value": {
                      "toast": {
                        "title": "Section lock applied",
                        "message": [
                          "Locked 2 section(s) on 264 rule(s)"
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "A section name is not in the lockable vocabulary",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "invalid_section": {
                    "summary": "Unknown section name",
                    "value": {
                      "toast": {
                        "title": "Invalid sections",
                        "message": [
                          "Not recognized: fixtext"
                        ],
                        "variant": "danger"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/components/{componentId}/histories": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ComponentId"
        }
      ],
      "get": {
        "operationId": "getComponentHistories",
        "tags": [
          "Components"
        ],
        "summary": "Audit history for the component",
        "description": "Returns the 50 most recent audit trail entries for the component, including rule changes, review actions, and metadata updates. Used by the component history sidebar. Requires project membership.",
        "responses": {
          "200": {
            "description": "Recent audit entries",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/AuditEntry"
                  }
                },
                "examples": {
                  "history": {
                    "summary": "Recent component changes",
                    "value": [
                      {
                        "id": 600,
                        "auditable_type": "Rule",
                        "auditable_id": 812,
                        "action": "update",
                        "name": "Demo Admin",
                        "created_at": "2026-05-28T15:00:00Z",
                        "audited_changes": [
                          {
                            "field": "status",
                            "prev_value": "Not Yet Determined",
                            "new_value": "Applicable - Configurable"
                          }
                        ]
                      }
                    ]
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/components/{componentId}/rules": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ComponentId"
        }
      ],
      "get": {
        "operationId": "listComponentRules",
        "tags": [
          "Rules"
        ],
        "summary": "List all rules for a component",
        "description": "Returns all rules in the component with full content fields (title, fixtext, check_content, vuln_discussion, etc.). Used by the component editor to populate the rule list. Requires project membership.",
        "responses": {
          "200": {
            "description": "All component rules",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "description": "Items are shaped by the component's document_type: STIG rules or authored SRG requirements.",
                  "items": {
                    "oneOf": [
                      {
                        "$ref": "#/components/schemas/RuleEditorResponse"
                      },
                      {
                        "$ref": "#/components/schemas/AuthoredSrgRuleEditorResponse"
                      }
                    ]
                  }
                },
                "examples": {
                  "rules": {
                    "summary": "Two rules",
                    "value": [
                      {
                        "id": 100,
                        "rule_id": "CNTR-00-000050",
                        "title": "Container images must be signed",
                        "status": "Applicable - Configurable",
                        "locked": false,
                        "satisfies": [],
                        "satisfied_by": []
                      },
                      {
                        "id": 101,
                        "rule_id": "CNTR-00-000051",
                        "title": "Container images must come from approved registries",
                        "status": "Applicable - Configurable",
                        "locked": false,
                        "satisfies": [],
                        "satisfied_by": []
                      }
                    ]
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      },
      "post": {
        "operationId": "createRule",
        "tags": [
          "Rules"
        ],
        "summary": "Create a new requirement in a component",
        "description": "Creates a requirement in one call, shaped by the component's document_type — a STIG rule (seeded from the source SRG's CCI-000366 baseline row; severity and weight are inherited from it) or an authored SRG requirement. Content fields apply at creation: provided values win over seeded defaults, and provided nested attributes replace the built defaults. Duplicate mode copies an existing requirement of THIS component. Requirement numbers are server-owned and assigned from the component's sequence. Blank/content creation requires project admin; duplication requires author. Returns 422 when a STIG component's source SRG has no CCI-000366 baseline row, or when provided content fails the kind's validations.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "rule": {
                    "description": "Creation mode plus the one-call content surface (the same fields the update endpoint accepts; nested attributes carry no id at creation).",
                    "allOf": [
                      {
                        "type": "object",
                        "properties": {
                          "duplicate": {
                            "type": "boolean",
                            "description": "Duplicate an existing requirement instead of creating a blank one.",
                            "example": false
                          },
                          "id": {
                            "type": "integer",
                            "description": "Source requirement id to duplicate (required when duplicate is true; must belong to this component).",
                            "example": 100
                          }
                        }
                      },
                      {
                        "$ref": "#/components/schemas/RuleInput"
                      }
                    ]
                  }
                }
              },
              "examples": {
                "create": {
                  "summary": "Create a blank requirement",
                  "value": {
                    "rule": {
                      "duplicate": false
                    }
                  }
                },
                "one_call_content": {
                  "summary": "Create a STIG rule with content in one call",
                  "value": {
                    "rule": {
                      "duplicate": false,
                      "title": "The container platform must enforce approved authorizations.",
                      "status": "Applicable - Configurable",
                      "vendor_comments": "Met by the recommended runtime configuration."
                    }
                  }
                },
                "authored_srg": {
                  "summary": "Create an authored SRG requirement with content",
                  "value": {
                    "rule": {
                      "duplicate": false,
                      "title": "The application must enforce approved authorizations for logical access.",
                      "status": "Applicable",
                      "fixtext": "Configure the application to enforce approved authorizations."
                    }
                  }
                },
                "duplicate": {
                  "summary": "Duplicate an existing requirement",
                  "value": {
                    "rule": {
                      "duplicate": true,
                      "id": 100
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rule created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RuleCreateResponse"
                },
                "examples": {
                  "created": {
                    "summary": "Control created",
                    "value": {
                      "toast": {
                        "title": "Control created.",
                        "message": [
                          "Successfully created control."
                        ],
                        "variant": "success"
                      },
                      "data": {
                        "id": 5000,
                        "rule_id": "000204",
                        "title": "New container security requirement",
                        "status": "Not Yet Determined",
                        "locked": false,
                        "satisfies": [],
                        "satisfied_by": []
                      }
                    }
                  }
                }
              }
            },
            "links": {
              "GetCreatedRule": {
                "operationId": "getRule",
                "parameters": {
                  "ruleId": "$response.body#/data/id"
                },
                "description": "Fetch the newly created rule."
              },
              "UpdateCreatedRule": {
                "operationId": "updateRule",
                "parameters": {
                  "ruleId": "$response.body#/data/id"
                },
                "description": "Update the newly created rule."
              },
              "DeleteCreatedRule": {
                "operationId": "deleteRule",
                "parameters": {
                  "ruleId": "$response.body#/data/id"
                },
                "description": "Delete the newly created rule."
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/components/{componentId}/rules_picker": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ComponentId"
        }
      ],
      "get": {
        "operationId": "getRulesPicker",
        "tags": [
          "Rules"
        ],
        "summary": "Lightweight rule list for picker UI",
        "description": "Returns a compact list of requirements for dropdown/picker selection (e.g., the move-to-rule admin action or duplicate-of picker). Lighter than the full rules list — omits content fields. Rows are shaped by the component's document_type: STIG rule objects carry the satisfaction relationships, authored SRG requirement objects omit them entirely. Requires project membership.",
        "responses": {
          "200": {
            "description": "Requirements for picker selection",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "rules"
                  ],
                  "properties": {
                    "rules": {
                      "type": "array",
                      "description": "All requirements in picker shape, one variant per document kind — the shapes are disjoint and never collapse into one list.",
                      "items": {
                        "oneOf": [
                          {
                            "$ref": "#/components/schemas/RulePickerResponse"
                          },
                          {
                            "$ref": "#/components/schemas/AuthoredSrgRulePickerResponse"
                          }
                        ]
                      }
                    }
                  }
                },
                "examples": {
                  "stig_picker": {
                    "summary": "STIG rules for dropdown",
                    "value": {
                      "rules": [
                        {
                          "id": 100,
                          "rule_id": "000050",
                          "displayed_name": "CNTR-00-000050",
                          "title": "Container images must be signed",
                          "locked": false,
                          "satisfies": [],
                          "satisfied_by": []
                        },
                        {
                          "id": 101,
                          "rule_id": "000051",
                          "displayed_name": "CNTR-00-000051",
                          "title": "Container images must come from approved registries",
                          "locked": true,
                          "satisfies": [],
                          "satisfied_by": []
                        }
                      ]
                    }
                  },
                  "srg_picker": {
                    "summary": "Authored SRG requirements for dropdown",
                    "value": {
                      "rules": [
                        {
                          "id": 10633,
                          "rule_id": "000001",
                          "displayed_name": "RCPK-00-000001",
                          "title": "The container platform must enforce approved authorizations",
                          "status": "Applicable",
                          "locked": false
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/components/detect_srg": {
      "post": {
        "operationId": "detectSrg",
        "tags": [
          "Components"
        ],
        "summary": "Detect which SRG a spreadsheet belongs to",
        "description": "Analyzes an uploaded spreadsheet (CSV/XLSX) to determine which SRG its rule IDs match. Used by the component creation flow to auto-select the correct SRG when importing from a spreadsheet. Returns the matched SRG's id, srg_id, title, and version. Returns 422 if no file provided, no SRG IDs found, no matching SRG exists, or IDs map to multiple SRGs.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "CSV or XLSX spreadsheet containing rule IDs."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Matched SRG",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "srg_id",
                    "title",
                    "version"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "id": {
                      "type": "integer",
                      "description": "Database ID of the matched SRG.",
                      "example": 1
                    },
                    "srg_id": {
                      "type": "string",
                      "description": "DISA SRG identifier string.",
                      "example": "Container_Platform_SRG"
                    },
                    "title": {
                      "type": "string",
                      "description": "Full title of the matched SRG.",
                      "example": "Container Platform Security Requirements Guide"
                    },
                    "version": {
                      "type": "string",
                      "description": "Version string (e.g. V2R4).",
                      "example": "V2R4"
                    }
                  }
                },
                "examples": {
                  "detected": {
                    "summary": "SRG detected from spreadsheet",
                    "value": {
                      "id": 1,
                      "srg_id": "Container_Platform_SRG",
                      "title": "Container Platform Security Requirements Guide",
                      "version": "V2R4"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "No file, no SRG IDs found, no match, or ambiguous match",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "No SRG IDs found in spreadsheet"
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/components/history": {
      "get": {
        "operationId": "getComponentHistory",
        "tags": [
          "Components"
        ],
        "summary": "Revision history for a named component across versions",
        "description": "Traces the version history of a component by name within a project. Returns an ordered array alternating between milestone entries (marking a version point) and diff entries (showing rule-level changes between consecutive releases). Used by the DiffViewer feature. Requires project membership. Component objects use ComponentBlueprint default view (9 fields).",
        "parameters": [
          {
            "name": "project_id",
            "in": "query",
            "required": true,
            "description": "ID of the project containing the component versions.",
            "schema": {
              "type": "integer"
            },
            "example": 1
          },
          {
            "name": "name",
            "in": "query",
            "required": true,
            "description": "Component name to trace across versions.",
            "schema": {
              "type": "string"
            },
            "example": "Photon OS 3"
          }
        ],
        "responses": {
          "200": {
            "description": "Ordered revision history. The array alternates between two entry types: 1. Milestone entries: { component: ComponentSummary } — marks a version point 2. Diff entries: { base_component: ComponentSummary, diff_component: ComponentSummary, changes: { rule_id: HistoryChangeEntry } }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "description": "Mixed array of milestone and diff entries. Milestones have a 'component' key. Diff entries have 'base_component', 'diff_component', and 'changes' keys.",
                  "items": {
                    "type": "object",
                    "properties": {
                      "component": {
                        "description": "Milestone entry — marks this version in the timeline.",
                        "$ref": "#/components/schemas/ComponentSummary"
                      },
                      "base_component": {
                        "description": "Previous version in a diff pair.",
                        "$ref": "#/components/schemas/ComponentSummary"
                      },
                      "diff_component": {
                        "description": "Current version in a diff pair.",
                        "$ref": "#/components/schemas/ComponentSummary"
                      },
                      "changes": {
                        "type": "object",
                        "description": "Rule-level changes keyed by rule_id. Each value describes what changed between the base and diff versions.",
                        "additionalProperties": {
                          "$ref": "#/components/schemas/HistoryChangeEntry"
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "two_versions": {
                    "summary": "Two versions of Photon OS 3 with milestone + diff",
                    "value": [
                      {
                        "component": {
                          "id": 1,
                          "name": "Photon OS 3",
                          "prefix": "PHOS-03",
                          "version": 1,
                          "release": 1
                        }
                      },
                      {
                        "base_component": {
                          "id": 1,
                          "name": "Photon OS 3",
                          "prefix": "PHOS-03",
                          "version": 1,
                          "release": 1
                        },
                        "diff_component": {
                          "id": 2,
                          "name": "Photon OS 3",
                          "prefix": "PHOS-03",
                          "version": 1,
                          "release": 2
                        },
                        "changes": {
                          "000050": {
                            "change": "updated",
                            "base": {
                              "rule_id": "000050",
                              "title": "Original title",
                              "fix": "Original fix text"
                            },
                            "diff": {
                              "rule_id": "000050",
                              "title": "Updated title",
                              "fix": "Updated fix text"
                            }
                          }
                        }
                      },
                      {
                        "component": {
                          "id": 2,
                          "name": "Photon OS 3",
                          "prefix": "PHOS-03",
                          "version": 1,
                          "release": 2
                        }
                      }
                    ]
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/components/{componentId}/related": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ComponentId"
        }
      ],
      "get": {
        "operationId": "searchRelatedComponents",
        "tags": [
          "Components"
        ],
        "summary": "Find components sharing the same SRG baseline",
        "description": "Returns other components that are based on the same SRG as this component. Used by the DiffViewer to find peer components for side-by-side comparison. Scoped to components the current user can access plus released components. Returns a hand-built hash (NOT ComponentBlueprint).",
        "responses": {
          "200": {
            "description": "Related components sharing the same SRG",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "integer",
                        "example": 4
                      },
                      "name": {
                        "type": "string",
                        "example": "Photon OS 3"
                      },
                      "version": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "example": 1
                      },
                      "prefix": {
                        "type": "string",
                        "example": "PHOS-03"
                      },
                      "release": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "example": 1
                      },
                      "project_id": {
                        "type": "integer",
                        "example": 3
                      },
                      "project_name": {
                        "type": "string",
                        "example": "vSphere 7.0"
                      }
                    }
                  }
                },
                "examples": {
                  "related": {
                    "summary": "Two peer components from different projects",
                    "value": [
                      {
                        "id": 4,
                        "name": "Photon OS 3",
                        "version": 1,
                        "prefix": "PHOS-03",
                        "release": 1,
                        "project_id": 3,
                        "project_name": "vSphere 7.0"
                      }
                    ]
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/components/{componentId}/find": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ComponentId"
        }
      ],
      "post": {
        "operationId": "findRulesInComponent",
        "tags": [
          "Components"
        ],
        "summary": "Search requirements within a component by text",
        "description": "Full-text search across requirement titles, fix text, vendor comments, status justification, artifact description, vulnerability discussion, mitigations, and check content within a single component. The same field list applies to both document kinds; results are shaped by the component's document_type. Returns matching requirements in rule_id order. Used by the in-component search feature.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "find"
                ],
                "properties": {
                  "find": {
                    "type": "string",
                    "description": "Case-insensitive search term matched against all rule text fields.",
                    "example": "container image"
                  }
                }
              },
              "examples": {
                "search": {
                  "summary": "Search for container image references",
                  "value": {
                    "find": "container image"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Matching requirements in rule_id order, shaped by the component's document_type: STIG rule objects for stig components, authored SRG requirement objects for srg components.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "oneOf": [
                      {
                        "$ref": "#/components/schemas/RuleEditorResponse"
                      },
                      {
                        "$ref": "#/components/schemas/AuthoredSrgRuleEditorResponse"
                      }
                    ]
                  }
                },
                "examples": {
                  "results": {
                    "summary": "Two matching rules",
                    "value": [
                      {
                        "id": 100,
                        "rule_id": "CNTR-00-000050",
                        "title": "Container images must be signed",
                        "status": "Applicable - Configurable",
                        "locked": false,
                        "satisfies": [],
                        "satisfied_by": []
                      },
                      {
                        "id": 101,
                        "rule_id": "CNTR-00-000051",
                        "title": "Container images must come from approved registries",
                        "status": "Applicable - Configurable",
                        "locked": false,
                        "satisfies": [],
                        "satisfied_by": []
                      }
                    ]
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/components/{componentId}/preview_spreadsheet_update": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ComponentId"
        }
      ],
      "post": {
        "operationId": "previewSpreadsheetUpdate",
        "tags": [
          "Components"
        ],
        "summary": "Preview changes from a spreadsheet import",
        "description": "Parses an uploaded spreadsheet (CSV/XLSX) and returns a diff of what would change if applied, without modifying any data. Returns four arrays: updated (rules with changes), unchanged (no diff), skipped_locked (locked rules or inherited rules), and warnings (SRG IDs not found). Requires author role on the component.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "CSV or XLSX spreadsheet file containing rule updates."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Preview of changes grouped by outcome",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "updated",
                    "unchanged",
                    "skipped_locked",
                    "warnings"
                  ],
                  "properties": {
                    "updated": {
                      "type": "array",
                      "description": "Rules with detected changes.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "rule_id": {
                            "type": "string",
                            "example": "000050"
                          },
                          "srg_id": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "example": "SRG-APP-000014-CTR-000035"
                          },
                          "changes": {
                            "type": "object",
                            "description": "Changed fields with [old_value, new_value] pairs.",
                            "additionalProperties": {
                              "type": "array",
                              "items": {}
                            }
                          }
                        }
                      }
                    },
                    "unchanged": {
                      "type": "array",
                      "description": "Rules with no differences from spreadsheet.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "rule_id": {
                            "type": "string"
                          },
                          "srg_id": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "reason": {
                            "type": "string",
                            "example": "no changes"
                          }
                        }
                      }
                    },
                    "skipped_locked": {
                      "type": "array",
                      "description": "Rules skipped because they are locked or inherited.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "rule_id": {
                            "type": "string"
                          },
                          "srg_id": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "reason": {
                            "type": "string",
                            "enum": [
                              "locked",
                              "inherited",
                              "section locked"
                            ],
                            "example": "locked"
                          },
                          "skipped_fields": {
                            "type": "array",
                            "description": "Fields that were locked (present only for section-locked skips).",
                            "items": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    },
                    "warnings": {
                      "type": "array",
                      "description": "SRG IDs from the spreadsheet that were not found in the component.",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "SRG ID SRG-APP-999999 not found in component"
                      ]
                    }
                  }
                },
                "examples": {
                  "preview": {
                    "summary": "Mixed preview with updates, unchanged, and skipped rules",
                    "value": {
                      "updated": [
                        {
                          "rule_id": "000050",
                          "srg_id": "SRG-APP-000014-CTR-000035",
                          "changes": {
                            "fixtext": [
                              "Old fix text",
                              "Updated fix text"
                            ]
                          }
                        }
                      ],
                      "unchanged": [
                        {
                          "rule_id": "000051",
                          "srg_id": "SRG-APP-000023-CTR-000040",
                          "reason": "no changes"
                        }
                      ],
                      "skipped_locked": [
                        {
                          "rule_id": "000001",
                          "srg_id": "SRG-APP-000001-CTR-000001",
                          "reason": "locked"
                        }
                      ],
                      "warnings": []
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "No file provided or parse error",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Error message."
                    }
                  }
                },
                "examples": {
                  "no_file": {
                    "summary": "No file uploaded",
                    "value": {
                      "error": "No file was provided. Please upload a CSV or XLSX file."
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/components/{componentId}/apply_spreadsheet_update": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ComponentId"
        }
      ],
      "patch": {
        "operationId": "applySpreadsheetUpdate",
        "tags": [
          "Components"
        ],
        "summary": "Apply changes from a spreadsheet import",
        "description": "Applies rule updates from an uploaded spreadsheet (CSV/XLSX) to the component. Requires admin role on the component. Use the preview endpoint first to review changes before applying. Creates audit trail entries for each modified rule.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "CSV or XLSX spreadsheet file containing rule updates."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Spreadsheet changes applied successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "success": {
                    "summary": "Rules updated from spreadsheet",
                    "value": {
                      "toast": {
                        "title": "Spreadsheet applied.",
                        "message": [
                          "Successfully updated 12 rules from spreadsheet."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "No file provided or application error",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                },
                "examples": {
                  "no_file": {
                    "summary": "No file uploaded",
                    "value": {
                      "error": "No file was provided. Please upload a CSV or XLSX file."
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/components/{componentId}/release": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ComponentId"
        }
      ],
      "post": {
        "operationId": "releaseComponent",
        "tags": [
          "Components"
        ],
        "summary": "Release an SRG component to the catalog",
        "description": "Releases an SRG-kind component in one transaction: verifies every live requirement is decided (never Not Yet Determined) and locked, mints the final published identifiers, generates the published SRG XCCDF, creates the catalog SecurityRequirementsGuide entry with its columns derived from that document, copies the published requirements onto the entry, and flags the component released. The released entry behaves exactly like an uploaded SRG — new components can base on it immediately. Requires the author role on the component. STIG readiness components do not use this endpoint (their release is the released flag on component update).",
        "responses": {
          "200": {
            "description": "Component released and attached to the catalog",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ComponentReleaseResponse"
                },
                "examples": {
                  "released": {
                    "summary": "First release of a derived SRG",
                    "value": {
                      "toast": {
                        "title": "Component released.",
                        "message": [
                          "Container Best Practice SRG - Ver 1, Rel 1 is now in the SRG catalog."
                        ],
                        "variant": "success"
                      },
                      "catalog_srg": {
                        "id": 42,
                        "srg_id": "Container_Best_Practice_SRG",
                        "version": "V1R1",
                        "name": "Container Best Practice SRG - Ver 1, Rel 1"
                      },
                      "changelog": {
                        "version": "V1R1",
                        "removals": [],
                        "text": "Container Best Practice SRG V1R1 — Release Changelog\n\nNo requirements were removed in this release."
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Release blocked — the component is not an SRG component, is already released, is missing its version/release pair or abbreviation, still has Not Yet Determined requirements, or has unlocked requirements. The toast messages carry every blocking reason.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "undecided": {
                    "summary": "Undecided requirements block the release",
                    "value": {
                      "toast": {
                        "title": "Could not release component.",
                        "message": [
                          "release is blocked: 3 live requirement(s) still Not Yet Determined — every requirement must be decided"
                        ],
                        "variant": "danger"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/components/bulk_export/{type}": {
      "parameters": [
        {
          "name": "type",
          "in": "path",
          "required": true,
          "description": "Export format for the bulk download.",
          "schema": {
            "type": "string",
            "enum": [
              "csv",
              "xccdf",
              "inspec"
            ]
          },
          "example": "csv"
        }
      ],
      "get": {
        "operationId": "bulkExportComponents",
        "tags": [
          "Components"
        ],
        "summary": "Bulk export multiple released components",
        "description": "Exports multiple released components as a single download in the specified format. Component IDs are passed as a comma-separated query parameter. Returns a binary file (zip for multiple components). Returns a JSON error toast if the export type is unsupported or no component IDs are provided.",
        "parameters": [
          {
            "name": "component_ids",
            "in": "query",
            "required": true,
            "description": "Comma-separated list of released component IDs to export.",
            "schema": {
              "type": "string"
            },
            "example": "29,30,31"
          }
        ],
        "responses": {
          "200": {
            "description": "Binary file download",
            "content": {
              "application/zip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/xml": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "Invalid export type or no components selected",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "bad_type": {
                    "summary": "Unsupported export type",
                    "value": {
                      "toast": {
                        "title": "Export error.",
                        "message": [
                          "Unsupported export type: pdf"
                        ],
                        "variant": "danger"
                      }
                    }
                  },
                  "no_components": {
                    "summary": "No components selected",
                    "value": {
                      "toast": {
                        "title": "Export error.",
                        "message": [
                          "No components selected for export."
                        ],
                        "variant": "danger"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/api/components/compare": {
      "get": {
        "operationId": "compareComponents",
        "tags": [
          "Components"
        ],
        "summary": "Side-by-side rule comparison between two peer components",
        "description": "Compares rules between a base (older) and diff (newer) component sharing the same SRG. Returns a rule-by-rule diff keyed by field name, with metadata about both components. Used by the DiffViewer stepper in the component editor. Requires authentication and access to both components.",
        "parameters": [
          {
            "name": "base_id",
            "in": "query",
            "required": true,
            "description": "ID of the base (older) component to compare from.",
            "schema": {
              "type": "integer"
            },
            "example": 29
          },
          {
            "name": "diff_id",
            "in": "query",
            "required": true,
            "description": "ID of the diff (newer) component to compare against.",
            "schema": {
              "type": "integer"
            },
            "example": 30
          }
        ],
        "responses": {
          "200": {
            "description": "Rule-by-rule diff with metadata envelope",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "description": "Diff keyed by rule_id. Each entry compares the inspec_control_file content between the base and diff components for that rule.",
                      "additionalProperties": {
                        "type": "object",
                        "properties": {
                          "base": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "InSpec control file content from the base component. Null if rule not present."
                          },
                          "diff": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "InSpec control file content from the diff component. Null if rule not present."
                          },
                          "changed": {
                            "type": "boolean",
                            "description": "Whether the InSpec content differs between versions."
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Metadata about the compared components.",
                      "properties": {
                        "base_id": {
                          "type": "integer",
                          "example": 29
                        },
                        "diff_id": {
                          "type": "integer",
                          "example": 30
                        },
                        "rules_count": {
                          "type": "integer",
                          "example": 264
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "diff": {
                    "summary": "Two rules compared by generated InSpec control",
                    "value": {
                      "data": {
                        "000050": {
                          "base": "control 'CNTR-00-000050' do\n  title 'Container images must be signed'\nend",
                          "diff": "control 'CNTR-00-000050' do\n  title 'Container images must be signed and verified'\nend",
                          "changed": true
                        },
                        "000051": {
                          "base": "control 'CNTR-00-000051' do\n  title 'Approved registries only'\nend",
                          "diff": "control 'CNTR-00-000051' do\n  title 'Approved registries only'\nend",
                          "changed": false
                        }
                      },
                      "meta": {
                        "base_id": 29,
                        "diff_id": 30,
                        "rules_count": 264
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/rules/{ruleId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/RuleId"
        }
      ],
      "get": {
        "operationId": "getRule",
        "tags": [
          "Rules"
        ],
        "summary": "Rule detail",
        "description": "Returns the full requirement editor payload for either document kind — checks, descriptions, and review history for both; satisfactions (satisfies/satisfied_by) appear on stig rules only, and authored SRG requirements omit those keys entirely.",
        "responses": {
          "200": {
            "description": "Rule data",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Shaped by the parent component's document_type: a STIG rule or an authored SRG requirement.",
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/RuleEditorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/AuthoredSrgRuleEditorResponse"
                    }
                  ]
                }
              }
            },
            "links": {
              "UpdateThisRule": {
                "operationId": "updateRule",
                "parameters": {
                  "ruleId": "$response.body#/id"
                }
              },
              "DeleteThisRule": {
                "operationId": "deleteRule",
                "parameters": {
                  "ruleId": "$response.body#/id"
                }
              },
              "RevertThisRule": {
                "operationId": "revertRule",
                "parameters": {
                  "ruleId": "$response.body#/id"
                }
              },
              "CreateReviewOnRule": {
                "operationId": "createRuleReview",
                "parameters": {
                  "ruleId": "$response.body#/id"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      },
      "patch": {
        "operationId": "patchRule",
        "tags": [
          "Rules"
        ],
        "summary": "Partial update of rule attributes",
        "description": "Partial update — send only changed fields. Updates one or more rule fields. Requires author or admin role on the component.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "rule": {
                    "$ref": "#/components/schemas/RuleInput"
                  }
                }
              },
              "examples": {
                "stig_update": {
                  "summary": "STIG rule status change",
                  "value": {
                    "rule": {
                      "status": "Applicable - Configurable",
                      "vendor_comments": "Met by the recommended runtime configuration."
                    }
                  }
                },
                "authored_srg_update": {
                  "summary": "Authored SRG requirement decided Not Applicable",
                  "value": {
                    "rule": {
                      "status": "Not Applicable",
                      "status_justification": "The platform provides no wireless interfaces to configure."
                    }
                  }
                },
                "nested_content": {
                  "summary": "Update check content and record an audit comment",
                  "value": {
                    "rule": {
                      "audit_comment": "Check procedure clarified per review feedback.",
                      "checks_attributes": [
                        {
                          "id": 311,
                          "content": "Review the container platform configuration to verify approved authorizations are enforced."
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rule updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      },
      "put": {
        "operationId": "updateRule",
        "tags": [
          "Rules"
        ],
        "summary": "Full replacement of rule attributes",
        "description": "Full replacement — all fields required. Updates rule fields. Requires author or admin role on the component.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "rule": {
                    "$ref": "#/components/schemas/RuleInput"
                  }
                }
              },
              "examples": {
                "stig_update": {
                  "summary": "STIG rule full update",
                  "value": {
                    "rule": {
                      "status": "Applicable - Configurable",
                      "title": "The container platform must enforce approved authorizations.",
                      "fixtext": "Configure the container platform to enforce approved authorizations.",
                      "vendor_comments": "Met by the recommended runtime configuration."
                    }
                  }
                },
                "authored_srg_update": {
                  "summary": "Authored SRG requirement update",
                  "value": {
                    "rule": {
                      "status": "Applicable",
                      "title": "The application must enforce approved authorizations for logical access.",
                      "fixtext": "Configure the application to enforce approved authorizations."
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rule updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      },
      "delete": {
        "operationId": "deleteRule",
        "tags": [
          "Rules"
        ],
        "summary": "Soft-delete a rule",
        "description": "Marks the rule as deleted. Requires admin role on the component.",
        "responses": {
          "200": {
            "description": "Rule deleted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/rules/{ruleId}/revert": {
      "parameters": [
        {
          "$ref": "#/components/parameters/RuleId"
        }
      ],
      "post": {
        "operationId": "revertRule",
        "tags": [
          "Rules"
        ],
        "summary": "Revert requirement fields to a previous audit version",
        "description": "Restores the named audited fields to their prior values from the specified history entry. Applies to both document kinds — stig rules and authored SRG requirements. Requires author or admin role.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "audit_id": {
                    "type": "integer",
                    "description": "The audit history entry to revert from.",
                    "example": 4211
                  },
                  "fields": {
                    "type": "array",
                    "description": "The audited field names to restore.",
                    "items": {
                      "type": "string"
                    },
                    "example": [
                      "title"
                    ]
                  },
                  "audit_comment": {
                    "type": "string",
                    "description": "Recorded on the audit trail with the revert.",
                    "example": "Restore the original wording"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rule reverted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/rules/{ruleId}/section_locks": {
      "parameters": [
        {
          "$ref": "#/components/parameters/RuleId"
        }
      ],
      "patch": {
        "operationId": "updateSectionLocks",
        "tags": [
          "Rules"
        ],
        "summary": "Lock or unlock a single section on a rule",
        "description": "Locks or unlocks a single section (e.g., Fix, Check) on a rule. Requires reviewer role or higher on the parent component. Creates an audit entry.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "section",
                  "locked"
                ],
                "properties": {
                  "section": {
                    "type": "string",
                    "description": "Section name to lock or unlock, from the lockable-section vocabulary.",
                    "enum": [
                      "Title",
                      "Severity",
                      "Status",
                      "Fix",
                      "Check",
                      "Vulnerability Discussion",
                      "DISA Metadata",
                      "Vendor Comments",
                      "Artifact Description",
                      "XCCDF Metadata"
                    ],
                    "example": "Fix"
                  },
                  "locked": {
                    "type": "boolean",
                    "description": "Whether to lock (true) or unlock (false).",
                    "example": true
                  },
                  "comment": {
                    "type": "string",
                    "description": "Optional audit comment.",
                    "example": "Locking the Fix section for final review"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Section locks updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RuleSectionLockResponse"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/rules/{ruleId}/bulk_section_locks": {
      "parameters": [
        {
          "$ref": "#/components/parameters/RuleId"
        }
      ],
      "patch": {
        "operationId": "bulkSectionLocks",
        "tags": [
          "Rules"
        ],
        "summary": "Bulk update locked sections",
        "description": "Locks or unlocks multiple sections at once on a rule. Requires reviewer role or higher on the parent component. Creates an audit trail entry.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "sections",
                  "locked"
                ],
                "properties": {
                  "sections": {
                    "type": "array",
                    "description": "Section names to lock or unlock, from the lockable-section vocabulary.",
                    "items": {
                      "type": "string",
                      "enum": [
                        "Title",
                        "Severity",
                        "Status",
                        "Fix",
                        "Check",
                        "Vulnerability Discussion",
                        "DISA Metadata",
                        "Vendor Comments",
                        "Artifact Description",
                        "XCCDF Metadata"
                      ]
                    },
                    "example": [
                      "Fix",
                      "Check"
                    ]
                  },
                  "locked": {
                    "type": "boolean",
                    "description": "Whether to lock (true) or unlock (false) the sections.",
                    "example": true
                  },
                  "comment": {
                    "type": "string",
                    "description": "Optional audit comment explaining the lock change.",
                    "example": "Locking the Fix and Check sections for review"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Section locks updated, returns updated rule + toast",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RuleSectionLockResponse"
                }
              }
            }
          },
          "422": {
            "description": "Invalid section names",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Names every unrecognized section.",
                      "example": "Invalid sections: NotASection"
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/rules/{ruleId}/search/related_rules": {
      "parameters": [
        {
          "$ref": "#/components/parameters/RuleId"
        }
      ],
      "get": {
        "operationId": "getRelatedRules",
        "tags": [
          "Rules"
        ],
        "summary": "Find rules sharing the same SRG requirement",
        "description": "Returns rules from other components and published STIGs that implement the same SRG requirement (matched by version/srg_id). Results are scoped to components the current user can access unless the user is an admin. The rules array is a mix of RuleEditorResponse (component rules) and StigRuleSummary (published STIG rules). The parents array is a mix of ComponentBlueprint :related and StigBlueprint :index for grouping. Stig rules only — the search keys off the stig rule's SRG-version linkage, so requests for an authored SRG requirement return 404 (no related-rules surface exists for that kind).",
        "responses": {
          "200": {
            "description": "Related rules with parent containers",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "rules",
                    "parents"
                  ],
                  "properties": {
                    "rules": {
                      "type": "array",
                      "description": "Mixed array of component rules (RuleEditorResponse, 38 fields) and published STIG rules (StigRuleSummary, 16 fields).",
                      "items": {}
                    },
                    "parents": {
                      "type": "array",
                      "description": "Mixed array of parent containers: ComponentBlueprint :related (with nested project) and StigBlueprint :index (with severity_counts).",
                      "items": {}
                    }
                  }
                },
                "examples": {
                  "related": {
                    "summary": "Rules from another component and a published STIG",
                    "value": {
                      "rules": [
                        {
                          "id": 200,
                          "rule_id": "SV-222387r960735_rule",
                          "title": "The application must limit logon sessions",
                          "version": "APSC-DV-000010",
                          "rule_severity": "medium"
                        },
                        {
                          "id": 1800,
                          "rule_id": "000001",
                          "title": "The operating system must provide automated mechanisms",
                          "status": "Not Yet Determined",
                          "component_id": 1
                        }
                      ],
                      "parents": [
                        {
                          "id": 1,
                          "stig_id": "Application_Security_Development_STIG",
                          "name": "Application Security Development STIG - Ver 6, Rel 4",
                          "title": "Application Security and Development STIG"
                        },
                        {
                          "id": 1,
                          "name": "Photon OS 3",
                          "prefix": "PHOS-03",
                          "project": {
                            "id": 1,
                            "name": "Photon 3"
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/rule_satisfactions": {
      "post": {
        "operationId": "addSatisfaction",
        "tags": [
          "Rules"
        ],
        "summary": "Create a satisfaction relationship between two rules",
        "description": "Links a child rule to a parent rule that satisfies it. Triggers ADNM status automation on the child.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "rule_id",
                  "satisfied_by_rule_id"
                ],
                "properties": {
                  "rule_id": {
                    "type": "integer",
                    "description": "The rule that is satisfied. Absence is a 400.",
                    "example": 101
                  },
                  "satisfied_by_rule_id": {
                    "type": "integer",
                    "description": "The rule that satisfies it. Absence is a 400.",
                    "example": 102
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Satisfaction created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/rule_satisfactions/{ruleId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/RuleId"
        }
      ],
      "delete": {
        "operationId": "removeSatisfaction",
        "tags": [
          "Rules"
        ],
        "summary": "Remove a satisfaction relationship",
        "description": "Removes the parent-child satisfaction link and reverts ADNM status on the child rule.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "rule_id": {
                    "type": "integer"
                  },
                  "satisfied_by_rule_id": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Satisfaction removed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/rules/{ruleId}/relocations": {
      "parameters": [
        {
          "$ref": "#/components/parameters/RuleId"
        }
      ],
      "post": {
        "operationId": "createRequirementRelocation",
        "tags": [
          "Rules"
        ],
        "summary": "Propose relocating an authored SRG requirement",
        "description": "Creates an OPEN relocation proposal for an authored requirement of an SRG component, naming the destination SRG's abbreviation. Requires author role on the component. One open proposal per requirement; a second returns 422. The proposal is a record, never a status change on the requirement — the destination SRG's authors concur or non-concur.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "requirement_relocation"
                ],
                "properties": {
                  "requirement_relocation": {
                    "type": "object",
                    "required": [
                      "target_technology_token"
                    ],
                    "properties": {
                      "target_technology_token": {
                        "type": "string",
                        "description": "The destination SRG's abbreviation — the short code its requirement IDs start with (CTR, GPOS, DB).",
                        "example": "CTR"
                      }
                    }
                  }
                }
              },
              "examples": {
                "mark": {
                  "summary": "Propose relocation to the Container SRG",
                  "value": {
                    "requirement_relocation": {
                      "target_technology_token": "CTR"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Relocation proposed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "proposed": {
                    "summary": "Proposal created",
                    "value": {
                      "toast": {
                        "title": "Relocation proposed.",
                        "message": [
                          "Proposed for the CTR SRG."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Duplicate open proposal or ineligible source",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "duplicate": {
                    "summary": "Already proposed",
                    "value": {
                      "toast": {
                        "title": "Could not propose the relocation.",
                        "message": [
                          "Source rule already has a pending relocation"
                        ],
                        "variant": "danger"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/requirement_relocations": {
      "get": {
        "operationId": "listRequirementRelocations",
        "tags": [
          "Rules"
        ],
        "summary": "List the open relocation-proposal backlog",
        "description": "Returns relocation proposals, optionally filtered to one destination SRG's abbreviation — the standing per-SRG backlog and the creation/open-time prompt count both read this. Rows are scoped to projects the caller can see. Executed records are immutable history and never listed.",
        "parameters": [
          {
            "name": "target_technology_token",
            "in": "query",
            "required": false,
            "description": "The destination SRG's abbreviation to filter by.",
            "schema": {
              "type": "string"
            },
            "example": "CTR"
          }
        ],
        "responses": {
          "200": {
            "description": "Open proposals (and retained declines), oldest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/RequirementRelocationSummary"
                  }
                },
                "examples": {
                  "backlog": {
                    "summary": "One open proposal for the CTR SRG",
                    "value": [
                      {
                        "id": 12,
                        "source_rule_id": 5137,
                        "target_technology_token": "CTR",
                        "created_at": "2026-07-20 15:36:06 UTC",
                        "source_displayed_name": "CNTR-00-000051",
                        "component_id": 42,
                        "component_name": "Container Platform SRG",
                        "requested_by_name": "Jane Doe"
                      }
                    ]
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/requirement_relocations/destinations": {
      "get": {
        "operationId": "listRelocationDestinations",
        "tags": [
          "Rules"
        ],
        "summary": "List destination SRG options for the propose flow",
        "description": "Serves the Destination SRG picker: one row per SRG abbreviation across the SRG components in projects the caller can see (member or discoverable). An open component wins its abbreviation's row; released true marks the queued next-release case. Hidden projects' SRGs never appear — proposing to them remains possible through the free abbreviation entry, which discloses nothing.",
        "responses": {
          "200": {
            "description": "Destination options, ordered by abbreviation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/RelocationDestination"
                  }
                },
                "examples": {
                  "destinations": {
                    "summary": "One open component and one queued SRG",
                    "value": [
                      {
                        "token": "CTR",
                        "name": "Container Platform SRG",
                        "released": false
                      },
                      {
                        "token": "GPOS",
                        "name": "General Purpose Operating System SRG",
                        "released": true
                      }
                    ]
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/requirement_relocations/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Numeric ID of the pending relocation record.",
          "schema": {
            "type": "integer"
          },
          "example": 12
        }
      ],
      "delete": {
        "operationId": "deleteRequirementRelocation",
        "tags": [
          "Rules"
        ],
        "summary": "Un-mark a requirement for relocation",
        "description": "Destroys a PENDING relocation record, removing the move marker. Audited. Requires author role on the source component. Executed records are immutable — they answer 404 exactly like a record that never existed.",
        "responses": {
          "200": {
            "description": "Marker removed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "unmarked": {
                    "summary": "Marker removed",
                    "value": {
                      "toast": {
                        "title": "Relocation marker removed.",
                        "message": [
                          "The requirement is no longer marked for relocation."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/requirement_relocations/{id}/dry_run": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Numeric ID of the open relocation proposal.",
          "schema": {
            "type": "integer"
          },
          "example": 12
        }
      ],
      "post": {
        "operationId": "dryRunRequirementRelocation",
        "tags": [
          "Rules"
        ],
        "summary": "Preview accepting a relocation with zero writes",
        "description": "Returns exactly what accepting the proposal would do for this destination component — or every reason it cannot run — without writing anything. This preview is the adjudication review artifact. Requires author role on the DESTINATION component only. Adjudicated proposals (declined or executed) answer 404 like a record that never existed.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "target_component_id"
                ],
                "properties": {
                  "target_component_id": {
                    "type": "integer",
                    "description": "The destination SRG component.",
                    "example": 42
                  }
                }
              },
              "examples": {
                "preview": {
                  "summary": "Preview a move into component 42",
                  "value": {
                    "target_component_id": 42
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The preview, valid or not",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RequirementRelocationDryRun"
                },
                "examples": {
                  "valid": {
                    "summary": "Move can run",
                    "value": {
                      "valid": true,
                      "errors": [],
                      "source_displayed_name": "CNTR-00-000051",
                      "target_component_id": 42,
                      "target_component_name": "Container Platform SRG",
                      "would_create": {
                        "title": "The application must enforce approved authorizations",
                        "status": "Applicable",
                        "derived_from_srg_rule_id": 5137
                      },
                      "would_tombstone_source": true
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/requirement_relocations/{id}/accept": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Numeric ID of the open relocation proposal.",
          "schema": {
            "type": "integer"
          },
          "example": 12
        }
      ],
      "post": {
        "operationId": "acceptRequirementRelocation",
        "tags": [
          "Rules"
        ],
        "summary": "Concur with a relocation proposal — land the requirement",
        "description": "Receiver-side adjudication (displayed as Concur): acceptance and landing are ONE transaction — creates the requirement in the destination component with content and core lineage carried over, tombstones the source row, and stamps the record executed with the accepting actor — atomically, or not at all. Requires author role on the DESTINATION component only; the proposal itself carries source consent. Audited. Adjudicated proposals (declined or executed) answer 404 like a record that never existed.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "target_component_id"
                ],
                "properties": {
                  "target_component_id": {
                    "type": "integer",
                    "description": "The destination SRG component.",
                    "example": 42
                  }
                }
              },
              "examples": {
                "accept": {
                  "summary": "Concur — land the move into component 42",
                  "value": {
                    "target_component_id": 42
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Proposal concurred with and requirement landed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RequirementRelocationAcceptResponse"
                },
                "examples": {
                  "accepted": {
                    "summary": "Move landed",
                    "value": {
                      "toast": {
                        "title": "Concurred.",
                        "message": [
                          "Moved to Container Platform SRG — the source requirement is now history."
                        ],
                        "variant": "success"
                      },
                      "landed_rule_id": 5137
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "The move cannot run (validation errors from the preview)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "invalid": {
                    "summary": "Ineligible destination",
                    "value": {
                      "toast": {
                        "title": "Could not accept the proposal.",
                        "message": [
                          "target component must be an SRG component"
                        ],
                        "variant": "danger"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/requirement_relocations/{id}/decline": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Numeric ID of the open relocation proposal.",
          "schema": {
            "type": "integer"
          },
          "example": 12
        }
      ],
      "post": {
        "operationId": "declineRequirementRelocation",
        "tags": [
          "Rules"
        ],
        "summary": "Non-concur with a relocation proposal, with a rationale",
        "description": "Receiver-side adjudication (displayed as Non-concur): declines the proposal with a REQUIRED rationale. The record is retained as terminal history — never destroyed — and the rationale surfaces to the source author in the backlog, so the refusal communicates back across the ownership boundary. Requires author role on the destination component named in the request, and that component must be an ELIGIBLE receiver — an unreleased SRG component, other than the source, that declares the source requirement's core SRG (the same eligibility accept enforces). Audited. A declined source may be proposed again.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "target_component_id",
                  "requirement_relocation"
                ],
                "properties": {
                  "target_component_id": {
                    "type": "integer",
                    "description": "The destination component the decliner adjudicates for — establishes receiver-side authority.",
                    "example": 42
                  },
                  "requirement_relocation": {
                    "type": "object",
                    "required": [
                      "adjudication_rationale"
                    ],
                    "properties": {
                      "adjudication_rationale": {
                        "type": "string",
                        "description": "Why the proposal is declined — shown to the source author.",
                        "example": "Covered by CNTR-00-000001 already."
                      }
                    }
                  }
                }
              },
              "examples": {
                "decline": {
                  "summary": "Decline with the reason",
                  "value": {
                    "target_component_id": 42,
                    "requirement_relocation": {
                      "adjudication_rationale": "Covered by CNTR-00-000001 already."
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Proposal non-concurred with and retained with the rationale",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "declined": {
                    "summary": "Non-concur recorded",
                    "value": {
                      "toast": {
                        "title": "Non-concurred.",
                        "message": [
                          "The source author can see your rationale in the backlog."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "The rationale is missing, or the named component is not an eligible receiver",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "missing_rationale": {
                    "summary": "Rationale required",
                    "value": {
                      "toast": {
                        "title": "Could not decline the proposal.",
                        "message": [
                          "Adjudication rationale can't be blank"
                        ],
                        "variant": "danger"
                      }
                    }
                  },
                  "ineligible_receiver": {
                    "summary": "Anchored to an ineligible component",
                    "value": {
                      "toast": {
                        "title": "Could not decline the proposal.",
                        "message": [
                          "target component must be an SRG component"
                        ],
                        "variant": "danger"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/rules/{ruleId}/reviews": {
      "parameters": [
        {
          "$ref": "#/components/parameters/RuleId"
        }
      ],
      "post": {
        "operationId": "createRuleReview",
        "tags": [
          "Reviews"
        ],
        "summary": "Create a review on a rule (comment, request_review, etc.)",
        "description": "Posts a new comment or review action on the specified rule. Requires viewer role or above on the component.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "review": {
                    "$ref": "#/components/schemas/ReviewInput"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Review created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/components/{componentId}/reviews": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ComponentId"
        }
      ],
      "post": {
        "operationId": "createComponentReview",
        "tags": [
          "Reviews"
        ],
        "summary": "Post a public comment on a component",
        "description": "Creates a new comment-action review on the component (component-level comment) or on a specific rule within it. The component must have an open comment period (accepting_new_comments? check). Requires project membership with at least viewer role.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "review": {
                    "$ref": "#/components/schemas/ReviewInput"
                  }
                }
              },
              "examples": {
                "component_comment": {
                  "summary": "Post a component-level comment",
                  "value": {
                    "review": {
                      "action": "comment",
                      "comment": "The overall approach to container isolation is sound.",
                      "section": null
                    }
                  }
                },
                "threaded_reply": {
                  "summary": "Reply to an existing component-level comment",
                  "value": {
                    "review": {
                      "action": "comment",
                      "comment": "Agreed — the isolation model covers this case.",
                      "section": null,
                      "responding_to_review_id": 44
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Comment created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "created": {
                    "summary": "Comment posted",
                    "value": {
                      "toast": {
                        "title": "Comment posted.",
                        "message": [
                          "Your comment has been submitted for review."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/reviews/bulk_triage": {
      "patch": {
        "operationId": "bulkTriageReviews",
        "tags": [
          "Reviews"
        ],
        "summary": "Triage multiple reviews at once",
        "description": "Applies the same triage status to all specified reviews in a single request. Parameters are FLAT (no wrapper object). All reviews must belong to the same component (enforced server-side). Terminal statuses (duplicate, informational, withdrawn, addressed_by) auto-adjudicate every selected review. Conditional requirements: non_concur requires response_comment; duplicate requires duplicate_of_review_id; addressed_by requires addressed_by_rule_id. The target is ONE shared value applied to every selected review — many comments duplicating one canonical thread, or addressed by one rule. A duplicate canonical that is itself among review_ids is rejected. Every per-comment validator runs per selected review (same-component canonical, no chained duplicates); any failing review rolls back the whole batch. Optionally creates a response comment on each triaged review. Requires author+ role on the component's project.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "review_ids",
                  "triage_status"
                ],
                "properties": {
                  "review_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "IDs of the reviews to triage.",
                    "example": [
                      10,
                      11,
                      12
                    ]
                  },
                  "triage_status": {
                    "type": "string",
                    "description": "The triage decision applied to all selected reviews. Every value except \"pending\" is accepted; \"pending\" is rejected as a non-decision.",
                    "enum": [
                      "concur",
                      "concur_with_comment",
                      "non_concur",
                      "needs_clarification",
                      "duplicate",
                      "informational",
                      "withdrawn",
                      "addressed_by"
                    ],
                    "example": "concur"
                  },
                  "duplicate_of_review_id": {
                    "type": "integer",
                    "description": "REQUIRED when triage_status is \"duplicate\" — the one canonical review every selected review duplicates. Must be a comment in the same component, must not itself be a duplicate, and must not appear in review_ids.",
                    "example": 42
                  },
                  "addressed_by_rule_id": {
                    "type": "integer",
                    "description": "REQUIRED when triage_status is \"addressed_by\" — the one rule that addresses every selected review.",
                    "example": 7
                  },
                  "response_comment": {
                    "type": "string",
                    "description": "REQUIRED when triage_status is \"non_concur\"; optional otherwise. Posted as a reply to each triaged review.",
                    "example": "Accepted — will update check_content in next release."
                  }
                }
              },
              "examples": {
                "concur": {
                  "summary": "Accept several comments at once (flat body)",
                  "value": {
                    "review_ids": [
                      10,
                      11,
                      12
                    ],
                    "triage_status": "concur"
                  }
                },
                "duplicate": {
                  "summary": "Mark a group as duplicates of one canonical thread",
                  "value": {
                    "review_ids": [
                      10,
                      11,
                      12
                    ],
                    "triage_status": "duplicate",
                    "duplicate_of_review_id": 42
                  }
                },
                "addressed_by": {
                  "summary": "Mark a group as addressed by one rule",
                  "value": {
                    "review_ids": [
                      10,
                      11,
                      12
                    ],
                    "triage_status": "addressed_by",
                    "addressed_by_rule_id": 7
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "All reviews triaged successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkTriageResponse"
                },
                "examples": {
                  "triaged": {
                    "summary": "Three reviews accepted",
                    "value": {
                      "reviews": [
                        {
                          "id": 10,
                          "action": "comment",
                          "comment": "Original comment 1",
                          "triage_status": "concur",
                          "rule_id": 100,
                          "created_at": "2026-05-19 14:08:17 UTC",
                          "reactions": {
                            "up": 0,
                            "down": 0,
                            "mine": null
                          }
                        }
                      ],
                      "response_reviews": []
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error — invalid or \"pending\" triage status, reviews spanning components, a missing conditional target, a duplicate canonical among review_ids, a cross-component or chained-duplicate canonical, or any selected review failing its per-comment validators (the batch rolls back atomically).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                }
              }
            }
          }
        }
      }
    },
    "/reviews/merge": {
      "patch": {
        "operationId": "mergeReviews",
        "tags": [
          "Reviews"
        ],
        "summary": "Merge duplicate reviews into one survivor",
        "description": "Combines multiple same-author reviews within one component into a designated survivor. Non-survivor reviews are marked as duplicates (triage_status=duplicate, duplicate_of_review_id=survivor). Requires admin role on the component's project.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "review_ids",
                  "survivor_id"
                ],
                "properties": {
                  "review_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "IDs of all reviews to merge (including the survivor).",
                    "example": [
                      10,
                      11,
                      12
                    ]
                  },
                  "survivor_id": {
                    "type": "integer",
                    "description": "ID of the review that absorbs the others.",
                    "example": 10
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reviews merged successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MergeResponse"
                },
                "examples": {
                  "merged": {
                    "summary": "Two duplicates merged into survivor",
                    "value": {
                      "survivor": {
                        "id": 10,
                        "action": "comment",
                        "comment": "The original comment",
                        "triage_status": "pending",
                        "rule_id": 100,
                        "created_at": "2026-05-19 14:08:17 UTC",
                        "reactions": {
                          "up": 0,
                          "down": 0,
                          "mine": null
                        }
                      },
                      "duplicates": [
                        {
                          "id": 11,
                          "action": "comment",
                          "comment": "Duplicate of 10",
                          "triage_status": "duplicate",
                          "duplicate_of_review_id": 10,
                          "rule_id": 100,
                          "created_at": "2026-05-19 14:10:00 UTC",
                          "reactions": {
                            "up": 0,
                            "down": 0,
                            "mine": null
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error (reviews from different components, different authors, etc.)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                }
              }
            }
          }
        }
      }
    },
    "/reviews/{reviewId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ReviewId"
        }
      ],
      "put": {
        "operationId": "updateReview",
        "tags": [
          "Reviews"
        ],
        "summary": "Update a review comment",
        "description": "Edits the comment text of an existing review. Only the original author or an admin can update.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "review"
                ],
                "properties": {
                  "review": {
                    "$ref": "#/components/schemas/ReviewUpdateInput"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Review updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReviewWrapper"
                }
              }
            },
            "links": {
              "GetReviewResponses": {
                "operationId": "getReviewResponses",
                "parameters": {
                  "reviewId": "$response.body#/review/id"
                }
              },
              "TriageReview": {
                "operationId": "triageReview",
                "parameters": {
                  "reviewId": "$response.body#/review/id"
                }
              },
              "WithdrawReview": {
                "operationId": "withdrawReview",
                "parameters": {
                  "reviewId": "$response.body#/review/id"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/reviews/{reviewId}/triage": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ReviewId"
        }
      ],
      "patch": {
        "operationId": "triageReview",
        "tags": [
          "Reviews"
        ],
        "summary": "Set triage status on a comment (concur, non_concur, etc.)",
        "description": "Assigns a triage decision to the comment. Parameters are FLAT (no wrapper object). Terminal statuses (duplicate, informational, withdrawn, addressed_by) auto-adjudicate. Submitting \"pending\" is rejected — it is the initial state, not a decision. Conditional requirements: non_concur requires response_comment; duplicate requires duplicate_of_review_id; addressed_by requires addressed_by_rule_id. Requires author or admin role.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "triage_status"
                ],
                "properties": {
                  "triage_status": {
                    "type": "string",
                    "description": "The triage decision. Every value except \"pending\" is accepted; \"pending\" is rejected as a non-decision.",
                    "enum": [
                      "concur",
                      "concur_with_comment",
                      "non_concur",
                      "needs_clarification",
                      "duplicate",
                      "informational",
                      "withdrawn",
                      "addressed_by"
                    ],
                    "example": "concur"
                  },
                  "duplicate_of_review_id": {
                    "type": "integer",
                    "description": "REQUIRED when triage_status is \"duplicate\" — the canonical review this comment duplicates.",
                    "example": 42
                  },
                  "addressed_by_rule_id": {
                    "type": "integer",
                    "description": "REQUIRED when triage_status is \"addressed_by\" — the rule that addresses this comment.",
                    "example": 7
                  },
                  "response_comment": {
                    "type": "string",
                    "description": "REQUIRED when triage_status is \"non_concur\"; optional otherwise. Posted as a reply to the comment.",
                    "example": "Declining — the requirement text already covers this."
                  }
                }
              },
              "examples": {
                "concur": {
                  "summary": "Simple concur (flat body)",
                  "value": {
                    "triage_status": "concur"
                  }
                },
                "non_concur": {
                  "summary": "Decline with the required response",
                  "value": {
                    "triage_status": "non_concur",
                    "response_comment": "Declining — covered by the fix text."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Triage status set",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TriageResponse"
                }
              }
            },
            "links": {
              "AdjudicateTriagedReview": {
                "operationId": "adjudicateReview",
                "parameters": {
                  "reviewId": "$response.body#/review/id"
                },
                "description": "Close the review after triage is complete."
              },
              "ReopenTriagedReview": {
                "operationId": "reopenReview",
                "parameters": {
                  "reviewId": "$response.body#/review/id"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/reviews/{reviewId}/adjudicate": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ReviewId"
        }
      ],
      "patch": {
        "operationId": "adjudicateReview",
        "tags": [
          "Reviews"
        ],
        "summary": "Adjudicate (close) a triaged comment",
        "description": "Marks the comment as adjudicated, finalizing the triage decision. Requires author or admin role.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "resolution_comment": {
                    "type": "string",
                    "description": "Optional final-resolution text. When supplied, a child Review (action=comment, threaded via responding_to_review_id with the parent's section) is created atomically with the adjudication and returned as response_review, so the resolution renders inline in the comment thread.",
                    "example": "Fix text updated in R2 — closing as concur."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Review adjudicated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TriageResponse"
                }
              }
            },
            "links": {
              "ReopenAdjudicatedReview": {
                "operationId": "reopenReview",
                "parameters": {
                  "reviewId": "$response.body#/review/id"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/reviews/{reviewId}/withdraw": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ReviewId"
        }
      ],
      "patch": {
        "operationId": "withdrawReview",
        "tags": [
          "Reviews"
        ],
        "summary": "Author withdraws their own comment",
        "description": "Allows the comment author to retract their comment. Sets triage_status to withdrawn and auto-adjudicates.",
        "responses": {
          "200": {
            "description": "Review withdrawn",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReviewWrapper"
                }
              }
            },
            "links": {
              "AdminRestoreWithdrawnReview": {
                "operationId": "adminRestoreReview",
                "parameters": {
                  "reviewId": "$response.body#/review/id"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/reviews/{reviewId}/admin_withdraw": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ReviewId"
        }
      ],
      "patch": {
        "operationId": "adminWithdrawReview",
        "tags": [
          "Reviews"
        ],
        "summary": "Admin force-withdraws a comment",
        "description": "Admin-only force withdrawal that bypasses the frozen-for-writes check. Requires an audit comment explaining the action.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "audit_comment"
                ],
                "properties": {
                  "audit_comment": {
                    "type": "string",
                    "maxLength": 4096,
                    "description": "Operator-supplied reason for the force-withdrawal, recorded on the audit trail. Required — a blank comment returns 422.",
                    "example": "Removing PII discovered after the comment window closed."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Review withdrawn by admin",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReviewWrapper"
                }
              }
            },
            "links": {
              "AdminRestoreWithdrawnReview": {
                "operationId": "adminRestoreReview",
                "parameters": {
                  "reviewId": "$response.body#/review/id"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/reviews/{reviewId}/admin_restore": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ReviewId"
        }
      ],
      "patch": {
        "operationId": "adminRestoreReview",
        "tags": [
          "Reviews"
        ],
        "summary": "Admin restores an adjudicated comment to pending",
        "description": "Inverse of admin_withdraw and any other adjudication — resets the comment to pending status so it can be re-triaged through the normal flow. Rejects comments that are not adjudicated (nothing to restore from). Requires admin role on the project and an audit comment explaining the action.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "audit_comment"
                ],
                "properties": {
                  "audit_comment": {
                    "type": "string",
                    "maxLength": 4096,
                    "description": "Operator-supplied reason for the restore, recorded on the audit trail. Required — a blank comment returns 422.",
                    "example": "Restoring — the wrong comment was force-withdrawn."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Review restored",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReviewWrapper"
                }
              }
            },
            "links": {
              "TriageRestoredReview": {
                "operationId": "triageReview",
                "parameters": {
                  "reviewId": "$response.body#/review/id"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/reviews/{reviewId}/move_to_rule": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ReviewId"
        }
      ],
      "patch": {
        "operationId": "moveReviewToRule",
        "tags": [
          "Reviews"
        ],
        "summary": "Admin moves a comment thread to a different rule",
        "description": "Reassigns the comment and all its replies to a different requirement row in the same component. The target may be either document kind — a stig rule or an authored SRG requirement (the lookup spans both). Walks parent-first to satisfy validators and records an outbound audit entry on the source rule. Requires admin role.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "rule_id",
                  "audit_comment"
                ],
                "properties": {
                  "rule_id": {
                    "type": "integer",
                    "description": "Database id of the target requirement row (either kind), which must belong to the same component as the source rule.",
                    "example": 15117
                  },
                  "audit_comment": {
                    "type": "string",
                    "maxLength": 4096,
                    "description": "Operator-supplied reason for the move, recorded on the audit trail. Required — a blank comment returns 422.",
                    "example": "Comment applies to the account-management requirement."
                  }
                }
              },
              "examples": {
                "move": {
                  "summary": "Move a thread to another requirement",
                  "value": {
                    "rule_id": 15117,
                    "audit_comment": "Comment applies to the account-management requirement."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Review moved — returns the updated review.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReviewWrapper"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Move refused — the target is the source rule itself, the target belongs to a different component, or the audit comment is blank.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "same_rule": {
                    "summary": "Target is the source rule (no move performed)",
                    "value": {
                      "toast": {
                        "title": "Cannot move.",
                        "message": [
                          "Target rule is the same as the source rule."
                        ],
                        "variant": "warning"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/reviews/{reviewId}/admin_destroy": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ReviewId"
        }
      ],
      "delete": {
        "operationId": "adminDestroyReview",
        "tags": [
          "Reviews"
        ],
        "summary": "Admin permanently deletes a comment (irreversible)",
        "description": "Hard-deletes a comment and all its replies. Requires an audit comment explaining the action — the pre-destroy snapshot plus that comment become the audit record. (The web UI additionally asks the admin to type the comment ID to confirm; the API has no confirmation field.) This action cannot be undone.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "audit_comment"
                ],
                "properties": {
                  "audit_comment": {
                    "type": "string",
                    "maxLength": 4096,
                    "description": "Operator-supplied reason for the hard-delete, recorded on the audit trail. Required — a blank comment returns 422.",
                    "example": "Legal hard-delete — comment contained PII."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Review destroyed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdminDestroyResponse"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/reviews/{reviewId}/responses": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ReviewId"
        }
      ],
      "get": {
        "operationId": "getReviewResponses",
        "tags": [
          "Reviews"
        ],
        "summary": "List replies to a review",
        "description": "Returns threaded replies to a parent review, ordered by creation time. Each reply is serialized via ReviewBlueprint with commenter attribution, reaction counts, and the current user's reaction.",
        "responses": {
          "200": {
            "description": "Reply thread",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "rows"
                  ],
                  "properties": {
                    "rows": {
                      "type": "array",
                      "description": "Reply rows ordered by created_at.",
                      "items": {
                        "$ref": "#/components/schemas/ReviewSummary"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/reviews/{reviewId}/reactions": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ReviewId"
        }
      ],
      "get": {
        "operationId": "listReactions",
        "tags": [
          "Reactions"
        ],
        "summary": "List reactions on a review",
        "description": "Returns the reacting users' display names, grouped by reaction kind (up/down), for the specified review.",
        "responses": {
          "200": {
            "description": "Reactions list",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReactionsSummary"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      },
      "post": {
        "operationId": "createReaction",
        "tags": [
          "Reactions"
        ],
        "summary": "Add a reaction (up/down) to a comment review",
        "description": "Toggles a thumbs-up or thumbs-down reaction. If the user already reacted with the same kind, the reaction is removed.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "kind"
                ],
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "up",
                      "down"
                    ],
                    "description": "Reaction kind to toggle. Sent at the top level, not nested.",
                    "example": "up"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reaction created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReactionToggleResponse"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/reviews/{reviewId}/reopen": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ReviewId"
        }
      ],
      "patch": {
        "operationId": "reopenReview",
        "tags": [
          "Reviews"
        ],
        "summary": "Re-open an adjudicated review",
        "description": "Reverts the adjudication on a closed review, clearing adjudicated_at and adjudicated_by_id so the triage decision can be revised. Requires author role or higher on the parent project. Returns 422 warning toast if the review has not been adjudicated or was withdrawn by the commenter.",
        "responses": {
          "200": {
            "description": "Review re-opened",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReviewWrapper"
                }
              }
            },
            "links": {
              "TriageReopenedReview": {
                "operationId": "triageReview",
                "parameters": {
                  "reviewId": "$response.body#/review/id"
                }
              },
              "UpdateReopenedReview": {
                "operationId": "updateReview",
                "parameters": {
                  "reviewId": "$response.body#/review/id"
                }
              }
            },
            "examples": {
              "reopened": {
                "summary": "Successfully re-opened",
                "value": {
                  "review": {
                    "id": 44,
                    "action": "comment",
                    "comment": "This requirement needs clarification.",
                    "triage_status": "pending",
                    "adjudicated_at": null
                  }
                }
              }
            }
          },
          "422": {
            "description": "Cannot re-open (not adjudicated or withdrawn)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/reviews/{reviewId}/section": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ReviewId"
        }
      ],
      "patch": {
        "operationId": "updateReviewSection",
        "tags": [
          "Reviews"
        ],
        "summary": "Update the section field on a review",
        "description": "Changes which requirement section (fixtext, check_content, vuln_discussion, etc.) a comment addresses. Idempotent — re-saving the same section returns the review with an `idempotent: true` flag. Requires author role or higher. An audit trail entry is created for non-idempotent changes.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "audit_comment"
                ],
                "properties": {
                  "section": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Target section key from Review::SECTION_KEYS. Null for overall requirement.",
                    "enum": [
                      "title",
                      "severity",
                      "status",
                      "fixtext",
                      "check_content",
                      "vuln_discussion",
                      "disa_metadata",
                      "vendor_comments",
                      "artifact_description",
                      "xccdf_metadata",
                      null
                    ],
                    "example": "fixtext"
                  },
                  "audit_comment": {
                    "type": "string",
                    "maxLength": 4096,
                    "description": "Reason for the section change (audit trail). Required — a blank comment returns 422.",
                    "example": "Commenter clarified this applies to the fix text."
                  }
                }
              },
              "examples": {
                "change_section": {
                  "summary": "Move comment to vuln_discussion",
                  "value": {
                    "section": "vuln_discussion",
                    "audit_comment": "Re-categorized after author clarification."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Section updated (or idempotent no-op)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "review"
                  ],
                  "properties": {
                    "review": {
                      "$ref": "#/components/schemas/ReviewSummary"
                    },
                    "idempotent": {
                      "type": "boolean",
                      "description": "Present and true when the section was already the requested value."
                    }
                  }
                },
                "examples": {
                  "changed": {
                    "summary": "Section updated",
                    "value": {
                      "review": {
                        "id": 44,
                        "action": "comment",
                        "comment": "This requirement needs clarification...",
                        "rule_id": 812,
                        "section": "vuln_discussion",
                        "triage_status": "pending",
                        "created_at": "2026-05-19T16:15:00Z",
                        "reactions": {
                          "up": 1,
                          "down": 0
                        }
                      }
                    }
                  },
                  "idempotent": {
                    "summary": "No change — same section",
                    "value": {
                      "review": {
                        "id": 44,
                        "action": "comment",
                        "comment": "This requirement needs clarification...",
                        "rule_id": 812,
                        "section": "fixtext",
                        "triage_status": "pending",
                        "created_at": "2026-05-19T16:15:00Z",
                        "reactions": {
                          "up": 1,
                          "down": 0
                        }
                      },
                      "idempotent": true
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/memberships": {
      "post": {
        "operationId": "createMembership",
        "tags": [
          "Memberships"
        ],
        "summary": "Add a user to a project or component",
        "description": "Creates a membership linking a user to a project or component with the specified role (viewer, author, reviewer, admin). Requires admin role on the target project. Duplicate memberships are rejected.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "membership": {
                    "$ref": "#/components/schemas/MembershipInput"
                  }
                }
              },
              "examples": {
                "add_author": {
                  "summary": "Add a user as author",
                  "value": {
                    "membership": {
                      "user_id": 42,
                      "membership_id": 4,
                      "membership_type": "Project",
                      "role": "author"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Membership created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "added": {
                    "summary": "User added to project",
                    "value": {
                      "toast": {
                        "title": "Member added.",
                        "message": [
                          "Jane Doe added as author."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/memberships/{membershipId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/MembershipId"
        }
      ],
      "patch": {
        "operationId": "patchMembership",
        "tags": [
          "Memberships"
        ],
        "summary": "Partial update of membership role",
        "description": "Partial update — send only the fields to change. Changes the role (viewer, author, reviewer, admin) for an existing membership. Requires admin role on the parent project. Cannot demote the last admin on a project.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "membership": {
                    "$ref": "#/components/schemas/MembershipUpdateInput"
                  }
                }
              },
              "examples": {
                "promote": {
                  "summary": "Promote to reviewer",
                  "value": {
                    "membership": {
                      "role": "reviewer"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Membership updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "promoted": {
                    "summary": "Role changed",
                    "value": {
                      "toast": {
                        "title": "Role updated.",
                        "message": [
                          "Jane Doe is now a reviewer."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation failed — e.g. downgrading the project's last admin",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "last_admin": {
                    "summary": "Last project admin cannot be downgraded",
                    "value": {
                      "toast": {
                        "title": "Could not update membership.",
                        "message": [
                          "Role cannot be changed — this user is the last admin of project 'Photon OS 5'. Transfer the admin role to another member first."
                        ],
                        "variant": "danger"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      },
      "put": {
        "operationId": "updateMembership",
        "tags": [
          "Memberships"
        ],
        "summary": "Full replacement of membership role",
        "description": "Full replacement — all fields required. Changes the role (viewer, author, reviewer, admin) for an existing membership. Requires admin role on the parent project. Cannot demote the last admin on a project.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "membership": {
                    "$ref": "#/components/schemas/MembershipUpdateInput"
                  }
                }
              },
              "examples": {
                "promote": {
                  "summary": "Promote to reviewer",
                  "value": {
                    "membership": {
                      "role": "reviewer"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Membership updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "promoted": {
                    "summary": "Role changed",
                    "value": {
                      "toast": {
                        "title": "Role updated.",
                        "message": [
                          "Jane Doe is now a reviewer."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation failed — e.g. downgrading the project's last admin",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "last_admin": {
                    "summary": "Last project admin cannot be downgraded",
                    "value": {
                      "toast": {
                        "title": "Could not update membership.",
                        "message": [
                          "Role cannot be changed — this user is the last admin of project 'Photon OS 5'. Transfer the admin role to another member first."
                        ],
                        "variant": "danger"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      },
      "delete": {
        "operationId": "deleteMembership",
        "tags": [
          "Memberships"
        ],
        "summary": "Remove a user from a project or component",
        "description": "Deletes the membership, revoking the user's access. Requires admin role on the parent project. Removed users lose authority over all project-scoped resources including their own pending comments. The last admin of a project cannot be removed — transfer the admin role first.",
        "responses": {
          "200": {
            "description": "Membership removed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "removed": {
                    "summary": "User removed from project",
                    "value": {
                      "toast": {
                        "title": "Member removed.",
                        "message": [
                          "Jane Doe removed from project."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Removal blocked — the project's last admin",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "last_admin": {
                    "summary": "Last project admin cannot be removed",
                    "value": {
                      "toast": {
                        "title": "Could not remove membership.",
                        "message": [
                          "Cannot remove the last admin of project 'Photon OS 5'. Transfer the admin role to another member first."
                        ],
                        "variant": "danger"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/srgs": {
      "get": {
        "operationId": "listSrgs",
        "tags": [
          "Benchmarks"
        ],
        "summary": "List uploaded Security Requirements Guides",
        "description": "Returns all uploaded SRGs sorted by title. Requires authentication. SRGs are the DISA baseline requirement documents that Components implement. Each SRG contains the rules that Component authors map their controls to.",
        "responses": {
          "200": {
            "description": "All SRGs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SrgSummary"
                  }
                },
                "examples": {
                  "srgs": {
                    "summary": "Two SRGs",
                    "value": [
                      {
                        "id": 1,
                        "srg_id": "Container_Platform_SRG",
                        "name": "Container Platform SRG - Ver 2, Rel 4",
                        "title": "Container Platform Security Requirements Guide",
                        "version": "V2R4",
                        "release_date": "2025-10-28",
                        "core": false,
                        "severity_counts": {
                          "high": 8,
                          "medium": 177,
                          "low": 3
                        }
                      },
                      {
                        "id": 2,
                        "srg_id": "General_Purpose_Operating_System_SRG",
                        "name": "General Purpose Operating System - Ver 3, Rel 3",
                        "title": "General Purpose Operating System SRG",
                        "version": "V3R3",
                        "release_date": "2025-06-15",
                        "core": false,
                        "severity_counts": {
                          "high": 15,
                          "medium": 150,
                          "low": 8
                        }
                      }
                    ]
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      },
      "post": {
        "operationId": "uploadSrg",
        "tags": [
          "Benchmarks"
        ],
        "summary": "Upload an SRG XCCDF XML file",
        "description": "Parses and imports a DISA SRG from an XCCDF XML file. Requires admin role. Extracts all rules, CCIs, and metadata from the XML. Duplicate SRGs (same title + version) are rejected.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "DISA XCCDF XML file (.xml) containing the SRG."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "SRG uploaded and parsed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "created": {
                    "summary": "SRG imported",
                    "value": {
                      "toast": {
                        "title": "SRG created.",
                        "message": [
                          "Successfully created SRG."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/srgs/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SrgId"
        }
      ],
      "get": {
        "operationId": "getSrg",
        "tags": [
          "Benchmarks"
        ],
        "summary": "SRG detail with rules and metadata",
        "description": "Returns full SRG details including title, version, release date, severity counts, and all embedded rules with DISA metadata and check content. Requires authentication. Used by the SRG detail page (BenchmarkViewer).",
        "responses": {
          "200": {
            "description": "SRG detail with nested rules",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SrgDetailResponse"
                },
                "examples": {
                  "srg": {
                    "summary": "Container Platform SRG",
                    "value": {
                      "id": 1,
                      "srg_id": "Container_Platform_SRG",
                      "name": "Container Platform SRG - Ver 2, Rel 4",
                      "title": "Container Platform Security Requirements Guide",
                      "version": "V2R4",
                      "release_date": "2025-10-28",
                      "core": false,
                      "severity_counts": {
                        "high": 8,
                        "medium": 177,
                        "low": 3
                      },
                      "srg_rules": [
                        {
                          "id": 1,
                          "rule_id": "SV-233015r960759_rule",
                          "title": "The container platform must use TLS 1.2 or greater...",
                          "version": "SRG-APP-000014-CTR-000035",
                          "rule_severity": "medium"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      },
      "delete": {
        "operationId": "deleteSrg",
        "tags": [
          "Benchmarks"
        ],
        "summary": "Delete an uploaded SRG (admin only)",
        "description": "Permanently deletes an SRG and all its embedded rules. Requires admin role. Components based on this SRG will lose their baseline reference. This action cannot be undone.",
        "responses": {
          "200": {
            "description": "SRG removed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "removed": {
                    "summary": "SRG removed",
                    "value": {
                      "toast": {
                        "title": "SRG removed.",
                        "message": [
                          "Successfully removed SRG."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/srgs/{id}/export/{type}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SrgId"
        },
        {
          "name": "type",
          "in": "path",
          "required": true,
          "description": "Export format.",
          "schema": {
            "type": "string",
            "enum": [
              "csv",
              "xccdf"
            ]
          },
          "example": "xccdf"
        }
      ],
      "get": {
        "operationId": "exportSrg",
        "tags": [
          "Benchmarks"
        ],
        "summary": "Export SRG in the specified format",
        "description": "Downloads the SRG as a CSV spreadsheet or XCCDF XML file. Requires authentication. CSV exports all rules in a flat table. XCCDF exports the original DISA XML format.",
        "responses": {
          "200": {
            "description": "Binary file download",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/xml": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/stigs": {
      "get": {
        "operationId": "listStigs",
        "tags": [
          "Benchmarks"
        ],
        "summary": "List uploaded STIGs",
        "description": "Returns all uploaded STIGs sorted by title. Requires authentication. STIGs are published security guidance that can be used as reference when authoring Components.",
        "responses": {
          "200": {
            "description": "All STIGs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/StigSummary"
                  }
                },
                "examples": {
                  "stigs": {
                    "summary": "Two STIGs",
                    "value": [
                      {
                        "id": 1,
                        "stig_id": "Application_Security_Development_STIG",
                        "name": "Application Security Development STIG - Ver 6, Rel 4",
                        "title": "Application Security and Development Security Technical Implementation Guide",
                        "version": "V6R4",
                        "benchmark_date": "2025-10-01",
                        "severity_counts": {
                          "high": 34,
                          "medium": 230,
                          "low": 22
                        }
                      },
                      {
                        "id": 2,
                        "stig_id": "Crunchy_Data_PostgreSQL_STIG",
                        "name": "Crunchy Data PostgreSQL STIG - Ver 3, Rel 1",
                        "title": "Crunchy Data PostgreSQL Security Technical Implementation Guide",
                        "version": "V3R1",
                        "benchmark_date": "2025-09-15",
                        "severity_counts": {
                          "high": 10,
                          "medium": 85,
                          "low": 5
                        }
                      }
                    ]
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      },
      "post": {
        "operationId": "uploadStig",
        "tags": [
          "Benchmarks"
        ],
        "summary": "Upload a STIG XCCDF XML file",
        "description": "Parses and imports a published STIG from an XCCDF XML file. Requires admin role. Extracts all rules, CCIs, and metadata. Duplicate STIGs (same title + version) are rejected.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "DISA XCCDF XML file (.xml) containing the STIG."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "STIG uploaded and parsed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "added": {
                    "summary": "STIG imported",
                    "value": {
                      "toast": {
                        "title": "STIG added.",
                        "message": [
                          "Successfully added Application Security and Development Security Technical Implementation Guide."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/stigs/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/StigId"
        }
      ],
      "get": {
        "operationId": "getStig",
        "tags": [
          "Benchmarks"
        ],
        "summary": "STIG detail with rules and metadata",
        "description": "Returns full STIG details including title, version, benchmark date, description, severity counts, and all embedded rules with DISA metadata and check content. Requires authentication. Used by the STIG detail page (BenchmarkViewer).",
        "responses": {
          "200": {
            "description": "STIG detail with nested rules",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StigDetailResponse"
                },
                "examples": {
                  "stig": {
                    "summary": "Application Security Development STIG",
                    "value": {
                      "id": 1,
                      "stig_id": "Application_Security_Development_STIG",
                      "name": "Application Security Development STIG - Ver 6, Rel 4",
                      "title": "Application Security and Development Security Technical Implementation Guide",
                      "version": "V6R4",
                      "benchmark_date": "2025-10-01",
                      "severity_counts": {
                        "high": 34,
                        "medium": 230,
                        "low": 22
                      },
                      "description": "This Security Technical Implementation Guide is published...",
                      "stig_rules": [
                        {
                          "id": 660,
                          "rule_id": "SV-222387r960735_rule",
                          "title": "The application must limit logon sessions...",
                          "version": "APSC-DV-000010",
                          "rule_severity": "medium"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      },
      "delete": {
        "operationId": "deleteStig",
        "tags": [
          "Benchmarks"
        ],
        "summary": "Delete an uploaded STIG (admin only)",
        "description": "Permanently deletes a STIG and all its embedded rules. Requires admin role. This action cannot be undone.",
        "responses": {
          "200": {
            "description": "STIG removed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "removed": {
                    "summary": "STIG removed",
                    "value": {
                      "toast": {
                        "title": "STIG removed.",
                        "message": [
                          "Successfully removed Application Security and Development Security Technical Implementation Guide."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/stigs/{id}/export/{type}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/StigId"
        },
        {
          "name": "type",
          "in": "path",
          "required": true,
          "description": "Export format. STIGs support CSV and XCCDF only (InSpec export is Components-only).",
          "schema": {
            "type": "string",
            "enum": [
              "csv",
              "xccdf"
            ]
          },
          "example": "xccdf"
        }
      ],
      "get": {
        "operationId": "exportStig",
        "tags": [
          "Benchmarks"
        ],
        "summary": "Export STIG in the specified format",
        "description": "Downloads the STIG as CSV or XCCDF XML. Requires authentication. CSV exports all rules in a flat table. XCCDF exports the original DISA XML format.",
        "responses": {
          "200": {
            "description": "Binary file download",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/xml": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "Unsupported export type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/users": {
      "get": {
        "operationId": "listUsers",
        "tags": [
          "Users"
        ],
        "summary": "List all users (admin only)",
        "description": "Returns all user accounts sorted alphabetically. Includes login tracking fields (last_sign_in_at, failed_attempts, locked_at) for admin monitoring. Requires admin role — returns 403 for non-admin users.",
        "responses": {
          "200": {
            "description": "All user accounts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/UserSummary"
                  }
                },
                "examples": {
                  "users": {
                    "summary": "Two users",
                    "value": [
                      {
                        "id": 1,
                        "name": "Demo Admin",
                        "email": "admin@example.com",
                        "provider": null,
                        "admin": true,
                        "last_sign_in_at": "2026-05-28T15:00:00Z",
                        "failed_attempts": 0,
                        "locked_at": null
                      },
                      {
                        "id": 5,
                        "name": "Bernice Deckow",
                        "email": "bernice.deckow@example.com",
                        "provider": null,
                        "admin": false,
                        "last_sign_in_at": null,
                        "failed_attempts": 0,
                        "locked_at": null
                      }
                    ]
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      },
      "put": {
        "operationId": "updateProfile",
        "tags": [
          "Users"
        ],
        "summary": "Update the signed-in user's own profile",
        "description": "Updates the current user's profile (Devise registration update). Field-sensitivity policy: name and slack_user_id save without a password; changing the email — the login identifier — requires current_password (re-authentication for sensitive changes). When email confirmation is enabled, an email change is held in unconfirmed_email until the confirmation link is followed; otherwise it applies immediately. Provider-managed users (OIDC/LDAP) cannot change email here — the identity provider owns it and the parameter is ignored.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "user"
                ],
                "properties": {
                  "user": {
                    "type": "object",
                    "description": "Profile fields to update.",
                    "properties": {
                      "name": {
                        "type": "string",
                        "description": "Display name.",
                        "example": "Jane Doe"
                      },
                      "email": {
                        "type": "string",
                        "format": "email",
                        "description": "New login email. Requires current_password when it differs from the current address. Ignored for provider-managed users.",
                        "example": "jane@example.com"
                      },
                      "slack_user_id": {
                        "type": "string",
                        "description": "Slack user ID for notifications.",
                        "example": "U123456"
                      },
                      "current_password": {
                        "type": "string",
                        "description": "The user's current password — required only when changing the email.",
                        "example": "MyCurrentP@ssw0rd!"
                      }
                    }
                  }
                }
              },
              "examples": {
                "rename": {
                  "summary": "Passwordless non-sensitive save",
                  "value": {
                    "user": {
                      "name": "Jane Doe",
                      "slack_user_id": "U123456"
                    }
                  }
                },
                "email_change": {
                  "summary": "Email change with re-authentication",
                  "value": {
                    "user": {
                      "email": "new-address@example.com",
                      "current_password": "MyCurrentP@ssw0rd!"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Profile updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "updated": {
                    "summary": "Successful update",
                    "value": {
                      "toast": {
                        "title": "Account updated.",
                        "message": [
                          "Profile updated successfully."
                        ],
                        "variant": "success"
                      }
                    }
                  },
                  "confirmation_pending": {
                    "summary": "Email change held for confirmation",
                    "value": {
                      "toast": {
                        "title": "Account updated.",
                        "message": [
                          "A confirmation link has been sent to new-address@example.com. Please follow the link to verify your new email address."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation failed (e.g. email change without the current password)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "missing_password": {
                    "summary": "Email change without current password",
                    "value": {
                      "toast": {
                        "title": "Could not update profile.",
                        "message": [
                          "Current password can't be blank"
                        ],
                        "variant": "danger"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      },
      "delete": {
        "operationId": "deleteOwnAccount",
        "tags": [
          "Auth"
        ],
        "summary": "Delete the signed-in user's own account",
        "description": "Permanently deletes the current user's account and signs them out. Local-credential users must re-authenticate with current_password (OWASP ASVS 3.7.1); provider-managed and SSO-created accounts are exempt — their identity provider owns re-authentication. Blocked with 422 when the user is the only system administrator or the only admin of any project (transfer the admin role first). Repeated wrong passwords count toward account lockout and return 423 once locked.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "user": {
                    "type": "object",
                    "properties": {
                      "current_password": {
                        "type": "string",
                        "format": "password",
                        "description": "The user's current password. Required for local-credential accounts; ignored for provider-managed accounts.",
                        "example": "MyCurrentP@ssw0rd!"
                      }
                    }
                  }
                }
              },
              "examples": {
                "local_user": {
                  "summary": "Local-credential user re-authenticates",
                  "value": {
                    "user": {
                      "current_password": "MyCurrentP@ssw0rd!"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Account deleted and session ended",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "deleted": {
                    "summary": "Successful self-delete",
                    "value": {
                      "toast": {
                        "title": "Account deleted.",
                        "message": [
                          "Account deleted successfully."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Wrong/missing password, or user is the only system administrator",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "wrong_password": {
                    "summary": "Re-authentication failed",
                    "value": {
                      "toast": {
                        "title": "Cannot delete account.",
                        "message": [
                          "Incorrect password. Please enter your current password to delete your account."
                        ],
                        "variant": "danger"
                      }
                    }
                  },
                  "last_admin": {
                    "summary": "Only system administrator",
                    "value": {
                      "toast": {
                        "title": "Cannot delete account.",
                        "message": [
                          "You are the only administrator. Promote another user to admin before deleting your account."
                        ],
                        "variant": "danger"
                      }
                    }
                  },
                  "sole_project_admin": {
                    "summary": "Only admin of a project",
                    "value": {
                      "toast": {
                        "title": "Cannot delete account.",
                        "message": [
                          "You are the only admin of: 'Photon OS 5'. Transfer the admin role to another member of each project first."
                        ],
                        "variant": "danger"
                      }
                    }
                  }
                }
              }
            }
          },
          "423": {
            "description": "Account locked by repeated failed re-authentication attempts",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "locked": {
                    "summary": "Locked mid-request",
                    "value": {
                      "toast": {
                        "title": "Cannot delete account.",
                        "message": [
                          "Your account has been locked due to too many failed attempts. Please try again later."
                        ],
                        "variant": "danger"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/users/{userId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/UserId"
        }
      ],
      "patch": {
        "operationId": "patchUser",
        "tags": [
          "Users"
        ],
        "summary": "Partial update of user attributes (admin only)",
        "description": "Partial update — send only changed fields. Updates a user's name, email, or admin status. Requires admin role. Prevents the last admin from demoting themselves. Sends a Slack notification when admin status changes (if Slack is configured). Returns both a toast and the updated user object.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "user": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "email": {
                        "type": "string",
                        "format": "email"
                      },
                      "admin": {
                        "type": "boolean"
                      },
                      "slack_user_id": {
                        "type": "string",
                        "description": "Slack member ID driving the user's Slack notifications. Send an empty string to clear it. Admin-made changes are audited.",
                        "example": "U0123456789"
                      }
                    }
                  }
                }
              },
              "examples": {
                "promote": {
                  "summary": "Promote user to admin",
                  "value": {
                    "user": {
                      "admin": true
                    }
                  }
                },
                "set_slack": {
                  "summary": "Set the user's Slack member ID",
                  "value": {
                    "user": {
                      "name": "Jane Doe",
                      "email": "jane.doe@example.org",
                      "admin": false,
                      "slack_user_id": "U0123456789"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "User updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserToastResponse"
                },
                "examples": {
                  "updated": {
                    "summary": "User promoted to admin",
                    "value": {
                      "toast": {
                        "title": "User updated.",
                        "message": [
                          "Successfully updated user."
                        ],
                        "variant": "success"
                      },
                      "user": {
                        "id": 42,
                        "name": "Jane Doe",
                        "email": "jane.doe@example.org",
                        "admin": true
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Cannot remove last admin",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "last_admin": {
                    "summary": "Cannot demote last admin",
                    "value": {
                      "toast": {
                        "title": "Cannot remove admin.",
                        "message": [
                          "You are the only admin. Promote another user first."
                        ],
                        "variant": "danger"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      },
      "put": {
        "operationId": "updateUser",
        "tags": [
          "Users"
        ],
        "summary": "Full replacement of user attributes (admin only)",
        "description": "Full replacement — all fields required. Updates a user's name, email, or admin status. Requires admin role. Prevents the last admin from demoting themselves. Sends a Slack notification when admin status changes (if Slack is configured). Returns both a toast and the updated user object.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "user": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "email": {
                        "type": "string",
                        "format": "email"
                      },
                      "admin": {
                        "type": "boolean"
                      },
                      "slack_user_id": {
                        "type": "string",
                        "description": "Slack member ID driving the user's Slack notifications. Send an empty string to clear it. Admin-made changes are audited.",
                        "example": "U0123456789"
                      }
                    }
                  }
                }
              },
              "examples": {
                "promote": {
                  "summary": "Promote user to admin",
                  "value": {
                    "user": {
                      "admin": true
                    }
                  }
                },
                "set_slack": {
                  "summary": "Set the user's Slack member ID",
                  "value": {
                    "user": {
                      "name": "Jane Doe",
                      "email": "jane.doe@example.org",
                      "admin": false,
                      "slack_user_id": "U0123456789"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "User updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserToastResponse"
                },
                "examples": {
                  "updated": {
                    "summary": "User promoted to admin",
                    "value": {
                      "toast": {
                        "title": "User updated.",
                        "message": [
                          "Successfully updated user."
                        ],
                        "variant": "success"
                      },
                      "user": {
                        "id": 42,
                        "name": "Jane Doe",
                        "email": "jane.doe@example.org",
                        "admin": true
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Cannot remove last admin",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "last_admin": {
                    "summary": "Cannot demote last admin",
                    "value": {
                      "toast": {
                        "title": "Cannot remove admin.",
                        "message": [
                          "You are the only admin. Promote another user first."
                        ],
                        "variant": "danger"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      },
      "delete": {
        "operationId": "deleteUser",
        "tags": [
          "Users"
        ],
        "summary": "Delete a user (admin only)",
        "description": "Permanently deletes a user account. Requires admin role. Cannot delete the last remaining site admin, nor a user who is the only admin of any project — transfer the admin role first. All project memberships and associated data are cleaned up via dependent destroy.",
        "responses": {
          "200": {
            "description": "User removed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "removed": {
                    "summary": "User removed",
                    "value": {
                      "toast": {
                        "title": "User removed.",
                        "message": [
                          "Successfully removed user."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Deletion blocked — the last site admin, or the only admin of one or more projects",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "last_site_admin": {
                    "summary": "Only site admin",
                    "value": {
                      "toast": {
                        "title": "Cannot delete user.",
                        "message": [
                          "This is the only admin. Promote another user first."
                        ],
                        "variant": "danger"
                      }
                    }
                  },
                  "sole_project_admin": {
                    "summary": "Only admin of a project",
                    "value": {
                      "toast": {
                        "title": "Cannot delete user.",
                        "message": [
                          "This user is the only admin of: 'Photon OS 5'. Transfer the admin role to another member of each project first."
                        ],
                        "variant": "danger"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/users/{userId}/comments": {
      "parameters": [
        {
          "$ref": "#/components/parameters/UserId"
        }
      ],
      "get": {
        "operationId": "getUserComments",
        "tags": [
          "Users"
        ],
        "summary": "My Comments page — user's comments across accessible projects",
        "description": "Returns paginated comments authored by the specified user across all projects the requesting user can access. Scoped by project visibility, not identity — this is a filtered view, not a privacy boundary. Supports triage status filtering, project filtering, and pagination.",
        "parameters": [
          {
            "name": "triage_status",
            "in": "query",
            "required": false,
            "description": "Filter the user's comments by triage disposition. Absent or \"all\" returns comments in any status — this page has no default filter.",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "pending",
                "concur",
                "concur_with_comment",
                "non_concur",
                "duplicate",
                "informational",
                "needs_clarification",
                "withdrawn",
                "addressed_by"
              ],
              "default": "all"
            },
            "example": "concur"
          },
          {
            "$ref": "#/components/parameters/PageParam"
          },
          {
            "$ref": "#/components/parameters/PerPageParam"
          },
          {
            "name": "project_id",
            "in": "query",
            "required": false,
            "description": "Filter comments to a specific project.",
            "schema": {
              "type": "integer"
            },
            "example": 3
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated user comments with project/component context",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "rows",
                    "pagination"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "rows": {
                      "type": "array",
                      "description": "Comment rows for the current page.",
                      "items": {
                        "$ref": "#/components/schemas/UserCommentRow"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "required": [
                        "page",
                        "per_page",
                        "total"
                      ],
                      "additionalProperties": false,
                      "properties": {
                        "page": {
                          "type": "integer",
                          "description": "Current page number (1-based).",
                          "example": 1
                        },
                        "per_page": {
                          "type": "integer",
                          "description": "Number of comments per page.",
                          "example": 25
                        },
                        "total": {
                          "type": "integer",
                          "description": "Total number of matching comments.",
                          "example": 12
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "comments": {
                    "summary": "User's pending comments across projects",
                    "value": {
                      "rows": [
                        {
                          "id": 29,
                          "project_id": 3,
                          "project_name": "vSphere 7.0",
                          "component_id": 4,
                          "component_name": "Photon OS 3",
                          "rule_id": 2397,
                          "rule_displayed_name": "PHOS-03-000039",
                          "commentable_type": "BaseRule",
                          "section": "fixtext",
                          "comment": "vSphere 7.0: fix command targets ESXi 6.7 path.",
                          "created_at": "2026-05-19 14:08:18 UTC",
                          "triage_status": "pending",
                          "responses_count": 0,
                          "reactions": {
                            "up": 0,
                            "down": 0,
                            "mine": null
                          }
                        }
                      ],
                      "pagination": {
                        "page": 1,
                        "per_page": 25,
                        "total": 12
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/users/unlink_identity": {
      "post": {
        "operationId": "unlinkIdentity",
        "tags": [
          "Users"
        ],
        "summary": "Unlink an OAuth provider from the current user",
        "description": "Removes an OAuth provider link (GitHub, LDAP, OIDC) from the current user's account. Requires the user's current password to confirm. The user must have local login credentials before unlinking to avoid being locked out. Available from the user profile page.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "current_password",
                  "provider"
                ],
                "properties": {
                  "current_password": {
                    "type": "string",
                    "description": "Current password to confirm the action.",
                    "example": "MyCurrentP@ssw0rd!"
                  },
                  "provider": {
                    "type": "string",
                    "description": "OAuth provider to unlink.",
                    "enum": [
                      "github",
                      "ldap",
                      "oidc"
                    ],
                    "example": "github"
                  }
                }
              },
              "examples": {
                "unlink_github": {
                  "summary": "Unlink GitHub provider",
                  "value": {
                    "current_password": "MyCurrentP@ssw0rd!",
                    "provider": "github"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Provider unlinked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "unlinked": {
                    "summary": "GitHub unlinked",
                    "value": {
                      "toast": {
                        "title": "Provider unlinked.",
                        "message": [
                          "GitHub identity removed from your account."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/users/admin_create": {
      "post": {
        "operationId": "adminCreateUser",
        "tags": [
          "Users"
        ],
        "summary": "Create a new user (admin only)",
        "description": "Creates a new user account. Requires admin role. Three modes based on password and SMTP config: (1) password provided — user can sign in immediately, (2) no password + SMTP enabled — sends setup email via Devise, (3) no password + no SMTP — returns a reset URL the admin delivers manually. Skips email confirmation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "user": {
                    "type": "object",
                    "required": [
                      "email"
                    ],
                    "properties": {
                      "name": {
                        "type": "string",
                        "description": "Display name for the new user.",
                        "example": "Jane Doe"
                      },
                      "email": {
                        "type": "string",
                        "format": "email",
                        "description": "Login email address. Must be unique.",
                        "example": "jane.doe@example.org"
                      },
                      "admin": {
                        "type": "boolean",
                        "description": "Whether to grant admin privileges.",
                        "example": false
                      },
                      "slack_user_id": {
                        "type": "string",
                        "description": "Slack member ID driving the new user's Slack notifications. Optional; the user can change it later. Admin-made changes are audited.",
                        "example": "U0123456789"
                      },
                      "password": {
                        "type": "string",
                        "description": "Initial password. If omitted, a reset link is generated.",
                        "example": "SecureP@ssw0rd2026!"
                      }
                    }
                  }
                }
              },
              "examples": {
                "with_password": {
                  "summary": "Create with explicit password",
                  "value": {
                    "user": {
                      "name": "Jane Doe",
                      "email": "jane.doe@example.org",
                      "admin": false,
                      "password": "SecureP@ssw0rd2026!"
                    }
                  }
                },
                "without_password": {
                  "summary": "Create without password (reset link generated)",
                  "value": {
                    "user": {
                      "name": "Jane Doe",
                      "email": "jane.doe@example.org"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "User created (response varies by mode)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdminCreateResponse"
                },
                "examples": {
                  "with_password": {
                    "summary": "Created with provided password",
                    "value": {
                      "toast": {
                        "title": "User created.",
                        "message": [
                          "User jane.doe@example.org created with the provided password."
                        ],
                        "variant": "success"
                      },
                      "user": {
                        "id": 42,
                        "name": "Jane Doe",
                        "email": "jane.doe@example.org",
                        "admin": false
                      }
                    }
                  },
                  "no_smtp": {
                    "summary": "Created without SMTP — reset URL returned",
                    "value": {
                      "toast": {
                        "title": "User created.",
                        "message": [
                          "User jane.doe@example.org created. Deliver the reset link to the user."
                        ],
                        "variant": "success"
                      },
                      "user": {
                        "id": 42,
                        "name": "Jane Doe",
                        "email": "jane.doe@example.org",
                        "admin": false
                      },
                      "reset_url": "https://vulcan.example.org/users/password/edit?reset_password_token=abc123"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error (duplicate email, invalid fields)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "duplicate": {
                    "summary": "Email already taken",
                    "value": {
                      "toast": {
                        "title": "Could not create user.",
                        "message": [
                          "Email has already been taken"
                        ],
                        "variant": "danger"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/users/{userId}/send_password_reset": {
      "parameters": [
        {
          "$ref": "#/components/parameters/UserId"
        }
      ],
      "post": {
        "operationId": "sendPasswordReset",
        "tags": [
          "Users"
        ],
        "summary": "Send Devise password reset email (admin only)",
        "description": "Triggers a Devise password reset email to the specified user. Requires admin role and SMTP to be configured. Returns 422 if SMTP is disabled — use the generate_reset_link endpoint instead. Returns 500 if email delivery fails.",
        "responses": {
          "200": {
            "description": "Reset email sent successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "sent": {
                    "summary": "Email delivered",
                    "value": {
                      "toast": {
                        "title": "Reset email sent.",
                        "message": [
                          "Password reset email sent to jane.doe@example.org."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "SMTP not configured",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "no_smtp": {
                    "summary": "SMTP disabled",
                    "value": {
                      "toast": {
                        "title": "SMTP not configured.",
                        "message": [
                          "Email delivery is not available. Use \"Generate Reset Link\" instead."
                        ],
                        "variant": "danger"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Email delivery failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/users/{userId}/generate_reset_link": {
      "parameters": [
        {
          "$ref": "#/components/parameters/UserId"
        }
      ],
      "post": {
        "operationId": "generateResetLink",
        "tags": [
          "Users"
        ],
        "summary": "Generate a password reset URL without sending email (admin only)",
        "description": "Generates a Devise reset token and returns the full reset URL. Does not send email — the admin copies the link and delivers it to the user through a secure channel. Requires admin role. Works regardless of SMTP config.",
        "responses": {
          "200": {
            "description": "Reset link generated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResetLinkResponse"
                },
                "examples": {
                  "link": {
                    "summary": "Reset link generated",
                    "value": {
                      "toast": {
                        "title": "Reset link generated.",
                        "message": [
                          "Reset link generated. Copy it and deliver to the user."
                        ],
                        "variant": "success"
                      },
                      "reset_url": "https://vulcan.example.org/users/password/edit?reset_password_token=abc123def456"
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/users/{userId}/set_password": {
      "parameters": [
        {
          "$ref": "#/components/parameters/UserId"
        }
      ],
      "post": {
        "operationId": "setPassword",
        "tags": [
          "Users"
        ],
        "summary": "Directly set a user's password (admin only)",
        "description": "Sets the password for a user account without requiring the old password. Requires admin role. The password must meet the configured minimum length (default 15 characters). Does not send any email notification. Used for urgent password resets when SMTP is unavailable.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "user": {
                    "type": "object",
                    "required": [
                      "password"
                    ],
                    "properties": {
                      "password": {
                        "type": "string",
                        "description": "New password (must meet minimum length requirement).",
                        "example": "SecureP@ssw0rd2026!"
                      }
                    }
                  }
                }
              },
              "examples": {
                "set": {
                  "summary": "Set a new password",
                  "value": {
                    "user": {
                      "password": "SecureP@ssw0rd2026!"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Password updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "success": {
                    "summary": "Password set successfully",
                    "value": {
                      "toast": {
                        "title": "Password updated.",
                        "message": [
                          "Password updated for jane.doe@example.org."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Blank password or Devise validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "blank": {
                    "summary": "Empty password",
                    "value": {
                      "toast": {
                        "title": "Password required.",
                        "message": [
                          "Password cannot be blank."
                        ],
                        "variant": "danger"
                      }
                    }
                  },
                  "too_short": {
                    "summary": "Password too short",
                    "value": {
                      "toast": {
                        "title": "Could not set password.",
                        "message": [
                          "Password is too short (minimum is 15 characters)"
                        ],
                        "variant": "danger"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Internal error during password update",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/users/{userId}/lock": {
      "parameters": [
        {
          "$ref": "#/components/parameters/UserId"
        }
      ],
      "post": {
        "operationId": "lockUser",
        "tags": [
          "Users"
        ],
        "summary": "Lock a user account (admin only)",
        "description": "Prevents the user from signing in. Requires admin role. Returns 422 if the admin attempts to lock their own account. Creates an audit trail entry recording who locked the account and when.",
        "responses": {
          "200": {
            "description": "Account locked successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserToastResponse"
                },
                "examples": {
                  "locked": {
                    "summary": "Account locked",
                    "value": {
                      "toast": {
                        "title": "Account locked.",
                        "message": [
                          "Account jane.doe@example.org locked."
                        ],
                        "variant": "success"
                      },
                      "user": {
                        "id": 42,
                        "name": "Jane Doe",
                        "email": "jane.doe@example.org",
                        "admin": false,
                        "locked_at": "2026-05-28T15:00:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Cannot lock own account",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "self_lock": {
                    "summary": "Admin tried to lock themselves",
                    "value": {
                      "toast": {
                        "title": "Cannot lock yourself.",
                        "message": [
                          "You cannot lock your own account."
                        ],
                        "variant": "danger"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/users/{userId}/unlock": {
      "parameters": [
        {
          "$ref": "#/components/parameters/UserId"
        }
      ],
      "post": {
        "operationId": "unlockUser",
        "tags": [
          "Users"
        ],
        "summary": "Unlock a locked user account (admin only)",
        "description": "Restores sign-in access for a locked user account. Requires admin role. Clears failed_attempts counter and locked_at timestamp. Creates an audit trail entry recording who unlocked the account.",
        "responses": {
          "200": {
            "description": "Account unlocked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserToastResponse"
                },
                "examples": {
                  "unlocked": {
                    "summary": "Account unlocked",
                    "value": {
                      "toast": {
                        "title": "Account unlocked.",
                        "message": [
                          "Account jane.doe@example.org unlocked."
                        ],
                        "variant": "success"
                      },
                      "user": {
                        "id": 42,
                        "name": "Jane Doe",
                        "email": "jane.doe@example.org",
                        "admin": false,
                        "locked_at": null
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/users/edit": {
      "get": {
        "operationId": "getProfile",
        "tags": [
          "Auth"
        ],
        "summary": "Get current user profile (Devise edit)",
        "description": "Returns the authenticated user's profile as JSON. This is the Devise registration edit endpoint with JSON support. The SPA may prefer GET /api/auth/me which returns the same CurrentUserResponse shape. Requires authentication.",
        "responses": {
          "200": {
            "description": "Current user profile",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CurrentUserResponse"
                },
                "examples": {
                  "profile": {
                    "summary": "User profile",
                    "value": {
                      "id": 42,
                      "name": "Jane Doe",
                      "email": "jane@example.com",
                      "admin": false,
                      "provider": null
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/users/password": {
      "post": {
        "operationId": "requestPasswordReset",
        "tags": [
          "Auth"
        ],
        "security": [],
        "summary": "Request password reset instructions",
        "description": "Sends a password reset email to the given address. In paranoid mode (default), always returns success — even if the email is not registered — to prevent email enumeration. Blank email returns 422.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "user"
                ],
                "properties": {
                  "user": {
                    "type": "object",
                    "required": [
                      "email"
                    ],
                    "properties": {
                      "email": {
                        "type": "string",
                        "format": "email",
                        "description": "Email address of the account to reset.",
                        "example": "jane@example.com"
                      }
                    }
                  }
                }
              },
              "examples": {
                "reset_request": {
                  "summary": "Request password reset",
                  "value": {
                    "user": {
                      "email": "jane@example.com"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Instructions sent (or paranoid success)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "sent": {
                    "summary": "Reset instructions sent",
                    "value": {
                      "toast": {
                        "title": "Instructions sent.",
                        "message": [
                          "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error (blank email)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "blank_email": {
                    "summary": "Email is blank",
                    "value": {
                      "toast": {
                        "title": "Could not send instructions.",
                        "message": [
                          "Email can't be blank"
                        ],
                        "variant": "danger"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      },
      "put": {
        "operationId": "executePasswordReset",
        "tags": [
          "Auth"
        ],
        "security": [],
        "summary": "Reset password using token from email",
        "description": "Resets the user's password using the token from the reset email. On success, signs the user in and returns a success toast. On failure (invalid token, mismatched passwords, complexity violation), returns 422 with error details.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "user"
                ],
                "properties": {
                  "user": {
                    "type": "object",
                    "required": [
                      "reset_password_token",
                      "password",
                      "password_confirmation"
                    ],
                    "properties": {
                      "reset_password_token": {
                        "type": "string",
                        "description": "Token from the password reset email link.",
                        "example": "abc123def456"
                      },
                      "password": {
                        "type": "string",
                        "format": "password",
                        "description": "New password (must meet complexity requirements).",
                        "example": "N3wS3cure!#Pass"
                      },
                      "password_confirmation": {
                        "type": "string",
                        "format": "password",
                        "description": "Must match the new password.",
                        "example": "N3wS3cure!#Pass"
                      }
                    }
                  }
                }
              },
              "examples": {
                "reset": {
                  "summary": "Reset with valid token",
                  "value": {
                    "user": {
                      "reset_password_token": "abc123def456",
                      "password": "N3wS3cure!#Pass",
                      "password_confirmation": "N3wS3cure!#Pass"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Password reset successfully — user signed in",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "reset": {
                    "summary": "Successful password reset",
                    "value": {
                      "toast": {
                        "title": "Password reset.",
                        "message": [
                          "Your password has been changed successfully. You are now signed in."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Invalid token, expired token, or password validation failure",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "invalid_token": {
                    "summary": "Bad reset token",
                    "value": {
                      "toast": {
                        "title": "Could not reset password.",
                        "message": [
                          "Reset password token is invalid"
                        ],
                        "variant": "danger"
                      }
                    }
                  },
                  "complexity": {
                    "summary": "Password complexity failure",
                    "value": {
                      "toast": {
                        "title": "Could not reset password.",
                        "message": [
                          "Password must include at least 2 numbers"
                        ],
                        "variant": "danger"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/users/password/edit": {
      "get": {
        "operationId": "validateResetToken",
        "tags": [
          "Auth"
        ],
        "security": [],
        "summary": "Validate a password reset token",
        "description": "Checks whether a password reset token is valid and not expired. The SPA calls this when the user follows the reset link to determine whether to show the reset form or an error message. Returns the minimum password length for client-side validation.",
        "parameters": [
          {
            "name": "reset_password_token",
            "in": "query",
            "required": true,
            "description": "The raw reset token from the email link.",
            "schema": {
              "type": "string"
            },
            "example": "abc123def456"
          }
        ],
        "responses": {
          "200": {
            "description": "Token is valid",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "valid",
                    "minimum_password_length"
                  ],
                  "properties": {
                    "valid": {
                      "type": "boolean",
                      "description": "Whether the token is valid and not expired.",
                      "example": true
                    },
                    "minimum_password_length": {
                      "type": "integer",
                      "description": "Minimum password length for client-side validation.",
                      "example": 15
                    }
                  }
                },
                "examples": {
                  "valid": {
                    "summary": "Valid token",
                    "value": {
                      "valid": true,
                      "minimum_password_length": 15
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Token is invalid, expired, or missing",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "valid",
                    "error"
                  ],
                  "properties": {
                    "valid": {
                      "type": "boolean",
                      "description": "Always false for error responses.",
                      "example": false
                    },
                    "error": {
                      "type": "string",
                      "description": "Human-readable error message.",
                      "example": "Reset token is invalid or has expired."
                    }
                  }
                },
                "examples": {
                  "invalid": {
                    "summary": "Invalid token",
                    "value": {
                      "valid": false,
                      "error": "Reset token is invalid or has expired."
                    }
                  },
                  "missing": {
                    "summary": "No token provided",
                    "value": {
                      "valid": false,
                      "error": "No reset token provided."
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/users/confirmation": {
      "post": {
        "operationId": "resendConfirmation",
        "tags": [
          "Auth"
        ],
        "security": [],
        "summary": "Resend email confirmation instructions",
        "description": "Sends a new confirmation email to the given address. In paranoid mode (default), always returns success — even if the email is not registered or already confirmed — to prevent email enumeration. Blank email returns 422.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "user"
                ],
                "properties": {
                  "user": {
                    "type": "object",
                    "required": [
                      "email"
                    ],
                    "properties": {
                      "email": {
                        "type": "string",
                        "format": "email",
                        "description": "Email address to send confirmation to.",
                        "example": "jane@example.com"
                      }
                    }
                  }
                }
              },
              "examples": {
                "resend": {
                  "summary": "Resend confirmation",
                  "value": {
                    "user": {
                      "email": "jane@example.com"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Instructions sent (or paranoid success)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "sent": {
                    "summary": "Confirmation instructions sent",
                    "value": {
                      "toast": {
                        "title": "Instructions sent.",
                        "message": [
                          "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error (blank email)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "blank_email": {
                    "summary": "Email is blank",
                    "value": {
                      "toast": {
                        "title": "Could not send instructions.",
                        "message": [
                          "Email can't be blank"
                        ],
                        "variant": "danger"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/users/unlock": {
      "post": {
        "operationId": "requestUnlockInstructions",
        "tags": [
          "Auth"
        ],
        "security": [],
        "summary": "Request account unlock instructions",
        "description": "Sends unlock instructions email to the given address. In paranoid mode (default), always returns success — even if the email is not registered or the account is not locked — to prevent email enumeration. Blank email returns 422. Requires lockout to be enabled with an email-based unlock strategy (both or email).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "user"
                ],
                "properties": {
                  "user": {
                    "type": "object",
                    "required": [
                      "email"
                    ],
                    "properties": {
                      "email": {
                        "type": "string",
                        "format": "email",
                        "description": "Email address of the locked account.",
                        "example": "jane@example.com"
                      }
                    }
                  }
                }
              },
              "examples": {
                "unlock_request": {
                  "summary": "Request unlock",
                  "value": {
                    "user": {
                      "email": "jane@example.com"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Instructions sent (or paranoid success)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "sent": {
                    "summary": "Unlock instructions sent",
                    "value": {
                      "toast": {
                        "title": "Instructions sent.",
                        "message": [
                          "If your email address exists in our database, you will receive an email with instructions for how to unlock your account in a few minutes."
                        ],
                        "variant": "success"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error (blank email)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "blank_email": {
                    "summary": "Email is blank",
                    "value": {
                      "toast": {
                        "title": "Could not send instructions.",
                        "message": [
                          "Email can't be blank"
                        ],
                        "variant": "danger"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/consent/acknowledge": {
      "post": {
        "operationId": "acknowledgeConsent",
        "tags": [
          "System"
        ],
        "summary": "Acknowledge consent banner (AC-8)",
        "description": "Records the user's acknowledgment of the system consent banner in the Rails session. Required by NIST AC-8 before interacting with the application. Does not require authentication — the consent modal appears before login.",
        "security": [],
        "responses": {
          "200": {
            "description": "Consent acknowledged (timestamp stored in session)"
          }
        }
      }
    },
    "/projects/{projectId}/project_access_requests": {
      "post": {
        "operationId": "createProjectAccessRequest",
        "tags": [
          "Projects"
        ],
        "summary": "Request access to a project",
        "description": "Creates an access request for the current user on the specified project. Project admins are notified via email (if SMTP is enabled). Returns JSON with toast and the new request ID, or an HTML redirect for browser requests. Returns 422 if the user has already requested access.",
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "Numeric ID of the project to request access to.",
            "schema": {
              "type": "integer"
            },
            "example": 7
          }
        ],
        "responses": {
          "200": {
            "description": "Access request created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccessRequestToastResponse"
                },
                "examples": {
                  "submitted": {
                    "summary": "Access request submitted",
                    "value": {
                      "toast": {
                        "title": "Access request submitted.",
                        "message": [
                          "Your request for access has been sent."
                        ],
                        "variant": "success"
                      },
                      "id": 42
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error — the user has already requested access",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                },
                "examples": {
                  "duplicate_request": {
                    "summary": "Duplicate access request",
                    "value": {
                      "toast": {
                        "title": "Could not request access.",
                        "message": [
                          "User has already requested access to this project"
                        ],
                        "variant": "danger"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/projects/{projectId}/project_access_requests/{requestId}": {
      "delete": {
        "operationId": "destroyProjectAccessRequest",
        "tags": [
          "Projects"
        ],
        "summary": "Deny or cancel a project access request",
        "description": "Admins can deny a pending access request; the requesting user can cancel their own request. If SMTP is enabled, a rejection email is sent when an admin denies. Returns JSON with toast and destroyed request ID, or HTML redirect for browser requests.",
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "Numeric ID of the project.",
            "schema": {
              "type": "integer"
            },
            "example": 7
          },
          {
            "name": "requestId",
            "in": "path",
            "required": true,
            "description": "Numeric ID of the access request.",
            "schema": {
              "type": "integer"
            },
            "example": 42
          }
        ],
        "responses": {
          "200": {
            "description": "Access request destroyed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccessRequestToastResponse"
                },
                "examples": {
                  "denied": {
                    "summary": "Admin denied access request",
                    "value": {
                      "toast": {
                        "title": "Access request denied.",
                        "message": [
                          "Successfully denied Jane Doe's request to access project."
                        ],
                        "variant": "success"
                      },
                      "id": 42
                    }
                  },
                  "cancelled": {
                    "summary": "Requester cancelled their own request",
                    "value": {
                      "toast": {
                        "title": "Access request cancelled.",
                        "message": [
                          "Your request to access Photon OS 5 has been cancelled."
                        ],
                        "variant": "success"
                      },
                      "id": 42
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "Request could not be destroyed."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/projects/{projectId}/triage_response_templates": {
      "get": {
        "operationId": "listTriageResponseTemplates",
        "tags": [
          "Projects"
        ],
        "summary": "List triage response templates for a project",
        "description": "Returns all response templates for the specified project, ordered by name. Viewer+ role required. Templates are reusable canned responses that triagers can insert into the response textarea when making triage decisions.",
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "Numeric ID of the project.",
            "schema": {
              "type": "integer"
            },
            "example": 42
          }
        ],
        "responses": {
          "200": {
            "description": "List of templates",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "triage_response_templates"
                  ],
                  "properties": {
                    "triage_response_templates": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TriageResponseTemplate"
                      }
                    }
                  }
                },
                "examples": {
                  "with_templates": {
                    "summary": "Project with templates",
                    "value": {
                      "triage_response_templates": [
                        {
                          "id": 1,
                          "name": "Accept - standard",
                          "body": "Concur with the finding as written.",
                          "created_by_id": 42,
                          "created_at": "2026-06-01T10:00:00Z"
                        }
                      ]
                    }
                  },
                  "empty": {
                    "summary": "No templates yet",
                    "value": {
                      "triage_response_templates": []
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      },
      "post": {
        "operationId": "createTriageResponseTemplate",
        "tags": [
          "Projects"
        ],
        "summary": "Create a triage response template",
        "description": "Creates a new reusable response template for the project. Admin role required. Template names must be unique within the project (case-insensitive).",
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "Numeric ID of the project.",
            "schema": {
              "type": "integer"
            },
            "example": 42
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "triage_response_template"
                ],
                "properties": {
                  "triage_response_template": {
                    "type": "object",
                    "required": [
                      "name",
                      "body"
                    ],
                    "properties": {
                      "name": {
                        "type": "string",
                        "description": "Short display name (max 200 chars).",
                        "example": "Decline - needs evidence"
                      },
                      "body": {
                        "type": "string",
                        "description": "Markdown response text.",
                        "example": "Unable to incorporate without supporting evidence."
                      }
                    }
                  }
                }
              },
              "examples": {
                "basic": {
                  "summary": "Create a template",
                  "value": {
                    "triage_response_template": {
                      "name": "Decline - needs evidence",
                      "body": "Unable to incorporate without supporting evidence."
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Template created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "triage_response_template": {
                      "$ref": "#/components/schemas/TriageResponseTemplate"
                    }
                  }
                },
                "examples": {
                  "created": {
                    "summary": "Newly created template",
                    "value": {
                      "triage_response_template": {
                        "id": 3,
                        "name": "Decline - needs evidence",
                        "body": "Unable to incorporate without supporting evidence.",
                        "created_by_id": 42,
                        "created_at": "2026-06-03T01:00:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/projects/{projectId}/triage_response_templates/{id}": {
      "patch": {
        "operationId": "updateTriageResponseTemplate",
        "tags": [
          "Projects"
        ],
        "summary": "Update a triage response template",
        "description": "Updates an existing response template. Admin role required. Name uniqueness is enforced within the project.",
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "Numeric ID of the project.",
            "schema": {
              "type": "integer"
            },
            "example": 42
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric ID of the template.",
            "schema": {
              "type": "integer"
            },
            "example": 1
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "triage_response_template"
                ],
                "properties": {
                  "triage_response_template": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string",
                        "example": "Accept - updated"
                      },
                      "body": {
                        "type": "string",
                        "example": "Updated response text."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Template updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "triage_response_template": {
                      "$ref": "#/components/schemas/TriageResponseTemplate"
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      },
      "put": {
        "operationId": "replaceTriageResponseTemplate",
        "tags": [
          "Projects"
        ],
        "summary": "Replace a triage response template",
        "description": "Full replacement of an existing template. Same behavior as PATCH. Admin role required.",
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "Numeric ID of the project.",
            "schema": {
              "type": "integer"
            },
            "example": 42
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric ID of the template.",
            "schema": {
              "type": "integer"
            },
            "example": 1
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "triage_response_template"
                ],
                "properties": {
                  "triage_response_template": {
                    "type": "object",
                    "required": [
                      "name",
                      "body"
                    ],
                    "properties": {
                      "name": {
                        "type": "string",
                        "example": "Accept - standard"
                      },
                      "body": {
                        "type": "string",
                        "example": "Concur with the finding as written."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Template replaced",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "triage_response_template": {
                      "$ref": "#/components/schemas/TriageResponseTemplate"
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      },
      "delete": {
        "operationId": "deleteTriageResponseTemplate",
        "tags": [
          "Projects"
        ],
        "summary": "Delete a triage response template",
        "description": "Permanently deletes a response template. Admin role required. Returns 204 No Content on success.",
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "Numeric ID of the project.",
            "schema": {
              "type": "integer"
            },
            "example": 42
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric ID of the template.",
            "schema": {
              "type": "integer"
            },
            "example": 1
          }
        ],
        "responses": {
          "204": {
            "description": "Template deleted"
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/personal_access_tokens": {
      "get": {
        "operationId": "listPersonalAccessTokens",
        "tags": [
          "Personal Access Tokens"
        ],
        "summary": "List current user's API tokens",
        "description": "Returns all personal access tokens for the authenticated user, ordered by creation date (newest first). Admins can pass user_id to list another user's tokens. Token digests are never exposed. Session auth only — token-authenticated requests receive 403.",
        "security": [
          {
            "cookieAuth": []
          }
        ],
        "parameters": [
          {
            "name": "user_id",
            "in": "query",
            "required": false,
            "description": "Admin-only. List tokens for this user instead of the current user.",
            "schema": {
              "type": "integer"
            },
            "example": 42
          }
        ],
        "responses": {
          "200": {
            "description": "Token list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "personal_access_tokens"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "personal_access_tokens": {
                      "type": "array",
                      "items": {
                        "description": "Shaped by the caller: the owner's own tokens, or — when an administrator passes user_id — that user's tokens with the owner identity attached.",
                        "oneOf": [
                          {
                            "$ref": "#/components/schemas/PersonalAccessTokenSummary"
                          },
                          {
                            "$ref": "#/components/schemas/PersonalAccessTokenAdminSummary"
                          }
                        ]
                      }
                    }
                  }
                },
                "examples": {
                  "with_tokens": {
                    "summary": "User has two active tokens",
                    "value": {
                      "personal_access_tokens": [
                        {
                          "id": 1,
                          "name": "CI Pipeline",
                          "token_prefix": "vulcan_a",
                          "scopes": [
                            "read",
                            "write"
                          ],
                          "expires_at": "2026-08-30",
                          "last_used_at": "2026-05-30 14:22:01 UTC",
                          "revoked_at": null,
                          "allowed_ips": null,
                          "created_at": "2026-05-30 10:00:00 UTC"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Token-authenticated request rejected — token management requires a session (RFC 9457 problem details)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "type",
                    "title",
                    "status",
                    "detail"
                  ],
                  "properties": {
                    "type": {
                      "type": "string",
                      "description": "Stable machine identifier for the error class.",
                      "example": "/docs/api/errors#session_authentication_required"
                    },
                    "title": {
                      "type": "string",
                      "description": "Short human summary of the error class.",
                      "example": "Session authentication required"
                    },
                    "status": {
                      "type": "integer",
                      "description": "HTTP status code, repeated in the body.",
                      "example": 403
                    },
                    "detail": {
                      "type": "string",
                      "description": "Occurrence-specific human explanation.",
                      "example": "Token management requires a signed-in browser session; API tokens cannot create or revoke tokens."
                    }
                  }
                },
                "examples": {
                  "session_required": {
                    "summary": "API token used against token management",
                    "value": {
                      "type": "/docs/api/errors#session_authentication_required",
                      "title": "Session authentication required",
                      "status": 403,
                      "detail": "Token management requires a signed-in browser session; API tokens cannot create or revoke tokens."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      },
      "post": {
        "operationId": "createPersonalAccessToken",
        "tags": [
          "Personal Access Tokens"
        ],
        "summary": "Create a new API token",
        "description": "Creates a personal access token ON THE SIGNED-IN ACCOUNT. The raw token is returned ONCE in the response — it is never stored or retrievable after this. Requires current password for session hijack protection. Session auth only. Max 20 active tokens per user, max 365-day lifetime. Ownership can never be redirected: a token authenticates AS its owner, so no caller — administrators included — may mint one on another user's account, and any user_id supplied in the body is ignored. Administrators oversee other users' tokens by listing (GET with user_id) and revoking them; account recovery goes through a password reset, where the user re-authenticates.",
        "security": [
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "personal_access_token"
                ],
                "properties": {
                  "personal_access_token": {
                    "type": "object",
                    "required": [
                      "name",
                      "scopes",
                      "current_password"
                    ],
                    "properties": {
                      "name": {
                        "type": "string",
                        "example": "CI Pipeline"
                      },
                      "scopes": {
                        "type": "array",
                        "items": {
                          "type": "string",
                          "enum": [
                            "read",
                            "write",
                            "admin"
                          ]
                        },
                        "example": [
                          "read",
                          "write"
                        ]
                      },
                      "expires_at": {
                        "type": "string",
                        "format": "date",
                        "example": "2026-08-30"
                      },
                      "allowed_ips": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        },
                        "example": [
                          "10.0.0.0/8"
                        ]
                      },
                      "current_password": {
                        "type": "string",
                        "description": "Required for session hijack protection."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Token created — raw token shown once",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PersonalAccessTokenCreateResponse"
                }
              }
            },
            "links": {
              "RevokeCreatedToken": {
                "operationId": "revokePersonalAccessToken",
                "parameters": {
                  "tokenId": "$response.body#/personal_access_token/id"
                },
                "description": "Revoke the newly created token."
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "description": "Validation failed. Toast JSON for domain validation (invalid scopes, max tokens reached, etc.); RFC 9457 problem details when the current password re-verification fails — token creation re-verifies the signed-in user's password before minting a credential, and 422 (not 401) says the authenticated caller's input was wrong.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "type",
                    "title",
                    "status",
                    "detail"
                  ],
                  "properties": {
                    "type": {
                      "type": "string",
                      "description": "Stable machine identifier for the error class.",
                      "example": "/docs/api/errors#incorrect_password"
                    },
                    "title": {
                      "type": "string",
                      "description": "Short human summary of the error class.",
                      "example": "Incorrect password"
                    },
                    "status": {
                      "type": "integer",
                      "description": "HTTP status code, repeated in the body.",
                      "example": 422
                    },
                    "detail": {
                      "type": "string",
                      "description": "Occurrence-specific human explanation.",
                      "example": "Creating or managing API tokens re-verifies your identity, and the current password provided does not match."
                    }
                  }
                },
                "examples": {
                  "incorrect_password": {
                    "summary": "Password re-verification failed",
                    "value": {
                      "type": "/docs/api/errors#incorrect_password",
                      "title": "Incorrect password",
                      "status": 422,
                      "detail": "Creating or managing API tokens re-verifies your identity, and the current password provided does not match."
                    }
                  }
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/personal_access_tokens/{tokenId}": {
      "parameters": [
        {
          "name": "tokenId",
          "in": "path",
          "required": true,
          "description": "Numeric ID of the personal access token.",
          "schema": {
            "type": "integer"
          },
          "example": 1
        }
      ],
      "delete": {
        "operationId": "revokePersonalAccessToken",
        "tags": [
          "Personal Access Tokens"
        ],
        "summary": "Revoke an API token",
        "description": "Soft-deletes the token by setting revoked_at. The token immediately stops working for API authentication. Audit trail is preserved. Session auth only — users can only revoke their own tokens.",
        "security": [
          {
            "cookieAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Token revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    },
    "/personal_access_tokens/{tokenId}/admin_revoke": {
      "parameters": [
        {
          "name": "tokenId",
          "in": "path",
          "required": true,
          "description": "Numeric ID of the personal access token to admin-revoke.",
          "schema": {
            "type": "integer"
          },
          "example": 1
        }
      ],
      "delete": {
        "operationId": "adminRevokePersonalAccessToken",
        "tags": [
          "Personal Access Tokens"
        ],
        "summary": "Admin revoke any user's token",
        "description": "Admin-only endpoint. Revokes any user's token with a required audit comment explaining the reason (e.g. compromised credentials). The audit comment is recorded in the audit trail. Session auth only.",
        "security": [
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "audit_comment"
                ],
                "properties": {
                  "audit_comment": {
                    "type": "string",
                    "description": "Reason for admin revocation (recorded in audit trail).",
                    "example": "Compromised credentials reported by user."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token revoked by admin",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToastResponse"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "4XX": {
            "$ref": "#/components/responses/ErrorResponse"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "cookieAuth": {
        "type": "apiKey",
        "in": "cookie",
        "name": "_vulcan_session"
      },
      "tokenAuth": {
        "type": "http",
        "scheme": "token",
        "description": "Personal access token authentication. Send via Authorization header: `Authorization: Token vulcan_xxx`. Tokens are SHA-256 hashed server-side (never stored in plaintext). Scopes: read (GET), write (mutations), admin (everything). Create tokens via Settings → API Tokens in the web UI."
      }
    },
    "schemas": {
      "CurrentUserResponse": {
        "description": "Authenticated user identity for SPA route guards and UI state.",
        "type": "object",
        "required": [
          "id",
          "email",
          "admin"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique user identifier.",
            "example": 42
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display name. Null for users who haven't set one.",
            "example": "Jane Doe"
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "Login email address.",
            "example": "jane@example.com"
          },
          "admin": {
            "type": "boolean",
            "description": "Whether the user has admin privileges.",
            "example": false
          },
          "provider": {
            "type": [
              "string",
              "null"
            ],
            "description": "Authentication provider (local, github, ldap, oidc). Null for local.",
            "example": null
          }
        }
      },
      "NavigationResponse": {
        "description": "App shell navigation data for SPA navbar. Includes static nav links, pending access request notifications, and locked user alerts.",
        "type": "object",
        "required": [
          "nav_links",
          "access_requests",
          "locked_users"
        ],
        "properties": {
          "nav_links": {
            "type": "array",
            "description": "Primary navigation links for the sidebar/navbar. Every entry is a top-level link; the documentation entry points at the docs site served by the application.",
            "items": {
              "type": "object",
              "properties": {
                "icon": {
                  "type": "string",
                  "description": "Bootstrap icon name.",
                  "example": "folder2-open"
                },
                "name": {
                  "type": "string",
                  "description": "Display label.",
                  "example": "Projects"
                },
                "link": {
                  "type": "string",
                  "description": "URL path.",
                  "example": "/projects"
                }
              }
            }
          },
          "access_requests": {
            "type": "array",
            "description": "Pending project access requests visible to the current user. Empty for non-admin users.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "description": "Access request ID.",
                  "example": 1
                },
                "user": {
                  "type": "object",
                  "description": "Requesting user summary.",
                  "properties": {
                    "id": {
                      "type": "integer",
                      "example": 42
                    },
                    "name": {
                      "type": "string",
                      "example": "Jane Doe"
                    },
                    "email": {
                      "type": "string",
                      "format": "email",
                      "example": "jane@example.com"
                    }
                  }
                },
                "project": {
                  "type": "object",
                  "description": "Target project summary.",
                  "properties": {
                    "id": {
                      "type": "integer",
                      "example": 7
                    },
                    "name": {
                      "type": "string",
                      "example": "RHEL 9 STIG"
                    }
                  }
                }
              }
            }
          },
          "locked_users": {
            "type": "array",
            "description": "Locked user accounts (admin-only, lockout enabled). Empty for non-admin or when lockout disabled.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 42
                },
                "name": {
                  "type": "string",
                  "example": "Locked User"
                },
                "email": {
                  "type": "string",
                  "format": "email",
                  "example": "locked@example.com"
                }
              }
            }
          }
        }
      },
      "SettingsResponse": {
        "description": "Public application settings for pre-auth SPA UI.",
        "type": "object",
        "required": [
          "banner",
          "consent",
          "local_login",
          "user_registration",
          "ldap",
          "oidc",
          "smtp",
          "password",
          "lockout"
        ],
        "properties": {
          "banner": {
            "type": "object",
            "description": "Classification banner configuration.",
            "properties": {
              "enabled": {
                "type": "boolean",
                "example": true
              },
              "text": {
                "type": "string",
                "example": "UNCLASSIFIED"
              },
              "background_color": {
                "type": "string",
                "example": "#007a33"
              },
              "text_color": {
                "type": "string",
                "example": "#ffffff"
              }
            }
          },
          "consent": {
            "type": "object",
            "description": "Consent/terms-of-use modal configuration.",
            "properties": {
              "enabled": {
                "type": "boolean",
                "example": false
              },
              "version": {
                "type": "integer",
                "description": "Consent version number. Incrementing re-prompts all users.",
                "example": 1
              },
              "title": {
                "type": "string",
                "example": "Terms of Use"
              },
              "content": {
                "type": "string",
                "example": ""
              },
              "ttl": {
                "type": "integer",
                "description": "Time-to-live in seconds. 0 means consent never expires.",
                "example": 0
              }
            }
          },
          "local_login": {
            "type": "object",
            "description": "Local email/password login availability.",
            "properties": {
              "enabled": {
                "type": "boolean",
                "example": true
              }
            }
          },
          "user_registration": {
            "type": "object",
            "description": "Self-service registration availability.",
            "properties": {
              "enabled": {
                "type": "boolean",
                "example": true
              }
            }
          },
          "ldap": {
            "type": "object",
            "description": "LDAP authentication provider.",
            "properties": {
              "enabled": {
                "type": "boolean",
                "example": false
              },
              "title": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Display name for the LDAP login button.",
                "example": null
              }
            }
          },
          "oidc": {
            "type": "object",
            "description": "OpenID Connect authentication provider.",
            "properties": {
              "enabled": {
                "type": "boolean",
                "example": false
              },
              "title": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Display name for the OIDC login button.",
                "example": null
              }
            }
          },
          "smtp": {
            "type": "object",
            "description": "Whether outbound email is available.",
            "properties": {
              "enabled": {
                "type": "boolean",
                "example": false
              }
            }
          },
          "password": {
            "type": "object",
            "description": "Password complexity policy for client-side validation.",
            "properties": {
              "min_length": {
                "type": "integer",
                "example": 15
              },
              "min_uppercase": {
                "type": "integer",
                "example": 2
              },
              "min_lowercase": {
                "type": "integer",
                "example": 2
              },
              "min_number": {
                "type": "integer",
                "example": 2
              },
              "min_special": {
                "type": "integer",
                "example": 2
              }
            }
          },
          "lockout": {
            "type": "object",
            "description": "Account lockout policy.",
            "properties": {
              "enabled": {
                "type": "boolean",
                "example": true
              },
              "maximum_attempts": {
                "type": "integer",
                "example": 3
              },
              "last_attempt_warning": {
                "type": "boolean",
                "example": true
              }
            }
          }
        }
      },
      "VersionResponse": {
        "description": "Application version and runtime information. No authentication required.",
        "type": "object",
        "required": [
          "name",
          "version",
          "rails",
          "ruby",
          "environment"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "description": "Application name.",
            "example": "Vulcan"
          },
          "version": {
            "type": "string",
            "description": "Current Vulcan application version.",
            "example": "2.4.1"
          },
          "rails": {
            "type": "string",
            "description": "Rails framework version running the application.",
            "example": "8.1.3.1"
          },
          "ruby": {
            "type": "string",
            "description": "Ruby interpreter version running the application.",
            "example": "3.4.10"
          },
          "environment": {
            "type": "string",
            "description": "Current Rails environment.",
            "enum": [
              "development",
              "test",
              "production"
            ],
            "example": "development"
          }
        }
      },
      "GlobalSearchResponse": {
        "description": "Results from a global search across projects, components, rules, SRGs, STIGs, and their rules. Serialized by Api::SearchController#global. Each sub-array uses controller-specific inline hashes, NOT Blueprints.",
        "type": "object",
        "required": [
          "projects",
          "components",
          "rules",
          "srgs",
          "stigs",
          "stig_rules",
          "srg_rules"
        ],
        "properties": {
          "projects": {
            "type": "array",
            "description": "Projects matching the search query (from user's available_projects).",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "description": "Unique project identifier.",
                  "example": 1
                },
                "name": {
                  "type": "string",
                  "description": "Project name.",
                  "example": "Photon 3"
                },
                "description": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Project description.",
                  "example": null
                },
                "components_count": {
                  "type": "integer",
                  "description": "Number of components in the project.",
                  "example": 1
                }
              }
            }
          },
          "components": {
            "type": "array",
            "description": "Components matching the search query, scoped to what the caller can access — project or component membership, or released components; admins unrestricted.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "description": "Unique component identifier.",
                  "example": 1
                },
                "name": {
                  "type": "string",
                  "description": "Component name.",
                  "example": "Photon OS 3"
                },
                "version": {
                  "type": [
                    "integer",
                    "null"
                  ],
                  "description": "Component version number.",
                  "example": 1
                },
                "release": {
                  "type": [
                    "integer",
                    "null"
                  ],
                  "description": "Component release number.",
                  "example": 1
                },
                "project_id": {
                  "type": "integer",
                  "description": "ID of the parent project.",
                  "example": 1
                },
                "project_name": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Name of the parent project.",
                  "example": "Photon 3"
                },
                "metadata": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "description": "Component metadata key-value pairs (from component_metadata.data).",
                  "example": null
                }
              }
            }
          },
          "rules": {
            "type": "array",
            "description": "Requirement rows of both document kinds — stig rules and authored SRG requirements — matching the search query, scoped to what the caller can access: project or component membership, or released components; admins unrestricted.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "description": "Unique rule identifier.",
                  "example": 1786
                },
                "rule_id": {
                  "type": "string",
                  "description": "Six-digit zero-padded rule number.",
                  "example": "000001"
                },
                "title": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Title of the security control.",
                  "example": "The operating system must provide automated mechanisms for supporting account management functions."
                },
                "status": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Current applicability status of the rule.",
                  "example": "Not Yet Determined"
                },
                "component_id": {
                  "type": "integer",
                  "description": "ID of the component this rule belongs to.",
                  "example": 1
                },
                "component_prefix": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Prefix of the parent component.",
                  "example": "PHOS-03"
                },
                "snippet": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Text excerpt from the matched field showing the search hit in context.",
                  "example": "[Vuln discussion] ...provide automated mechanisms for supporting..."
                },
                "matched_field": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Name of the field where the search term was found.",
                  "example": "title"
                },
                "comment_count": {
                  "type": "integer",
                  "description": "Number of comments on this rule.",
                  "example": 0
                },
                "parent_rule_id": {
                  "type": [
                    "integer",
                    "null"
                  ],
                  "description": "ID of the parent rule (via satisfies relationship). Null if no parent; always null for authored SRG requirements (satisfies is stig-only).",
                  "example": null
                },
                "parent_display_name": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Display name of the parent rule. Null if no parent; always null for authored SRG requirements.",
                  "example": null
                }
              }
            }
          },
          "srgs": {
            "type": "array",
            "description": "Security Requirements Guides matching the search query.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "description": "Unique SRG identifier.",
                  "example": 1
                },
                "srg_id": {
                  "type": "string",
                  "description": "DISA SRG identifier.",
                  "example": "Container_Platform_SRG"
                },
                "name": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "SRG name.",
                  "example": "Container Platform SRG - Ver 2, Rel 4"
                },
                "title": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Full SRG title.",
                  "example": "Container Platform Security Requirements Guide"
                },
                "version": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Version string.",
                  "example": "V2R4"
                }
              }
            }
          },
          "stigs": {
            "type": "array",
            "description": "Published STIGs matching the search query.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "description": "Unique STIG identifier.",
                  "example": 1
                },
                "stig_id": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "DISA STIG identifier.",
                  "example": "Application_Security_Development_STIG"
                },
                "name": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "STIG name.",
                  "example": "Application Security Development STIG - Ver 6, Rel 4"
                },
                "title": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Full STIG title.",
                  "example": "Application Security and Development Security Technical Implementation Guide"
                },
                "version": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Version string.",
                  "example": "V6R4"
                },
                "description": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "STIG description.",
                  "example": null
                }
              }
            }
          },
          "stig_rules": {
            "type": "array",
            "description": "Individual rules from published STIGs matching the search query.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "description": "Unique STIG rule identifier.",
                  "example": 600
                },
                "rule_id": {
                  "type": "string",
                  "description": "DISA rule identifier.",
                  "example": "SV-222396r857506_rule"
                },
                "vuln_id": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Vulnerability identifier (V-number).",
                  "example": "V-222396"
                },
                "title": {
                  "type": "string",
                  "description": "Requirement title.",
                  "example": "The application must enforce approved authorizations."
                },
                "fixtext": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Fix text.",
                  "example": "Configure the application to enforce approved authorizations."
                },
                "ident": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "CCI identifiers.",
                  "example": "CCI-000213"
                },
                "stig_id": {
                  "type": "integer",
                  "description": "ID of the parent STIG.",
                  "example": 1
                },
                "stig_name": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Name of the parent STIG.",
                  "example": "Application Security Development STIG - Ver 6, Rel 4"
                }
              }
            }
          },
          "srg_rules": {
            "type": "array",
            "description": "Requirements from the published SRG catalog matching the search query. Component-authored requirements are project content and never appear here.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "description": "Unique SRG rule identifier.",
                  "example": 500
                },
                "rule_id": {
                  "type": "string",
                  "description": "DISA rule identifier.",
                  "example": "SV-222396r857506_rule"
                },
                "title": {
                  "type": "string",
                  "description": "Requirement title.",
                  "example": "The application must enforce approved authorizations."
                },
                "fixtext": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Fix text.",
                  "example": "Configure the application to enforce approved authorizations."
                },
                "ident": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "CCI identifiers.",
                  "example": "CCI-000213"
                },
                "srg_id": {
                  "type": "integer",
                  "description": "ID of the parent SRG.",
                  "example": 1
                },
                "srg_name": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Name of the parent SRG.",
                  "example": "Container Platform SRG - Ver 2, Rel 4"
                }
              }
            }
          }
        }
      },
      "ProjectSummary": {
        "description": "Base project fields shared by all views. Serialized by ProjectBlueprint (default view). View-specific schemas (ProjectShowResponse, ProjectIndexResponse) extend this with additional fields.",
        "type": "object",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique project identifier.",
            "example": 1
          },
          "name": {
            "type": "string",
            "description": "Human-readable project name.",
            "example": "Photon 3"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional longer description of the project purpose and scope.",
            "example": null
          },
          "visibility": {
            "type": [
              "string",
              "null"
            ],
            "description": "Project visibility setting controlling discoverability by non-members.",
            "enum": [
              "discoverable",
              "hidden",
              null
            ],
            "example": "hidden"
          },
          "memberships_count": {
            "type": "integer",
            "description": "Number of members in this project.",
            "example": 14
          },
          "admin_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display name of the project's point of contact / admin.",
            "example": "Demo Admin"
          },
          "admin_email": {
            "type": [
              "string",
              "null"
            ],
            "format": "email",
            "description": "Email address of the project's point of contact / admin.",
            "example": "admin@example.org"
          },
          "created_at": {
            "type": "string",
            "description": "Timestamp when the project was created. Format is \"YYYY-MM-DD HH:MM:SS UTC\" (Blueprinter default).",
            "example": "2026-05-19 14:07:37 UTC"
          },
          "updated_at": {
            "type": "string",
            "description": "Timestamp when the project was last modified. Format is \"YYYY-MM-DD HH:MM:SS UTC\" (Blueprinter default).",
            "example": "2026-05-19 14:08:17 UTC"
          }
        }
      },
      "PaginationMeta": {
        "description": "Pagination metadata for paginated list endpoints (pagy-backed). Shared by every endpoint returning the canonical { rows, pagination } envelope.",
        "type": "object",
        "required": [
          "page",
          "per_page",
          "total"
        ],
        "additionalProperties": false,
        "properties": {
          "page": {
            "type": "integer",
            "description": "Current page number (1-based).",
            "example": 1
          },
          "per_page": {
            "type": "integer",
            "description": "Records per page (default 25, maximum 100).",
            "example": 25
          },
          "total": {
            "type": "integer",
            "description": "Total number of records across all pages.",
            "example": 47
          }
        }
      },
      "BenchmarkLatestResponse": {
        "description": "Latest-version benchmark listing for dropdown population. One row per benchmark, ranked by numeric V{major}R{minor} version (V10R1 above V4R4). SRG rows carry srg_id; STIG rows carry stig_id.",
        "type": "object",
        "required": [
          "rows"
        ],
        "additionalProperties": false,
        "properties": {
          "rows": {
            "type": "array",
            "description": "One entry per benchmark, ordered by title.",
            "items": {
              "type": "object",
              "required": [
                "id",
                "title",
                "version",
                "name"
              ],
              "additionalProperties": false,
              "properties": {
                "id": {
                  "type": "integer",
                  "description": "Unique benchmark record identifier.",
                  "example": 42
                },
                "srg_id": {
                  "type": "string",
                  "description": "The SRG's stable benchmark identifier across releases. Present only in /api/srgs/latest responses.",
                  "example": "General_Purpose_Operating_System"
                },
                "stig_id": {
                  "type": "string",
                  "description": "The STIG's stable benchmark identifier across releases. Present only in /api/stigs/latest responses.",
                  "example": "RHEL_9_STIG"
                },
                "title": {
                  "type": "string",
                  "description": "Full benchmark title.",
                  "example": "General Purpose Operating System Security Requirements Guide"
                },
                "version": {
                  "type": "string",
                  "description": "DISA version string in V{major}R{minor} format.",
                  "example": "V3R3"
                },
                "name": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Human-friendly display name with version and release.",
                  "example": "General Purpose Operating System - Ver 3, Rel 3"
                }
              }
            }
          }
        }
      },
      "BenchmarkStatsResponse": {
        "description": "Aggregate stats for a benchmark. SRG responses include a usage section — which components are based on the SRG, scoped to components the caller can see (member projects or released). STIG responses carry counts only.",
        "type": "object",
        "required": [
          "rule_count",
          "severity_counts"
        ],
        "additionalProperties": false,
        "properties": {
          "rule_count": {
            "type": "integer",
            "description": "Total number of rules in the benchmark.",
            "example": 250
          },
          "severity_counts": {
            "type": "object",
            "description": "Rule counts grouped by severity (SQL aggregate).",
            "required": [
              "high",
              "medium",
              "low"
            ],
            "additionalProperties": false,
            "properties": {
              "high": {
                "type": "integer",
                "description": "Count of CAT I (high severity) rules.",
                "example": 30
              },
              "medium": {
                "type": "integer",
                "description": "Count of CAT II (medium severity) rules.",
                "example": 200
              },
              "low": {
                "type": "integer",
                "description": "Count of CAT III (low severity) rules.",
                "example": 20
              }
            }
          },
          "usage": {
            "type": "object",
            "description": "Components based on this SRG, scoped to the caller's visibility (member projects or released). Present only on SRG stats responses.",
            "required": [
              "count",
              "components"
            ],
            "additionalProperties": false,
            "properties": {
              "count": {
                "type": "integer",
                "description": "Number of caller-visible components based on this SRG.",
                "example": 2
              },
              "components": {
                "type": "array",
                "description": "Caller-visible components, ordered by prefix.",
                "items": {
                  "type": "object",
                  "required": [
                    "id",
                    "name",
                    "project_id",
                    "project_name"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "id": {
                      "type": "integer",
                      "description": "Component identifier.",
                      "example": 38
                    },
                    "name": {
                      "type": "string",
                      "description": "Component name.",
                      "example": "RHEL 9 Hardened Baseline"
                    },
                    "project_id": {
                      "type": "integer",
                      "description": "Owning project identifier.",
                      "example": 7
                    },
                    "project_name": {
                      "type": "string",
                      "description": "Owning project name.",
                      "example": "Photon OS 5 Hardening"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "ComponentLatestResponse": {
        "description": "Latest released component per prefix for dropdown population. A component is a STIG in progress — released ones are instance-wide reference data. Ranking is numeric on the integer version/release pair; unreleased drafts never appear.",
        "type": "object",
        "required": [
          "rows"
        ],
        "additionalProperties": false,
        "properties": {
          "rows": {
            "type": "array",
            "description": "One entry per prefix, ordered by prefix.",
            "items": {
              "type": "object",
              "required": [
                "id",
                "prefix",
                "name"
              ],
              "additionalProperties": false,
              "properties": {
                "id": {
                  "type": "integer",
                  "description": "Unique component identifier.",
                  "example": 38
                },
                "prefix": {
                  "type": "string",
                  "description": "STIG ID prefix — the component's identity across releases.",
                  "example": "RHEL-09"
                },
                "name": {
                  "type": "string",
                  "description": "Component name.",
                  "example": "RHEL 9 Hardened Baseline"
                },
                "title": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Component title. Null when not set.",
                  "example": "Red Hat Enterprise Linux 9"
                },
                "version": {
                  "type": [
                    "integer",
                    "null"
                  ],
                  "description": "Component version number.",
                  "example": 2
                },
                "release": {
                  "type": [
                    "integer",
                    "null"
                  ],
                  "description": "Component release number within the version.",
                  "example": 1
                }
              }
            }
          }
        }
      },
      "ComponentSummaryResponse": {
        "description": "Lightweight component header for SPA triage/settings routes: identity, counts, SRG info, the caller's effective permissions, and the serialized comment-phase state machine. Never includes the rules/reviews/histories arrays — fetch the full component for those. The phase booleans mirror the server's write-guards, so clients can predict rejections without reimplementing phase logic.",
        "type": "object",
        "required": [
          "id",
          "name",
          "prefix",
          "document_type",
          "title",
          "released",
          "project_id",
          "rules_count",
          "severity_counts",
          "comment_phase",
          "accepting_new_comments",
          "triaging_active",
          "frozen_for_writes"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique component identifier.",
            "example": 38
          },
          "name": {
            "type": "string",
            "description": "Component name.",
            "example": "RHEL 9 Hardened Baseline"
          },
          "prefix": {
            "type": "string",
            "description": "STIG ID prefix.",
            "example": "RHEL-09"
          },
          "document_type": {
            "type": "string",
            "description": "Authoring profile of the component (stig or srg). Immutable after creation.",
            "enum": [
              "stig",
              "srg"
            ],
            "example": "stig"
          },
          "title": {
            "type": "string",
            "description": "Component title.",
            "example": "Red Hat Enterprise Linux 9"
          },
          "version": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Component version number.",
            "example": 2
          },
          "release": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Component release number within the version.",
            "example": 1
          },
          "released": {
            "type": "boolean",
            "description": "Whether the component has been released.",
            "example": false
          },
          "project_id": {
            "type": "integer",
            "description": "Owning project identifier.",
            "example": 7
          },
          "component_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Parent component identifier when this is an overlay. Null otherwise.",
            "example": null
          },
          "security_requirements_guide_id": {
            "type": "integer",
            "description": "Identifier of the SRG this component is based on.",
            "example": 3
          },
          "based_on_title": {
            "type": "string",
            "description": "Title of the SRG this component is based on.",
            "example": "General Purpose Operating System Security Requirements Guide"
          },
          "based_on_version": {
            "type": "string",
            "description": "Version of the SRG this component is based on.",
            "example": "V3R3"
          },
          "rules_count": {
            "type": "integer",
            "description": "Number of requirement rows in the component — rules for stig-kind components, authored requirements for srg-kind components.",
            "example": 203
          },
          "memberships_count": {
            "type": "integer",
            "description": "Number of direct component memberships.",
            "example": 4
          },
          "severity_counts": {
            "type": "object",
            "description": "Rule counts grouped by severity.",
            "required": [
              "high",
              "medium",
              "low"
            ],
            "additionalProperties": false,
            "properties": {
              "high": {
                "type": "integer",
                "description": "Count of CAT I (high severity) rules.",
                "example": 20
              },
              "medium": {
                "type": "integer",
                "description": "Count of CAT II (medium severity) rules.",
                "example": 173
              },
              "low": {
                "type": "integer",
                "description": "Count of CAT III (low severity) rules.",
                "example": 10
              }
            }
          },
          "pending_comment_count": {
            "type": "integer",
            "description": "Number of pending top-level comments awaiting triage.",
            "example": 5
          },
          "effective_permissions": {
            "type": [
              "string",
              "null"
            ],
            "description": "The caller's effective role on this component (admin, author, reviewer, viewer). Null when the caller has no membership-derived permissions (e.g., reading a released component as a non-member).",
            "example": "viewer"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "Last update timestamp.",
            "example": "2026-07-10T14:07:37.142Z"
          },
          "comment_phase": {
            "type": "string",
            "enum": [
              "open",
              "closed"
            ],
            "description": "Public comment period phase.",
            "example": "open"
          },
          "closed_reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "adjudicating",
              "finalized",
              null
            ],
            "description": "Why the comment period is closed. Null while open or when unset.",
            "example": null
          },
          "comment_period_starts_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Comment period start timestamp. Null when not scheduled.",
            "example": "2026-07-01T00:00:00.000Z"
          },
          "comment_period_ends_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Comment period end timestamp. Null when not scheduled.",
            "example": "2026-07-15T00:00:00.000Z"
          },
          "accepting_new_comments": {
            "type": "boolean",
            "description": "Server write-guard — whether new comments are accepted (phase is open).",
            "example": true
          },
          "triaging_active": {
            "type": "boolean",
            "description": "Server write-guard — whether triage continues (open, or closed while adjudicating).",
            "example": true
          },
          "frozen_for_writes": {
            "type": "boolean",
            "description": "Server write-guard — whether the component is read-only (closed and finalized).",
            "example": false
          },
          "comment_period_days_remaining": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Days until the comment period ends. Null when comments are not being accepted, no end date is set, or the end date has passed.",
            "example": 5
          }
        }
      },
      "RulesByStatus": {
        "description": "Rule counts grouped into the five canonical DISA status buckets (SQL aggregate).",
        "type": "object",
        "required": [
          "not_yet_determined",
          "applicable_configurable",
          "applicable_inherently_meets",
          "applicable_does_not_meet",
          "not_applicable"
        ],
        "additionalProperties": false,
        "properties": {
          "not_yet_determined": {
            "type": "integer",
            "description": "Rules with status Not Yet Determined.",
            "example": 50
          },
          "applicable_configurable": {
            "type": "integer",
            "description": "Rules with status Applicable - Configurable.",
            "example": 120
          },
          "applicable_inherently_meets": {
            "type": "integer",
            "description": "Rules with status Applicable - Inherently Meets.",
            "example": 15
          },
          "applicable_does_not_meet": {
            "type": "integer",
            "description": "Rules with status Applicable - Does Not Meet.",
            "example": 8
          },
          "not_applicable": {
            "type": "integer",
            "description": "Rules with status Not Applicable.",
            "example": 10
          }
        }
      },
      "SrgRulesByStatus": {
        "description": "Requirement counts for an SRG-kind component, grouped into the three-value SRG authoring vocabulary. Disjoint from the five-bucket STIG shape — the two never collapse into one list.",
        "type": "object",
        "required": [
          "not_yet_determined",
          "applicable",
          "not_applicable"
        ],
        "additionalProperties": false,
        "properties": {
          "not_yet_determined": {
            "type": "integer",
            "description": "Authored requirements with status Not Yet Determined.",
            "example": 12
          },
          "applicable": {
            "type": "integer",
            "description": "Authored requirements with status Applicable.",
            "example": 180
          },
          "not_applicable": {
            "type": "integer",
            "description": "Authored requirements with status Not Applicable.",
            "example": 3
          }
        }
      },
      "SeverityBuckets": {
        "description": "Rule counts grouped by severity (SQL aggregate).",
        "type": "object",
        "required": [
          "high",
          "medium",
          "low"
        ],
        "additionalProperties": false,
        "properties": {
          "high": {
            "type": "integer",
            "description": "Count of CAT I (high severity) rules.",
            "example": 20
          },
          "medium": {
            "type": "integer",
            "description": "Count of CAT II (medium severity) rules.",
            "example": 173
          },
          "low": {
            "type": "integer",
            "description": "Count of CAT III (low severity) rules.",
            "example": 10
          }
        }
      },
      "ComponentStatsResponse": {
        "description": "Rule statistics for one component: counts by status and severity plus completion and lock percentages. Percentages are null when the component has no rules — never a fabricated 0.0.",
        "type": "object",
        "required": [
          "document_type",
          "rules_by_status",
          "rules_by_severity",
          "rule_count",
          "completion_pct",
          "lock_pct"
        ],
        "additionalProperties": false,
        "properties": {
          "document_type": {
            "type": "string",
            "description": "Authoring profile of the component — tells clients which rules_by_status branch to expect.",
            "enum": [
              "stig",
              "srg"
            ],
            "example": "stig"
          },
          "rules_by_status": {
            "description": "Status buckets shaped by the component's document_type: the five STIG buckets or the three SRG buckets. The two branch shapes are disjoint; the component's document_type tells clients which to expect.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/RulesByStatus"
              },
              {
                "$ref": "#/components/schemas/SrgRulesByStatus"
              }
            ]
          },
          "rules_by_severity": {
            "$ref": "#/components/schemas/SeverityBuckets"
          },
          "rule_count": {
            "type": "integer",
            "description": "Total number of rules in the component.",
            "example": 203
          },
          "completion_pct": {
            "type": [
              "number",
              "null"
            ],
            "description": "Percentage of rules with a determined status (anything other than Not Yet Determined), rounded to one decimal. Null when there are no rules.",
            "example": 75.4
          },
          "lock_pct": {
            "type": [
              "number",
              "null"
            ],
            "description": "Percentage of locked rules, rounded to one decimal. Null when there are no rules.",
            "example": 12.3
          }
        }
      },
      "ComponentWorkflowStateResponse": {
        "description": "Workflow readiness for a component across the authoring, lock, review, comment, triage, and export stages. Counts are SQL aggregates; the comment booleans mirror the server's write-guards.",
        "type": "object",
        "required": [
          "document_type",
          "authoring",
          "locks",
          "reviews",
          "comment",
          "triage",
          "export"
        ],
        "additionalProperties": false,
        "properties": {
          "document_type": {
            "type": "string",
            "description": "Authoring profile of the component.",
            "enum": [
              "stig",
              "srg"
            ],
            "example": "stig"
          },
          "authoring": {
            "type": "object",
            "description": "Rule authoring progress.",
            "required": [
              "rules_total",
              "rules_determined"
            ],
            "additionalProperties": false,
            "properties": {
              "rules_total": {
                "type": "integer",
                "description": "Total rules in the component.",
                "example": 203
              },
              "rules_determined": {
                "type": "integer",
                "description": "Rules with a determined status (not Not Yet Determined).",
                "example": 153
              }
            }
          },
          "locks": {
            "type": "object",
            "description": "Rule lock progress toward release.",
            "required": [
              "locked",
              "total",
              "all_locked"
            ],
            "additionalProperties": false,
            "properties": {
              "locked": {
                "type": "integer",
                "description": "Number of locked rules.",
                "example": 25
              },
              "total": {
                "type": "integer",
                "description": "Total rules in the component.",
                "example": 203
              },
              "all_locked": {
                "type": "boolean",
                "description": "True when every rule is locked and at least one rule exists — the precondition for release.",
                "example": false
              }
            }
          },
          "reviews": {
            "type": "object",
            "description": "Open review-request workload.",
            "required": [
              "under_review"
            ],
            "additionalProperties": false,
            "properties": {
              "under_review": {
                "type": "integer",
                "description": "Rules currently under review (review requested).",
                "example": 4
              }
            }
          },
          "comment": {
            "type": "object",
            "description": "Public comment period state (server write-guards).",
            "required": [
              "phase",
              "accepting_new_comments",
              "triaging_active",
              "frozen_for_writes",
              "pending_comments"
            ],
            "additionalProperties": false,
            "properties": {
              "phase": {
                "type": "string",
                "enum": [
                  "open",
                  "closed"
                ],
                "description": "Public comment period phase.",
                "example": "open"
              },
              "accepting_new_comments": {
                "type": "boolean",
                "description": "Whether new comments are accepted (phase is open).",
                "example": true
              },
              "triaging_active": {
                "type": "boolean",
                "description": "Whether triage continues (open, or closed while adjudicating).",
                "example": true
              },
              "frozen_for_writes": {
                "type": "boolean",
                "description": "Whether the component is read-only (closed and finalized).",
                "example": false
              },
              "pending_comments": {
                "type": "integer",
                "description": "Top-level comments awaiting triage — rule-attached and component-attached both count.",
                "example": 3
              }
            }
          },
          "triage": {
            "type": "object",
            "description": "Triage and adjudication workload.",
            "required": [
              "pending",
              "awaiting_adjudication"
            ],
            "additionalProperties": false,
            "properties": {
              "pending": {
                "type": "integer",
                "description": "Top-level comments awaiting triage.",
                "example": 3
              },
              "awaiting_adjudication": {
                "type": "integer",
                "description": "Comments triaged concur/concur_with_comment/non_concur but not yet adjudicated.",
                "example": 1
              }
            }
          },
          "export": {
            "type": "object",
            "description": "Release/export readiness.",
            "required": [
              "released",
              "releasable"
            ],
            "additionalProperties": false,
            "properties": {
              "released": {
                "type": "boolean",
                "description": "Whether the component has been released.",
                "example": false
              },
              "releasable": {
                "type": "boolean",
                "description": "Whether the component can be released (all rules locked, not already released).",
                "example": false
              }
            }
          }
        }
      },
      "TriageSummaryResponse": {
        "description": "Triage aggregates over top-level comments — rule-attached and component-attached both count. Served for a single component or aggregated across a project's components. adjudication_pct is null when there are no top-level comments.",
        "type": "object",
        "required": [
          "by_triage_status",
          "total",
          "adjudicated",
          "adjudication_pct"
        ],
        "additionalProperties": false,
        "properties": {
          "by_triage_status": {
            "type": "object",
            "description": "Count of top-level comments per triage status (every status key present).",
            "required": [
              "pending",
              "concur",
              "concur_with_comment",
              "non_concur",
              "duplicate",
              "informational",
              "needs_clarification",
              "withdrawn",
              "addressed_by"
            ],
            "additionalProperties": false,
            "properties": {
              "pending": {
                "type": "integer",
                "description": "Comments awaiting triage.",
                "example": 3
              },
              "concur": {
                "type": "integer",
                "description": "Comments triaged as concur.",
                "example": 1
              },
              "concur_with_comment": {
                "type": "integer",
                "description": "Comments triaged as concur with comment.",
                "example": 0
              },
              "non_concur": {
                "type": "integer",
                "description": "Comments triaged as non-concur.",
                "example": 0
              },
              "duplicate": {
                "type": "integer",
                "description": "Comments marked duplicate of another comment.",
                "example": 0
              },
              "informational": {
                "type": "integer",
                "description": "Comments triaged as informational.",
                "example": 1
              },
              "needs_clarification": {
                "type": "integer",
                "description": "Comments needing clarification from the author.",
                "example": 0
              },
              "withdrawn": {
                "type": "integer",
                "description": "Comments withdrawn by their author.",
                "example": 0
              },
              "addressed_by": {
                "type": "integer",
                "description": "Comments addressed by another rule.",
                "example": 0
              }
            }
          },
          "total": {
            "type": "integer",
            "description": "Total top-level comments.",
            "example": 5
          },
          "adjudicated": {
            "type": "integer",
            "description": "Top-level comments with an adjudication timestamp.",
            "example": 1
          },
          "adjudication_pct": {
            "type": [
              "number",
              "null"
            ],
            "description": "Percentage of top-level comments adjudicated, rounded to one decimal. Null when there are no top-level comments.",
            "example": 20
          }
        }
      },
      "ProjectAggregateStats": {
        "description": "Project-wide requirement statistics aggregated across every component. Per-document-type status sections never collapse into each other; rule_count, completion and lock percentages are type-agnostic.",
        "type": "object",
        "required": [
          "rules_by_status_by_type",
          "rules_by_severity",
          "rule_count",
          "completion_pct",
          "lock_pct"
        ],
        "additionalProperties": false,
        "properties": {
          "rules_by_status_by_type": {
            "type": "object",
            "description": "Per-document-type status sections.",
            "required": [
              "stig",
              "srg"
            ],
            "additionalProperties": false,
            "properties": {
              "stig": {
                "$ref": "#/components/schemas/RulesByStatus"
              },
              "srg": {
                "$ref": "#/components/schemas/SrgRulesByStatus"
              }
            }
          },
          "rules_by_severity": {
            "$ref": "#/components/schemas/SeverityBuckets"
          },
          "rule_count": {
            "type": "integer",
            "description": "Total live requirements across all components, every document kind.",
            "example": 207
          },
          "completion_pct": {
            "type": [
              "number",
              "null"
            ],
            "description": "Percentage of determined requirements (anything other than Not Yet Determined) across all kinds. Null when the project has no requirements.",
            "example": 75.4
          },
          "lock_pct": {
            "type": [
              "number",
              "null"
            ],
            "description": "Percentage of locked requirements across all kinds. Null when empty.",
            "example": 12.3
          }
        }
      },
      "ProjectStatsResponse": {
        "description": "Project-level rule statistics: totals aggregated across every component plus a per-component breakdown. Percentages are null when a denominator is zero.",
        "type": "object",
        "required": [
          "aggregate",
          "components"
        ],
        "additionalProperties": false,
        "properties": {
          "aggregate": {
            "$ref": "#/components/schemas/ProjectAggregateStats"
          },
          "components": {
            "type": "array",
            "description": "Per-component breakdown, ordered by prefix.",
            "items": {
              "type": "object",
              "required": [
                "id",
                "name",
                "prefix",
                "document_type",
                "rule_count",
                "completion_pct",
                "lock_pct"
              ],
              "additionalProperties": false,
              "properties": {
                "id": {
                  "type": "integer",
                  "description": "Component identifier.",
                  "example": 38
                },
                "name": {
                  "type": "string",
                  "description": "Component name.",
                  "example": "RHEL 9 Hardened Baseline"
                },
                "prefix": {
                  "type": "string",
                  "description": "STIG ID prefix.",
                  "example": "RHEL-09"
                },
                "document_type": {
                  "type": "string",
                  "description": "Authoring profile of the component — each row carries its own kind.",
                  "enum": [
                    "stig",
                    "srg"
                  ],
                  "example": "stig"
                },
                "rule_count": {
                  "type": "integer",
                  "description": "Rules in this component.",
                  "example": 203
                },
                "completion_pct": {
                  "type": [
                    "number",
                    "null"
                  ],
                  "description": "Percentage of determined rules. Null when the component has no rules.",
                  "example": 75.4
                },
                "lock_pct": {
                  "type": [
                    "number",
                    "null"
                  ],
                  "description": "Percentage of locked rules. Null when the component has no rules.",
                  "example": 12.3
                }
              }
            }
          }
        }
      },
      "MembershipSummary": {
        "description": "Project or component membership linking a user to a role.",
        "type": "object",
        "required": [
          "id",
          "user_id",
          "role",
          "membership_type",
          "membership_id"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique membership identifier.",
            "example": 15
          },
          "user_id": {
            "type": "integer",
            "description": "ID of the member user.",
            "example": 42
          },
          "role": {
            "type": "string",
            "description": "Access level for this membership.",
            "enum": [
              "viewer",
              "author",
              "reviewer",
              "admin"
            ],
            "example": "author"
          },
          "membership_type": {
            "type": "string",
            "description": "Type of resource this membership belongs to.",
            "enum": [
              "Project",
              "Component"
            ],
            "example": "Project"
          },
          "membership_id": {
            "type": "integer",
            "description": "ID of the project or component this membership belongs to.",
            "example": 4
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display name of the member (delegated from User).",
            "example": "Jane Doe"
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "format": "email",
            "description": "Email address of the member (delegated from User).",
            "example": "jane.doe@example.org"
          }
        }
      },
      "ProjectIndexResponse": {
        "description": "Project listing entry with per-user computed fields (admin, is_member, access_request_id).",
        "type": "object",
        "required": [
          "id",
          "name"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique project identifier.",
            "example": 4
          },
          "name": {
            "type": "string",
            "description": "Project name.",
            "example": "Container Platform"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Project description.",
            "example": "STIG for container orchestration platforms"
          },
          "visibility": {
            "type": "string",
            "description": "Project visibility level.",
            "enum": [
              "discoverable",
              "hidden"
            ],
            "example": "discoverable"
          },
          "memberships_count": {
            "type": "integer",
            "description": "Number of members in this project.",
            "example": 14
          },
          "admin_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the project's point of contact.",
            "example": "Demo Admin"
          },
          "admin_email": {
            "type": [
              "string",
              "null"
            ],
            "format": "email",
            "description": "Email of the project's point of contact.",
            "example": "admin@example.org"
          },
          "created_at": {
            "type": "string",
            "description": "When the project was created. Format is \"YYYY-MM-DD HH:MM:SS UTC\" (Blueprinter default).",
            "example": "2026-05-19 14:07:37 UTC"
          },
          "updated_at": {
            "type": "string",
            "description": "When the project was last modified. Format is \"YYYY-MM-DD HH:MM:SS UTC\" (Blueprinter default).",
            "example": "2026-05-19 14:08:17 UTC"
          },
          "memberships": {
            "type": "array",
            "description": "Project members with roles.",
            "items": {
              "$ref": "#/components/schemas/MembershipSummary"
            }
          },
          "admin": {
            "type": "boolean",
            "description": "Whether the current user is an admin on this project.",
            "example": true
          },
          "is_member": {
            "type": "boolean",
            "description": "Whether the current user is a member of this project.",
            "example": true
          },
          "access_request_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "ID of the current user's pending access request, if any.",
            "example": null
          },
          "pending_comment_count": {
            "type": "integer",
            "description": "Number of pending (untriaged) comments across all components.",
            "example": 10
          },
          "total_comment_count": {
            "type": "integer",
            "description": "Total comment count across all components.",
            "example": 108
          },
          "pending_comment_link": {
            "type": [
              "string",
              "null"
            ],
            "description": "Deep-link to the triage page for pending comments. Null if no comments exist.",
            "example": "/components/29/triage"
          }
        }
      },
      "ProjectInput": {
        "description": "Input fields for creating or updating a Vulcan project.",
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Human-readable project name.",
            "example": "Container Platform"
          },
          "description": {
            "type": "string",
            "description": "Optional longer description of the project purpose and scope.",
            "example": "STIG for container orchestration platforms."
          },
          "visibility": {
            "type": "string",
            "description": "Project visibility setting controlling discoverability by non-members.",
            "enum": [
              "discoverable",
              "hidden"
            ],
            "example": "discoverable"
          },
          "project_metadata_attributes": {
            "type": "object",
            "description": "Project metadata as a nested-attributes object — the one metadata shape, accepted at creation and update alike. The data hash holds free-form string keys (points of contact, Slack channel, and so on).",
            "properties": {
              "data": {
                "type": "object",
                "description": "Free-form metadata keys and values.",
                "additionalProperties": {
                  "type": "string"
                },
                "example": {
                  "POC Name": "Jane Doe",
                  "POC Email": "jane@example.com",
                  "Slack Channel ID": "C0123456789"
                }
              }
            }
          }
        }
      },
      "ToastObject": {
        "description": "The inner toast notification object with title, message array, and variant. This is the reusable shape — ToastResponse wraps it as { toast: ToastObject }. Use this via $ref when composing multi-key responses (toast + user, toast + reset_url, etc.).",
        "type": "object",
        "required": [
          "title",
          "message",
          "variant"
        ],
        "additionalProperties": false,
        "properties": {
          "title": {
            "type": "string",
            "description": "Short human-readable summary of the outcome.",
            "example": "User updated."
          },
          "message": {
            "type": "array",
            "description": "Detail lines providing additional context about the operation result.",
            "items": {
              "type": "string"
            },
            "example": [
              "Successfully updated user."
            ]
          },
          "variant": {
            "type": "string",
            "description": "Bootstrap alert variant controlling how the toast is styled in the UI.",
            "enum": [
              "success",
              "danger",
              "warning",
              "info"
            ],
            "example": "success"
          }
        }
      },
      "ProjectCreateResponse": {
        "description": "Response from project creation. Success includes a toast notification and a redirect URL to the new project page. Error includes only the toast (no redirect_url).",
        "type": "object",
        "required": [
          "toast"
        ],
        "properties": {
          "toast": {
            "$ref": "#/components/schemas/ToastObject"
          },
          "redirect_url": {
            "type": "string",
            "description": "Relative URL to the newly created project page. Present on success, absent on error.",
            "example": "/projects/1"
          }
        }
      },
      "ProjectDetails": {
        "description": "Project-wide requirement statistics with per-document-type sections. The stig and srg sections never collapse into each other; top-level numbers (total, lck, nur, ur) are type-agnostic.",
        "type": "object",
        "required": [
          "stig",
          "srg",
          "nur",
          "ur",
          "lck",
          "total"
        ],
        "additionalProperties": false,
        "properties": {
          "stig": {
            "type": "object",
            "description": "STIG-kind requirement counts in the five DISA buckets.",
            "required": [
              "ac",
              "aim",
              "adnm",
              "na",
              "nyd",
              "total"
            ],
            "additionalProperties": false,
            "properties": {
              "ac": {
                "type": "integer",
                "description": "Applicable - Configurable.",
                "example": 120
              },
              "aim": {
                "type": "integer",
                "description": "Applicable - Inherently Meets.",
                "example": 15
              },
              "adnm": {
                "type": "integer",
                "description": "Applicable - Does Not Meet.",
                "example": 8
              },
              "na": {
                "type": "integer",
                "description": "Not Applicable.",
                "example": 10
              },
              "nyd": {
                "type": "integer",
                "description": "Not Yet Determined.",
                "example": 50
              },
              "total": {
                "type": "integer",
                "description": "Total STIG-kind requirements.",
                "example": 203
              }
            }
          },
          "srg": {
            "type": "object",
            "description": "SRG-kind requirement counts in the three-value vocabulary.",
            "required": [
              "applicable",
              "na",
              "nyd",
              "total"
            ],
            "additionalProperties": false,
            "properties": {
              "applicable": {
                "type": "integer",
                "description": "Applicable.",
                "example": 180
              },
              "na": {
                "type": "integer",
                "description": "Not Applicable.",
                "example": 3
              },
              "nyd": {
                "type": "integer",
                "description": "Not Yet Determined.",
                "example": 12
              },
              "total": {
                "type": "integer",
                "description": "Total SRG-kind requirements.",
                "example": 195
              }
            }
          },
          "nur": {
            "type": "integer",
            "description": "Unlocked requirements not under review (all kinds).",
            "example": 40
          },
          "ur": {
            "type": "integer",
            "description": "Unlocked requirements under review (all kinds).",
            "example": 5
          },
          "lck": {
            "type": "integer",
            "description": "Locked requirements (all kinds).",
            "example": 158
          },
          "total": {
            "type": "integer",
            "description": "Total live requirements across every document kind.",
            "example": 398
          }
        }
      },
      "ComponentSummary": {
        "description": "Base component fields shared by all views. Serialized by ComponentBlueprint (default view). View-specific schemas (ComponentEditorResponse, ComponentIndexResponse) extend this with additional fields.",
        "type": "object",
        "required": [
          "id",
          "name",
          "prefix"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique component identifier.",
            "example": 1
          },
          "name": {
            "type": "string",
            "description": "Human-readable component name.",
            "example": "Photon OS 3"
          },
          "prefix": {
            "type": "string",
            "description": "Short alphanumeric prefix used to construct rule IDs (e.g. PHOS-03 in PHOS-03-000010).",
            "example": "PHOS-03"
          },
          "document_type": {
            "type": "string",
            "description": "Authoring profile of the component: stig components author STIG requirements, srg components author SRG requirements. Immutable after creation.",
            "enum": [
              "stig",
              "srg"
            ],
            "example": "stig"
          },
          "version": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Version number of the component. Null if not yet set.",
            "example": 1
          },
          "release": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Release number of the component. Null if not yet set.",
            "example": 1
          },
          "based_on_title": {
            "type": [
              "string",
              "null"
            ],
            "description": "Title of the SRG this component is based on. Null if the SRG has no title or component has no SRG.",
            "example": "VMware vSphere 7.0 STIG Readiness Guide"
          },
          "based_on_version": {
            "type": [
              "string",
              "null"
            ],
            "description": "Version of the SRG this component is based on. Null if no SRG.",
            "example": "1"
          },
          "severity_counts": {
            "type": "object",
            "description": "Count of rules at each severity level within this component.",
            "properties": {
              "high": {
                "type": "integer",
                "example": 20
              },
              "medium": {
                "type": "integer",
                "example": 173
              },
              "low": {
                "type": "integer",
                "example": 10
              }
            },
            "example": {
              "high": 20,
              "medium": 173,
              "low": 10
            }
          },
          "pending_comment_count": {
            "type": "integer",
            "description": "Number of pending (untriaged) top-level comments on this component. Pre-batched via Component.pending_comment_counts.",
            "example": 3
          }
        }
      },
      "ComponentIndexResponse": {
        "description": "Component listing entry for project pages. Serialized by ComponentBlueprint :index view. Extends ComponentSummary (default fields) with index-specific fields.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ComponentSummary"
          },
          {
            "type": "object",
            "properties": {
              "updated_at": {
                "type": "string",
                "description": "Timestamp when the component was last modified. Format is \"YYYY-MM-DD HH:MM:SS UTC\" (Blueprinter default).",
                "example": "2026-05-29 15:36:06 UTC"
              },
              "released": {
                "type": "boolean",
                "description": "Whether this component has been marked as released (finalized).",
                "example": true
              },
              "rules_count": {
                "type": "integer",
                "description": "Number of requirement rows (security controls) in this component — rules for stig-kind components, authored requirements for srg-kind components.",
                "example": 203
              },
              "component_id": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "ID of the source component if this is an overlay. Null for original components.",
                "example": null
              }
            }
          }
        ]
      },
      "ProjectShowResponse": {
        "description": "Full project detail for the project show page. Serialized by ProjectBlueprint :show view. 18 total fields including nested components, memberships, users, access_requests.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ProjectSummary"
          },
          {
            "type": "object",
            "properties": {
              "effective_permissions": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Current user's role on this project (admin, reviewer, author, viewer). Null when the user is not a member. System admins always get 'admin'.",
                "enum": [
                  "admin",
                  "reviewer",
                  "author",
                  "viewer",
                  null
                ],
                "example": "admin"
              },
              "pending_comment_count": {
                "type": "integer",
                "description": "Aggregate pending comment count across all components.",
                "example": 1
              },
              "details": {
                "description": "Project-wide requirement statistics. Per-document-type sections (stig with the five DISA buckets, srg with the three SRG buckets) never collapse into each other; total, lock, and review-state numbers are type-agnostic and span every kind.",
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/ProjectDetails"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "histories": {
                "type": "array",
                "description": "Recent audit trail entries (last 50).",
                "items": {
                  "type": "object"
                }
              },
              "metadata": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "Project metadata key-value pairs.",
                "example": null
              },
              "memberships": {
                "type": "array",
                "description": "Project members with roles.",
                "items": {
                  "$ref": "#/components/schemas/MembershipSummary"
                }
              },
              "components": {
                "type": "array",
                "description": "Components in this project (ComponentBlueprint :index view).",
                "items": {
                  "$ref": "#/components/schemas/ComponentIndexResponse"
                }
              },
              "available_components": {
                "type": "array",
                "description": "Components available to add (from other projects the user can access).",
                "items": {
                  "$ref": "#/components/schemas/ComponentIndexResponse"
                }
              },
              "users": {
                "type": "array",
                "description": "All users associated with this project.",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "name": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "email": {
                      "type": "string",
                      "format": "email"
                    }
                  }
                }
              },
              "access_requests": {
                "type": "array",
                "description": "Pending access requests from non-member users.",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer",
                      "description": "Access request ID."
                    },
                    "user": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "email": {
                          "type": "string",
                          "format": "email"
                        }
                      }
                    },
                    "project_id": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "ToastResponse": {
        "description": "Canonical mutation response returned by all POST/PUT/PATCH/DELETE endpoints that return only a toast notification (no additional data).",
        "type": "object",
        "required": [
          "toast"
        ],
        "additionalProperties": false,
        "properties": {
          "toast": {
            "$ref": "#/components/schemas/ToastObject"
          }
        }
      },
      "ImportSummary": {
        "description": "Summary of what was imported (or would be imported in dry_run mode) from a JSON archive backup. Returned by JsonArchiveImporter.",
        "type": "object",
        "properties": {
          "dry_run": {
            "type": "boolean",
            "description": "Whether this was a preview (true) or real import (false). Only present in dry_run responses.",
            "example": true
          },
          "components_imported": {
            "type": "integer",
            "description": "Number of components imported.",
            "example": 2
          },
          "rules_imported": {
            "type": "integer",
            "description": "Total rules imported across all components.",
            "example": 264
          },
          "satisfactions_imported": {
            "type": "integer",
            "description": "Number of rule satisfaction relationships imported.",
            "example": 12
          },
          "reviews_imported": {
            "type": "integer",
            "description": "Number of review records imported.",
            "example": 48
          },
          "memberships_imported": {
            "type": "integer",
            "description": "Number of project memberships imported.",
            "example": 3
          },
          "srgs_imported": {
            "type": "integer",
            "description": "Number of SRGs auto-imported from the archive.",
            "example": 1
          },
          "srg_details": {
            "type": "array",
            "description": "Details of SRGs found in the archive (dry_run only).",
            "items": {
              "type": "object",
              "properties": {
                "title": {
                  "type": "string"
                },
                "version": {
                  "type": "string"
                }
              }
            }
          },
          "component_details": {
            "type": "array",
            "description": "Per-component breakdown of what was imported.",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "example": "Photon OS 3"
                },
                "rule_count": {
                  "type": "integer",
                  "example": 132
                },
                "srg_title": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "example": "General Purpose Operating System SRG"
                },
                "srg_version": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "example": "V3R3"
                }
              }
            }
          }
        }
      },
      "CreateFromBackupDryRunResponse": {
        "description": "Response from a dry-run preview of creating a project from backup. Returns import summary, warnings, and extracted project defaults.",
        "type": "object",
        "required": [
          "summary",
          "warnings",
          "project_defaults"
        ],
        "properties": {
          "summary": {
            "$ref": "#/components/schemas/ImportSummary"
          },
          "warnings": {
            "type": "array",
            "description": "Non-fatal warnings found during preview.",
            "items": {
              "type": "string"
            },
            "example": []
          },
          "project_defaults": {
            "type": "object",
            "description": "Default values extracted from the archive for pre-filling the create form.",
            "properties": {
              "name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Original project name from the archive.",
                "example": "My Original Project"
              },
              "description": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Original project description.",
                "example": "A test project"
              },
              "visibility": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Original project visibility setting.",
                "example": "discoverable"
              }
            }
          }
        }
      },
      "CreateFromBackupResponse": {
        "description": "Response from actually creating a project from a backup archive. Returns redirect URL, import summary, and toast notification.",
        "type": "object",
        "required": [
          "redirect_url",
          "summary",
          "toast"
        ],
        "properties": {
          "redirect_url": {
            "type": "string",
            "description": "URL to redirect to after successful project creation.",
            "example": "/projects/123"
          },
          "summary": {
            "$ref": "#/components/schemas/ImportSummary"
          },
          "toast": {
            "$ref": "#/components/schemas/ToastObject"
          }
        }
      },
      "ImportBackupResponse": {
        "description": "Response from importing a JSON archive backup into an existing project. Includes the toast notification, import summary, and any warnings.",
        "type": "object",
        "required": [
          "toast",
          "summary",
          "warnings"
        ],
        "properties": {
          "toast": {
            "$ref": "#/components/schemas/ToastObject"
          },
          "summary": {
            "$ref": "#/components/schemas/ImportSummary"
          },
          "warnings": {
            "type": "array",
            "description": "Non-fatal warnings encountered during import.",
            "items": {
              "type": "string"
            },
            "example": []
          }
        }
      },
      "AuditEntry": {
        "description": "A single audit trail entry recording a change to an auditable record. Serialized by VulcanAudit#format. Note: the field is \"name\" (username), NOT \"user_id\" — VulcanAudit#format does not expose user IDs.",
        "type": "object",
        "required": [
          "id",
          "action",
          "auditable_type",
          "auditable_id",
          "created_at",
          "audited_changes"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique audit entry identifier.",
            "example": 224
          },
          "action": {
            "type": "string",
            "description": "Type of change that was made.",
            "enum": [
              "create",
              "update",
              "destroy"
            ],
            "example": "update"
          },
          "auditable_type": {
            "type": "string",
            "description": "Rails model class name of the audited record.",
            "example": "Component"
          },
          "auditable_id": {
            "type": "integer",
            "description": "Primary key of the audited record.",
            "example": 1
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display name of the user who made the change. Null for system-initiated changes or seed data.",
            "example": "Demo Admin"
          },
          "audited_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display name of the audited record's owner. Null when not applicable.",
            "example": null
          },
          "comment": {
            "type": [
              "string",
              "null"
            ],
            "description": "Audit comment text (e.g. reason for admin action). Null for most changes.",
            "example": null
          },
          "created_at": {
            "type": "string",
            "description": "Timestamp when the audit entry was recorded.",
            "example": "2026-05-19 14:07:49 UTC"
          },
          "audited_changes": {
            "type": "array",
            "description": "Array of changed attributes with before/after values. Each entry has a field name, previous value, and new value.",
            "items": {
              "type": "object",
              "required": [
                "field"
              ],
              "properties": {
                "field": {
                  "type": "string",
                  "description": "Name of the changed attribute. For AdditionalAnswer changes, this is the question name rather than the column name.",
                  "example": "released"
                },
                "prev_value": {
                  "description": "Previous value before the change. Null for create actions.",
                  "example": false
                },
                "new_value": {
                  "description": "New value after the change.",
                  "example": true
                }
              }
            }
          }
        }
      },
      "ComponentInput": {
        "description": "Input fields for creating or updating a component within a project.",
        "type": "object",
        "required": [
          "name",
          "prefix",
          "title",
          "security_requirements_guide_id"
        ],
        "properties": {
          "security_requirements_guide_id": {
            "type": "integer",
            "description": "The PRIMARY source SRG (based_on). Always a declared source: it joins the declared source set automatically even when declared_source_srg_ids omits it. STIG-kind components derive from non-core SRGs, SRG-kind components from core SRGs.",
            "example": 1
          },
          "declared_source_srg_ids": {
            "type": "array",
            "description": "The full declared source-SRG set for multi-parent derivation. Every listed SRG becomes a declared parent and contributes its requirements at creation (full union by default). Eligibility is enforced per document_type — non-core SRGs for stig, core SRGs for srg. Omit for the classic single-source create.",
            "items": {
              "type": "integer"
            },
            "example": [
              1,
              2
            ]
          },
          "requirement_selections": {
            "type": "object",
            "description": "Selective-import filter. Keys are declared source SRG ids, values the requirement versions to import from that source. When present, only the listed requirements are imported and a declared source absent from the map contributes none; when omitted, creation imports the full union of every declared source.",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "example": {
              "1": [
                "SRG-OS-000001",
                "SRG-OS-000002"
              ],
              "2": [
                "SRG-APP-000101"
              ]
            }
          },
          "name": {
            "type": "string",
            "description": "Human-readable component name.",
            "example": "Container SRG"
          },
          "prefix": {
            "type": "string",
            "description": "Short alphanumeric prefix used to construct rule IDs (e.g. CNTR in CNTR-00-000050).",
            "example": "CNTR"
          },
          "document_type": {
            "type": "string",
            "description": "Authoring profile for the new component: stig authors STIG requirements from a source SRG; srg authors SRG requirements. Defaults to stig; immutable after creation.",
            "enum": [
              "stig",
              "srg"
            ],
            "example": "stig"
          },
          "version": {
            "type": "integer",
            "description": "Version number of the component.",
            "example": 1
          },
          "release": {
            "type": "integer",
            "description": "Release number of the component.",
            "example": 1
          },
          "title": {
            "type": "string",
            "description": "Full official title of the component.",
            "example": "Container Platform Security Technical Implementation Guide"
          },
          "description": {
            "type": "string",
            "description": "Optional longer description of the component scope and purpose.",
            "example": "STIG guidance for container orchestration platform deployments."
          },
          "advanced_fields": {
            "type": "boolean",
            "description": "Whether the editor shows the advanced field set for this component.",
            "example": false
          },
          "additional_questions_attributes": {
            "type": "array",
            "description": "Nested additional-question definitions — the same shape the update path accepts, minus id/_destroy (meaningless at creation and not accepted). On the duplicate path, provided questions REPLACE the ones copied from the source component.",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "example": "Deployment environment"
                },
                "question_type": {
                  "type": "string",
                  "enum": [
                    "dropdown",
                    "freeform",
                    "url"
                  ],
                  "example": "dropdown"
                },
                "options": {
                  "type": "array",
                  "description": "Choice list — required when question_type is dropdown.",
                  "items": {
                    "type": "string"
                  },
                  "example": [
                    "Cloud",
                    "On-prem"
                  ]
                }
              }
            }
          },
          "component_metadata_attributes": {
            "type": "object",
            "description": "Nested metadata payload; data is a free-form string map — the same shape the update path accepts. On the duplicate path, provided metadata replaces the copied record. The slack_channel_id convenience field merges into this map as \"Slack Channel ID\" and wins over a same-key entry.",
            "properties": {
              "data": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                },
                "example": {
                  "Vendor": "Acme",
                  "POC": "Sam"
                }
              }
            }
          },
          "slack_channel_id": {
            "type": "string",
            "description": "Convenience field — stored as the metadata key \"Slack Channel ID\". Accepted at creation only; use component_metadata_attributes on update.",
            "example": "C0123456789"
          }
        }
      },
      "RuleSummary": {
        "description": "Base rule fields shared by all views. Serialized by RuleBlueprint (default view / navigator). View-specific schemas (RuleEditorResponse, RulePickerResponse) extend this with additional fields.",
        "type": "object",
        "required": [
          "id",
          "rule_id",
          "locked"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique rule identifier.",
            "example": 1786
          },
          "rule_id": {
            "type": "string",
            "description": "Six-digit zero-padded rule number combined with the component prefix to form the displayed ID (e.g. PHOS-03-000001).",
            "example": "000001"
          },
          "title": {
            "type": [
              "string",
              "null"
            ],
            "description": "Title of the security control. Null if not yet authored.",
            "example": "The operating system must provide automated mechanisms for supporting account management functions."
          },
          "version": {
            "type": [
              "string",
              "null"
            ],
            "description": "SRG requirement version identifier mapping this rule to its SRG source.",
            "example": "SRG-OS-000001-GPOS-00001"
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "description": "Current applicability status of the rule. Null if not yet determined.",
            "enum": [
              "Not Yet Determined",
              "Applicable - Configurable",
              "Applicable - Inherently Meets",
              "Applicable - Does Not Meet",
              "Not Applicable",
              null
            ],
            "example": "Not Yet Determined"
          },
          "rule_severity": {
            "type": [
              "string",
              "null"
            ],
            "description": "DISA severity level of the rule.",
            "enum": [
              "low",
              "medium",
              "high",
              null
            ],
            "example": "medium"
          },
          "locked": {
            "type": "boolean",
            "description": "Whether the rule is locked from further edits pending review.",
            "example": true
          },
          "review_requestor_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "ID of the user who requested review for this rule. Null if no review requested.",
            "example": null
          },
          "changes_requested": {
            "type": "boolean",
            "description": "Whether changes have been requested on this rule during review.",
            "example": false
          },
          "comment_summary": {
            "type": "object",
            "description": "Per-rule comment counts. Computed in-memory from eager-loaded reviews.",
            "required": [
              "open",
              "total"
            ],
            "properties": {
              "open": {
                "type": "integer",
                "description": "Comments not yet adjudicated (pending, triaged, needs_clarification) including replies under open parents.",
                "example": 0
              },
              "total": {
                "type": "integer",
                "description": "Total comment count across all statuses.",
                "example": 0
              }
            }
          }
        }
      },
      "DisaRuleDescription": {
        "description": "DISA rule description fields — vulnerability discussion, mitigations, and related metadata.",
        "type": "object",
        "required": [
          "id"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique description record identifier.",
            "example": 300
          },
          "vuln_discussion": {
            "type": [
              "string",
              "null"
            ],
            "description": "Vulnerability discussion explaining why this requirement matters.",
            "example": "Without verification, containers may execute untrusted code..."
          },
          "false_positives": {
            "type": [
              "string",
              "null"
            ],
            "description": "Known false positive scenarios for this check.",
            "example": null
          },
          "false_negatives": {
            "type": [
              "string",
              "null"
            ],
            "description": "Known false negative scenarios for this check.",
            "example": null
          },
          "documentable": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Whether this requirement is documentable.",
            "example": false
          },
          "mitigations": {
            "type": [
              "string",
              "null"
            ],
            "description": "Available mitigations if the requirement cannot be met directly.",
            "example": null
          },
          "severity_override_guidance": {
            "type": [
              "string",
              "null"
            ],
            "description": "Guidance for when severity may be overridden by the assessor.",
            "example": null
          },
          "potential_impacts": {
            "type": [
              "string",
              "null"
            ],
            "description": "Potential impacts of non-compliance.",
            "example": null
          },
          "third_party_tools": {
            "type": [
              "string",
              "null"
            ],
            "description": "Third-party tools that can assist with compliance checking.",
            "example": null
          },
          "mitigation_control": {
            "type": [
              "string",
              "null"
            ],
            "description": "Compensating controls that mitigate the risk.",
            "example": null
          },
          "responsibility": {
            "type": [
              "string",
              "null"
            ],
            "description": "Who is responsible for implementing this requirement.",
            "example": null
          },
          "ia_controls": {
            "type": [
              "string",
              "null"
            ],
            "description": "Information assurance controls associated with this requirement.",
            "example": null
          },
          "mitigations_available": {
            "type": [
              "string",
              "null"
            ],
            "description": "Whether mitigations are available for this requirement.",
            "example": null
          },
          "poam_available": {
            "type": [
              "string",
              "null"
            ],
            "description": "Whether a Plan of Action and Milestones is available.",
            "example": null
          },
          "poam": {
            "type": [
              "string",
              "null"
            ],
            "description": "Plan of Action and Milestones content.",
            "example": null
          },
          "_destroy": {
            "type": "boolean",
            "description": "Nested attributes destroy flag (always false in read responses).",
            "example": false
          }
        }
      },
      "CheckSummary": {
        "description": "STIG check content — how to verify a security requirement is met.",
        "type": "object",
        "required": [
          "id"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique check identifier.",
            "example": 200
          },
          "system": {
            "type": [
              "string",
              "null"
            ],
            "description": "Check system identifier (e.g., C-OVAL for automated checks).",
            "example": "C-56947r840354_chk"
          },
          "content_ref_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Reference name for the check content.",
            "example": "M"
          },
          "content_ref_href": {
            "type": [
              "string",
              "null"
            ],
            "description": "URL reference for the check content.",
            "example": "DPMS_XCCDF-Container_Platform_SRG.xml"
          },
          "content": {
            "type": [
              "string",
              "null"
            ],
            "description": "The check procedure text — steps to verify compliance.",
            "example": "Verify the container platform restricts access to container images..."
          },
          "_destroy": {
            "type": "boolean",
            "description": "Nested attributes destroy flag (always false in read responses).",
            "example": false
          }
        }
      },
      "SatisfactionSummary": {
        "description": "Rule satisfaction relationship — this rule satisfies (implements) an SRG requirement.",
        "type": "object",
        "required": [
          "id",
          "rule_id"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique satisfaction record identifier.",
            "example": 50
          },
          "rule_id": {
            "type": "integer",
            "description": "ID of the rule that is satisfied.",
            "example": 100
          },
          "srg_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "SRG requirement version identifier (from the SRG rule's version field).",
            "example": "CNTR-00-000050"
          }
        }
      },
      "SatisfiedBySummary": {
        "description": "Satisfied-by relationship — extends SatisfactionSummary with the parent rule's fix text.",
        "type": "object",
        "required": [
          "id",
          "rule_id"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique satisfaction record identifier.",
            "example": 50
          },
          "rule_id": {
            "type": "integer",
            "description": "ID of the parent rule that satisfies this one.",
            "example": 100
          },
          "srg_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "SRG requirement version identifier.",
            "example": "CNTR-00-000050"
          },
          "fixtext": {
            "type": [
              "string",
              "null"
            ],
            "description": "Fix text from the parent rule (inherited content).",
            "example": "Configure the container platform to restrict access..."
          }
        }
      },
      "RuleDescriptionSummary": {
        "description": "Rule description record — the HTML/text description block for a rule.",
        "type": "object",
        "required": [
          "id"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique description record identifier.",
            "example": 400
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "The rule description text (may contain HTML from XCCDF source).",
            "example": "<VulnDiscussion>Without verification, containers may execute untrusted code.</VulnDiscussion>"
          },
          "_destroy": {
            "type": "boolean",
            "description": "Nested attributes destroy flag (always false in read responses).",
            "example": false
          }
        }
      },
      "ReviewSummary": {
        "description": "A review action recorded on a rule. Serialized by ReviewBlueprint (default view). The user_id field is intentionally excluded as a public-comment correlation guard.",
        "type": "object",
        "required": [
          "id",
          "action",
          "comment",
          "rule_id",
          "created_at",
          "triage_status",
          "reactions"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique review identifier.",
            "example": 1
          },
          "action": {
            "type": "string",
            "description": "The type of review action that was performed.",
            "enum": [
              "comment",
              "request_review",
              "revoke_review_request",
              "approve",
              "lock"
            ],
            "example": "comment"
          },
          "comment": {
            "type": "string",
            "description": "The comment text accompanying this review action.",
            "example": "The check says \"verify that TLS 1.2 or greater is being used\" but does not specify HOW to verify."
          },
          "created_at": {
            "type": "string",
            "description": "Timestamp when the review was created. Format is \"YYYY-MM-DD HH:MM:SS UTC\" (Blueprinter default).",
            "example": "2026-05-19 14:08:17 UTC"
          },
          "triage_status": {
            "type": [
              "string",
              "null"
            ],
            "description": "Current triage disposition of the comment.",
            "enum": [
              "pending",
              "concur",
              "concur_with_comment",
              "non_concur",
              "duplicate",
              "informational",
              "needs_clarification",
              "withdrawn",
              "addressed_by",
              null
            ],
            "example": "pending"
          },
          "triage_set_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Timestamp when the triage status was last changed. Null if still pending.",
            "example": null
          },
          "adjudicated_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Timestamp when the comment reached a terminal triage state. Null if not yet adjudicated.",
            "example": null
          },
          "rule_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "ID of the rule this review is attached to. Null for component-scoped reviews.",
            "example": 2976
          },
          "section": {
            "type": [
              "string",
              "null"
            ],
            "description": "Rule section the comment applies to (e.g. fixtext, check_content, vuln_discussion). Null for general comments.",
            "example": "check_content"
          },
          "responding_to_review_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "ID of the parent review if this is a threaded reply. Null for top-level reviews.",
            "example": null
          },
          "duplicate_of_review_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "ID of the review this is marked as a duplicate of. Null if not a duplicate.",
            "example": null
          },
          "addressed_by_rule_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "ID of the rule that addresses this comment. Set when triage_status is addressed_by. Null otherwise.",
            "example": null
          },
          "triage_set_by_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "ID of the user who last changed the triage status. Null if still pending.",
            "example": null
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display name of the review author (delegated from User).",
            "example": "Demo Viewer"
          },
          "author_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display name of the review author (explicit field, same value as name).",
            "example": "Demo Viewer"
          },
          "triager_display_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display name of the user who triaged this comment. Null if not yet triaged. Uses ImportedAttribution resolution (DB user name → imported name → imported email).",
            "example": null
          },
          "triager_imported": {
            "type": "boolean",
            "description": "Whether the triager attribution came from an import (no matching DB user).",
            "example": false
          },
          "adjudicator_display_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display name of the user who adjudicated this comment. Null if not yet adjudicated.",
            "example": null
          },
          "adjudicator_imported": {
            "type": "boolean",
            "description": "Whether the adjudicator attribution came from an import.",
            "example": false
          },
          "commenter_display_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display name of the comment author. Uses ImportedAttribution resolution.",
            "example": "Demo Viewer"
          },
          "commenter_imported": {
            "type": "boolean",
            "description": "Whether the commenter attribution came from an import.",
            "example": false
          },
          "commentable_type": {
            "type": [
              "string",
              "null"
            ],
            "description": "Polymorphic type — BaseRule for rule-scoped, Component for component-scoped.",
            "example": "BaseRule"
          },
          "responses_count": {
            "type": "integer",
            "description": "Number of direct replies to this review.",
            "example": 0
          },
          "rule_displayed_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Human-readable rule label (PREFIX-RULE_ID). Populated when the serializer is called with rule_names option. Null otherwise.",
            "example": "PHOS-03-000001"
          },
          "author_email": {
            "type": [
              "string",
              "null"
            ],
            "format": "email",
            "description": "Author's email. Only present when include_email option is true (admin-tier surfaces). Null or absent for public endpoints.",
            "example": null
          },
          "commenter_email": {
            "type": [
              "string",
              "null"
            ],
            "format": "email",
            "description": "Commenter's email (same as author_email but follows the commenter attribution naming convention). Only present when include_email option is true. Null for imported reviews with no matching DB user.",
            "example": null
          },
          "reactions": {
            "type": "object",
            "description": "Aggregate reaction counts and current user's reaction for this review.",
            "required": [
              "up",
              "down"
            ],
            "properties": {
              "up": {
                "type": "integer",
                "description": "Number of upvote reactions.",
                "example": 1
              },
              "down": {
                "type": "integer",
                "description": "Number of downvote reactions.",
                "example": 0
              },
              "mine": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The current user's reaction type (up or down). Null if no active reaction.",
                "example": null
              }
            }
          }
        }
      },
      "AdditionalAnswerSummary": {
        "description": "Answer to an additional question on a component rule.",
        "type": "object",
        "required": [
          "id",
          "additional_question_id"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique answer identifier.",
            "example": 10
          },
          "additional_question_id": {
            "type": "integer",
            "description": "ID of the question this answers.",
            "example": 3
          },
          "answer": {
            "type": [
              "string",
              "null"
            ],
            "description": "The answer text provided by the author.",
            "example": "Yes, this applies to all container runtime environments."
          }
        }
      },
      "SrgRuleSummary": {
        "description": "SRG rule — the original SRG requirement that a component rule implements.",
        "type": "object",
        "required": [
          "id",
          "rule_id",
          "title"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique SRG rule identifier.",
            "example": 500
          },
          "rule_id": {
            "type": "string",
            "description": "DISA rule identifier (e.g., SV-222xxx_rule).",
            "example": "SV-222396r857506_rule"
          },
          "title": {
            "type": "string",
            "description": "Requirement title.",
            "example": "The container platform must enforce approved authorizations for access."
          },
          "version": {
            "type": [
              "string",
              "null"
            ],
            "description": "SRG version identifier.",
            "example": "CNTR-00-000050"
          },
          "rule_severity": {
            "type": [
              "string",
              "null"
            ],
            "description": "Severity level.",
            "enum": [
              "low",
              "medium",
              "high"
            ],
            "example": "medium"
          },
          "rule_weight": {
            "type": [
              "string",
              "null"
            ],
            "description": "Rule weight for scoring.",
            "example": "10.0"
          },
          "ident": {
            "type": [
              "string",
              "null"
            ],
            "description": "CCI identifiers (comma-separated).",
            "example": "CCI-000213"
          },
          "ident_system": {
            "type": [
              "string",
              "null"
            ],
            "description": "Identifier system URI.",
            "example": "http://cyber.mil/cci"
          },
          "fixtext": {
            "type": [
              "string",
              "null"
            ],
            "description": "Fix text describing how to remediate.",
            "example": "Configure the container platform to enforce approved authorizations..."
          },
          "fixtext_fixref": {
            "type": [
              "string",
              "null"
            ],
            "description": "Fix reference identifier.",
            "example": "F-25073r857505_fix"
          },
          "fix_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Fix identifier.",
            "example": "F-25073r857505_fix"
          },
          "inspec_control_body": {
            "type": [
              "string",
              "null"
            ],
            "description": "InSpec control Ruby source code.",
            "example": null
          },
          "inspec_control_file": {
            "type": [
              "string",
              "null"
            ],
            "description": "InSpec control file path.",
            "example": null
          },
          "inspec_control_body_lang": {
            "type": [
              "string",
              "null"
            ],
            "description": "Language of the InSpec control body (usually ruby).",
            "example": null
          },
          "inspec_control_file_lang": {
            "type": [
              "string",
              "null"
            ],
            "description": "Language of the InSpec control file.",
            "example": null
          },
          "vuln_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Vulnerability identifier (V-number).",
            "example": "V-222396"
          },
          "legacy_ids": {
            "type": [
              "string",
              "null"
            ],
            "description": "Legacy rule identifiers from previous STIG versions (comma-separated string).",
            "example": "SV-42474, V-32157"
          },
          "rule_descriptions_attributes": {
            "type": "array",
            "description": "Rule description records.",
            "items": {
              "$ref": "#/components/schemas/RuleDescriptionSummary"
            }
          },
          "disa_rule_descriptions_attributes": {
            "type": "array",
            "description": "DISA rule description records (vuln discussion, mitigations, etc.).",
            "items": {
              "$ref": "#/components/schemas/DisaRuleDescription"
            }
          },
          "checks_attributes": {
            "type": "array",
            "description": "Check content records.",
            "items": {
              "$ref": "#/components/schemas/CheckSummary"
            }
          }
        }
      },
      "RuleEditorResponse": {
        "description": "Full rule for the editing form. Serialized by RuleBlueprint :editor view. Includes viewer fields (text content, nested associations) plus editor-only fields (InSpec, reviews, additional answers, SRG source data). 38 total fields. Used by GET /rules/:id, GET /components/:id/rules, and nested inside ComponentEditorResponse.",
        "allOf": [
          {
            "$ref": "#/components/schemas/RuleSummary"
          },
          {
            "type": "object",
            "required": [
              "satisfies",
              "satisfied_by"
            ],
            "properties": {
              "rule_weight": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Rule weight for DISA scoring.",
                "example": "10.0"
              },
              "fixtext": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Remediation instructions.",
                "example": "Configure the operating system to provide automated mechanisms..."
              },
              "fixtext_fixref": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Fix reference identifier.",
                "example": "F-3716r557030_fix"
              },
              "ident": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "CCI identifiers (comma-separated).",
                "example": "CCI-000015"
              },
              "ident_system": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Identifier system URI.",
                "example": "http://cyber.mil/cci"
              },
              "vendor_comments": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Vendor-provided implementation notes.",
                "example": null
              },
              "vuln_id": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Vulnerability identifier (V-number).",
                "example": null
              },
              "legacy_ids": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Legacy rule identifiers (comma-separated string).",
                "example": "V-56571, SV-70831"
              },
              "component_id": {
                "type": "integer",
                "description": "ID of the component this rule belongs to.",
                "example": 1
              },
              "status_justification": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Justification text for NA or DNM status.",
                "example": null
              },
              "artifact_description": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Description of compliance evidence artifacts.",
                "example": null
              },
              "locked_fields": {
                "type": "object",
                "description": "Map of section names to locked state. Empty object means no sections locked.",
                "additionalProperties": {
                  "type": "boolean"
                },
                "example": {}
              },
              "nist_control_family": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "NIST SP 800-53 control family mapping.",
                "example": "AC-2 (1)"
              },
              "srg_id": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "SRG requirement version this rule implements.",
                "example": "SRG-OS-000001-GPOS-00001"
              },
              "inspec_control_body": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "InSpec control Ruby source code.",
                "example": null
              },
              "inspec_control_file": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "InSpec control file path.",
                "example": null
              },
              "inspec_control_body_lang": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Language of the InSpec control body.",
                "example": "ruby"
              },
              "inspec_control_file_lang": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Language of the InSpec control file.",
                "example": "ruby"
              },
              "fix_id": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Fix identifier.",
                "example": "F-3716r557030_fix"
              },
              "disa_rule_descriptions_attributes": {
                "type": "array",
                "description": "DISA rule description records.",
                "items": {
                  "$ref": "#/components/schemas/DisaRuleDescription"
                }
              },
              "checks_attributes": {
                "type": "array",
                "description": "Check content records.",
                "items": {
                  "$ref": "#/components/schemas/CheckSummary"
                }
              },
              "satisfies": {
                "type": "array",
                "description": "Rules that this rule satisfies (child-to-parent relationships).",
                "items": {
                  "$ref": "#/components/schemas/SatisfactionSummary"
                }
              },
              "satisfied_by": {
                "type": "array",
                "description": "Rules that satisfy this rule (parent-to-child relationships).",
                "items": {
                  "$ref": "#/components/schemas/SatisfiedBySummary"
                }
              },
              "histories": {
                "type": "array",
                "description": "Audit trail entries for this rule, ordered by creation time. Present only when a single rule is requested. The trail cannot be fetched for a whole collection in one query, so a component response omits it for every rule and callers read it from the per-rule endpoint for the one being shown.",
                "items": {
                  "$ref": "#/components/schemas/AuditEntry"
                }
              },
              "rule_descriptions_attributes": {
                "type": "array",
                "description": "Raw rule description records.",
                "items": {
                  "$ref": "#/components/schemas/RuleDescriptionSummary"
                }
              },
              "reviews": {
                "type": "array",
                "description": "Review actions on this rule (comments, approvals, etc.).",
                "items": {
                  "$ref": "#/components/schemas/ReviewSummary"
                }
              },
              "additional_answers_attributes": {
                "type": "array",
                "description": "Answers to component-level additional questions.",
                "items": {
                  "$ref": "#/components/schemas/AdditionalAnswerSummary"
                }
              },
              "srg_rule_attributes": {
                "description": "Source SRG rule data (the baseline requirement this rule implements). Null if no SRG rule linked.",
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/SrgRuleSummary"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "srg_info": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "SRG version metadata.",
                "properties": {
                  "version": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "example": "V2R4"
                  }
                }
              }
            }
          }
        ]
      },
      "AuthoredSrgRuleEditorResponse": {
        "description": "A component-authored SRG requirement for the editing form. Shares the common requirement surface with STIG rules — status, locking, reviews, audit history, content — but its status vocabulary is the three-value SRG set, and it carries none of the STIG-only surfaces: no satisfies/satisfied_by, no srg_rule_attributes, no additional answers, no InSpec fields. Lineage is the portable derived_from_version identifier.",
        "type": "object",
        "required": [
          "id",
          "rule_id",
          "locked",
          "srg_id",
          "derived_from_version"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique requirement identifier.",
            "example": 10633
          },
          "rule_id": {
            "type": "string",
            "description": "Six-digit zero-padded requirement number combined with the component prefix to form the displayed ID.",
            "example": "000001"
          },
          "title": {
            "type": [
              "string",
              "null"
            ],
            "description": "Title of the requirement. Null if not yet authored.",
            "example": "The container platform must enforce approved authorizations."
          },
          "version": {
            "type": [
              "string",
              "null"
            ],
            "description": "Requirement version identifier. Minted at release for authored requirements.",
            "example": null
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "description": "Applicability status in the SRG authoring vocabulary.",
            "enum": [
              "Not Yet Determined",
              "Applicable",
              "Not Applicable",
              null
            ],
            "example": "Applicable"
          },
          "rule_severity": {
            "type": [
              "string",
              "null"
            ],
            "description": "DISA severity level.",
            "enum": [
              "low",
              "medium",
              "high",
              null
            ],
            "example": "medium"
          },
          "locked": {
            "type": "boolean",
            "description": "Whether the requirement is locked from further edits.",
            "example": false
          },
          "review_requestor_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "ID of the user who requested review. Null if no review requested.",
            "example": null
          },
          "changes_requested": {
            "type": "boolean",
            "description": "Whether changes have been requested during review.",
            "example": false
          },
          "comment_summary": {
            "type": "object",
            "description": "Per-requirement comment counts.",
            "required": [
              "open",
              "total"
            ],
            "additionalProperties": false,
            "properties": {
              "open": {
                "type": "integer",
                "description": "Comments not yet adjudicated, including replies under open parents.",
                "example": 1
              },
              "total": {
                "type": "integer",
                "description": "Total comment count across all statuses.",
                "example": 1
              }
            }
          },
          "rule_weight": {
            "type": [
              "string",
              "null"
            ],
            "description": "Rule weight for DISA scoring.",
            "example": "10.0"
          },
          "fixtext": {
            "type": [
              "string",
              "null"
            ],
            "description": "Remediation guidance authored for this requirement.",
            "example": "Configure the container platform to enforce approved authorizations."
          },
          "fixtext_fixref": {
            "type": [
              "string",
              "null"
            ],
            "description": "Fix reference identifier.",
            "example": null
          },
          "ident": {
            "type": [
              "string",
              "null"
            ],
            "description": "Comma-separated CCI identifiers.",
            "example": "CCI-000366"
          },
          "ident_system": {
            "type": [
              "string",
              "null"
            ],
            "description": "Identifier system URI.",
            "example": "http://iase.disa.mil/cci"
          },
          "vendor_comments": {
            "type": [
              "string",
              "null"
            ],
            "description": "Vendor comments.",
            "example": null
          },
          "vuln_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Vulnerability identifier. Assigned at release for authored requirements.",
            "example": null
          },
          "legacy_ids": {
            "type": [
              "string",
              "null"
            ],
            "description": "Comma-separated legacy identifiers.",
            "example": null
          },
          "component_id": {
            "type": "integer",
            "description": "The SRG-kind component this requirement is authored on.",
            "example": 46
          },
          "status_justification": {
            "type": [
              "string",
              "null"
            ],
            "description": "Justification for the chosen status.",
            "example": null
          },
          "artifact_description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Artifact description.",
            "example": null
          },
          "locked_fields": {
            "type": "object",
            "description": "Per-section lock map (section name to true).",
            "example": {}
          },
          "nist_control_family": {
            "type": "string",
            "description": "NIST control families derived from the CCI identifiers.",
            "example": "CM"
          },
          "srg_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The SRG requirement identifier shown by requirement lists (the editor sidebar's SRG ID display) — one kind-agnostic key across document kinds. For an authored requirement this is the row's version value, the same identifier the editor header displays. Null when the row has no requirement identifier yet.",
            "example": "SRG-APP-000003"
          },
          "derived_from_version": {
            "type": [
              "string",
              "null"
            ],
            "description": "Version (SRG-ID) of the core-catalog requirement this authored requirement was derived from. Null for requirements authored from scratch. Always present — it distinguishes authored requirements from STIG rules wherever the two shapes appear in a oneOf.",
            "example": "SRG-OS-000023"
          },
          "disa_rule_descriptions_attributes": {
            "type": "array",
            "description": "DISA rule description records.",
            "items": {
              "$ref": "#/components/schemas/DisaRuleDescription"
            }
          },
          "checks_attributes": {
            "type": "array",
            "description": "Check content records.",
            "items": {
              "$ref": "#/components/schemas/CheckSummary"
            }
          },
          "rule_descriptions_attributes": {
            "type": "array",
            "description": "Raw rule description records.",
            "items": {
              "$ref": "#/components/schemas/RuleDescriptionSummary"
            }
          },
          "histories": {
            "type": "array",
            "description": "Audit trail entries for this requirement, ordered by creation time. Present only when a single requirement is requested. The trail cannot be fetched for a whole collection in one query, so a component response omits it for every requirement and callers read it from the per-requirement endpoint for the one being shown.",
            "items": {
              "$ref": "#/components/schemas/AuditEntry"
            }
          },
          "reviews": {
            "type": "array",
            "description": "Review actions on this requirement (comments, locks, etc.).",
            "items": {
              "$ref": "#/components/schemas/ReviewSummary"
            }
          }
        }
      },
      "ComponentEditorResponse": {
        "description": "Full component for the editing page. Serialized by ComponentBlueprint :editor view. 35 total fields including nested rules, memberships, histories, reviews. Used by GET /components/:id (member), GET /components/:id/rules (via index).",
        "allOf": [
          {
            "$ref": "#/components/schemas/ComponentSummary"
          },
          {
            "type": "object",
            "properties": {
              "effective_permissions": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Current user's role on this component's project (admin, reviewer, author, viewer). Null when user is not a member. System admins always get 'admin'.",
                "enum": [
                  "admin",
                  "reviewer",
                  "author",
                  "viewer",
                  null
                ],
                "example": "admin"
              },
              "title": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Full official title of the component.",
                "example": "Photon OS 3 STIG Readiness Guide"
              },
              "description": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Optional longer description.",
                "example": null
              },
              "admin_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Point of contact name.",
                "example": "Photon OS Maintainer"
              },
              "admin_email": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "email",
                "description": "Point of contact email.",
                "example": "photon-team@example.org"
              },
              "released": {
                "type": "boolean",
                "description": "Whether finalized.",
                "example": true
              },
              "advanced_fields": {
                "type": "boolean",
                "description": "Whether advanced DISA fields are shown in the editor.",
                "example": false
              },
              "project_id": {
                "type": "integer",
                "description": "ID of the parent project.",
                "example": 1
              },
              "component_id": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "ID of the source component if this is an overlay. Null for originals.",
                "example": null
              },
              "security_requirements_guide_id": {
                "type": "integer",
                "description": "ID of the SRG this component is based on.",
                "example": 3
              },
              "memberships_count": {
                "type": "integer",
                "description": "Number of direct memberships on this component.",
                "example": 0
              },
              "rules_count": {
                "type": "integer",
                "description": "Number of requirement rows in this component — rules for stig-kind components, authored requirements for srg-kind components.",
                "example": 203
              },
              "updated_at": {
                "type": "string",
                "description": "Last modified timestamp. Blueprinter format.",
                "example": "2026-05-29 15:36:06 UTC"
              },
              "created_at": {
                "type": "string",
                "description": "Creation timestamp. Blueprinter format.",
                "example": "2026-05-19 14:07:48 UTC"
              },
              "comment_phase": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Current comment period phase.",
                "enum": [
                  "draft",
                  "open",
                  "closed",
                  null
                ],
                "example": "open"
              },
              "closed_reason": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Reason the comment period was closed.",
                "example": null
              },
              "comment_period_starts_at": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Start of the public comment period.",
                "example": null
              },
              "comment_period_ends_at": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "End of the public comment period.",
                "example": null
              },
              "releasable": {
                "type": "boolean",
                "description": "Whether all rules meet release criteria.",
                "example": false
              },
              "srg_is_latest": {
                "type": "boolean",
                "description": "Whether EVERY declared source SRG of the component is at its latest release. A component with any stale parent — including a secondary parent under dual lineage — reports false.",
                "example": true
              },
              "srg_latest_version": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Version string of the newest release available for the first stale parent (primary first, then secondaries). Null when srg_is_latest=true.",
                "example": null
              },
              "srg_latest_id": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "ID of that newest release for linking. Null when srg_is_latest=true.",
                "example": null
              },
              "status_counts": {
                "description": "Requirement counts bucketed by the component's document_type: five STIG buckets or three SRG buckets. The shapes are disjoint and never collapse into one list.",
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/RulesByStatus"
                  },
                  {
                    "$ref": "#/components/schemas/SrgRulesByStatus"
                  }
                ]
              },
              "moved_out_count": {
                "type": "integer",
                "description": "Requirements that relocated OUT of this component — a lifecycle fact counted from executed relocation records, never one of the status buckets. Always 0 for stig-kind components.",
                "example": 0
              },
              "additional_questions": {
                "type": "array",
                "description": "Component-level custom questions for rule authors.",
                "items": {
                  "type": "object"
                }
              },
              "rules": {
                "type": "array",
                "description": "All requirements, shaped by document_type: STIG rule objects for stig components, authored SRG requirement objects for srg components.",
                "items": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/RuleEditorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/AuthoredSrgRuleEditorResponse"
                    }
                  ]
                }
              },
              "reviews": {
                "type": "array",
                "description": "Recent reviews (last 20, hand-built hashes with displayed_rule_name).",
                "items": {
                  "type": "object"
                }
              },
              "histories": {
                "type": "array",
                "description": "Audit trail entries (last 50).",
                "items": {
                  "type": "object"
                }
              },
              "memberships": {
                "type": "array",
                "description": "Direct component memberships.",
                "items": {
                  "$ref": "#/components/schemas/MembershipSummary"
                }
              },
              "metadata": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "Component metadata key-value pairs.",
                "example": null
              },
              "inherited_memberships": {
                "type": "array",
                "description": "Memberships inherited from the parent project.",
                "items": {
                  "$ref": "#/components/schemas/MembershipSummary"
                }
              }
            }
          }
        ]
      },
      "ComponentShowResponse": {
        "description": "Component detail for non-member read-only view. Serialized by ComponentBlueprint :show view. Includes rules (viewer view), reviews, and effective_permissions.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ComponentSummary"
          },
          {
            "type": "object",
            "properties": {
              "effective_permissions": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Current user's role on this component's project (admin, reviewer, author, viewer). Null when user is not a member.",
                "enum": [
                  "admin",
                  "reviewer",
                  "author",
                  "viewer",
                  null
                ],
                "example": "viewer"
              },
              "title": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Full official title of the component.",
                "example": "Photon OS 3 STIG Readiness Guide"
              },
              "description": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Optional longer description.",
                "example": null
              },
              "admin_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Point of contact name.",
                "example": "Jane Doe"
              },
              "admin_email": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "email",
                "description": "Point of contact email.",
                "example": "jane@example.com"
              },
              "released": {
                "type": "boolean",
                "description": "Whether the component has been released.",
                "example": false
              },
              "updated_at": {
                "type": "string",
                "description": "Last update timestamp.",
                "example": "2026-05-28 15:00:00 UTC"
              },
              "comment_phase": {
                "type": "string",
                "description": "Current comment workflow phase.",
                "enum": [
                  "open",
                  "closed"
                ],
                "example": "open"
              },
              "closed_reason": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Reason for closure (adjudicating or finalized).",
                "example": null
              },
              "comment_period_starts_at": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "When the comment period started.",
                "example": null
              },
              "comment_period_ends_at": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "When the comment period ends.",
                "example": null
              },
              "rules": {
                "type": "array",
                "description": "Rules in viewer view.",
                "items": {
                  "type": "object"
                }
              },
              "reviews": {
                "type": "array",
                "description": "Reviews with displayed_rule_name.",
                "items": {
                  "type": "object"
                }
              }
            }
          }
        ]
      },
      "ComponentUpdateInput": {
        "description": "Partial update for an existing component — every field is optional and only provided attributes change. The authoring profile (document_type) is immutable after creation and is not accepted here.",
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Human-readable component name.",
            "example": "Container SRG"
          },
          "prefix": {
            "type": "string",
            "description": "Short alphanumeric prefix used to construct rule IDs (e.g. CNTR in CNTR-00-000050).",
            "example": "CNTR"
          },
          "version": {
            "type": "integer",
            "description": "Version number of the component.",
            "example": 1
          },
          "release": {
            "type": "integer",
            "description": "Release number of the component.",
            "example": 1
          },
          "title": {
            "type": "string",
            "description": "Full official title of the component.",
            "example": "Container Platform Security Technical Implementation Guide"
          },
          "description": {
            "type": "string",
            "description": "Optional longer description of the component scope and purpose.",
            "example": "STIG guidance for container orchestration platform deployments."
          },
          "released": {
            "type": "boolean",
            "description": "Marks the component as released (admin-gated workflow action).",
            "example": false
          },
          "admin_name": {
            "type": "string",
            "description": "Point-of-contact name.",
            "example": "Jane Analyst"
          },
          "admin_email": {
            "type": "string",
            "description": "Point-of-contact email.",
            "example": "jane.analyst@example.com"
          },
          "advanced_fields": {
            "type": "boolean",
            "description": "Whether the editor shows the advanced field set for this component.",
            "example": false
          },
          "comment_phase": {
            "type": "string",
            "description": "Public comment phase for the component.",
            "enum": [
              "draft",
              "open",
              "triage",
              "final"
            ],
            "example": "open"
          },
          "closed_reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Reason recorded when the comment phase closes.",
            "example": null
          },
          "comment_period_starts_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Public comment period opening time.",
            "example": "2026-07-01T00:00:00Z"
          },
          "comment_period_ends_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Public comment period closing time.",
            "example": "2026-08-01T00:00:00Z"
          },
          "additional_questions_attributes": {
            "type": "array",
            "description": "Nested additional-question definitions (id + _destroy for removal).",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 3
                },
                "name": {
                  "type": "string",
                  "example": "Deployment environment"
                },
                "question_type": {
                  "type": "string",
                  "example": "dropdown"
                },
                "_destroy": {
                  "type": "boolean",
                  "example": false
                },
                "options": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "example": [
                    "Cloud",
                    "On-prem"
                  ]
                }
              }
            }
          },
          "component_metadata_attributes": {
            "type": "object",
            "description": "Nested metadata payload; data is a free-form string map.",
            "properties": {
              "data": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                },
                "example": {
                  "Slack Channel ID": "C123456"
                }
              }
            }
          }
        }
      },
      "RuleContent": {
        "description": "Rule content fields for the split-pane triage view, serialized by Component#serialize_rule_content. Present on comment rows only when the include_rule_content query parameter is true and the comment targets a rule (null for component-level comments).",
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "title": {
            "type": [
              "string",
              "null"
            ],
            "description": "Rule title.",
            "example": "The container platform must use TLS 1.2 or greater for secure communication."
          },
          "rule_severity": {
            "type": [
              "string",
              "null"
            ],
            "description": "Rule severity.",
            "example": "medium"
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "description": "Applicability status of the rule.",
            "example": "Applicable - Configurable"
          },
          "fixtext": {
            "type": [
              "string",
              "null"
            ],
            "description": "Fix text content.",
            "example": "Configure the container platform to use TLS 1.2 or greater."
          },
          "status_justification": {
            "type": [
              "string",
              "null"
            ],
            "description": "Justification for the current status.",
            "example": "TLS configuration is exposed by the platform and must be set by the operator."
          },
          "vendor_comments": {
            "type": [
              "string",
              "null"
            ],
            "description": "Vendor comments.",
            "example": "Verified against platform version 4.12."
          },
          "artifact_description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Artifact description.",
            "example": "Screenshot of the TLS configuration panel."
          },
          "fix_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "XCCDF fix identifier.",
            "example": "F-59616r921424_fix"
          },
          "fixtext_fixref": {
            "type": [
              "string",
              "null"
            ],
            "description": "XCCDF fixtext reference.",
            "example": "F-59616r921424_fix"
          },
          "version": {
            "type": [
              "string",
              "null"
            ],
            "description": "STIG/SRG identifier of the rule (document order key).",
            "example": "CNTR-00-000050"
          },
          "rule_weight": {
            "type": [
              "string",
              "null"
            ],
            "description": "XCCDF rule weight.",
            "example": "10.0"
          },
          "ident": {
            "type": [
              "string",
              "null"
            ],
            "description": "CCI identifier string.",
            "example": "CCI-000068"
          },
          "ident_system": {
            "type": [
              "string",
              "null"
            ],
            "description": "Identifier system URI.",
            "example": "http://iase.disa.mil/cci"
          },
          "vuln_discussion": {
            "type": [
              "string",
              "null"
            ],
            "description": "Vulnerability discussion from the DISA rule description.",
            "example": "Without cryptographic integrity protections, information can be altered in transit."
          },
          "documentable": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Whether the requirement is documentable.",
            "example": false
          },
          "false_positives": {
            "type": [
              "string",
              "null"
            ],
            "description": "Known false positives.",
            "example": null
          },
          "false_negatives": {
            "type": [
              "string",
              "null"
            ],
            "description": "Known false negatives.",
            "example": null
          },
          "mitigations_available": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Whether mitigations are available.",
            "example": false
          },
          "mitigations": {
            "type": [
              "string",
              "null"
            ],
            "description": "Mitigation description.",
            "example": null
          },
          "poam_available": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Whether a POA&M is available.",
            "example": false
          },
          "poam": {
            "type": [
              "string",
              "null"
            ],
            "description": "POA&M description.",
            "example": null
          },
          "potential_impacts": {
            "type": [
              "string",
              "null"
            ],
            "description": "Potential impacts of applying the fix.",
            "example": "Legacy clients that only support TLS 1.1 will fail to connect."
          },
          "third_party_tools": {
            "type": [
              "string",
              "null"
            ],
            "description": "Third-party tooling notes.",
            "example": null
          },
          "mitigation_control": {
            "type": [
              "string",
              "null"
            ],
            "description": "Mitigation control description.",
            "example": null
          },
          "responsibility": {
            "type": [
              "string",
              "null"
            ],
            "description": "Responsible party.",
            "example": "System Administrator"
          },
          "ia_controls": {
            "type": [
              "string",
              "null"
            ],
            "description": "IA controls reference.",
            "example": null
          },
          "severity_override_guidance": {
            "type": [
              "string",
              "null"
            ],
            "description": "Guidance for severity overrides.",
            "example": null
          },
          "check_content": {
            "type": [
              "string",
              "null"
            ],
            "description": "Check procedure content.",
            "example": "Review the platform TLS configuration and verify the minimum version is 1.2."
          },
          "locked": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Whether the rule is locked.",
            "example": false
          },
          "rule_updated_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 8601 timestamp of the rule's last update, for staleness detection.",
            "example": "2026-05-19T16:15:00Z"
          },
          "satisfied_by": {
            "type": "array",
            "description": "Parent rules that satisfy this rule. Empty for standalone rules. Powers the satisfied-by indicator in the triage rule panel.",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "id",
                "rule_id",
                "component_prefix"
              ],
              "properties": {
                "id": {
                  "type": "integer",
                  "description": "Parent rule database identifier.",
                  "example": 1901
                },
                "rule_id": {
                  "type": "string",
                  "description": "Parent rule identifier within the component.",
                  "example": "000020"
                },
                "component_prefix": {
                  "type": "string",
                  "description": "Component prefix used to build the displayed name.",
                  "example": "CNTR-00"
                }
              }
            }
          }
        }
      },
      "CommentRow": {
        "description": "A single comment row in the triage comment listing. Serialized by CommentQueryService#serialize_rows — NOT by ReviewBlueprint. This is a hand-built hash with triage attribution, grouping, and rule context fields.",
        "type": "object",
        "required": [
          "id",
          "commentable_type",
          "comment",
          "created_at",
          "triage_status",
          "responses_count",
          "reactions"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique review/comment identifier.",
            "example": 26
          },
          "rule_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "ID of the rule this comment is attached to. Null for component-level comments.",
            "example": 1823
          },
          "rule_displayed_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Human-readable rule identifier (PREFIX-rule_id). Null or \"(component)\" for component-level comments.",
            "example": "PHOS-03-000038"
          },
          "commentable_type": {
            "type": "string",
            "description": "Polymorphic type indicating whether the comment targets a rule or a component.",
            "enum": [
              "BaseRule",
              "Component"
            ],
            "example": "BaseRule"
          },
          "section": {
            "type": [
              "string",
              "null"
            ],
            "description": "Rule section the comment applies to (e.g. fixtext, check_content, vuln_discussion). Null for general or component-level comments.",
            "example": null
          },
          "author_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display name of the comment author (from commenter_display_name).",
            "example": "Demo Viewer"
          },
          "author_email": {
            "type": [
              "string",
              "null"
            ],
            "format": "email",
            "description": "Email address of the comment author.",
            "example": "viewer@example.org"
          },
          "comment": {
            "type": "string",
            "description": "The comment text content.",
            "example": "FYI we already shipped this in our internal hardening guide."
          },
          "created_at": {
            "type": "string",
            "description": "Timestamp when the comment was created.",
            "example": "2026-05-19 14:08:18 UTC"
          },
          "triage_status": {
            "type": [
              "string",
              "null"
            ],
            "description": "Current triage disposition of the comment.",
            "enum": [
              "pending",
              "concur",
              "concur_with_comment",
              "non_concur",
              "duplicate",
              "informational",
              "needs_clarification",
              "withdrawn",
              "addressed_by",
              null
            ],
            "example": "informational"
          },
          "triage_set_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Timestamp when the triage status was last changed. Null if still pending.",
            "example": "2026-05-19 14:08:18 UTC"
          },
          "adjudicated_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Timestamp when the comment reached a terminal triage state. Null if not yet adjudicated.",
            "example": "2026-05-19 14:08:18 UTC"
          },
          "duplicate_of_review_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "ID of the review this is marked as a duplicate of. Null if not a duplicate.",
            "example": null
          },
          "addressed_by_rule_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "ID of the rule that addresses this comment (for addressed_by triage status). Null otherwise.",
            "example": null
          },
          "srg_info": {
            "type": [
              "object",
              "null"
            ],
            "description": "SRG baseline metadata for the comment's component — title, version, and whether it's the latest available.",
            "properties": {
              "title": {
                "type": "string",
                "description": "Full title of the SRG.",
                "example": "General Purpose Operating System Security Requirements Guide"
              },
              "version": {
                "type": "string",
                "description": "SRG version in DISA V{major}R{minor} format.",
                "example": "V3R3"
              },
              "is_latest": {
                "type": "boolean",
                "description": "Whether this SRG version is that SRG's latest available release. Green dot = true, yellow dot = false.",
                "example": true
              }
            }
          },
          "addressed_by_rule_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display name of the rule that addresses this comment. Null if not addressed_by.",
            "example": null
          },
          "triager_display_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display name of the user who triaged this comment. Uses ImportedAttribution resolution.",
            "example": "Demo Author"
          },
          "triager_imported": {
            "type": "boolean",
            "description": "Whether the triager attribution came from an import (no matching DB user).",
            "example": false
          },
          "adjudicator_display_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display name of the user who adjudicated this comment.",
            "example": "Demo Author"
          },
          "adjudicator_imported": {
            "type": "boolean",
            "description": "Whether the adjudicator attribution came from an import.",
            "example": false
          },
          "commenter_display_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display name of the comment author. Uses ImportedAttribution resolution.",
            "example": "Demo Viewer"
          },
          "commenter_imported": {
            "type": "boolean",
            "description": "Whether the commenter attribution came from an import.",
            "example": false
          },
          "responses_count": {
            "type": "integer",
            "description": "Number of threaded replies to this comment.",
            "example": 0
          },
          "reactions": {
            "type": "object",
            "description": "Aggregate reaction counts for this comment (no mine field — unlike ReviewSummary).",
            "required": [
              "up",
              "down"
            ],
            "properties": {
              "up": {
                "type": "integer",
                "description": "Number of upvote reactions.",
                "example": 0
              },
              "down": {
                "type": "integer",
                "description": "Number of downvote reactions.",
                "example": 0
              }
            }
          },
          "updated_at": {
            "type": "string",
            "description": "Timestamp when the comment was last modified.",
            "example": "2026-05-19 14:08:18 UTC"
          },
          "rule_status": {
            "type": [
              "string",
              "null"
            ],
            "description": "Current applicability status of the rule this comment is on. Null for component-level comments.",
            "example": "Not Yet Determined"
          },
          "parent_rule_displayed_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display name of the parent rule (if this rule is a child via satisfies relationship). Null if no parent.",
            "example": null
          },
          "group_rule_displayed_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Grouping key — parent_rule_displayed_name if present, otherwise rule_displayed_name. Used for by-rule grouping in the triage table.",
            "example": "PHOS-03-000038"
          },
          "rule_content": {
            "description": "Rule content for the split-pane triage view. Present only when the include_rule_content query parameter is true; null for component-level comments.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/RuleContent"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "PaginatedComments": {
        "description": "Paginated list of comments with pagination metadata and triage status counts.",
        "type": "object",
        "required": [
          "rows",
          "pagination",
          "status_counts"
        ],
        "properties": {
          "rows": {
            "type": "array",
            "description": "Comment rows for the current page.",
            "items": {
              "$ref": "#/components/schemas/CommentRow"
            }
          },
          "pagination": {
            "type": "object",
            "description": "Pagination metadata for the comment listing.",
            "required": [
              "page",
              "per_page",
              "total",
              "total_comments"
            ],
            "properties": {
              "page": {
                "type": "integer",
                "description": "Current page number (1-based).",
                "example": 1
              },
              "per_page": {
                "type": "integer",
                "description": "Number of comments per page.",
                "example": 25
              },
              "total": {
                "type": "integer",
                "description": "Total number of pages available.",
                "example": 4
              },
              "total_comments": {
                "type": "integer",
                "description": "Total number of comments across all pages.",
                "example": 87
              }
            }
          },
          "status_counts": {
            "type": "object",
            "description": "Count of comments grouped by triage status.",
            "additionalProperties": {
              "type": "integer"
            },
            "example": {
              "pending": 12,
              "concur": 45,
              "non_concur": 8,
              "withdrawn": 3
            }
          }
        }
      },
      "RuleInput": {
        "description": "The full write surface for a requirement — every field the server accepts, shared by the update endpoint and the one-call create body. The endpoint serves both document kinds. Per-kind behavior is noted on the fields that carry it: the status vocabulary is per-kind, and the STIG-only nested attributes (additional_answers_attributes) are dropped server-side when the target is an authored SRG requirement. Nested attributes follow Rails semantics — include id to update an existing record, omit id to create one, send _destroy true to remove (where accepted); at one-call creation ids and _destroy are stripped server-side.",
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "description": "Applicability status. The accepted vocabulary depends on the parent component's document_type — STIG rules accept Not Yet Determined, Applicable - Configurable, Applicable - Inherently Meets, Applicable - Does Not Meet, and Not Applicable; authored SRG requirements accept Not Yet Determined, Applicable, and Not Applicable. A value outside the target's vocabulary returns 422.",
            "enum": [
              "Not Yet Determined",
              "Applicable - Configurable",
              "Applicable - Inherently Meets",
              "Applicable - Does Not Meet",
              "Applicable",
              "Not Applicable"
            ],
            "example": "Applicable - Configurable"
          },
          "status_justification": {
            "type": "string",
            "description": "Justification for the status decision. For an authored SRG requirement the server requires it whenever status is Not Applicable (422 otherwise) — the justification records the decision and is excluded from the released catalog copy.",
            "example": "The platform provides no wireless interfaces to configure."
          },
          "title": {
            "type": "string",
            "description": "Title of the security control.",
            "example": "The container platform must enforce approved authorizations for access."
          },
          "artifact_description": {
            "type": "string",
            "description": "Description of artifacts or evidence supporting the rule implementation.",
            "example": "Screenshot of the container platform access control configuration panel."
          },
          "vendor_comments": {
            "type": "string",
            "description": "Vendor-provided comments or implementation notes for this rule.",
            "example": "This requirement is met by default when using the recommended container runtime configuration."
          },
          "rule_severity": {
            "type": "string",
            "description": "Severity category (maps to DISA CAT III / CAT II / CAT I). A value outside the accepted list returns 422.",
            "enum": [
              "low",
              "medium",
              "high"
            ],
            "example": "medium"
          },
          "rule_weight": {
            "type": "string",
            "description": "Rule weight for DISA scoring.",
            "example": "10.0"
          },
          "version": {
            "type": "string",
            "description": "SRG requirement version identifier mapping this rule to its SRG source.",
            "example": "SRG-OS-000001-GPOS-00001"
          },
          "ident": {
            "type": "string",
            "description": "CCI identifiers (comma-separated).",
            "example": "CCI-000015"
          },
          "ident_system": {
            "type": "string",
            "description": "Identifier system URI for the ident value. Server default is http://iase.disa.mil/cci when never set.",
            "example": "http://cyber.mil/cci"
          },
          "fixtext": {
            "type": "string",
            "description": "Remediation instructions describing how to fix a non-compliant finding.",
            "example": "Configure the container platform to enforce approved authorizations for logical access."
          },
          "fix_id": {
            "type": "string",
            "description": "Fix identifier.",
            "example": "F-3716r557030_fix"
          },
          "fixtext_fixref": {
            "type": "string",
            "description": "Fix reference identifier linking the fixtext to its fix element.",
            "example": "F-3716r557030_fix"
          },
          "audit_comment": {
            "type": "string",
            "description": "Free-text comment recorded on the audit-trail entry for this change. Not stored on the requirement itself — it annotates the history row that documents who changed what and why.",
            "example": "Adjusted severity per authorizing official direction."
          },
          "inspec_control_body": {
            "type": "string",
            "description": "InSpec control Ruby source code. InSpec is a stig-kind surface: the server accepts this field on an authored SRG requirement but no read surface returns it for that kind — sending it there writes data nothing serves back.",
            "example": "control 'CNTR-00-000050' do\n  impact 0.5\nend"
          },
          "inspec_control_file": {
            "type": "string",
            "description": "InSpec control file path. Stig-kind surface — same write/read asymmetry on authored SRG requirements as inspec_control_body.",
            "example": "controls/CNTR-00-000050.rb"
          },
          "inspec_control_body_lang": {
            "type": "string",
            "description": "Language of the InSpec control body. Server default is ruby. Stig-kind surface — same write/read asymmetry on authored SRG requirements as inspec_control_body.",
            "example": "ruby"
          },
          "inspec_control_file_lang": {
            "type": "string",
            "description": "Language of the InSpec control file. Server default is ruby. Stig-kind surface — same write/read asymmetry on authored SRG requirements as inspec_control_body.",
            "example": "ruby"
          },
          "checks_attributes": {
            "type": "array",
            "description": "Check content records (the XCCDF check element). Rails nested attributes — id updates, no id creates, _destroy true removes.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "description": "Existing check record to update. Omit to create a new one.",
                  "example": 311
                },
                "system": {
                  "type": "string",
                  "description": "Check system identifier.",
                  "example": "C-Container Platform"
                },
                "content_ref_name": {
                  "type": "string",
                  "description": "Name of the referenced check content document.",
                  "example": "M"
                },
                "content_ref_href": {
                  "type": "string",
                  "description": "Href of the referenced check content document.",
                  "example": "DPMS_XCCDF_Benchmark_Container_Platform_SRG.xml"
                },
                "content": {
                  "type": "string",
                  "description": "The check procedure text.",
                  "example": "Review the container platform configuration to verify approved authorizations are enforced."
                },
                "_destroy": {
                  "type": "boolean",
                  "description": "Send true to delete this check record.",
                  "example": false
                }
              }
            }
          },
          "rule_descriptions_attributes": {
            "type": "array",
            "description": "Plain rule description records. Rails nested attributes — id updates, no id creates, _destroy true removes.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "description": "Existing description record to update. Omit to create a new one.",
                  "example": 154
                },
                "description": {
                  "type": "string",
                  "description": "The description text.",
                  "example": "Enforcing approved authorizations limits lateral movement inside the platform."
                },
                "_destroy": {
                  "type": "boolean",
                  "description": "Send true to delete this description record.",
                  "example": false
                }
              }
            }
          },
          "additional_answers_attributes": {
            "type": "array",
            "description": "Answers to the project's additional questions. STIG rules only — dropped server-side when the target is an authored SRG requirement (on update and at one-call creation alike). One answer per question per rule; records are created or updated, never destroyed through this surface (_destroy is not accepted).",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "description": "Existing answer record to update. Omit to create a new one.",
                  "example": 87
                },
                "additional_question_id": {
                  "type": "integer",
                  "description": "The additional question this answer belongs to.",
                  "example": 3
                },
                "answer": {
                  "type": "string",
                  "description": "The answer text.",
                  "example": "Yes — enforced through the platform's admission controller."
                }
              }
            }
          },
          "disa_rule_descriptions_attributes": {
            "type": "array",
            "description": "DISA rule description records (the vulnerability discussion and DISA metadata block). Rails nested attributes — id updates, no id creates, _destroy true removes.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "description": "Existing DISA description record to update. Omit to create a new one.",
                  "example": 209
                },
                "vuln_discussion": {
                  "type": "string",
                  "description": "Vulnerability discussion text.",
                  "example": "Unapproved authorizations allow users to exceed their intended privileges."
                },
                "false_positives": {
                  "type": "string",
                  "description": "Known false-positive conditions for the check.",
                  "example": "None identified."
                },
                "false_negatives": {
                  "type": "string",
                  "description": "Known false-negative conditions for the check.",
                  "example": "None identified."
                },
                "documentable": {
                  "type": "boolean",
                  "description": "Whether a finding may be addressed through documentation.",
                  "example": false
                },
                "mitigations_available": {
                  "type": "boolean",
                  "description": "Whether mitigations are available for this requirement.",
                  "example": true
                },
                "mitigations": {
                  "type": "string",
                  "description": "Available mitigation description.",
                  "example": "Network segmentation limits exposure until the control is configured."
                },
                "poam_available": {
                  "type": "boolean",
                  "description": "Whether a plan of action and milestones applies.",
                  "example": false
                },
                "poam": {
                  "type": "string",
                  "description": "Plan of action and milestones text.",
                  "example": "Remediation scheduled for the next maintenance window."
                },
                "severity_override_guidance": {
                  "type": "string",
                  "description": "Guidance for overriding the assigned severity.",
                  "example": "Severity may be lowered when the platform is not internet-facing."
                },
                "potential_impacts": {
                  "type": "string",
                  "description": "Potential operational impacts of applying the fix.",
                  "example": "Existing sessions may be terminated when authorization enforcement is enabled."
                },
                "third_party_tools": {
                  "type": "string",
                  "description": "Third-party tool considerations.",
                  "example": "None."
                },
                "mitigation_control": {
                  "type": "string",
                  "description": "Mitigation control description.",
                  "example": "Compensating access controls at the ingress proxy."
                },
                "responsibility": {
                  "type": "string",
                  "description": "Responsible party for the requirement.",
                  "example": "System Administrator"
                },
                "ia_controls": {
                  "type": "string",
                  "description": "Legacy IA control mapping.",
                  "example": "ECAN-1"
                },
                "_destroy": {
                  "type": "boolean",
                  "description": "Send true to delete this DISA description record.",
                  "example": false
                }
              }
            }
          }
        }
      },
      "RuleCreateResponse": {
        "description": "Response from requirement creation. Toast notification plus the created row under the \"data\" key, shaped by the parent component's document_type: a STIG rule or an authored SRG requirement.",
        "type": "object",
        "required": [
          "toast",
          "data"
        ],
        "properties": {
          "toast": {
            "$ref": "#/components/schemas/ToastObject"
          },
          "data": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/RuleEditorResponse"
              },
              {
                "$ref": "#/components/schemas/AuthoredSrgRuleEditorResponse"
              }
            ]
          }
        }
      },
      "RulePickerResponse": {
        "description": "Lightweight rule for picker dropdowns. Serialized by RuleBlueprint :picker view. Extends RuleSummary defaults with displayed_name and satisfaction relationships. 13 total fields.",
        "allOf": [
          {
            "$ref": "#/components/schemas/RuleSummary"
          },
          {
            "type": "object",
            "required": [
              "displayed_name",
              "satisfies",
              "satisfied_by"
            ],
            "properties": {
              "displayed_name": {
                "type": "string",
                "description": "Human-readable rule identifier (PREFIX-rule_id).",
                "example": "PHOS-03-000001"
              },
              "satisfies": {
                "type": "array",
                "description": "Rules that this rule satisfies.",
                "items": {
                  "$ref": "#/components/schemas/SatisfactionSummary"
                }
              },
              "satisfied_by": {
                "type": "array",
                "description": "Rules that satisfy this rule.",
                "items": {
                  "$ref": "#/components/schemas/SatisfiedBySummary"
                }
              }
            }
          }
        ]
      },
      "AuthoredSrgRulePickerResponse": {
        "description": "A component-authored SRG requirement for picker dropdowns. Serialized by AuthoredSrgRuleBlueprint :picker view — the default requirement fields plus displayed_name. Carries no satisfaction relationships: authored requirements have no satisfies graph, and the keys are omitted entirely rather than sent empty. The closed property set keeps this shape disjoint from RulePickerResponse wherever the two appear in a oneOf.",
        "type": "object",
        "required": [
          "id",
          "rule_id",
          "locked",
          "displayed_name"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique requirement identifier.",
            "example": 10633
          },
          "rule_id": {
            "type": "string",
            "description": "Six-digit zero-padded requirement number combined with the component prefix to form the displayed ID.",
            "example": "000001"
          },
          "title": {
            "type": [
              "string",
              "null"
            ],
            "description": "Title of the requirement. Null if not yet authored.",
            "example": "The container platform must enforce approved authorizations."
          },
          "version": {
            "type": [
              "string",
              "null"
            ],
            "description": "Requirement version identifier. Minted at release for authored requirements.",
            "example": null
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "description": "Applicability status in the SRG authoring vocabulary.",
            "enum": [
              "Not Yet Determined",
              "Applicable",
              "Not Applicable",
              null
            ],
            "example": "Applicable"
          },
          "rule_severity": {
            "type": [
              "string",
              "null"
            ],
            "description": "DISA severity level.",
            "enum": [
              "low",
              "medium",
              "high",
              null
            ],
            "example": "medium"
          },
          "locked": {
            "type": "boolean",
            "description": "Whether the requirement is locked from further edits.",
            "example": false
          },
          "review_requestor_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "ID of the user who requested review. Null if no review requested.",
            "example": null
          },
          "changes_requested": {
            "type": "boolean",
            "description": "Whether changes have been requested during review.",
            "example": false
          },
          "comment_summary": {
            "type": "object",
            "description": "Per-requirement comment counts.",
            "required": [
              "open",
              "total"
            ],
            "additionalProperties": false,
            "properties": {
              "open": {
                "type": "integer",
                "description": "Comments not yet adjudicated, including replies under open parents.",
                "example": 0
              },
              "total": {
                "type": "integer",
                "description": "Total comment count across all statuses.",
                "example": 0
              }
            }
          },
          "displayed_name": {
            "type": "string",
            "description": "Component-scoped display name (PREFIX-rule_id).",
            "example": "RCPK-00-000001"
          }
        }
      },
      "RuleBasicFields": {
        "description": "Lightweight rule snapshot used for version-to-version diff comparisons. Produced by Rule#basic_fields. Contains only the fields needed to detect content changes between component releases.",
        "type": "object",
        "required": [
          "rule_id"
        ],
        "additionalProperties": false,
        "properties": {
          "rule_id": {
            "type": "string",
            "description": "Six-digit zero-padded rule number.",
            "example": "000001"
          },
          "title": {
            "type": [
              "string",
              "null"
            ],
            "description": "Rule title.",
            "example": "The operating system must provide automated mechanisms for account management."
          },
          "vuln_discussion": {
            "type": [
              "string",
              "null"
            ],
            "description": "Vulnerability discussion text (from first DISA rule description).",
            "example": "Without automated mechanisms, account management becomes error-prone..."
          },
          "check": {
            "type": [
              "string",
              "null"
            ],
            "description": "Check procedure text (exported format from checks).",
            "example": "Verify the operating system provides automated mechanisms..."
          },
          "fix": {
            "type": [
              "string",
              "null"
            ],
            "description": "Fix text (exported format).",
            "example": "Configure the operating system to provide automated mechanisms..."
          }
        }
      },
      "HistoryChangeEntry": {
        "description": "A single rule change between two component versions. Part of the component version history diff. The change type determines which fields are present: 'added' has only diff, 'removed' has only base, 'updated' has both.",
        "type": "object",
        "required": [
          "change"
        ],
        "additionalProperties": false,
        "properties": {
          "change": {
            "type": "string",
            "description": "Type of change between versions.",
            "enum": [
              "added",
              "removed",
              "updated"
            ],
            "example": "updated"
          },
          "base": {
            "description": "Rule content in the previous version. Present for 'removed' and 'updated' changes.",
            "$ref": "#/components/schemas/RuleBasicFields"
          },
          "diff": {
            "description": "Rule content in the new version. Present for 'added' and 'updated' changes.",
            "$ref": "#/components/schemas/RuleBasicFields"
          }
        }
      },
      "ComponentReleaseResponse": {
        "type": "object",
        "description": "Successful release of an SRG component. The component's requirements are published to the SRG catalog: the catalog entry is shaped exactly like an uploaded SRG (other components can base on it immediately, and is-latest tracks it), and the changelog summarizes what left the document via executed relocations.",
        "required": [
          "toast",
          "catalog_srg",
          "changelog"
        ],
        "properties": {
          "toast": {
            "$ref": "#/components/schemas/ToastObject"
          },
          "catalog_srg": {
            "type": "object",
            "description": "The catalog entry created by this release.",
            "required": [
              "id",
              "srg_id",
              "version",
              "name"
            ],
            "properties": {
              "id": {
                "type": "integer",
                "description": "Catalog SecurityRequirementsGuide record id."
              },
              "srg_id": {
                "type": "string",
                "description": "The released document's benchmark id — the component name with spaces underscored.",
                "example": "Container_Best_Practice_SRG"
              },
              "version": {
                "type": "string",
                "description": "Catalog version string mapped from the component's integer version/release pair.",
                "example": "V1R1"
              },
              "name": {
                "type": "string",
                "description": "Display name composed from the benchmark id and version.",
                "example": "Container Best Practice SRG - Ver 1, Rel 1"
              }
            }
          },
          "changelog": {
            "type": "object",
            "description": "Release changelog content. Removals are the component's executed relocation records — requirements that left this document for another SRG. Storage is not implied; this is the generated content.",
            "required": [
              "version",
              "removals",
              "text"
            ],
            "properties": {
              "version": {
                "type": "string",
                "example": "V1R1"
              },
              "removals": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "identifier",
                    "destination_abbreviation",
                    "executed_on"
                  ],
                  "properties": {
                    "identifier": {
                      "type": "string",
                      "description": "The requirement's published identifier (its working display name if it was never minted).",
                      "example": "SRG-OS-000811-CTR-000042"
                    },
                    "destination_abbreviation": {
                      "type": "string",
                      "description": "The destination SRG's short abbreviation.",
                      "example": "WEB"
                    },
                    "executed_on": {
                      "type": "string",
                      "format": "date",
                      "example": "2026-07-20"
                    }
                  }
                }
              },
              "text": {
                "type": "string",
                "description": "Plain-text rendering of the changelog."
              }
            }
          }
        }
      },
      "RuleSectionLockResponse": {
        "description": "Response from section lock/unlock. Toast notification plus the updated rule. Used by PATCH /rules/:id/section_locks and PATCH /rules/:id/bulk_section_locks.",
        "type": "object",
        "required": [
          "rule",
          "toast"
        ],
        "properties": {
          "rule": {
            "$ref": "#/components/schemas/RuleEditorResponse"
          },
          "toast": {
            "$ref": "#/components/schemas/ToastObject"
          }
        }
      },
      "RequirementRelocationSummary": {
        "description": "A relocation proposal in the per-SRG backlog: the open proposal plus enough source identity to act on it, or a retained non-concur whose rationale communicates the refusal back to the source author. Executed relocation records are immutable history and never served here.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "source_rule_id",
          "target_technology_token",
          "source_displayed_name",
          "component_id"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "Relocation record identifier.",
            "example": 12
          },
          "source_rule_id": {
            "type": "integer",
            "description": "The authored requirement proposed for relocation.",
            "example": 5137
          },
          "target_technology_token": {
            "type": "string",
            "description": "The destination SRG's abbreviation (CTR, GPOS, DB — the SRG-world analogue of a STIG component prefix).",
            "example": "CTR"
          },
          "created_at": {
            "type": "string",
            "description": "When the marker was created. Blueprinter format.",
            "example": "2026-07-20 15:36:06 UTC"
          },
          "source_displayed_name": {
            "type": "string",
            "description": "Component-prefixed requirement name of the source.",
            "example": "CNTR-00-000051"
          },
          "component_id": {
            "type": "integer",
            "description": "The source requirement's component.",
            "example": 42
          },
          "component_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the source component.",
            "example": "Container Platform SRG"
          },
          "requested_by_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Who marked the requirement. Null when the account was removed.",
            "example": "Jane Doe"
          },
          "declined_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the receiving side declined the proposal. Null while the proposal is open. Blueprinter format.",
            "example": null
          },
          "adjudication_rationale": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why the proposal was declined — required on every decline, shown to the source author. Null while the proposal is open.",
            "example": null
          },
          "declined_by_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Who declined the proposal. Null while open or when the account was removed.",
            "example": null
          }
        }
      },
      "RelocationDestination": {
        "description": "A destination SRG option for the relocation propose flow: one row per SRG abbreviation across the SRG components the caller can discover. An open (unreleased) component wins the row; released true marks the queued next-release case (proposals wait until the receiving admin creates the next version).",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "token",
          "name",
          "released"
        ],
        "properties": {
          "token": {
            "type": "string",
            "description": "The destination SRG's abbreviation — the short code its requirement IDs start with (CTR, GPOS, DB).",
            "example": "CTR"
          },
          "name": {
            "type": "string",
            "description": "The component carrying the abbreviation — the open component when one exists, otherwise the latest released one.",
            "example": "Container Platform SRG"
          },
          "released": {
            "type": "boolean",
            "description": "True when no open component carries the abbreviation — a proposal queues for the SRG's next release.",
            "example": false
          }
        }
      },
      "RequirementRelocationDryRun": {
        "description": "Zero-write preview of executing a relocation: whether the move can run, every reason it cannot, and exactly what would be created and tombstoned.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "valid",
          "errors",
          "source_displayed_name",
          "would_create",
          "would_tombstone_source"
        ],
        "properties": {
          "valid": {
            "type": "boolean",
            "description": "Whether execute would run with these inputs.",
            "example": true
          },
          "errors": {
            "type": "array",
            "description": "Every reason the move cannot run. Empty when valid.",
            "items": {
              "type": "string"
            },
            "example": []
          },
          "source_displayed_name": {
            "type": "string",
            "description": "Component-prefixed name of the source requirement.",
            "example": "CNTR-00-000051"
          },
          "target_component_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The destination component.",
            "example": 42
          },
          "target_component_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the destination component.",
            "example": "Container Platform SRG"
          },
          "would_create": {
            "type": "object",
            "additionalProperties": false,
            "description": "The requirement that execute would create in the target.",
            "properties": {
              "title": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Title carried over from the source.",
                "example": "The application must enforce approved authorizations"
              },
              "status": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Authoring status carried over from the source.",
                "example": "Applicable"
              },
              "rule_id": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The number the moved requirement would land under — the next in the target component's own sequence (working numbers are local ordinals; identifiers mint at release).",
                "example": "000003"
              },
              "derived_from_srg_rule_id": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Core-requirement lineage preserved on the move.",
                "example": 5137
              }
            }
          },
          "would_tombstone_source": {
            "type": "boolean",
            "description": "Execute soft-deletes the source row — always true.",
            "example": true
          }
        }
      },
      "RequirementRelocationAcceptResponse": {
        "description": "Combined response for accepting a relocation proposal: the success toast plus the id of the requirement that landed in the destination component, so the editor can materialize the new row without a page reload.",
        "type": "object",
        "required": [
          "toast",
          "landed_rule_id"
        ],
        "additionalProperties": false,
        "properties": {
          "toast": {
            "$ref": "#/components/schemas/ToastObject"
          },
          "landed_rule_id": {
            "type": "integer",
            "description": "The requirement created in the destination component.",
            "example": 5137
          }
        }
      },
      "ReviewInput": {
        "description": "Input fields for creating a review action (comment, approval, review request, or lock) on a rule.",
        "type": "object",
        "required": [
          "action",
          "comment"
        ],
        "properties": {
          "action": {
            "type": "string",
            "description": "The review action to perform. Vocabulary and role tiers come from Review::ACTION_PERMISSIONS — comment (viewer+), request_review / revoke_review_request (author+), request_changes / approve (reviewer+), lock_control / unlock_control (admin).",
            "enum": [
              "comment",
              "request_review",
              "revoke_review_request",
              "request_changes",
              "approve",
              "lock_control",
              "unlock_control"
            ],
            "example": "comment"
          },
          "comment": {
            "type": "string",
            "description": "The comment text accompanying this review action.",
            "example": "The fix text should reference the container runtime configuration file."
          },
          "section": {
            "type": [
              "string",
              "null"
            ],
            "description": "Rule section this comment applies to (e.g. fixtext, check_content, vuln_discussion). Null for general comments; component-level comments are always stored section-less.",
            "example": "fixtext"
          },
          "responding_to_review_id": {
            "type": "integer",
            "description": "ID of the parent review when creating a threaded reply.",
            "example": 43
          }
        }
      },
      "BulkTriageResponse": {
        "description": "Response from bulk-triaging multiple reviews at once. Returns the updated reviews and any auto-created response reviews (one per triaged comment if a response_comment was provided).",
        "type": "object",
        "required": [
          "reviews",
          "response_reviews"
        ],
        "additionalProperties": false,
        "properties": {
          "reviews": {
            "type": "array",
            "description": "The triaged reviews with updated triage_status.",
            "items": {
              "$ref": "#/components/schemas/ReviewSummary"
            }
          },
          "response_reviews": {
            "type": "array",
            "description": "Auto-created response reviews (one per triaged comment when response_comment is provided). Empty array if no response_comment.",
            "items": {
              "$ref": "#/components/schemas/ReviewSummary"
            }
          }
        }
      },
      "MergeResponse": {
        "description": "Response from merging duplicate reviews into a single survivor. The survivor absorbs the merged content. Duplicates are marked triage_status=duplicate with duplicate_of_review_id pointing to the survivor.",
        "type": "object",
        "required": [
          "survivor",
          "duplicates"
        ],
        "additionalProperties": false,
        "properties": {
          "survivor": {
            "description": "The surviving review that absorbed the duplicates.",
            "$ref": "#/components/schemas/ReviewSummary"
          },
          "duplicates": {
            "type": "array",
            "description": "The merged reviews, now marked as duplicates of the survivor.",
            "items": {
              "$ref": "#/components/schemas/ReviewSummary"
            }
          }
        }
      },
      "ReviewUpdateInput": {
        "description": "Input for editing an existing comment's text. Editing is allowed only while the comment is still pending triage; lifecycle fields (action, section, triage status) are server-controlled and cannot be changed here.",
        "type": "object",
        "required": [
          "comment"
        ],
        "additionalProperties": false,
        "properties": {
          "comment": {
            "type": "string",
            "description": "The replacement comment text.",
            "example": "The fix text should reference the container runtime configuration file."
          }
        }
      },
      "ReviewWrapper": {
        "description": "Standard review mutation response. Wraps a single ReviewSummary in a { review: ... } envelope. Used by update, reopen, withdraw, admin_withdraw, admin_restore, move_to_rule, and section endpoints.",
        "type": "object",
        "required": [
          "review"
        ],
        "additionalProperties": false,
        "properties": {
          "review": {
            "$ref": "#/components/schemas/ReviewSummary"
          }
        }
      },
      "TriageResponse": {
        "description": "Response from triage or adjudicate actions. Contains the updated review and an optional response_review (child comment created atomically when the triager provides a response comment alongside the triage decision).",
        "type": "object",
        "required": [
          "review"
        ],
        "additionalProperties": false,
        "properties": {
          "review": {
            "$ref": "#/components/schemas/ReviewSummary"
          },
          "response_review": {
            "description": "Child review created as a response to the decision. Null if no response text was provided. Present when the request includes response_comment (triage) or resolution_comment (adjudicate).",
            "oneOf": [
              {
                "$ref": "#/components/schemas/ReviewSummary"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "AdminDestroyResponse": {
        "description": "Response from admin hard-delete of a review. Returns null for the review (confirming deletion) and the destroyed review's ID for frontend cleanup.",
        "type": "object",
        "required": [
          "review",
          "destroyed_id"
        ],
        "additionalProperties": false,
        "properties": {
          "review": {
            "type": "null",
            "description": "Always null — confirms the review was destroyed.",
            "example": null
          },
          "destroyed_id": {
            "type": "integer",
            "description": "ID of the destroyed review, for frontend to remove from local state.",
            "example": 44
          }
        }
      },
      "ReactionsSummary": {
        "description": "Detailed reaction lists showing which users reacted with each type.",
        "type": "object",
        "required": [
          "up",
          "down"
        ],
        "additionalProperties": false,
        "properties": {
          "up": {
            "type": "array",
            "description": "Users who upvoted.",
            "items": {
              "type": "object",
              "required": [
                "name"
              ],
              "properties": {
                "name": {
                  "type": "string",
                  "description": "Display name of the user who upvoted.",
                  "example": "Jane Doe"
                }
              }
            }
          },
          "down": {
            "type": "array",
            "description": "Users who downvoted.",
            "items": {
              "type": "object",
              "required": [
                "name"
              ],
              "properties": {
                "name": {
                  "type": "string",
                  "description": "Display name of the user who downvoted.",
                  "example": "John Smith"
                }
              }
            }
          }
        }
      },
      "ReactionToggleResponse": {
        "description": "Response after toggling a reaction on a comment, returning updated aggregate counts and the current user's reaction.",
        "type": "object",
        "required": [
          "reactions"
        ],
        "additionalProperties": false,
        "properties": {
          "reactions": {
            "type": "object",
            "description": "Updated reaction state for the comment.",
            "required": [
              "up",
              "down",
              "mine"
            ],
            "properties": {
              "up": {
                "type": "integer",
                "description": "Total number of upvote reactions.",
                "example": 4
              },
              "down": {
                "type": "integer",
                "description": "Total number of downvote reactions.",
                "example": 1
              },
              "mine": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The current user's reaction type (up or down). Null if the user has no active reaction.",
                "example": "up"
              }
            }
          }
        }
      },
      "MembershipInput": {
        "description": "Input fields for creating or updating a project or component membership.",
        "type": "object",
        "required": [
          "user_id",
          "role"
        ],
        "properties": {
          "user_id": {
            "type": "integer",
            "description": "ID of the user to add or update membership for.",
            "example": 42
          },
          "membership_id": {
            "type": "integer",
            "description": "ID of the existing membership record to update. Omit when creating a new membership.",
            "example": 15
          },
          "membership_type": {
            "type": "string",
            "description": "Whether this membership is for a project or a component.",
            "enum": [
              "Project",
              "Component"
            ],
            "example": "Project"
          },
          "role": {
            "type": "string",
            "description": "Access role granted to the user (viewer < author < reviewer < admin).",
            "enum": [
              "viewer",
              "author",
              "reviewer",
              "admin"
            ],
            "example": "author"
          }
        }
      },
      "MembershipUpdateInput": {
        "description": "Update for an existing membership — the membership is addressed by the URL and only the role may change; user and membership type are fixed at creation.",
        "type": "object",
        "required": [
          "role"
        ],
        "properties": {
          "role": {
            "type": "string",
            "description": "Access role granted to the user (viewer < author < reviewer < admin).",
            "enum": [
              "viewer",
              "author",
              "reviewer",
              "admin"
            ],
            "example": "reviewer"
          }
        }
      },
      "SrgSummary": {
        "description": "Security Requirements Guide (SRG) listing entry. Serialized by SrgBlueprint (default/index view). SRGs use release_date (not benchmark_date — that is STIGs).",
        "type": "object",
        "required": [
          "id",
          "srg_id",
          "severity_counts",
          "core"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique SRG identifier.",
            "example": 1
          },
          "srg_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "DISA SRG identifier string.",
            "example": "Container_Platform_SRG"
          },
          "core": {
            "type": "boolean",
            "description": "Whether this is a core SRG. Core SRGs are the raw material SRG-kind components derive from; derived (non-core) SRGs are the valid sources for STIG-kind components. Drives the creation-flow source picker's eligibility filtering.",
            "example": false
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Short name including version and release.",
            "example": "Container Platform SRG - Ver 2, Rel 4"
          },
          "title": {
            "type": [
              "string",
              "null"
            ],
            "description": "Full official title.",
            "example": "Container Platform Security Requirements Guide"
          },
          "version": {
            "type": [
              "string",
              "null"
            ],
            "description": "Version string (e.g. V2R4).",
            "example": "V2R4"
          },
          "release_date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "Date the SRG was published by DISA.",
            "example": "2025-10-28"
          },
          "is_latest": {
            "type": "boolean",
            "description": "Whether this is the latest available release of this SRG.",
            "example": true
          },
          "latest_available_version": {
            "type": [
              "string",
              "null"
            ],
            "description": "Version string of this SRG's latest available release. Null when is_latest=true.",
            "example": null
          },
          "latest_available_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "ID of the latest available SRG for linking. Null when is_latest=true.",
            "example": null
          },
          "severity_counts": {
            "type": "object",
            "description": "Count of rules at each severity level within this SRG.",
            "properties": {
              "high": {
                "type": "integer",
                "example": 8
              },
              "medium": {
                "type": "integer",
                "example": 177
              },
              "low": {
                "type": "integer",
                "example": 3
              }
            }
          }
        }
      },
      "SrgDetailResponse": {
        "description": "Full SRG detail with nested rules. Serialized by SrgBlueprint :show view. Extends SrgSummary with srg_rules array.",
        "allOf": [
          {
            "$ref": "#/components/schemas/SrgSummary"
          },
          {
            "type": "object",
            "properties": {
              "srg_rules": {
                "type": "array",
                "description": "All rules in this SRG, each with full DISA metadata and check content.",
                "items": {
                  "$ref": "#/components/schemas/SrgRuleSummary"
                }
              }
            }
          }
        ]
      },
      "StigSummary": {
        "description": "Security Technical Implementation Guide (STIG) listing entry. Serialized by StigBlueprint (default/index view). STIGs use benchmark_date (not release_date — that is SRGs).",
        "type": "object",
        "required": [
          "id",
          "stig_id",
          "severity_counts"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique STIG identifier.",
            "example": 1
          },
          "stig_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "DISA STIG identifier string.",
            "example": "Application_Security_Development_STIG"
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Short name including version and release.",
            "example": "Application Security Development STIG - Ver 6, Rel 4"
          },
          "title": {
            "type": [
              "string",
              "null"
            ],
            "description": "Full official title.",
            "example": "Application Security and Development Security Technical Implementation Guide"
          },
          "version": {
            "type": [
              "string",
              "null"
            ],
            "description": "Version string (e.g. V6R4).",
            "example": "V6R4"
          },
          "benchmark_date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "Date the STIG benchmark was published by DISA.",
            "example": "2025-10-01"
          },
          "is_latest": {
            "type": "boolean",
            "description": "Whether this is the latest available release of this STIG.",
            "example": true
          },
          "latest_available_version": {
            "type": [
              "string",
              "null"
            ],
            "description": "Version string of this STIG's latest available release. Null when is_latest=true.",
            "example": null
          },
          "latest_available_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "ID of the latest available STIG for linking. Null when is_latest=true.",
            "example": null
          },
          "severity_counts": {
            "type": "object",
            "description": "Count of rules at each severity level within this STIG.",
            "properties": {
              "high": {
                "type": "integer",
                "example": 34
              },
              "medium": {
                "type": "integer",
                "example": 230
              },
              "low": {
                "type": "integer",
                "example": 22
              }
            }
          }
        }
      },
      "StigRuleSummary": {
        "description": "Published STIG rule — a finalized security requirement from a published STIG.",
        "type": "object",
        "required": [
          "id",
          "rule_id",
          "title"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique STIG rule identifier.",
            "example": 600
          },
          "rule_id": {
            "type": "string",
            "description": "DISA rule identifier.",
            "example": "SV-222396r857506_rule"
          },
          "title": {
            "type": "string",
            "description": "Requirement title.",
            "example": "The container platform must enforce approved authorizations for access."
          },
          "version": {
            "type": [
              "string",
              "null"
            ],
            "description": "STIG version identifier.",
            "example": "CNTR-00-000050"
          },
          "rule_severity": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "low",
              "medium",
              "high"
            ],
            "example": "medium"
          },
          "rule_weight": {
            "type": [
              "string",
              "null"
            ],
            "example": "10.0"
          },
          "ident": {
            "type": [
              "string",
              "null"
            ],
            "description": "CCI identifiers (comma-separated).",
            "example": "CCI-000213"
          },
          "ident_system": {
            "type": [
              "string",
              "null"
            ],
            "example": "http://cyber.mil/cci"
          },
          "fixtext": {
            "type": [
              "string",
              "null"
            ],
            "description": "Fix text describing how to remediate.",
            "example": "Configure the container platform to enforce approved authorizations..."
          },
          "fixtext_fixref": {
            "type": [
              "string",
              "null"
            ],
            "example": "F-25073r857505_fix"
          },
          "fix_id": {
            "type": [
              "string",
              "null"
            ],
            "example": "F-25073r857505_fix"
          },
          "srg_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "SRG requirement this STIG rule implements.",
            "example": "SRG-APP-000033-CTR-000095"
          },
          "vuln_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Vulnerability identifier (V-number).",
            "example": "V-222396"
          },
          "legacy_ids": {
            "type": [
              "string",
              "null"
            ],
            "description": "Legacy rule identifiers from previous STIG versions (comma-separated string).",
            "example": "V-69239, SV-83861"
          },
          "vendor_comments": {
            "type": [
              "string",
              "null"
            ],
            "description": "Vendor-provided implementation guidance or additional context.",
            "example": "This is addressed by the vendor's default configuration."
          },
          "disa_rule_descriptions_attributes": {
            "type": "array",
            "description": "DISA rule description records.",
            "items": {
              "$ref": "#/components/schemas/DisaRuleDescription"
            }
          },
          "checks_attributes": {
            "type": "array",
            "description": "Check content records.",
            "items": {
              "$ref": "#/components/schemas/CheckSummary"
            }
          }
        }
      },
      "StigDetailResponse": {
        "description": "Full STIG detail with nested rules. Serialized by StigBlueprint :show view. Extends StigSummary with description and stig_rules array.",
        "allOf": [
          {
            "$ref": "#/components/schemas/StigSummary"
          },
          {
            "type": "object",
            "properties": {
              "description": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Full description of the STIG.",
                "example": "This Security Technical Implementation Guide is published as a tool to improve the security of Department of Defense information systems."
              },
              "stig_rules": {
                "type": "array",
                "description": "All rules in this STIG, each with full DISA metadata and check content.",
                "items": {
                  "$ref": "#/components/schemas/StigRuleSummary"
                }
              }
            }
          }
        ]
      },
      "IdentitySummary": {
        "description": "A linked external identity (provider + uid) for a user account.",
        "type": "object",
        "required": [
          "provider",
          "title"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique identity record identifier.",
            "example": 7
          },
          "provider": {
            "type": "string",
            "description": "Registry key of the authentication provider (e.g. okta, login_gov, oidc, ldap, github).",
            "example": "okta"
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "format": "email",
            "description": "Email address asserted by this provider. For display/audit only — not the link key.",
            "example": "jane@example.com"
          },
          "title": {
            "type": "string",
            "description": "Human-facing display name for the provider, from the OIDC registry or titleized fallback.",
            "example": "Okta"
          },
          "last_sign_in_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 8601 timestamp of the most recent sign-in via this identity. Null if never used.",
            "example": "2026-06-14T13:29:15Z"
          },
          "can_unlink": {
            "type": "boolean",
            "description": "Whether this identity can be unlinked (false when it is the user's only sign-in method). Only present in the profile view.",
            "example": true
          }
        }
      },
      "UserSummary": {
        "description": "Summary of a user account for admin management views.",
        "type": "object",
        "required": [
          "id",
          "email",
          "admin"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique user identifier.",
            "example": 42
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display name chosen by the user. Null if not yet set.",
            "example": "Jane Doe"
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "Login email address.",
            "example": "jane.doe@example.org"
          },
          "provider": {
            "type": [
              "string",
              "null"
            ],
            "description": "Authentication provider used for this account (local, github, ldap, or oidc). Null for local accounts.",
            "example": "local"
          },
          "admin": {
            "type": "boolean",
            "description": "Whether the user has administrator privileges.",
            "example": false
          },
          "last_sign_in_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Timestamp of the most recent successful sign-in. Null if the user has never signed in. Blueprinter format.",
            "example": "2026-05-28 15:00:00 UTC"
          },
          "failed_attempts": {
            "type": "integer",
            "description": "Number of consecutive failed login attempts since last successful sign-in.",
            "example": 0
          },
          "locked_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Timestamp when the account was locked due to excessive failed attempts. Null if not locked. Blueprinter format.",
            "example": null
          },
          "slack_user_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Slack member ID driving this user's Slack notifications. Settable by the user (profile) and by admins (create/update, audited). Null when not set.",
            "example": "U0123456789"
          },
          "identities": {
            "type": "array",
            "description": "Linked external identities for this user (provider, email, last sign-in). Present in admin and profile views.",
            "items": {
              "$ref": "#/components/schemas/IdentitySummary"
            }
          }
        }
      },
      "UserToastResponse": {
        "description": "Combined response containing a toast notification and the updated user. Used by PUT /users/:id, POST /users/:id/lock, POST /users/:id/unlock.",
        "type": "object",
        "required": [
          "toast",
          "user"
        ],
        "additionalProperties": false,
        "properties": {
          "toast": {
            "$ref": "#/components/schemas/ToastObject"
          },
          "user": {
            "$ref": "#/components/schemas/UserSummary"
          }
        }
      },
      "UserCommentRow": {
        "description": "A single comment row in the user's My Comments listing. Serialized by UsersController#comment_row_for — different shape from CommentRow (component triage listing). Includes project/component context for cross-project navigation but omits triage attribution fields.",
        "type": "object",
        "required": [
          "id",
          "commentable_type",
          "comment",
          "created_at",
          "triage_status",
          "responses_count",
          "reactions"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique review/comment identifier.",
            "example": 29
          },
          "project_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "ID of the project containing this comment's rule/component.",
            "example": 3
          },
          "project_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the project.",
            "example": "vSphere 7.0"
          },
          "component_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "ID of the component containing the rule.",
            "example": 4
          },
          "component_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the component.",
            "example": "Photon OS 3"
          },
          "author_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display name of the comment author.",
            "example": "Jane Doe"
          },
          "rule_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "ID of the rule this comment is attached to. Null for component-level comments.",
            "example": 2397
          },
          "rule_displayed_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Human-readable rule identifier (PREFIX-rule_id). \"(component)\" for component-level comments.",
            "example": "PHOS-03-000039"
          },
          "commentable_type": {
            "type": "string",
            "description": "Polymorphic type indicating whether the comment targets a rule or a component.",
            "enum": [
              "BaseRule",
              "Component"
            ],
            "example": "BaseRule"
          },
          "section": {
            "type": [
              "string",
              "null"
            ],
            "description": "Rule section the comment applies to.",
            "example": "fixtext"
          },
          "comment": {
            "type": "string",
            "description": "The comment text content.",
            "example": "vSphere 7.0: fix command targets ESXi 6.7 path — should reference 7.0 layout."
          },
          "created_at": {
            "type": "string",
            "description": "Timestamp when the comment was created. Blueprinter format.",
            "example": "2026-05-19 14:08:18 UTC"
          },
          "triage_status": {
            "type": [
              "string",
              "null"
            ],
            "description": "Current triage disposition of the comment.",
            "enum": [
              "pending",
              "concur",
              "concur_with_comment",
              "non_concur",
              "duplicate",
              "informational",
              "needs_clarification",
              "withdrawn",
              "addressed_by",
              null
            ],
            "example": "pending"
          },
          "triage_set_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Timestamp when the triage status was last changed.",
            "example": null
          },
          "adjudicated_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Timestamp when the comment reached a terminal triage state.",
            "example": null
          },
          "duplicate_of_review_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "ID of the review this is marked as a duplicate of.",
            "example": null
          },
          "addressed_by_rule_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "ID of the rule that addresses this comment.",
            "example": null
          },
          "srg_info": {
            "type": [
              "object",
              "null"
            ],
            "description": "SRG baseline metadata for the comment's component.",
            "properties": {
              "title": {
                "type": "string",
                "description": "Full title of the SRG.",
                "example": "General Purpose Operating System Security Requirements Guide"
              },
              "version": {
                "type": "string",
                "description": "SRG version in DISA V{major}R{minor} format.",
                "example": "V3R3"
              },
              "is_latest": {
                "type": "boolean",
                "description": "Whether this SRG version is the latest available.",
                "example": true
              }
            }
          },
          "parent_rule_displayed_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display name of the parent rule if this rule is a child via satisfies. Null if no parent.",
            "example": null
          },
          "addressed_by_rule_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display name of the rule that addresses this comment.",
            "example": null
          },
          "latest_activity_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Most recent activity timestamp (triage, adjudication, or reply).",
            "example": null
          },
          "responses_count": {
            "type": "integer",
            "description": "Number of threaded replies to this comment.",
            "example": 0
          },
          "reactions": {
            "type": "object",
            "description": "Aggregate reaction counts with current user's reaction.",
            "additionalProperties": false,
            "required": [
              "up",
              "down"
            ],
            "properties": {
              "up": {
                "type": "integer",
                "description": "Number of upvote reactions.",
                "example": 0
              },
              "down": {
                "type": "integer",
                "description": "Number of downvote reactions.",
                "example": 0
              },
              "mine": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The current user's reaction type. Null if no active reaction.",
                "example": null
              }
            }
          }
        }
      },
      "AdminCreateResponse": {
        "description": "Response from admin user creation. Success includes toast, user, and optional reset_url. Error includes only the toast (no user key).",
        "type": "object",
        "required": [
          "toast"
        ],
        "properties": {
          "toast": {
            "$ref": "#/components/schemas/ToastObject"
          },
          "user": {
            "description": "The created user (present on success, absent on error).",
            "$ref": "#/components/schemas/UserSummary"
          },
          "reset_url": {
            "type": "string",
            "format": "uri",
            "description": "One-time password reset URL. Present only when SMTP is unavailable and no password was provided — the admin must deliver this link manually.",
            "example": "https://vulcan.example.org/users/password/edit?reset_password_token=abc123def456"
          }
        }
      },
      "ResetLinkResponse": {
        "description": "Response containing a toast notification and a one-time password reset URL for admin-generated resets.",
        "type": "object",
        "required": [
          "toast",
          "reset_url"
        ],
        "additionalProperties": false,
        "properties": {
          "toast": {
            "$ref": "#/components/schemas/ToastObject"
          },
          "reset_url": {
            "type": "string",
            "format": "uri",
            "description": "One-time password reset URL to share with the user.",
            "example": "https://vulcan.example.org/users/password/edit?reset_password_token=abc123def456"
          }
        }
      },
      "AccessRequestToastResponse": {
        "description": "Response from creating or deleting a project access request. The id is the affected access request — newly created on POST, destroyed on DELETE.",
        "type": "object",
        "required": [
          "toast",
          "id"
        ],
        "additionalProperties": false,
        "properties": {
          "toast": {
            "$ref": "#/components/schemas/ToastObject"
          },
          "id": {
            "type": "integer",
            "description": "ID of the affected access request.",
            "example": 42
          }
        }
      },
      "TriageResponseTemplate": {
        "description": "A reusable response template for triage decisions, scoped to a project.",
        "type": "object",
        "required": [
          "id",
          "name",
          "body"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique template identifier.",
            "example": 1
          },
          "name": {
            "type": "string",
            "description": "Short name displayed in the template picker dropdown.",
            "example": "Accept - standard"
          },
          "body": {
            "type": "string",
            "description": "Markdown response text inserted into the triage response field.",
            "example": "Concur with the finding as written. No changes needed."
          },
          "created_by_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "ID of the user who created this template.",
            "example": 42
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the template was created.",
            "example": "2026-06-03T01:00:00Z"
          }
        }
      },
      "PersonalAccessTokenSummary": {
        "description": "A personal access token as seen by its OWNER (GET /personal_access_tokens). Carries no owner identity — administrators listing another user's tokens get PersonalAccessTokenAdminSummary instead. The secret itself is returned only once, at creation, and is never retrievable here.",
        "type": "object",
        "required": [
          "id",
          "name",
          "token_prefix",
          "scopes",
          "created_at"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique token identifier.",
            "example": 1
          },
          "name": {
            "type": "string",
            "description": "Human-readable label the owner gave the token.",
            "example": "CI Pipeline"
          },
          "token_prefix": {
            "type": "string",
            "example": "vulcan_a",
            "description": "First 8 characters of the token for identification. The full token is never stored or returned after creation."
          },
          "scopes": {
            "type": "array",
            "description": "Capabilities granted to the token.",
            "items": {
              "type": "string",
              "enum": [
                "read",
                "write",
                "admin"
              ]
            },
            "example": [
              "read",
              "write"
            ]
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "example": "2026-08-30",
            "description": "ISO 8601 date. Maximum 365 days from creation. Null means no expiry (warned in UI)."
          },
          "last_used_at": {
            "type": [
              "string",
              "null"
            ],
            "example": "2026-05-30 14:22:01 UTC",
            "description": "Timestamp of last API request using this token. Updated at most once per minute."
          },
          "revoked_at": {
            "type": [
              "string",
              "null"
            ],
            "example": null,
            "description": "Null when active. Timestamp when revoked (soft-delete preserves audit trail)."
          },
          "allowed_ips": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "example": [
              "10.0.0.0/8",
              "192.168.1.0/24"
            ],
            "description": "CIDR allowlist. Null or empty means any IP allowed."
          },
          "created_at": {
            "type": "string",
            "description": "When the token was created. Blueprinter format.",
            "example": "2026-05-30 10:00:00 UTC"
          }
        }
      },
      "PersonalAccessTokenAdminSummary": {
        "description": "A personal access token as seen by an administrator listing ANOTHER user's tokens (GET /personal_access_tokens?user_id=). Adds the owner's identity to the standard summary so oversight is attributable. Read-only oversight: the secret is never included here or anywhere after creation, and no endpoint lets an administrator mint a token on this owner's account.",
        "type": "object",
        "required": [
          "id",
          "name",
          "token_prefix",
          "scopes",
          "created_at",
          "user_id"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unique token identifier.",
            "example": 1
          },
          "name": {
            "type": "string",
            "description": "Human-readable label the owner gave the token.",
            "example": "CI Pipeline"
          },
          "token_prefix": {
            "type": "string",
            "description": "First 8 characters of the token for identification. The full token is never stored or returned after creation.",
            "example": "vulcan_a"
          },
          "scopes": {
            "type": "array",
            "description": "Capabilities granted to the token.",
            "items": {
              "type": "string",
              "enum": [
                "read",
                "write",
                "admin"
              ]
            },
            "example": [
              "read",
              "write"
            ]
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 8601 date. Maximum 365 days from creation. Null means no expiry (warned in UI).",
            "example": "2026-08-30"
          },
          "last_used_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Timestamp of last API request using this token. Updated at most once per minute.",
            "example": "2026-05-30 14:22:01 UTC"
          },
          "revoked_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Null when active. Timestamp when revoked (soft-delete preserves audit trail).",
            "example": null
          },
          "allowed_ips": {
            "type": [
              "array",
              "null"
            ],
            "description": "CIDR allowlist. Null or empty means any IP allowed.",
            "items": {
              "type": "string"
            },
            "example": [
              "10.0.0.0/8",
              "192.168.1.0/24"
            ]
          },
          "created_at": {
            "type": "string",
            "description": "When the token was created. Blueprinter format.",
            "example": "2026-05-30 10:00:00 UTC"
          },
          "user_id": {
            "type": "integer",
            "description": "Numeric id of the token's owner.",
            "example": 42
          },
          "user_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display name of the token's owner. Null if the owner has not set one.",
            "example": "Jane Doe"
          },
          "user_email": {
            "type": [
              "string",
              "null"
            ],
            "format": "email",
            "description": "Email address of the token's owner.",
            "example": "jane@example.com"
          }
        }
      },
      "PersonalAccessTokenCreateResponse": {
        "type": "object",
        "required": [
          "token",
          "personal_access_token"
        ],
        "additionalProperties": false,
        "description": "Returned only on token creation. The `token` field contains the raw token value — this is the ONLY time it is ever returned. After this response, only the SHA-256 digest is stored server-side.",
        "properties": {
          "token": {
            "type": "string",
            "example": "vulcan_a8BfDtSxd28fDXCuYaKxkeToqqJsmq5PekL6",
            "description": "The raw API token. Show to user once, then discard. Never stored or returned again."
          },
          "personal_access_token": {
            "$ref": "#/components/schemas/PersonalAccessTokenSummary"
          }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Authentication required (RFC 9457 problem details). Either no API token and no signed-in session, or an Authorization header token that is revoked, expired, or mistyped. The `type` URI names which case applied and `how_to_authenticate` spells out both supported methods.",
        "content": {
          "application/problem+json": {
            "schema": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "type",
                "title",
                "status",
                "detail"
              ],
              "properties": {
                "type": {
                  "type": "string",
                  "description": "Stable machine identifier for the error class — a URI reference anchoring into the live API docs.",
                  "example": "/docs/api/errors#not_authenticated"
                },
                "title": {
                  "type": "string",
                  "description": "Short human summary of the error class.",
                  "example": "Not authenticated"
                },
                "status": {
                  "type": "integer",
                  "description": "HTTP status code, repeated in the body.",
                  "example": 401
                },
                "detail": {
                  "type": "string",
                  "description": "Occurrence-specific human explanation of why.",
                  "example": "This request included no API token and no valid signed-in session. If you were signed in, the session may have timed out, been signed out, or ended because this account signed in from another location."
                },
                "how_to_authenticate": {
                  "type": "object",
                  "additionalProperties": false,
                  "description": "Both supported authentication methods (extension member).",
                  "properties": {
                    "session": {
                      "type": "string",
                      "description": "How to authenticate with a browser session.",
                      "example": "Sign in through the web UI (/users/sign_in) and retry with the session cookie."
                    },
                    "token": {
                      "type": "string",
                      "description": "How to authenticate with a personal access token.",
                      "example": "Create a personal access token (your profile page, or POST /personal_access_tokens) and send it in the request header: Authorization: Token <your-token>."
                    }
                  }
                }
              }
            },
            "examples": {
              "not_authenticated": {
                "summary": "No API token and no signed-in session",
                "value": {
                  "type": "/docs/api/errors#not_authenticated",
                  "title": "Not authenticated",
                  "status": 401,
                  "detail": "This request included no API token and no valid signed-in session. If you were signed in, the session may have timed out, been signed out, or ended because this account signed in from another location.",
                  "how_to_authenticate": {
                    "session": "Sign in through the web UI (/users/sign_in) and retry with the session cookie.",
                    "token": "Create a personal access token (your profile page, or POST /personal_access_tokens) and send it in the request header: Authorization: Token <your-token>."
                  }
                }
              },
              "invalid_token": {
                "summary": "Authorization header token revoked, expired, or mistyped",
                "value": {
                  "type": "/docs/api/errors#invalid_token",
                  "title": "Invalid or expired API token",
                  "status": 401,
                  "detail": "The Authorization header carried a token that does not match any active personal access token. It may be revoked, expired, or mistyped.",
                  "how_to_authenticate": {
                    "session": "Sign in through the web UI (/users/sign_in) and retry with the session cookie.",
                    "token": "Create a personal access token (your profile page, or POST /personal_access_tokens) and send it in the request header: Authorization: Token <your-token>."
                  }
                }
              },
              "session_superseded": {
                "summary": "Session ended because the account signed in from another location",
                "value": {
                  "type": "/docs/api/errors#session_superseded",
                  "title": "Session ended — signed in elsewhere",
                  "status": 401,
                  "detail": "You were signed out because this account signed in from another location. Only one active session per account is allowed at a time.",
                  "how_to_authenticate": {
                    "session": "Sign in through the web UI (/users/sign_in) and retry with the session cookie.",
                    "token": "Create a personal access token (your profile page, or POST /personal_access_tokens) and send it in the request header: Authorization: Token <your-token>."
                  }
                }
              },
              "session_timed_out": {
                "summary": "Session expired after inactivity",
                "value": {
                  "type": "/docs/api/errors#session_timed_out",
                  "title": "Session timed out",
                  "status": 401,
                  "detail": "Your session timed out after a period of inactivity. Sign in again to continue.",
                  "how_to_authenticate": {
                    "session": "Sign in through the web UI (/users/sign_in) and retry with the session cookie.",
                    "token": "Create a personal access token (your profile page, or POST /personal_access_tokens) and send it in the request header: Authorization: Token <your-token>."
                  }
                }
              }
            }
          }
        }
      },
      "ErrorResponse": {
        "description": "Catch-all 4XX error response. Domain and validation feedback arrives as the canonical toast object (application/json); auth and infrastructure errors (401/403/404/400) arrive as RFC 9457 problem details (application/problem+json) with a stable `type` URI, `title`, `status`, and `detail`.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": {
                  "type": "string",
                  "description": "Human-readable error message (domain errors).",
                  "example": "No SRG IDs found in spreadsheet"
                },
                "toast": {
                  "type": "object",
                  "description": "Optional toast notification with structured error details.",
                  "properties": {
                    "title": {
                      "type": "string",
                      "description": "Short error summary.",
                      "example": "Validation failed."
                    },
                    "message": {
                      "type": "array",
                      "description": "Detail lines describing what went wrong.",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "Name can't be blank.",
                        "Prefix is too short (minimum is 2 characters)."
                      ]
                    },
                    "variant": {
                      "type": "string",
                      "description": "Bootstrap alert variant (always danger for errors).",
                      "example": "danger"
                    }
                  }
                }
              }
            },
            "examples": {
              "validation_error": {
                "summary": "Validation failure",
                "value": {
                  "toast": {
                    "title": "Validation failed.",
                    "message": [
                      "Name can't be blank."
                    ],
                    "variant": "danger"
                  }
                }
              }
            }
          },
          "application/problem+json": {
            "schema": {
              "type": "object",
              "required": [
                "type",
                "title",
                "status",
                "detail"
              ],
              "properties": {
                "type": {
                  "type": "string",
                  "description": "Stable machine identifier for the error class — a URI reference anchoring into the live API docs.",
                  "example": "/docs/api/errors#not_found"
                },
                "title": {
                  "type": "string",
                  "description": "Short human summary of the error class.",
                  "example": "Not found"
                },
                "status": {
                  "type": "integer",
                  "description": "HTTP status code, repeated in the body.",
                  "example": 404
                },
                "detail": {
                  "type": "string",
                  "description": "Occurrence-specific human explanation.",
                  "example": "The requested resource could not be found."
                },
                "how_to_authenticate": {
                  "type": "object",
                  "description": "Extension member on 401s — both authentication methods."
                },
                "admins": {
                  "type": "array",
                  "description": "Extension member on permission denials — who to ask.",
                  "items": {
                    "type": "object"
                  }
                },
                "toast": {
                  "type": "object",
                  "description": "Legacy toast extension on permission denials."
                }
              }
            },
            "examples": {
              "not_found": {
                "summary": "Resource not found",
                "value": {
                  "type": "/docs/api/errors#not_found",
                  "title": "Not found",
                  "status": 404,
                  "detail": "The requested resource could not be found."
                }
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found (RFC 9457 problem details). The body is identical for a truly nonexistent resource and one the caller may not learn exists.",
        "content": {
          "application/problem+json": {
            "schema": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "type",
                "title",
                "status",
                "detail"
              ],
              "properties": {
                "type": {
                  "type": "string",
                  "description": "Stable machine identifier for the error class — a URI reference anchoring into the live API docs.",
                  "example": "/docs/api/errors#not_found"
                },
                "title": {
                  "type": "string",
                  "description": "Short human summary of the error class.",
                  "example": "Not found"
                },
                "status": {
                  "type": "integer",
                  "description": "HTTP status code, repeated in the body.",
                  "example": 404
                },
                "detail": {
                  "type": "string",
                  "description": "Occurrence-specific human explanation.",
                  "example": "The requested resource could not be found."
                }
              }
            },
            "examples": {
              "not_found": {
                "summary": "No such resource is reachable by this request",
                "value": {
                  "type": "/docs/api/errors#not_found",
                  "title": "Not found",
                  "status": 404,
                  "detail": "The requested resource could not be found."
                }
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "Insufficient permissions (RFC 9457 problem details). The `type` URI names the denial class; permission denials carry the project `admins` to ask for access, plus a legacy `toast` extension kept until every consumer reads the problem fields directly.",
        "content": {
          "application/problem+json": {
            "schema": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "type",
                "title",
                "status",
                "detail"
              ],
              "properties": {
                "type": {
                  "type": "string",
                  "description": "Stable machine identifier for the error class — a URI reference anchoring into the live API docs.",
                  "example": "/docs/api/errors#permission_denied"
                },
                "title": {
                  "type": "string",
                  "description": "Short human summary of the error class.",
                  "example": "Permission denied"
                },
                "status": {
                  "type": "integer",
                  "description": "HTTP status code, repeated in the body.",
                  "example": 403
                },
                "detail": {
                  "type": "string",
                  "description": "Occurrence-specific human explanation of why.",
                  "example": "You are not authorized to perform viewer actions on this project"
                },
                "admins": {
                  "type": "array",
                  "description": "Project admin contacts to ask for access (extension member, present on permission denials when a project or component is in scope; empty otherwise).",
                  "items": {
                    "type": "object",
                    "additionalProperties": false,
                    "required": [
                      "name",
                      "email"
                    ],
                    "properties": {
                      "name": {
                        "type": "string",
                        "description": "Admin display name.",
                        "example": "Alice Admin"
                      },
                      "email": {
                        "type": "string",
                        "format": "email",
                        "description": "Admin contact email.",
                        "example": "alice@example.com"
                      }
                    }
                  }
                },
                "toast": {
                  "type": "object",
                  "additionalProperties": false,
                  "description": "Legacy toast extension for older consumers (permission denials only).",
                  "properties": {
                    "title": {
                      "type": "string",
                      "description": "Toast heading.",
                      "example": "Not Authorized."
                    },
                    "message": {
                      "type": "array",
                      "description": "Toast body lines.",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "You are not authorized to perform viewer actions on this project"
                      ]
                    },
                    "variant": {
                      "type": "string",
                      "description": "Bootstrap alert variant.",
                      "example": "danger"
                    }
                  }
                }
              }
            },
            "examples": {
              "permission_denied": {
                "summary": "Authenticated but lacking the required project capability",
                "value": {
                  "type": "/docs/api/errors#permission_denied",
                  "title": "Permission denied",
                  "status": 403,
                  "detail": "You are not authorized to perform viewer actions on this project",
                  "admins": [
                    {
                      "name": "Alice Admin",
                      "email": "alice@example.com"
                    }
                  ],
                  "toast": {
                    "title": "Not Authorized.",
                    "message": [
                      "You are not authorized to perform viewer actions on this project"
                    ],
                    "variant": "danger"
                  }
                }
              },
              "insufficient_token_scope": {
                "summary": "Token lacks the scope this request requires",
                "value": {
                  "type": "/docs/api/errors#insufficient_token_scope",
                  "title": "Insufficient token scope",
                  "status": 403,
                  "detail": "This request requires the write scope, and the token does not grant it."
                }
              },
              "ip_not_allowed": {
                "summary": "Request IP outside the token allowlist",
                "value": {
                  "type": "/docs/api/errors#ip_not_allowed",
                  "title": "IP address not allowed",
                  "status": 403,
                  "detail": "The request came from an IP address outside this token's allowlist."
                }
              }
            }
          }
        }
      },
      "UnprocessableEntity": {
        "description": "Validation errors prevented the request from being processed.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "toast": {
                  "type": "object",
                  "description": "Toast notification containing validation error details.",
                  "properties": {
                    "title": {
                      "type": "string",
                      "description": "Short error summary.",
                      "example": "Unable to save changes."
                    },
                    "message": {
                      "type": "array",
                      "description": "Specific validation error messages.",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "Name can't be blank.",
                        "Prefix format is invalid."
                      ]
                    },
                    "variant": {
                      "type": "string",
                      "description": "Bootstrap alert variant (always danger for validation errors).",
                      "example": "danger"
                    }
                  }
                }
              }
            },
            "examples": {
              "validation_failure": {
                "summary": "Component creation with missing fields",
                "value": {
                  "toast": {
                    "title": "Unable to save changes.",
                    "message": [
                      "Name can't be blank.",
                      "Prefix format is invalid."
                    ],
                    "variant": "danger"
                  }
                }
              }
            }
          }
        }
      }
    },
    "parameters": {
      "SearchQuery": {
        "name": "q",
        "in": "query",
        "required": true,
        "description": "Search query string (minimum 2 characters).",
        "schema": {
          "type": "string",
          "minLength": 2
        },
        "example": "container platform"
      },
      "ProjectId": {
        "name": "projectId",
        "in": "path",
        "required": true,
        "description": "Numeric ID of the target project.",
        "schema": {
          "type": "integer"
        },
        "example": 7
      },
      "TriageStatusFilter": {
        "name": "triage_status",
        "in": "query",
        "description": "Filter comments by triage disposition. Defaults to \"pending\" — the triage table opens on undispositioned comments. Use \"all\" to return comments in any status.",
        "schema": {
          "type": "string",
          "enum": [
            "all",
            "pending",
            "concur",
            "concur_with_comment",
            "non_concur",
            "duplicate",
            "informational",
            "needs_clarification",
            "withdrawn",
            "addressed_by"
          ],
          "default": "pending"
        },
        "example": "pending"
      },
      "PageParam": {
        "name": "page",
        "in": "query",
        "description": "Page number for paginated results (1-based).",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "default": 1
        },
        "example": 1
      },
      "PerPageParam": {
        "name": "per_page",
        "in": "query",
        "description": "Number of items to return per page.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 1000,
          "default": 25
        },
        "example": 25
      },
      "ComponentId": {
        "name": "componentId",
        "in": "path",
        "required": true,
        "description": "Numeric ID of the target component.",
        "schema": {
          "type": "integer"
        },
        "example": 29
      },
      "RuleId": {
        "name": "ruleId",
        "in": "path",
        "required": true,
        "description": "Numeric ID of the target rule.",
        "schema": {
          "type": "integer"
        },
        "example": 100
      },
      "ReviewId": {
        "name": "reviewId",
        "in": "path",
        "required": true,
        "description": "Numeric ID of the target review.",
        "schema": {
          "type": "integer"
        },
        "example": 44
      },
      "MembershipId": {
        "name": "membershipId",
        "in": "path",
        "required": true,
        "description": "Numeric ID of the target membership record.",
        "schema": {
          "type": "integer"
        },
        "example": 15
      },
      "SrgId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Numeric ID of the target Security Requirements Guide (SRG).",
        "schema": {
          "type": "integer"
        },
        "example": 1
      },
      "StigId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Numeric ID of the target Security Technical Implementation Guide (STIG).",
        "schema": {
          "type": "integer"
        },
        "example": 1
      },
      "UserId": {
        "name": "userId",
        "in": "path",
        "required": true,
        "description": "Numeric ID of the target user.",
        "schema": {
          "type": "integer"
        },
        "example": 42
      }
    }
  }
}