Files
ss-tools/.opencode/command/speckit.openapi.md
busya d874a4dca6 feat(speckit): workflow architecture upgrades + rework 038 spec per new flow
Add prototype/openapi/validate/resume commands, wire edge-failure matrix into UX,
enforce traceability + validation gates, mandate C4/C5 belief-runtime verification.
Rework 038-dashboard-scenario-model artifacts: applicability, structured edge cases,
24-class UX state matrix, interactive HTML prototype, standardized OpenAPI 3.1 (7 ops),
full RTM with coverage gate, 56-task backlog, and PASS validation report.
2026-07-31 11:25:43 +03:00

18 KiB

description, handoffs
description handoffs
Generate and validate an OpenAPI 3.1 artifact at specs/<feature>/contracts/openapi.yaml from api-ux, data model, and spec. Requires operationId, reusable schemas, standard envelopes, auth/RBAC, pagination, examples, and schema validation.
label agent prompt send
Build Technical Plan speckit.plan Create a Python/Svelte implementation plan using the validated OpenAPI contract true

User Input

$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/<feature>/contracts/openapi.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/<feature>/

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:

import yaml, sys, json

with open("specs/<feature>/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/<feature>/contracts/openapi-traceability.md:

#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/<feature>/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