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.
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. |
|
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.uxFEATURE_DIR/data-model.md— data model with Pydantic schemasFEATURE_DIR/spec.mdsections describing endpoints, request/response shapes, or WebSocket channels
Outline
Phase 0: Pre-Flight
- Setup: Run
.specify/scripts/bash/check-prerequisites.sh --json --paths-onlyfrom repo root. ParseFEATURE_DIR. - Verify applicability: If no API surface, report skip and exit.
- Load context:
FEATURE_DIR/spec.md— functional requirements, endpoint descriptionsFEATURE_DIR/ux_reference.md— caller interaction referenceFEATURE_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 principlesdocs/adr/ADR-0005-auth-rbac.md— RBAC enforcement rulesbackend/src/api/— existing API route patterns to maintain consistencybackend/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:
- YAML syntax: Verify parseable via Python
import yaml; yaml.safe_load(file)— Python'spyyamlis inrequirements.txt. - Structural check: Verify
openapi,info,paths,componentskeys exist. - OperationId uniqueness: Every
operationIdMUST be unique across all paths. - Schema references: Every
$reftarget MUST exist incomponents/schemas/orcomponents/responses/orcomponents/parameters/. - Example completeness: Every response class (2xx, 4xx, 5xx) for every operation MUST have at least one example.
- 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