diff --git a/docs/requirements/api-artifacts.md b/docs/requirements/api-artifacts.md index b5504b91..2d641dac 100644 --- a/docs/requirements/api-artifacts.md +++ b/docs/requirements/api-artifacts.md @@ -102,6 +102,33 @@ event item to its owning family's entry; §7.5 rejects duplicate and wrong-family event items; and §7.2/§7.5 assert bidirectional set equality between the event inventory and the file's event items (F9). +Revision 6 (luna r5 residuals F1/F2/F4/F9 + NF1–NF5): the +emitted-status closure becomes a defined, total algorithm over five +named site classes — record-derived auth-guard refusals (each +`ApiAuthClass` carries a closed refusal-status set in the §1.6 +mapping table), bootstrap-registered global interceptors, decorator +status metadata, the handler's type-checker call-graph closure with +unresolved-edge failure, and raw response sinks by Node symbol +identity — and the comparison becomes bidirectional, rejecting +phantom map statuses no site produces (F1, NF1). §7.2 classifies +registration primitives as path-bearing or non-path-bearing, defines +the raw-mount rule for hook-internal URL dispatch, and defines the +framework path composition (class-prefix arrays × method suffixes, +default empty paths, normalization) applied before the exact +template comparison (F2, NF2). The `ApiAuthClass` → OpenAPI mapping +table is pinned in-contract with exact requirement-set shapes under +OpenAPI 3.1 OR/AND semantics (admin = two requirement objects; +federation = one object with two schemes), a new guard-equivalence +witness (§7.7(e)) ties the mapping to the live guard semantics, and +§7.4 resolves document-level security inheritance before comparison +(F4, NF3). The event scan gains a mechanical server/client boundary +by socket-package type identity, the comparison element is defined +as the (direction, name) pair, and the two registries' value sets +must be pairwise disjoint (F9, NF4). The ownership map's domain must +exactly equal the registry union (stale keys rejected), and +`Operations:` becomes conditional so an event-only family is +grammatical (NF5). + This contract binds the artifact definitions (§2), the production model (§3), coverage obligations (§4), legacy migration (§5), phase timing (§6), witnesses (§7), and disclosed drafting additions (§8). @@ -164,12 +191,30 @@ documentation placement rules stay with the docs atlas federation grant — both required), and `bootstrap` (identity §3 pre-epoch surface) — amendment-only, pinned here rather than deferred. Each `ApiAuthClass` value maps to exactly one OpenAPI - security-requirement set under a closed mapping table carried by - the generation script (§3.1): `none` maps to the empty security - array, and each other value maps to a named security-scheme - requirement set the implementing PR defines once in - `OPENAPI.yaml`'s components; the mapping is total over the - enumeration, and §7.4(b) compares security through it; the + security-requirement set under a closed mapping table pinned + HERE (the generation script carries it verbatim, and §7.4(b) + compares security through it). OpenAPI 3.1 semantics are + binding: a `security` array with multiple requirement objects + means OR across objects; multiple schemes inside one requirement + object mean AND. The table, over scheme names the implementing + PR defines exactly once in `OPENAPI.yaml`'s + `components.securitySchemes`: `none` → the empty security array; + `session` → exactly one requirement object naming the session + scheme; `api-key` → exactly one requirement object naming the + API-key scheme; `admin` → exactly two requirement objects, one + naming the admin-privileged-session scheme and one naming the + admin-bearer scheme (the alternation as OpenAPI OR); + `federation` → exactly one requirement object naming both the + mTLS scheme and the federation-grant scheme (the conjunction as + OpenAPI AND); `bootstrap` → exactly one requirement object + naming the bootstrap scheme. The table additionally assigns each + class a closed **refusal-status set** — the HTTP statuses its + record-derived guard emits on refusal — consumed by §7.7(d); the + implementing PR states each set from the live guard source, and + the §7.7(e) guard-equivalence witness ties both the + requirement-set shapes and the refusal statuses to the guards' + actual semantics. The mapping is total over the enumeration and + injective — no two classes map to the same requirement set; the permission rule reference (owning contract plus rule identifier); and the error-code set, drawn from the contract 5 §4.2 closed taxonomy. **Socket.IO event records** carry direction-specific @@ -196,9 +241,14 @@ documentation placement rules stay with the docs atlas file), followed by a bullet list whose items begin with exactly one of the literal field labels `Summary:`, `Operations:`, `Auth:`, `Permissions:`, `Errors:`, `Events:` — each label at most - once per entry; `Summary:`, `Operations:`, `Auth:`, - `Permissions:`, and `Errors:` REQUIRED in every entry, `Events:` - REQUIRED for a family with Socket.IO events and absent otherwise. + once per entry; `Summary:`, `Auth:`, `Permissions:`, and + `Errors:` REQUIRED in every entry; `Operations:` REQUIRED for a + family with at least one `OPENAPI.yaml` operation and ABSENT for + an **event-only family** (a family whose entire surface is + Socket.IO events — such a family MUST carry `Events:`, which is + how it satisfies the grammar with no operations to link); + `Events:` REQUIRED for a family with Socket.IO events and absent + otherwise. Field contents: `Summary:` — the family's behavior summary, free prose; `Operations:` — one or more Markdown links, each with link text equal to an `operationId` and link target @@ -357,6 +407,15 @@ documentation placement rules stay with the docs atlas family — and it is the deterministic rule for which entry documents which event; a registry event absent from the map, or mapped to a family with no `ENDPOINTS.md` entry, fails §7.5. + The map's domain EXACTLY equals the union of the two registries' + event names: a map key naming no registry event (a stale key) + fails §7.5 just as a missing key does. The two registries' + literal value sets are pairwise DISJOINT — no event name appears + in both the inbound and the outbound registry — witnessed by + §7.5; disjointness is what makes the name-keyed ownership map + well-defined over directional events. Everywhere this contract + compares event sets (§7.2, §7.5), the comparison element is the + **(direction, name) pair**, never the bare name. ## 5. Legacy migration @@ -436,27 +495,69 @@ scanned source roots, and compared files. callee the type-checker cannot resolve, is a scan FAILURE naming the site, never a silent skip — with one control per case (aliased call detected; wrapped call detected; dynamic-name - access fails). **Path-argument resolution:** for every detected - registration call site and every route decorator, the path - argument must be a string literal or resolve through the - type-checker to a compile-time string constant; the resolved - string is then compared for exact equality with the route's §1.6 - record path template. A path argument the checker cannot resolve - to a constant (`app.get(computedPath(), handler)`, a decorator - with a computed path value) is a scan FAILURE naming the site, - never an inventory entry with an undefined path — with one - control per case (computed path argument fails; computed - decorator path fails; a resolved path unequal to its record's - template fails). The inventory equals the documented set in both + access fails). **Primitive classification and path + resolution:** the primitive list classifies every registration + primitive as **path-bearing** (route registrations and mounts + that take a path or prefix argument) or **non-path-bearing** + (hook registrations such as request-lifecycle hooks, and plugin + registrations with no path/prefix option); the classification + is part of the asserted list, and an unclassified primitive is + a witness failure. Path resolution applies to path-bearing + sites; a non-path-bearing site is inventoried as a hook or + plugin site with no path witness and is never a route entry by + itself. A hook or plugin callback that routes requests by URL — + dispatching on the request path inside the callback, as the raw + auth and MCP mounts do — makes the enclosing registration a + **raw mount**: it must either appear in the §4.4 delegation + allowlist or carry §1.6 records, one per dispatched prefix, + each record's path template equal to a string constant the + checker resolves from the callback's dispatch comparison (the + prefix test against a compile-time constant); a dispatch + comparison the checker cannot resolve to a constant is a scan + failure naming the site. For path-bearing sites, the effective + path is computed by the framework's composition rules before + comparison: the class-level prefix (a string or an array — an + array expands to one route per element) concatenated with the + method-level suffix (which may be empty — the default empty + path contributes nothing), then normalized (exactly: join + segments with `/`, collapse duplicate separators, strip one + trailing slash, ensure one leading slash); the normalized + string is compared for exact equality with the route's §1.6 + record path template, normalized by the same rule. A path + argument, prefix element, or suffix the checker cannot resolve + to a compile-time string constant (`app.get(computedPath(), +handler)`, a computed decorator value) is a scan FAILURE naming + the site, never an inventory entry with an undefined path — + with one control per case (computed path argument fails; + computed decorator path fails; an array class prefix expands to + all elements; an empty method suffix composes with its class + prefix; hook-internal dispatch on a non-constant prefix fails; + a resolved path unequal to its record's template fails). The inventory equals the documented set in both directions: no undocumented live route, no documented phantom, each conditional operation carrying its §4.5 condition field, each delegated surface present exactly once. Socket.IO events are inventoried in both directions — inbound subscription handlers by - handler scan, server-emitted events by emit-call scan — with the - §4.6 registry as the comparison target and a scan failure on any - non-literal or non-registry event name. **Event-set closure:** the + handler scan, server-emitted events by emit-call scan — over + **server-side sites only**, with a mechanical boundary: a + subscribe/emit call site is server-side iff its receiver + expression's type resolves, through the type-checker, to the + server package's declarations (the `socket.io` package's + `Server`, `Namespace`, or server-side `Socket` types); a + receiver typed by the `socket.io-client` package is a client + site, excluded from BOTH direction inventories (the TUI socket + hook and the Discord/Telegram plugin clients are client sites); + a receiver whose type the checker cannot resolve to exactly one + of the two packages is a scan failure naming the site — with + controls (a client emit is not inventoried as server-emitted; a + client subscription is not inventoried as an inbound handler; + an unresolvable receiver fails). The comparison element is the + (direction, name) pair (§4.6), the registry of the matching + direction is the comparison target, and any non-literal or + non-registry event name at a server-side site is a scan + failure. **Event-set closure:** the inventoried event set (both directions) and the set of `Events:` - sub-items across all `ENDPOINTS.md` entries are equal as sets — + sub-items across all `ENDPOINTS.md` entries are equal as sets of + (direction, name) pairs — every inventoried event appears as an item in its owning family's entry, and every item corresponds to an inventoried event; an inventoried event absent from the file, and a documented event the @@ -505,8 +606,15 @@ scanned source roots, and compared files. equality of the security/authentication requirement sets — compared through the §1.6 `ApiAuthClass` → security-requirement mapping, i.e. the legacy - operation's security requirement set is compared with the set the - mapping assigns to the covering route's declared auth class — + operation's EFFECTIVE security requirement set is compared with + the set the mapping assigns to the covering route's declared + auth class, where "effective" resolves OpenAPI inheritance + before comparison: an operation object with no `security` + member inherits the legacy document's top-level `security`; an + operation-level `security` member, including the explicit empty + array, overrides the document default; step (a) materializes + the effective set onto every canonicalized operation, so the + comparison never reads the document level — and equality of the documented error semantics compared in a **common representation with two components** — the HTTP error-status set (the legacy operation's documented non-2xx @@ -534,8 +642,10 @@ scanned source roots, and compared files. `ENDPOINTS.md` into the §2.2 entry grammar via a Markdown AST and rejects, exactly: (a) an entry violating the grammar — a non-`##` entry heading, a duplicate family name, a missing - required field label, a duplicated field label, or an `Events:` - field on a non-event family; (b) an operation mention that is not + required field label under the §2.2 conditional rule (including + `Operations:` absent from a family with operations, or present + on an event-only family), a duplicated field label, or an + `Events:` field on a non-event family; (b) an operation mention that is not a Markdown link in the §2.2 anchor grammar (link text = `operationId`, target = `./OPENAPI.yaml#operation/{operationId}`); (c) any of the reserved tokens @@ -555,8 +665,11 @@ scanned source roots, and compared files. more than once in the file, whether within one entry or across entries; (i) an event item in an entry other than the one the §4.6 ownership map assigns to that event; (j) a §4.6 registry - event absent from the ownership map, or mapped to a family name - with no entry in the file. The witness + event absent from the ownership map, a map key that names no + registry event (a stale key — the map's domain must exactly + equal the registry union), a family name in the map with no + entry in the file, or an event name present in both the inbound + and outbound registries (the §4.6 disjointness rule). The witness proves it can fail with one control per reject rule. (Prose duplication a scanner cannot see is handled by review, but every structural duplication channel above is mechanical.) @@ -584,20 +697,68 @@ scanned source roots, and compared files. error entries are exactly the HTTP statuses its error-code set maps to under the contract 5 §4.2 status mapping (mechanical recomputation), with a mutated-record control; (d) - **emitted-status closure** — for every production route, every - status-emission site in the handler and its production-source - callee closure (each site identified by symbol identity against - the framework's reply/response type declarations — the status - setter, shorthand send-with-status calls, and framework error - constructors carrying a status) must resolve, as a literal or a - compile-time constant, to a status present in the route's record - response map; a status argument the checker cannot resolve, or a - resolved status absent from the record's response map, is a - witness FAILURE naming the site — with controls: a handler + **emitted-status closure** — a TOTAL, bidirectional witness per + production route. The route's **status-site set** is the union + of five site classes, each mechanically identified: (1) + auth-refusal statuses — the refusal-status set the §1.6 mapping + table assigns to the route's declared `ApiAuthClass` (the guard + is record-derived, so its refusal statuses attach to every + route of the class); (2) global-interceptor statuses — every + globally registered pipe, filter, or equivalent + bootstrap-installed interceptor is enumerated by the same + registration-primitive scan as §7.2, and each carries a + declared status set the witness asserts against its source (the + validation pipe's 400, for example), attributed to every route + it covers; (3) decorator status metadata — the framework's + route-level status metadata (explicit status-code decorators, + each method decorator's default status, and the implicit + default success status where no metadata overrides it), + resolved per route; (4) handler-closure emissions — every + status-emission site in the handler and its callee closure, + where the closure is COMPUTED, not assumed: the type-checker + call graph starting at the handler declaration, resolving each + call site's callee declaration, following every edge into + production source (the three roots) to a fixed point, not + following edges into non-production packages, and FAILING — + naming the site, never silently skipping — on any call site + whose callee the checker cannot resolve. Emission sites are + identified by symbol identity against the framework's + reply/response type declarations (the status setter, + send-with-status calls, status-carrying error constructors) AND + against Node's `http.ServerResponse` declarations (`writeHead`, + `statusCode` assignment), so raw handlers are covered; (5) + framework error defaults — the framework's own route-level + default error statuses, where the implementing PR declares them + route-attributable. Every site's status argument must resolve + to a literal or compile-time constant; an unresolvable status + is a FAILURE naming the site. Both directions: every resolved + status is a member of the route's record response map, AND + every response-map status is produced by at least one site in + the status-site set — a map status no site produces (a phantom) + is a failure naming the route and status. Controls: a handler emitting an unclassified status (`reply.status(418)`) fails; a - computed status argument fails; a framework-default emission - path (implicit 200) is attributed to the route and must appear - in its response map. + computed status argument fails; an unresolvable call edge + fails; a guard refusal status absent from the map fails; a + global-pipe status absent from the map fails; a decorator + status absent from the map fails; a raw `writeHead` status + absent from the map fails; a phantom map status fails; the + implicit default success status is attributed to the route and + must appear in its response map; (e) **guard-equivalence** — + for each `ApiAuthClass`, the witness asserts the derived + guard's acceptance structure against the §1.6 mapping table's + requirement-set shape: the `admin` guard accepts a request + satisfying EITHER named scheme and refuses one satisfying + neither (drive both alternatives and the refusal); the + `federation` guard refuses a request satisfying only one of its + two schemes and accepts one satisfying both (drive all three + cases); each single-scheme class accepts its scheme and refuses + its absence; `none` installs no guard. The refusal cases also + assert the emitted status is in the class's §1.6 refusal-status + set. Control: a mutated table copy assigning `admin` a + single-scheme requirement set is detected while the guard still + accepts the alternation. This ties the OpenAPI mapping to the + live guard semantics rather than to a lint-valid but wrong + table. ## 8. Drafting additions (PRD §12.1 disclosure) @@ -667,6 +828,22 @@ severable: ownership map, the §2.2 owning-family and at-most-once item rules, the §7.2 bidirectional event-set equality, and §7.5 reject rules (h)–(j). +17. The revision-6 closure rules: the pinned `ApiAuthClass` → + security-requirement table with its OpenAPI OR/AND + requirement-set shapes, per-class refusal-status sets, and + injectivity, plus the §7.7(e) guard-equivalence witness + (§1.6); the §7.7(d) five-class status-site algorithm with + computed call-graph closure, unresolved-edge failure, Node + raw-sink symbols, and the bidirectional no-phantom + comparison; the §7.2 primitive classification + (path-bearing/non-path-bearing), the raw-mount + hook-dispatch rule, and the path composition and + normalization algorithm; the §7.2 server/client socket + boundary by package type identity, the (direction, name) + comparison element, registry disjointness, and the + exact-domain ownership rule (§4.6, §7.5); the §7.4 + document-level security inheritance resolution; and the §2.2 + conditional `Operations:` rule for event-only families. ## Ruling request