Files
ss-tools/.kilo/command/speckit.openapi.md
busya 39aa4a7e0c chore(kilo): consolidate agent skills, commands and workflows
- remove legacy .ai/ knowledge shots and reports, semantic skills
  invariant assessment, and obsolete .kilo/workflows/
- update agent model selection (omniroute/terra) for qa-tester,
  security-auditor, svelte-coder
- add swarm-master agent and speckit openapi/prototype/resume/validate
  plus test.* commands; update speckit plan/ux/implement docs
- refresh skill SKILL.md files (semantics core/testing/svelte/belief,
  molecular-cot-logging, semantic-frontend)
- add semantic curation report
2026-08-02 23:54:30 +07:00

549 lines
18 KiB
Markdown

---
description: 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.
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/<feature>/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/<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:
```python
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`:
```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/<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`