--- description: Generate and validate an OpenAPI 3.1 artifact at specs//contracts/openapi.yaml from api-ux, data model, and spec. Requires operationId, reusable schemas, standard envelopes, auth/RBAC, pagination, examples, and schema validation. handoffs: - label: Build Technical Plan agent: speckit.plan prompt: Create a Python/Svelte implementation plan using the validated OpenAPI contract send: true --- ## User Input ```text $ARGUMENTS ``` You **MUST** consider the user input before proceeding (if not empty). ## Applicability This command is applicable when the feature has an API surface (REST endpoints, WebSocket channels). For UI-only features with no new or changed API endpoints, skip gracefully with: "No API surface detected — OpenAPI not applicable. Proceed to `/speckit.plan`." **Decision gate**: If any of the following exist, generate OpenAPI: - `FEATURE_DIR/contracts/ux/api-ux.md` — API shapes from `/speckit.ux` - `FEATURE_DIR/data-model.md` — data model with Pydantic schemas - `FEATURE_DIR/spec.md` sections describing endpoints, request/response shapes, or WebSocket channels ## Outline ### Phase 0: Pre-Flight 1. **Setup**: Run `.specify/scripts/bash/check-prerequisites.sh --json --paths-only` from repo root. Parse `FEATURE_DIR`. 2. **Verify applicability**: If no API surface, report skip and exit. 3. **Load context**: - `FEATURE_DIR/spec.md` — functional requirements, endpoint descriptions - `FEATURE_DIR/ux_reference.md` — caller interaction reference - `FEATURE_DIR/contracts/ux/api-ux.md` — API shapes from UX phase (if exists) - `FEATURE_DIR/data-model.md` — Pydantic schemas, SQLAlchemy models (if exists) - `FEATURE_DIR/contracts/modules.md` — module and service contracts (if exists) - `.specify/memory/constitution.md` — auth/RBAC principles - `docs/adr/ADR-0005-auth-rbac.md` — RBAC enforcement rules - `backend/src/api/` — existing API route patterns to maintain consistency - `backend/src/schemas/` — existing Pydantic schemas for reusable components ### Phase 1: Extract API Surface Build the API surface inventory from all available sources: | Source | Extraction | |--------|------------| | `api-ux.md` | Endpoint paths, methods, request/response shapes, error variants | | `data-model.md` | Pydantic schemas → reusable `#/components/schemas/` | | `spec.md` | Functional requirements → operation descriptions | | `contracts/modules.md` | `@DATA_CONTRACT` entries → Input/Output DTOs | | `ux_reference.md` | Result envelopes, warning states, recovery hints | **Surface completeness check**: For each endpoint, verify: - [ ] Path and HTTP method - [ ] Request body schema (if POST/PUT/PATCH) - [ ] Path/query parameters with types - [ ] Success response (200/201) schema - [ ] Error responses: 400, 401, 403, 404, 409, 422, 429, 500 - [ ] Auth requirement (RBAC role) - [ ] Pagination parameters (if list endpoint) ### Phase 2: Generate openapi.yaml Create `specs//contracts/openapi.yaml`: ```yaml openapi: "3.1.0" info: title: "[Feature Name] API" version: "1.0.0" description: > OpenAPI 3.1 contract for [feature]. Generated from UX contracts, data model, and specification. Source: specs// servers: - url: /api description: superset-tools API gateway tags: - name: [domain] description: [domain description from spec] paths: /[resource]: get: operationId: listResources tags: [[domain]] summary: List all resources description: Returns a paginated list of resources accessible to the caller. parameters: - $ref: "#/components/parameters/PageParam" - $ref: "#/components/parameters/PageSizeParam" - name: search in: query schema: { type: string } description: Full-text search filter responses: "200": description: Paginated list of resources content: application/json: schema: $ref: "#/components/schemas/ResourceListResponse" examples: withData: $ref: "#/components/examples/ResourceListWithData" empty: $ref: "#/components/examples/ResourceListEmpty" "401": $ref: "#/components/responses/UnauthorizedError" "403": $ref: "#/components/responses/ForbiddenError" "500": $ref: "#/components/responses/InternalError" post: operationId: createResource tags: [[domain]] summary: Create a new resource description: Creates a resource. Requires [ROLE] permission. security: - BearerAuth: [[role]] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ResourceCreateRequest" examples: valid: $ref: "#/components/examples/ResourceCreateValid" responses: "201": description: Resource created content: application/json: schema: $ref: "#/components/schemas/ResourceResponse" "400": $ref: "#/components/responses/BadRequestError" "401": $ref: "#/components/responses/UnauthorizedError" "403": $ref: "#/components/responses/ForbiddenError" "409": $ref: "#/components/responses/ConflictError" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/RateLimitError" "500": $ref: "#/components/responses/InternalError" /[resource]/{resourceId}: parameters: - name: resourceId in: path required: true schema: { type: string, format: uuid } get: operationId: getResource tags: [[domain]] summary: Get resource by ID responses: "200": description: Resource found content: application/json: schema: $ref: "#/components/schemas/ResourceResponse" "404": $ref: "#/components/responses/NotFoundError" # ... standard errors put: operationId: updateResource tags: [[domain]] summary: Full update of resource description: | Idempotent full update. Requires [ROLE] permission. Uses optimistic concurrency via If-Match header. parameters: - name: If-Match in: header schema: { type: string } description: Version hash for optimistic concurrency security: - BearerAuth: [[role]] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ResourceUpdateRequest" responses: "200": description: Resource updated "409": description: Version conflict — resource modified since If-Match $ref: "#/components/responses/ConflictError" "412": description: Precondition failed — If-Match missing or stale content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" # ... standard errors components: securitySchemes: BearerAuth: type: http scheme: bearer bearerFormat: JWT description: | superset-tools JWT. Roles encoded in `roles` claim. Required scopes noted per-operation. parameters: PageParam: name: page in: query schema: { type: integer, minimum: 1, default: 1 } description: Page number (1-indexed) PageSizeParam: name: page_size in: query schema: { type: integer, minimum: 1, maximum: 200, default: 20 } description: Items per page schemas: ErrorEnvelope: type: object required: [error] properties: error: type: object required: [code, detail] properties: code: type: string description: Machine-readable error code (e.g., NOT_FOUND, VALIDATION_ERROR) example: "NOT_FOUND" detail: type: string description: Human-readable error description example: "Resource 550e8400-e29b-41d4-a716-446655440000 not found" fields: type: object description: Per-field validation errors (422 only) additionalProperties: type: string example: { "name": "Name is required", "email": "Invalid email format" } retry_after: type: integer description: Seconds until retry is allowed (429 only) example: 30 SuccessEnvelope: type: object required: [data] properties: data: {} meta: type: object properties: total: type: integer description: Total items matching query page: type: integer page_size: type: integer pages: type: integer ResourceResponse: allOf: - $ref: "#/components/schemas/SuccessEnvelope" - type: object properties: data: $ref: "#/components/schemas/Resource" ResourceListResponse: allOf: - $ref: "#/components/schemas/SuccessEnvelope" - type: object properties: data: type: array items: $ref: "#/components/schemas/Resource" # ... domain-specific schemas derived from data-model.md responses: BadRequestError: description: Malformed request content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" example: error: code: "BAD_REQUEST" detail: "Request body is not valid JSON" UnauthorizedError: description: Missing or invalid authentication content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" example: error: code: "UNAUTHORIZED" detail: "Authentication required" ForbiddenError: description: Insufficient permissions content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" example: error: code: "FORBIDDEN" detail: "Requires role: admin" NotFoundError: description: Resource not found content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" example: error: code: "NOT_FOUND" detail: "Resource 550e8400-e29b-41d4-a716-446655440000 not found" ConflictError: description: Resource conflict (e.g., duplicate, version mismatch) content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" example: error: code: "CONFLICT" detail: "Resource with this name already exists" ValidationError: description: Request validation failed content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" example: error: code: "VALIDATION_ERROR" detail: "Request validation failed" fields: name: "Name is required" RateLimitError: description: Too many requests headers: Retry-After: schema: { type: integer } description: Seconds until next request is allowed content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" example: error: code: "RATE_LIMITED" detail: "Too many requests. Retry after 30 seconds." retry_after: 30 InternalError: description: Unexpected server error content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" example: error: code: "INTERNAL_ERROR" detail: "An unexpected error occurred. Please try again later." examples: ResourceListWithData: summary: List with items value: data: - id: "550e8400-e29b-41d4-a716-446655440000" name: "Example Resource" created_at: "2026-07-31T12:00:00Z" meta: total: 42 page: 1 page_size: 20 pages: 3 ResourceListEmpty: summary: Empty list value: data: [] meta: total: 0 page: 1 page_size: 20 pages: 0 ``` ### Phase 3: Schema Validation Validate the generated `openapi.yaml` using ONLY available repo tooling: 1. **YAML syntax**: Verify parseable via Python `import yaml; yaml.safe_load(file)` — Python's `pyyaml` is in `requirements.txt`. 2. **Structural check**: Verify `openapi`, `info`, `paths`, `components` keys exist. 3. **OperationId uniqueness**: Every `operationId` MUST be unique across all paths. 4. **Schema references**: Every `$ref` target MUST exist in `components/schemas/` or `components/responses/` or `components/parameters/`. 5. **Example completeness**: Every response class (2xx, 4xx, 5xx) for every operation MUST have at least one example. 6. **Auth coverage**: Every mutating operation (POST, PUT, PATCH, DELETE) MUST declare `security`. **Do NOT install new tools.** If `openapi-spec-validator` or `spectral` are not already in the project, use Python script inline: ```python import yaml, sys, json with open("specs//contracts/openapi.yaml") as f: spec = yaml.safe_load(f) errors = [] # Check required OpenAPI keys for key in ("openapi", "info", "paths"): if key not in spec: errors.append(f"Missing required key: {key}") # Check operationId uniqueness op_ids = set() for path, methods in spec.get("paths", {}).items(): for method, op in methods.items(): if method in ("parameters", "description", "summary"): continue oid = op.get("operationId") if not oid: errors.append(f"{method.upper()} {path}: missing operationId") elif oid in op_ids: errors.append(f"{method.upper()} {path}: duplicate operationId '{oid}'") else: op_ids.add(oid) # Check $ref targets schemas = set(spec.get("components", {}).get("schemas", {}).keys()) responses = set(spec.get("components", {}).get("responses", {}).keys()) params = set(spec.get("components", {}).get("parameters", {}).keys()) def check_refs(obj, path=""): if isinstance(obj, dict): if "$ref" in obj: ref = obj["$ref"] parts = ref.split("/") if len(parts) >= 4 and parts[1] == "components": if parts[2] == "schemas" and parts[3] not in schemas: errors.append(f"{path}: unresolved $ref {ref} (schema not found)") elif parts[2] == "responses" and parts[3] not in responses: errors.append(f"{path}: unresolved $ref {ref} (response not found)") elif parts[2] == "parameters" and parts[3] not in params: errors.append(f"{path}: unresolved $ref {ref} (parameter not found)") for k, v in obj.items(): check_refs(v, f"{path}.{k}") elif isinstance(obj, list): for i, v in enumerate(obj): check_refs(v, f"{path}[{i}]") check_refs(spec) if errors: print(f"VALIDATION FAILED: {len(errors)} errors") for e in errors: print(f" - {e}") sys.exit(1) else: print(f"VALIDATION PASSED: {len(op_ids)} operations, {len(schemas)} schemas") ``` Run: `cd /root/ss-tools && python -c "$(cat <<'PYEOF' ... PYEOF)"` ### Phase 4: Drift & Traceability Mappings Create `specs//contracts/openapi-traceability.md`: ```markdown #region Std.Opencode.OpenApiTraceability [C:3] [TYPE ADR] [SEMANTICS openapi,traceability,[DOMAIN]] @defgroup OpenAPI Trace OpenAPI operationId → data-model → spec → UX contract drift map. ## Operation Traceability | operationId | Spec Requirement | Data Model | UX Contract | Status | |-------------|-----------------|------------|-------------|--------| | listResources | [DOMAIN]-FR-001 | Resource (data-model.md: §Resources) | api-ux.md: GET /resources | ✅ | | createResource | [DOMAIN]-FR-002 | ResourceCreateRequest | api-ux.md: POST /resources | ✅ | | getResource | [DOMAIN]-FR-003 | Resource (data-model.md: §Resources) | api-ux.md: GET /resources/{id} | ✅ | ## Schema Traceability | Schema | Source | Purpose | |--------|--------|---------| | Resource | data-model.md: Resource entity | Shared response schema | | ResourceCreateRequest | api-ux.md: Create payload | Create request body | | ErrorEnvelope | ux_reference.md: Error shapes | Standard error response | ## Drift Detection (manual review) - [ ] Every operationId maps to at least one spec requirement - [ ] Every spec requirement with an API touchpoint maps to an operationId - [ ] Pydantic schema names match OpenAPI schema names - [ ] Error response shapes match ux_reference.md promises - [ ] Auth requirements match ADR-0005 RBAC model ## Coverage Gate - [ ] Success examples for every operation - [ ] Error examples for every response class - [ ] Pagination parameters on every list endpoint - [ ] operationId on every operation - [ ] Reusable schemas (no inline anonymous schemas) #endregion Std.Opencode.OpenApiTraceability ``` ### Phase 5: Report Report: - OpenAPI path: `specs//contracts/openapi.yaml` - Operations defined: N - Reusable schemas: N - Standard error responses: N - Validation: PASS/FAIL with N errors - Traceability: N operations mapped to requirements - Recommended next: `/speckit.plan`