App Works — OpenAPI fragments
Status: Draft · Created: 2026-09-17 · Program: App Works
Closes: the fragment half of SK-07
Companion documents: ../README.md (why fragments exist, the superset rule, the
generation pipeline) · ../agent-surfaces.md (the x-mcp values) ·
../../CONTRACTS.md §4 (the normative route table)
1. The convention
- One fragment per API-owning epic, named
apw-<nn>.openapi.yaml—apw-01.openapi.yaml,apw-12.openapi.yaml, … There is no fragment for an epic that owns no route (APW-13). - OpenAPI 3.0 (
openapi: 3.0.3), not 3.1 — the version the platform actually serves. The generator never callssetOpenAPIVersion, so@nestjs/swagger11's default applies: the emitted document'sopenapifield is3.0.0(@nestjs/swagger/dist/swagger-module.js:29;docs/api/index.md:24states "OpenAPI 3.0 JSON specification"). A fragment must be a subset of that document. - Nullability uses the 3.0 spelling:
nullable: true. This is not cosmetic. The MCP converter reads the 3.0 form (apps/mcp/src/openapi-tools/schema-converter.service.ts:20,71—/** OpenAPI 3.0 spelling of "may be null" … *//if (schema.nullable === true)), whereas 3.1 spells ittype: [x, 'null']— which the converter ignores. A 3.1 fragment would therefore encode nullability in a form the pipeline silently drops. - A fragment is never the source of truth. The generated
apps/api/openapi.jsonis (../README.md§1–§2). A fragment that disagrees with the generated document is a finding, not a specification change. - A fragment lists only what the plan writes down. Where a plan names a route but does not write its
request or response shape, the route is listed in §2 below as "route named at
file:line, schema to be derived" and no operation is written. Guessing a field name would make the guess the contract. - Every operation carries
x-source(thefile:line/section the operation was derived from) andx-mcp(the agent-surface decision:not-exposed, or the tool name and its hints).x-mcpis defined in../agent-surfaces.md§3. - No
info.versionbeyond a pointer. The platform serves one unversioned document titledEver Works APIat version1.0(apps/api/src/openapi/openapi-document.config.ts:12,16); a fragment'sinfoblock names its epic and its phase, nothing more. - No servers. The fragments are contract shapes, not deployments; an installation's origins are
configuration (
../../CONFIGURATION.md§4). - No secrets, no hostnames. Every example value is a placeholder, an RFC 2606 host or a synthetic digest — the public-repository rule (README §7 rule 10).
2. Per-epic status
| Fragment | Epic | Status | What is grounded | What is not |
|---|---|---|---|---|
apw-01.openapi.yaml | APW-01 | written | POST /api/works/app-source/inspect — request fields with their validators, the 200 semantics, 400 codes app_works_disabled / invalid_url, 429; the POST /api/works extensions (kind, repositoryUrl, repositoryMode, targetOwner, blueprintId, appEnv), the 200 response envelope and the documented 400/409/503 codes; the delete_stored_data + confirm_slug additions to POST /api/works/:id/delete with 422 confirmation_mismatch and 400 for a link + delete_data_repository | the full AppSourceInspectResponseDto and AppSourceInspectRequestDto property list — the plan writes the fields but not every type, so the fragment marks the response additionalProperties: true and cites the DTO |
apw-02.openapi.yaml | APW-02 | not written | — | GET /api/works/:id/upstream, POST /api/works/:id/upstream/sync (202), POST /api/works/:id/upstream/readiness/retry (202) — route named at ../../APW-02-fork-lifecycle/plan.md §4.1; schema to be derived |
apw-03.openapi.yaml | APW-03 | not written | — | the catalog reads and the app-spec/license routes — route named at ../../APW-03-app-spec-and-catalog/plan.md §4.1; AppsCatalogListResponse exists as a name but its properties are in the web components, not the API section; schema to be derived |
apw-04.openapi.yaml | APW-04 | not written | — | provision / provisioning / cancel / blueprint-suggestion / the two admin routes — route named at ../../APW-04-app-provisioner/plan.md §4; schema to be derived |
apw-05.openapi.yaml | APW-05 | not written | — | the builds routes and PUT /api/works/:id/builds/pull-token — route named at ../../APW-05-builds/plan.md §5; AppBuildSummary/AppBuildDetail are written out in the plan §3.2, so this fragment is the cheapest next one to write |
apw-06.openapi.yaml | APW-06 | not written | — | app-status / app-jobs / smoke / rollback / lifecycle / logs / target / deletion-preview plus the kind-app branches on the existing deploy routes — route named at ../../APW-06-app-runtime/plan.md §9.1; AppStatusSnapshot and AppRenderInput are named in ../../CONTRACTS.md §3 but not written field-by-field; schema to be derived |
apw-07.openapi.yaml | APW-07 | not written | — | app-env, app-dependencies and PUT /api/deploy/works/:id/runtime-env — route named at ../../APW-07-app-env-and-dependencies/plan.md §5; AppEnvEntryView is written out in plan §3.3, so this fragment is also cheap |
apw-08.openapi.yaml | APW-08 | not written | — | evolve, delivery, cost and the goals/missions field additions — route named at ../../APW-08-evolve-loop/plan.md §4; TaskDeliveryView/TaskCostView are written out in plan §3.4; schema to be derived for the controller DTOs |
apw-09.openapi.yaml | APW-09 | not written | — | the upstream-pull-request family — route named at ../../APW-09-upstream-pull-requests/plan.md §5; that plan writes the list envelope as { data: UpstreamPullRequestView[], meta: { total } }. ({ entries, summary } is APW-07's GET /api/works/:id/app-env envelope, not APW-09's — corrected 2026-09-17.) Properties are otherwise to be derived |
apw-10.openapi.yaml | APW-10 | not written | — | the operator family api/admin/apps-tier/* and the two owner reads — the plan's §6.1 table names the routes; CONTRACTS §4 abbreviates the family with *, so the fragment cannot be written until §6.1's members are treated as normative |
apw-11.openapi.yaml | APW-11 | not written | — | GET /api/me/apps, PUT /api/me/apps/preferences, GET /api/app-launcher/platforms — route named at ../../APW-11-app-launcher/plan.md §4; AppLauncherListResponse is written out in plan §3.3, so this fragment is cheap |
apw-12.openapi.yaml | APW-12 | written | method, path, auth class, throttle and request → response for all 13 /api/auth/ever-id/* routes plus the everId addition to GET /api/auth/providers, and the shared code-keyed error responses | the exact TypeScript type of EverIdCallbackOutcome's variants beyond the four outcome discriminators (written in plan §3.5, so they are groundable — the fragment marks the union without re-deriving it); TokenResponse and IdentityProviderCheck are existing/named types, cited not redefined |
Fragments are written in the order the plan documents make them cheap. APW-05 §3.2, APW-07 §3.3, APW-08 §3.4 and APW-11 §3.3 each write their response types out in full, so their fragments need no new decisions — they are the next four to add.
3. How a fragment is checked
- Lint — parse and dereference every fragment with
@apidevtools/swagger-parser, already a dependency ofapps/mcp(apps/mcp/package.json:25). No new dependency. - Superset —
apps/api/src/openapi/__tests__/app-works-contract.spec.ts(APW-13 P0) loads every fragment and the generatedapps/api/openapi.json, and asserts, per operation:- the
path+methodexist; - every property the fragment marks
requiredis required in the generated document; - every status code the fragment lists is declared in the generated document;
- every
x-mcpvalue in the fragment matches the whitelist decision in../agent-surfaces.md—not-exposedmeans no whitelist entry, a tool name means exactly that entry.
- the
- Regenerate before comparing —
pnpm --filter ever-works-api build && pnpm --filter ever-works-api generate:openapi(the generated file is git-ignored; see../README.md§1).
A fragment is stale the moment the generated document gains a required property the fragment does not
list only if the fragment also lists that operation's request body — which is why the two written fragments
mark the parts they do not fully know with additionalProperties: true and an x-source note rather than
claiming a closed shape.