docs: api-artifacts contract revision 6 (luna r5 residuals F1/F2/F4/F9 + NF1-NF5)
ci/woodpecker/pr/ci Pipeline was canceled

This commit is contained in:
fred
2026-08-26 21:34:11 -05:00
parent 26b3560ca2
commit 903c056b0f
+221 -44
View File
@@ -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 + NF1NF5): 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