diff --git a/docs/requirements/api-artifacts.md b/docs/requirements/api-artifacts.md index 2d641dac..1286553f 100644 --- a/docs/requirements/api-artifacts.md +++ b/docs/requirements/api-artifacts.md @@ -129,6 +129,36 @@ exactly equal the registry union (stale keys rejected), and `Operations:` becomes conditional so an event-only family is grammatical (NF5). +Revision 7 (luna r6 residuals F1/F2/F4/F9 + NF1–NF4 + NF6): the +§7.7(d) status-site class (2) becomes an interception-component +closure over EVERY binding scope — global-by-call, global-by-DI-token, +controller-scoped, and route-scoped pipes/filters/interceptors/guards +— with per-component source-asserted status sets, scope-exact route +attribution, and unenumerated-binding failure, and the §7.2 primitive +list names the global binding calls and DI tokens (F1). The raw-mount +rule records one route per (dispatched prefix × accepted method) +branch, requires segment-boundary prefix semantics, and fails on +non-constant method dispatch or a boundary-escaping prefix predicate +(F2, NF2). The §1.6 mapping table pins requirement-object exactness +(exact scheme keys, empty scope arrays), binds each scheme to a +concrete credential predicate, declares multi-credential evaluation +order, and REDEFINES each refusal-status set as the computed union of +the guard's refusal-branch statuses; §7.7(e) drives every enumerated +refusal branch and every mixed presentation and asserts the written +set equal to the computed union in both directions (F4, NF3). The +socket-event boundary gains a decorator-site rule — a subscribe +decorator resolved to the socket framework package on a +gateway-decorated class is a server-side inbound site by compile-time +package identity — and the event closure additionally requires the +registry union to equal the inventoried server-event set, rejecting +stale registry events (F9, NF4). §7.7(d)(4) call edges into +non-production packages that receive the reply/response object become +declared delegated-emission sites joined to a pinned external status +contract, and §7.7(d)(5) framework defaults each require a +pinned-version citation plus a runtime probe (NF1). §2.2 requires +every family to have a non-empty surface, rejected by a new §7.5 rule +(NF6). + 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). @@ -207,13 +237,38 @@ documentation placement rules stay with the docs atlas `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 + naming the bootstrap scheme. **Requirement-object exactness**: + each requirement object in the table contains EXACTLY its named + scheme key(s) and no other key, and every scheme key maps to + exactly the empty scope array (these artifacts define no OAuth + scopes); the generator emits these exact objects and §7.4(b) + compares against them — a requirement object with an extra key, a + missing named key, or a non-empty scope array is not the mapped + set. **Scheme binding**: the table binds each scheme name to a + concrete credential predicate — the credential's transport + location (header, cookie, or TLS peer identity) and the + acceptance predicate the record-derived guard implements for it — + so "a request satisfying the scheme" is constructed from the + binding, never improvised; a scheme with no bound predicate, or a + binding the guard source does not implement, fails §7.7(e). + **Evaluation order**: for a class whose guard evaluates multiple + credential alternatives in a fixed order (the live admin guard + checks the bearer credential before the session), the table + declares that order as part of the class's entry; the OpenAPI OR + states which credentials MAY authorize, the declared order states + what a mixed presentation does, and §7.7(e) drives the mixed + cases against the declaration. The table additionally assigns + each class a closed **refusal-status set**, DEFINED rather than + merely stated: the set EQUALS the union, over the refusal + branches of the class's record-derived guard, of each branch's + emitted status — branches and statuses enumerated mechanically by + §7.7(e) applying the §7.7(d)(4) emission-site rules to the guard + and its callee closure; the implementing PR writes each set into + the table from that enumeration, and §7.7(e) asserts the written + set equal to the enumerated union in both directions (a written + status no branch emits, and a branch status missing from the + written set, each fail). The sets are consumed by §7.7(d) class + (1). 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 @@ -248,7 +303,10 @@ documentation placement rules stay with the docs atlas 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. + otherwise. **Non-empty surface**: every entry carries at least + one of `Operations:` or `Events:` — a family with neither + documents nothing and is not grammatical (reject rule §7.5(k)), + so a stale zero-surface family cannot survive the grammar. 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 @@ -479,7 +537,13 @@ scanned source roots, and compared files. bootstrap module — covering: framework-decorated controllers and gateways; every raw registration primitive of the HTTP framework (the implementing PR enumerates the primitive list — route, hook, - middleware-mount, and plugin-registration calls — and the witness + middleware-mount, and plugin-registration calls, PLUS the + application-composition calls and DI tokens that install + cross-route interception components: the framework's global + binding calls — `useGlobalPipes`, `useGlobalFilters`, + `useGlobalInterceptors`, `useGlobalGuards` — and its `APP_PIPE`/ + `APP_FILTER`/`APP_INTERCEPTOR`/`APP_GUARD` provider tokens, both + consumed by §7.7(d)(2) — and the witness asserts the list against the framework's registration API surface) wherever it occurs, including middleware modules; SSE routes; conditional registrations evaluated per §4.5 profile and @@ -509,12 +573,26 @@ scanned source roots, and compared files. 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 + allowlist or carry §1.6 records, **one per (dispatched prefix × + accepted HTTP method) branch**. The checker resolves BOTH + dispatch dimensions: the prefix test against a compile-time + string constant, AND the callback's method dispatch — every + comparison of the request method against a constant, in the + callback and its callee closure (the §7.7(d)(4) call-graph + rules) — enumerating the accepted (prefix, method) pairs. Each + pair carries a record whose path template equals the prefix + constant and whose HTTP method equals the branch's method + constant; an accepted branch with no matching record, a record + no branch accepts, and a dispatch comparison (prefix or method) + the checker cannot resolve to a constant are each a scan + failure naming the site. **Prefix-boundary semantics**: a raw + mount's record covers exactly the URLs equal to its prefix or + extending it at a `/` segment boundary, and the code predicate + must implement exactly that match — a predicate that also + accepts a longer non-boundary URL (a bare prefix test accepting + `/mcpfoo` under the prefix `/mcp`) is a scan failure naming the + site: the code is corrected to a boundary test; the widened + match is never absorbed by documentation. 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 @@ -532,7 +610,11 @@ handler)`, a computed decorator value) is a scan FAILURE naming 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 + a resolved path unequal to its record's template fails; a + raw-mount method branch with no matching record fails; a + raw-mount record no branch accepts fails; a non-constant method + comparison fails; a non-boundary prefix predicate — accepting + `/mcpfoo` under `/mcp` — 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 @@ -547,10 +629,24 @@ handler)`, a computed decorator value) is a scan FAILURE naming 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 + of the two packages is a scan failure naming the site. + **Decorator sites** have no receiver expression, so the boundary + there is compile-time package identity instead: a method + decorated with the socket framework's subscribe decorator — + resolved by the type-checker to the framework socket package's + `SubscribeMessage` declaration — inside a class decorated with + that same package's gateway decorator (`WebSocketGateway`) is a + server-side inbound handler site; its event name is the + subscribe decorator's argument, which must resolve to a §4.6 + registry member by symbol reference exactly as call-site names + must; a subscribe decorator whose argument does not so resolve, + or one on a class without the gateway decorator, is a scan + failure naming the site. Controls: a client emit is not + inventoried as server-emitted; a client subscription is not + inventoried as an inbound handler; an unresolvable receiver + fails; a decorator-declared handler IS inventoried as an + inbound site; a subscribe decorator with a re-typed string + argument 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 @@ -562,7 +658,15 @@ handler)`, a computed decorator value) is a scan FAILURE naming entry, and every item corresponds to an inventoried event; an inventoried event absent from the file, and a documented event the inventory does not emit, each fail the witness, with one control - per direction. Additionally, every HTTP + per direction. The closure has a THIRD leg: the inventoried + server-event set also equals the §4.6 registry union, as sets of + (direction, name) pairs, in both directions — a server-side site + using a non-registry name is already a scan failure above, and a + registry event no server-side site subscribes to or emits (a + stale registry event, which §7.5(j)'s ownership-map checks alone + cannot catch because a mapped family entry can exist without the + item) fails the witness naming the event — with a + stale-registry-event control. Additionally, every HTTP route family in `OPENAPI.yaml` has an `ENDPOINTS.md` entry, and every `ENDPOINTS.md` operation reference resolves to an existing `operationId` — both directions, mechanical. @@ -669,7 +773,10 @@ handler)`, a computed decorator value) is a scan FAILURE naming 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 + and outbound registries (the §4.6 disjointness rule); (k) an + entry carrying neither `Operations:` nor `Events:` (the §2.2 + non-empty-surface rule — a zero-surface family documents + nothing). 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.) @@ -703,13 +810,32 @@ handler)`, a computed decorator value) is a scan FAILURE naming 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 of the class); (2) interception-component statuses — a + closure over EVERY binding scope the framework offers, not only + the global ones: (i) global-by-call — components installed by + the composition root's global binding calls (`useGlobalPipes`, + `useGlobalFilters`, `useGlobalInterceptors`, + `useGlobalGuards`), enumerated by the §7.2 primitive scan, + whose list names those calls; (ii) global-by-provider — + components registered under the framework's DI binding tokens + (`APP_PIPE`, `APP_FILTER`, `APP_INTERCEPTOR`, `APP_GUARD`), + identified by symbol identity against the framework's token + declarations; (iii) controller-scoped and (iv) route-scoped — + components bound by the framework's binding decorators + (`UsePipes`, `UseFilters`, `UseInterceptors`, `UseGuards`) at + class and at method level, resolved per route. Each bound + component carries a status set asserted against the + component's own source by the class-(4) emission-site rules + (the global validation pipe's 400; a route-scoped exception + filter's mapped status — the live interaction controller's + runtime-approval-denied filter emitting 403 is the migration + example), attributed to exactly the routes its binding scope + covers: global to every route, controller-scoped to the + declaring class's routes, route-scoped to that route alone. A + binding site in any of the four scopes the scan does not + enumerate, and a bound component with no source-asserted + status set, are each a witness failure naming the site; (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), @@ -719,7 +845,20 @@ handler)`, a computed decorator value) is a scan FAILURE naming 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 — + following edges into non-production packages — with ONE + carve-out: a call edge into a non-production package that + passes the framework reply/response object or the raw Node + request/response pair as an argument is a **delegated-emission + site**, because the external callee can emit statuses the + closure cannot see; such a site requires a declared external + status contract — the statuses the external package's pinned + version documents for that entry point, written into the + route's record response map and attributed to the site (the + live MCP raw handler passing the response into the external + MCP transport is the migration example) — and a + delegated-emission site with no declared external status + contract is a FAILURE naming the site, never a silent cut — + 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 @@ -728,8 +867,15 @@ handler)`, a computed decorator value) is a scan FAILURE naming 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 + default error statuses (the default unhandled-error status, the + default validation/parse-failure statuses, the default + not-found on a matched mount): each default the implementing PR + declares route-attributable carries BOTH a citation into the + pinned framework version's source or documentation for that + default AND a runtime probe that triggers the default and + asserts the observed status; a declared default missing its + citation or its probe is a witness failure — declaration alone + attributes nothing. 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 @@ -739,7 +885,12 @@ handler)`, a computed decorator value) is a scan FAILURE naming emitting an unclassified status (`reply.status(418)`) fails; a 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 + global-pipe status absent from the map fails; a route-scoped + filter status absent from the map fails; a component bound + through a DI binding token is detected and attributed; a + fixture handler passing the reply object into a stub external + package with no declared external status contract 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 @@ -752,12 +903,36 @@ handler)`, a computed decorator value) is a scan FAILURE naming `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 + its absence; `none` installs no guard. Every "satisfying" and + "not satisfying" request is constructed from the scheme's §1.6 + credential-predicate binding (location and acceptance + predicate), so the cases exercise the declared binding, not an + implementer's stand-in. **Refusal-branch closure**: the witness + additionally ENUMERATES the guard's refusal branches — every + status-emission site in the guard and its callee closure, + computed by the §7.7(d)(4) rules — and drives each branch with + a request constructed to take exactly that branch, asserting + the emitted status equals the branch's resolved status (the + live admin guard's distinct 401 and 403 paths are the + motivating example: representative alternatives are not + enough); the class's written §1.6 refusal-status set is + asserted EQUAL to the enumerated branch-status union in both + directions — a written status no branch emits, and a branch + status outside the written set, each fail. **Mixed + presentations**: for a multi-credential class the witness + drives each mixed case — an invalid credential of the + earlier-evaluated scheme presented together with a valid + credential of the later scheme, and the converse — asserting + the outcome the §1.6 declared evaluation order requires (the + live admin guard rejects an invalid bearer before considering + a valid session; that precedence must be declared and is + asserted, not discovered); a guard whose mixed-presentation + behavior diverges from the declared order fails. Controls: a + mutated table copy assigning `admin` a single-scheme + requirement set is detected while the guard still accepts the + alternation; a mutated written refusal-status set missing one + branch's status is detected. 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) @@ -805,7 +980,7 @@ severable: with its registration-primitive list, symbol-identity detection with its failure-not-skip rule, and bidirectional family/operation checks, the §7.3 content and taxonomy witness, - the §7.5 checker script with its ten reject rules, the §7.6 + the §7.5 checker script with its eleven reject rules, the §7.6 pinned validator (Redocly CLI), the §7.7 source-consistency witness, the per-field fail-closed controls in §7.1, and the §7.4 canonicalization/derivation/ @@ -844,6 +1019,22 @@ severable: 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. +18. The revision-7 closure rules: the §7.7(d)(2) + interception-component closure over all four binding scopes + with scope-exact attribution and unenumerated-binding failure, + and the §7.2 primitive-list extension naming the global binding + calls and DI binding tokens; the §7.2 raw-mount + per-(prefix × method) record rule with segment-boundary prefix + semantics; the §1.6 requirement-object exactness, scheme → + credential-predicate bindings, declared evaluation order, and + computed refusal-status-set definition, with §7.7(e)'s + refusal-branch closure and mixed-presentation cases; the §7.2 + decorator-site server boundary by socket-package identity and + the registry-union third leg of the event-set closure; the + §7.7(d)(4) delegated-emission rule with its pinned external + status contract and the §7.7(d)(5) citation-plus-probe rule + for framework defaults; and the §2.2 non-empty-surface rule + with §7.5 reject rule (k). ## Ruling request