Suluk (سُلوك, "the walk"; substrate codename
asl-ojs) — an independent v4.0 candidate forked from the OpenAPI "Moonwalk" effort. Authored end-to-end by a single contributor under the Adam substrate (burhan reasoning / daftar memory / mizan gates), having read the full SIG record (166 discussions, 22 issues, 20 PRs).
This is a CANDIDATE, not a standard.
doc/architecture/decisions/Cxxx-*.md). Each numbered section of
this spec is a projection of an ADR's ledger (plan/facts/*.bn + daftar receipts), not hand-written
prose. The document is downstream of the decisions; the decisions are the source of truth (C002).Decisions carry confidence ceilings (0.5–0.85), not certainty. The ceiling is part of the decision.
| Confidence band | What lives here | Examples |
|---|---|---|
| High (~0.85) — the structural backbone | The non-deferrable rules the rest of the design rests on. These are inherited from the SIG default or are referentially forced (a rule that holds under any of the deferred wire-shapes). | collections-are-maps for open name-spaces (C009); paths-as-uriTemplate keys (C005); per-location slots {query, path, header, cookie, body} (C004); DOM→ADA as the surface tools consume + the resolve-by-stable-NAME rule (C003/C009); aspect menu for signatures (C003). |
| Contested (~0.6–0.7) — flagged inline | Resolved, but at a deliberately lowered ceiling: thin record, single-witness, or a receipted Deviation from SIG-consensus shape. | responses-as-array-vs-map verdict (C009, lowest ceiling — the array door stays open on the reuse residual); the RFC6570 parseable-profile headline (C005 @0.62); the referencing-cluster frame (C013 @~0.70); media-type params (C016 @0.6–0.78). |
| Originated / DEFERRED (~0.5–0.6) — flagged inline | No strong SIG prior; constructed by burhan from principles, OR a grammar/policy explicitly deferred. | mechanical 3.x→4.0 upgrade (C017, semi-automated, @0.5–0.6); the fragment-reference SYNTAX (#26/#49/#72); JSON Schema dialect + Relative-JSON-Pointer value-equality vocabulary (#73); the collision-resolution POLICY (#16); query/header evaluative mapping (#108). |
⚠ Candidate @0.85 (the ceiling, not certainty): "high-confidence backbone" means high relative to the rest of this document and capped at 0.85 — most backbone claims are sole-witness (one ledger fact-file, no independent SIG ratification), which mizan caps at 0.85. A single new witness (a real SIG ratification or an independent corroboration) should trigger re-verification, not be treated as already-settled.
The hard honesty rule (C001). Throughout this document, any decision at ceiling ≤ 0.6 or marked DEFERRED carries an inline callout in this form:
⚠ Candidate @<ceiling>: <what is provisional, contested, or deferred — and to which open issue>
If a paragraph asserts a contested or deferred shape without such a callout, that is a defect in the projection, not a settled fact. The structural backbone (collections-are-maps, paths-as-uriTemplate, per-location slots, DOM→ADA) is the high-confidence spine; the contested wire-shapes and the deferred grammars are explicitly not.
An OpenAPI v4.0 (Candidate) Description is a JSON/YAML document whose open, user-named collections
are MAPS and whose closed vocabularies are fixed-key STRUCTs. Tools never consume the raw document
(the DOM) directly; they consume the ADA — the abstract description surface the DOM normalizes
into (C003). Identity is typed-component-name identity: every cross-reference resolves by a stable
NAME, never by array index and never by map-insertion order (C009/C013). The headline collections —
paths, and within a pathItem the name-keyed requests / responses maps — are therefore referentially
equivalent whether their eventual wire shape is a map or an array of named objects, which is exactly why
the by-name rule can be fixed at high confidence while the wire syntax is deferred. A pathItem is keyed
by an RFC6570 uriTemplate constrained to an injective, reverse-parseable matching profile (C005). An
operation is what a request matches; what identifies it is its signature — a uniform, ADA-exposed
composition over an aspect menu (method | uriTemplate | content-type | headers | request-body shape),
with best-effort, three-valued collision analysis (provably-disjoint | provable-collision | not-statically-determinable) that reports rather than gates (C003). How a request is
schema-validated is expressed in per-location parameterSchema SLOTS — {query, path, header, cookie, body}, a closed five-member struct (C004) — with a rare, opt-in cross-cutting construct for genuine
cross-type dependencies. All schemas are JSON Schema 2020-12. A mechanical 3.x→4.0 upgrade (C017,
Originated, low ceiling) is a semi-automated transformer with a human-review ledger, not full automation.
Description (v4.0 Candidate)
├─ info / servers / security ── document framing (inherited from 3.x)
├─ paths : MAP ── KEY = RFC6570 uriTemplate (C005, matching profile)
│ "speakers" : pathItem
│ "speakers/{id}" : pathItem
│ └─ requests : MAP ── KEY = friendly NAME (createSpeaker, getSpeakers)
│ <name> : operation
│ ├─ method, contentType
│ ├─ signature ── DOM→ADA; aspect menu; collision = report-not-gate (C003)
│ ├─ parameterSchema ── per-location SLOTS {query,path,header,cookie,body} (C004)
│ │ + opt-in crossDependencies (rare cross-type case)
│ ├─ contentSchema ── body; JSON Schema 2020-12
│ └─ responses : MAP ── KEY = friendly NAME; status is a FIELD (C009)
│ └─ pathResponses : MAP ── pathItem-scoped responses (NAME-keyed)
├─ apiResponses : MAP ── document-scoped responses (NAME-keyed)
├─ tags : MAP ── FLIPPED array→map to enable $ref-to-tag (C009, the one deviation)
└─ components : MAP ── dynamic-key store; resolve by NAME, never index (C009/C013)
Identity rule (C009/C013): every $ref resolves by stable NAME. Fragment SYNTAX is DEFERRED.
Surface rule (C003): tooling consumes the ADA (the normalized abstract surface), not the raw DOM.
⚠ Candidate @0.62 / DEFERRED: the wire shape of
responses/requests(map vs array of named objects) and the fragment-reference syntax (#schemas.Speakervs#/components/...vs a #72namespace:componentNameform) are NOT settled. The by-NAME identity rule is fixed; the spelling is deferred to #26/#49/#72/#73. The#schemas.Speakerform in the example below is illustrative only. Responses additionally carry the lowest ceiling in the model — the array door is left open on the keyless-inline-$ref reuse residual (#83).
⚠ Candidate @0.62: the uriTemplate matching profile (which RFC6570 operators are permitted in path-identity position) and its required reverse-parse grammar/algorithm are a receipted Deviation set (C005); richer operators (explode, reserved, fragment, label, lossy prefix) are FORBIDDEN in identity position, which makes slash-bearing single-value path params inexpressible — a deliberate expressiveness regression, the price of parseability.
A minimal speakers resource: create one (createSpeaker) and list them (getSpeakers). This is the
v4.0 Candidate shape — paths-as-uriTemplate maps, name-keyed requests/responses maps with status as a
field, by-NAME refs into components, and per-location parameterSchema slots.
openapi: 4.0.0
info:
title: Speakers API
version: 1.0.0
paths: # MAP — keys are RFC6570 uriTemplates (C005)
"speakers": # literal segment; profile-valid, reverse-parseable
requests: # MAP — keys are friendly operation NAMEs (C009)
createSpeaker:
method: post
contentType: application/json
contentSchema: { $ref: "#schemas.Speaker" } # by-NAME ref, never by index (C009/C013)
responses: # MAP keyed by NAME; HTTP status is a FIELD (C009)
created:
status: 201
contentSchema: { $ref: "#schemas.Speaker" }
getSpeakers:
method: get
parameterSchema: # per-location SLOTS {query,path,header,cookie,body} (C004)
query: # plain JSON Schema 2020-12 over the query slice
type: object
properties:
limit: { type: integer, minimum: 1, maximum: 100 }
# path / header / cookie / body slots omitted — absent slot ⇒ no constraint
responses:
ok:
status: 200
contentSchema:
type: array
items: { $ref: "#schemas.Speaker" }
"speakers/{id}": # single-segment var — MATCH-SAFE operator (C005)
requests:
getSpeaker:
method: get
parameterSchema:
path: # capture group of the uriTemplate populates the path slot
type: object
properties: { id: { type: string } }
required: [id]
responses:
ok: { status: 200, contentSchema: { $ref: "#schemas.Speaker" } }
pathResponses: # pathItem-scoped, NAME-keyed (C009)
notFound: { status: 404, contentType: application/http-problem }
apiResponses: # document-scoped fallbacks, NAME-keyed (C009)
serverError: { status: "5XX", contentType: application/http-problem }
components: # dynamic-key MAP; resolve by NAME (C009/C013)
schemas: # JSON Schema 2020-12 dialect
Speaker:
type: object
required: [id, name]
properties:
id: { type: string }
name: { type: string }
topic: { type: string }
⚠ Candidate @0.62 / DEFERRED — applies to the example above: the
#schemas.Speakerreference spelling is illustrative; the ratified fragment grammar is deferred to the referencing cluster (#26/#49/#72/#73, C013). The query/header evaluative mapping (how a raw URL query-string and header set become the JSON instance thequery/headerslots validate) is deferred to #108/#100 — thepathslot is closed by the C005 matcher, but the request-grammar is incomplete until #108 lands. JSON Schema dialect is 2020-12, but the Relative-JSON-Pointer value-equality vocabulary (e.g. PUT path-id == body-id) is DEFERRED to #73.
Deferred / low-ceiling markers added in this front matter (5):
x-suluk-agents)Appendix A. Tooling Profile (provisional buildable defaults)
An OpenAPI v4.0 document describes an HTTP API surface through a tree of typed object definitions. The root Document object contains metadata (openapi, info, servers, tags) and four primary collections: paths, requests, responses, and components. The object model is mixed — paths and components are dynamic-key MAPS; requests and responses are NAME-KEYED MAPS; per-location parameter schema slots (query, path, header, cookie, body) are a FIXED-KEY STRUCT. This structure achieves coherence via name-based identity (never array index, never map-insertion order) across all references.
The model operationalizes two critical layers:
| Field | Type | Required | Description |
|---|---|---|---|
openapi |
string (semver) | MUST | Version string; for v4 candidates, "4.0.0-candidate" or "4.0.0-rc.1", etc. |
info |
Info Object | MUST | Metadata: title, description, version, contact, license. |
servers |
array [Server Object] | MAY | Deployment endpoints. Default: [{url: "/"}] if absent. |
tags |
Map[tagName → Tag Object] | MAY | Named tag definitions for operation classification. Keyed by name for referenceability (C009). |
paths |
Map[uriTemplate → Path Item Object] | MUST | Keyed by RFC6570 parseable-profile uriTemplate (C005). |
apiResponses |
Map[responseName → Response Object] | MAY | API-level responses reusable across all operations (e.g. a shared serverError, §5). |
webhooks |
Map[name → Webhook Object] | MAY | Incoming operations the API receives but does not host at its own paths (§14). |
components |
Components Object | MAY | Reusable definitions keyed by name: schemas, requests, responses, securitySchemes, links, examples (§8). The dynamic-key referencing anchor (C013). |
(Reusable requests and responses live in components — §8 — not as separate root collections. pathResponses is a pathItem field, §1.3, not a root field.)
Normative language:
A Path Item describes the operations exposed at a single URI template. It is keyed in paths by its uriTemplate and contains a name-keyed requests map (each Request is an operation — §1.4), pathItem-scoped pathResponses, and an optional shared inheritance wrapper. This §1 object model is canonical; where a later section's field table differs in detail, §1 governs.
| Field | Type | Required | Description |
|---|---|---|---|
summary |
string | MAY | Short description of the path. |
description |
string | MAY | Detailed description. |
servers |
array [Server Object] | MAY | Path-specific deployment endpoints (override document root). |
requests |
Map[requestName → Request Object] | MUST | The operations at this path, keyed by stable name (C009). Each Request is an operation (§1.4). At least one required. |
pathResponses |
Map[responseName → Response Object] | MAY | Responses reusable across the requests of this pathItem only (§5). |
shared |
Shared Object | MAY | Optional per-level inheritance wrapper carrying parameterSchema down to the requests (§4.5, C012). |
Each Request carries an operation signature — its identity, computed by the ADA from the request's method and disambiguating aspects (content-type, headers, URI-template variables, request-body shape; §3, C003). The signature is an ADA concept used for request→operation matching; it is not a separate DOM collection — the DOM keys requests by stable name (C009), and the ADA derives each request's signature.
⚠ Candidate @0.5–0.6: The signature's exact composition and collision policy are contested (C003). The framing (uniform, aspect-based, detect-and-tolerate collision) is accepted; the literal signature encoding and the ambiguous-case policy (invalid vs. precedence vs. priority) remain OPEN.
Normative language:
shared wrapper (§4.5, C012 @0.55).A Request Object is an operation: a single HTTP operation's method, request schema, response schema(s), security, and behavior. It is keyed in the pathItem's requests map by a stable name (C009); its ADA identity is its signature (§3, C003) — the name is the DOM handle, the signature is the matching identity. (The 3.x requestBody/mediaType/operation nesting is flattened into this one object per the initial proposal.)
| Field | Type | Required | Description |
|---|---|---|---|
method |
string (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, TRACE) | MUST | HTTP method. |
summary |
string | MAY | Short description. |
description |
string | MAY | Detailed description. |
operationId |
string | MAY | Optional legacy handle (codegen/doc links). Coexists with the request name; not the primary identity in v4 (C009). |
tags |
array [string] | MAY | References to Tag names for classification (§11). |
deprecated |
boolean | MAY | Default false. |
contentType |
string | array[string] | MAY | The request body media type(s) (§1.5). Absent ⇒ no body. |
contentSchema |
Schema Object | MAY | JSON Schema 2020-12 for the request body (§6). |
parameterSchema |
Parameter Schema Object | MAY | Per-location typed parameter slots: query, path, header, cookie, body (C004, §4). |
responses |
Map[responseName → Response Object] | MUST | Named responses (§5); each carries its own status. At least one required. |
callbacks |
Map[callbackName → Callback Object] | MAY | Outbound callbacks (§14). |
security |
array [Security Requirement Object] | MAY | Applied security; inherits pathItem/document if absent (§9). |
servers |
array [Server Object] | MAY | Request-specific endpoints (override pathItem/document). |
Normative language:
method.5XX or default-named entry covers the unenumerated cases (§5).parameters array upgrades into parameterSchema slots (§4); see §12 for the migration rules.Request Object:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | MUST | Friendly name for the request (used as the key in requests maps). |
description |
string | MAY | Description of this request variant. |
contentType |
string or array [string] | MAY | Applicable MIME types (e.g. "application/json", "application/xml"). If array, the request matches if the Content-Type header matches any entry. |
content |
Map[mediaType → Media Type Object] | MAY | Schema and examples per media type. |
headers |
Map[headerName → Header Object] | MAY | Header definitions and constraints. |
body |
Schema Object (JSON Schema 2020-12) | MAY | Request body JSON Schema. Preferred over content for typed bodies; if both present, body is the normative schema. |
Response Object:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | MUST | Friendly name for the response (used as the key in responses maps). |
status |
string or integer | MAY | HTTP status code (e.g. "200", "404", "default"). Used if the response is defined inline; if keyed in a responses map, the key is the status. |
description |
string | MAY | Description of the response and when it is returned. |
contentType |
string or array [string] | MAY | Applicable MIME types for the response body. |
content |
Map[mediaType → Media Type Object] | MAY | Schema and examples per media type. |
headers |
Map[headerName → Header Object] | MAY | Response headers and their schemas. |
body |
Schema Object (JSON Schema 2020-12) | MAY | Response body JSON Schema. Preferred for typed bodies; if both present, body is normative. |
links |
Map[linkName → Link Object] | MAY | Links to follow-up operations (HATEOAS). |
Normative language:
requests, responses, pathResponses, or apiResponses maps MUST have a name field matching the key.content (media-type-keyed schemas) and body (single JSON Schema) are present, tooling MUST respect body as the canonic schema and derive media-type variants from it or validate both (application-defined). The spec does NOT mandate which takes precedence; document authors SHOULD use only one.content, body, or headers; a purely-metadata response (e.g., "204 No Content") MAY omit both content and body.Parameter Schema Object:
The operation's parameter validation is expressed through per-location schema SLOTS. Each slot receives a JSON Schema 2020-12 object that constrains that location's parameters.
| Slot | Type | Required | Applies to | Description |
|---|---|---|---|---|
query |
Schema Object | MAY | Query string parameters | JSON Schema over parsed query key-value pairs. Keys are the parameter names; no reserved in keywords. |
path |
Schema Object | MAY | Path template variables | JSON Schema over extracted RFC6570 uriTemplate variables. Keys match the curly-brace var names. |
header |
Schema Object | MAY | HTTP request headers | JSON Schema over request headers. Header names SHOULD be case-insensitive (per HTTP/1.1; normalization is tooling-defined). |
cookie |
Schema Object | MAY | HTTP Cookie header parameters | JSON Schema over parsed Cookie header values. Keys are cookie names. |
body |
Schema Object or Reference Object | MAY | Request body | A single JSON Schema for the entire request body (or a $ref to a schema in components). Replaces and supercedes the RequestBody.content[mediaType] schema for typed validation. |
Normative language (C004):
additionalProperties: true for object-typed locations).additionalProperties defaults to true for open-ended locations (query, header, cookie) and SHOULD be explicitly set if a closed schema is desired.{query, path, header, cookie, body} envelope when needed. This construct is evaluated at runtime and MUST NOT be compiled into the static signature matcher (C003 D1).⚠ Candidate @0.45–0.6: The exact shape and nesting of the cross-location envelope (handrews Position 3 in C004 context) is contested at lower ceiling due to unresolved prerequisites (header model #108, query-string deserialization #100, Relative-JSON-Pointer vocabulary #73). Per-location slots are stable; cross-type deps via an opt-in envelope are the direction, but detailed specification is deferred.
The document model uses maps for user-keyed collections and name-based identity throughout. This decision resolves a long-standing ambiguity: whether collections like responses should be JSON arrays (keyed by insertion order) or maps (keyed by explicit name).
Rule: All user-keyed collections resolve identity by stable NAME, never by array index or map-insertion order (C009). This is true whether the wire representation is a JSON object map or an array of named objects; the identity mechanism is name-based.
Affected collections:
Per-location parameter slots (query, path, header, cookie, body) are a FIXED-KEY STRUCT with exactly 5 members, not user-keyed.
Normative language (C009):
$ref to a named collection MUST resolve by name: #/components/responses/NotFound, #/paths/~1users, #/tags/auth (JSON Pointer escaping applies to special characters).A collection MAY include an optional order field (e.g., on each response or request entry) to indicate authoring or presentation order. This is a guidance affordance, not an identity or matching primitive.
⚠ Candidate @0.6: The
orderfield's semantics (integer index, relative ordering, presentation vs. routing) are not fully specified. It is a lowered-ceiling exploration for future tooling. Use only if your tooling explicitly requires explicit ordering; the absence oforderis the default and requires no special handling.
The Abstract Description API (ADA) is the consumption surface that tooling (code generators, routers, validators) uses to understand an API. The DOM→ADA boundary performs three critical operations:
Signature determination (C003): Given an inbound HTTP request (method, content-type, headers, URI, body), the ADA's matcher routes it to a single operation or produces a three-valued verdict (provably-disjoint, provable-collision, not-statically-determinable).
Per-location parameter extraction (C005): The RFC6570 parseable-profile uriTemplate is compiled into a deterministic matcher; captured variables populate the path slot, and form-query operators populate the query slot.
Collision analysis (C003): Static ambiguity detection surfaces a three-valued verdict but is not a validation gate. Collision policy (invalid, precedence, priority, strict-mode) is application-defined and outside this spec.
Normative language (C003):
provably-disjoint | provable-collision | not-statically-determinable.⚠ Candidate @0.5: The policy for resolving collisions (what an implementation MUST/SHOULD do when a request matches multiple operations) is OPEN. C003 specifies only the detection mechanism and the exposure of the verdict. SIG adoption of a collision policy (e.g., "runtime precedence by specificity") will upgrade this ceiling.
The following minimal Document illustrates the object model:
openapi: 4.0.0-candidate
info:
title: Pet Store API
version: 1.0.0
paths:
/pets:
requests: # pathItem.requests: a NAME-keyed map; each Request IS an operation (§1.3/§1.4)
CreatePet:
method: post
contentType: application/json
contentSchema: # §1.4 FLATTENED body — not 3.x requestBody/content/mediaType nesting
type: object
properties:
name: { type: string }
species: { type: string }
required: [name, species]
responses: # named responses live INSIDE the request; each carries its own status (§5, C009)
Created: { status: "201", contentType: application/json, contentSchema: { $ref: "#/components/schemas/Pet" } }
BadInput: { status: "400" }
/pets/{petId}:
requests:
GetPet:
method: get
parameterSchema: # per-location slots (§4); the path slot validates the captured {petId}
path:
type: object
properties:
petId: { type: integer, minimum: 1 }
required: [petId]
responses:
Found: { status: "200", contentType: application/json, contentSchema: { $ref: "#/components/schemas/Pet" } }
NotFound: { status: "404" }
components:
schemas:
Pet:
type: object
properties:
id: { type: integer }
name: { type: string }
species: { type: string }
| Principle | Basis | Ceiling |
|---|---|---|
| Collections are NAME-keyed maps; identity is by stable name | C009 | @0.85 |
| Paths are keyed by RFC6570 parseable-profile uriTemplate | C005 | @0.62 |
| Signatures are uniform, ADA-determined, collision-aware (detect-and-tolerate) | C003 | @0.85 (frame) / @0.5 (collision policy) |
| Per-location parameter schema slots (query, path, header, cookie, body) are a FIXED-KEY STRUCT | C004 | @0.6 (slots stable; cross-type deps deferred) |
| DOM→ADA layer performs matching, extraction, and collision analysis; policy is separate | C003 | @0.85 (matching) / @0.5 (policy) |
The following topics are OPEN and deferred to future ADRs or SIG ratification:
operations map DOM.A Paths Object is a map of path identifiers to pathItem Objects. Each path identifier is an RFC6570-compliant uriTemplate string (RFC 6570, §1.1), subject to a normative constrained operator profile (§2.2). The uriTemplate surface is inherited from RFC6570; the matching semantics and operator restrictions are Candidate-specific.
Paths are identity-primary: they identify operations in conjunction with HTTP methods (C003), not via a resource-oriented hierarchy decoupling. The path component is parseable in the reverse direction (URL to uriTemplate) by a deterministic algorithm (§2.4), enabling signature recognition and per-location parameter extraction. Expressiveness is bounded: certain RFC6570 operators are forbidden in path identity position to guarantee parseability and injectivity.
⚠ Candidate @0.62: The headline constraint-set ceiling reflects three lower-confidence components: (1) the operator restriction mechanism (D1-127), (2) the custom reverse-parse algorithm (D2-127, zero SIG precedent), and (3) the grammar artifact (D2-127, grafted from extended-Moonwalk-template design at 0.36). The foundational parseability and expressiveness trade-off is verified; the normalized operator taxonomy is verified; the specific implementation machinery is candidate.
The RFC6570 operator set is partitioned into three tiers. Only Tier 1 (MATCH-SAFE) operators MAY appear in path-identity position. Operators in Tiers 2 and 3 are author errors if placed in the pathItem key.
| Tier | Classification | Permitted Operators | Examples | Constraints |
|---|---|---|---|---|
| 1: MATCH-SAFE | Path identity | Literal text | /users |
No variable. |
| Single-segment variable | {id} |
Single RFC6570 variable, OAS-3.x charset (excludes unescaped /, ?, #). Matches exactly one path segment. |
||
| Leading single-var slash form | {+path} |
RFC6570 reserved operator + in a single variable at the segment boundary. Captures remaining path depth (multi-segment). See note on slash-bearing params below. |
||
| Name-bearing matrix | ;name={id} |
RFC6570 semicolon operator with variable NAME serialized into the path. Fully delimited; injective. Example: /users;id=123 unmaps to id=123. Zero risk of name-collision with adjacent variables. |
||
| 2: QUERY-ONLY | Query component only, evaluated order/repetition-insensitive | Form-query operators: ?{&x,y,z}, ?{?a,b} |
Query-string parsing. | Parsed as a key-set; operator choice (;, ,, &) affects value joining within a key but not key routing. See C004 (evaluative query mapping, deferred). |
| 3: FORBIDDEN in identity | Authoring error; MUST be rejected at schema-validation time or runtime | Explode {x*}, multi-segment explode {+x*}, reserved {+x} (in non-slash-boundary position), fragment {#x}, label {.x}, prefix {x:2}, arbitrary regex, list/composite-typed or comma-bearing scalar variable |
/products/{+cat*}, {#anchor}, {x:4} |
Use in path identity triggers a schema validation error (diagnostics: "operator forbidden in path identity") or forces the operation verdict to "not-statically-determinable" per C003. |
Single-segment variables (Tier 1) cannot capture a literal / in the captured value (OAS-3.x charset enforcement). The reserved single-var slash form {+path} is MATCH-SAFE and MUST be parsed as a multi-segment capture (the variable matches remaining path depth). Authors MUST NOT use the full explode form {+path*} (Tier 3, forbidden) in path identity.
⚠ Candidate @0.62: Slash-bearing single-value path parameters (e.g.,
/bucket/{+path}where the user intends to capture/folder/subfolder/file.txt) are INEXPRESSIBLE in the parseable profile without the reserved explode operator. This is a conscious expressiveness trade-off: forbidden operators are the price of deterministic reverse parsing. Non-slash-bearing recursion (e.g.,/api/v1/{resource}/{id}with independent segment variables) is fully supported.
A query parameter has up to two distinct, non-exclusive roles (resolves the placement question):
pet/findByStatus{?status} — participates in operation identity: it is part of the matcher input, so two requests distinguished only by query are different operations. The query component is parsed as a key-set, order- and repetition-insensitive (Tier-Q); the operator ({?…}/{&…}) affects value-joining within a key, not key routing.parameterSchema.query slot (§4, C004) is validated against that JSON Schema at runtime.The roles compose: a parameter MAY be in both (template for routing/identity, slot for value validation — see the petstore example, pet/findByStatus{?status}), in the slot only (a pure runtime filter that does not distinguish operations — the common case), or — rarely — in the template only (identity-bearing, validated by the template literal alone). Query parameters in the slot but absent from the template do not participate in signature matching.
⚠ Candidate @0.62 / DEFERRED: the evaluative mapping — how a raw query-string (always-string values, repeated keys, no native types) becomes the JSON instance the
queryslot validates — is deferred (Hudlow "a query string is not JSON", #100/#108). The identity role above is closed by the C005 matcher; the validation role's deserialization is not.
A uriTemplate is a sequence of literal segments (text) and template expressions (curly-brace var syntax). Parsing proceeds left-to-right, splitting the template on the / character boundary:
/. Each segment is either literal text, a template expression {...}, or a combination (e.g., prefix{var}suffix).{+var}) into the named variable. Captured values are percent-decoded per RFC3986 and assigned to the per-location PATH slot (C004).Leading slash — optional and normalized. A path identifier MAY begin with a leading
/(the OpenAPI 3.x convention, e.g./users); the matcher strips one leading slash before segmentation, so/usersandusersresolve to the same segment sequence and are equivalent keys. Both forms appear in this document —/petsin the general sections; bare keys (pet,v1/deliverables) where an example mirrors an implementation's literalpathskeys (e.g. the conin agent in §16, and the conformance fixtures). Neither is more correct; for readability an author SHOULD pick one form per document.
A profile-valid uriTemplate (containing only Tier 1 operators) satisfies the following property: For any two distinct incoming URLs, the parse result is either both-match with distinct variable values, or one/both do not match. Formally, no URL can match two different profile-valid uriTemplates with ambiguously overlapping variable assignments. This guarantee enables the reverse-parse algorithm (§2.4).
⚙ Tooling note: the precise, buildable reverse-parse algorithm (split-BEFORE-decode, capture-rest ranking) is in Appendix A — Tooling Profile (C019); where this narrative and Appendix A differ, Appendix A governs for implementers.
Given an incoming request URL and a set of profile-valid uriTemplates in the Paths Object, the reverse-parse algorithm MUST route the request to zero or one pathItem. This algorithm is the mechanism by which the evaluative URL-to-parameter mapping (C004 §4.2) extracts per-location PATH slot values.
Input: URL path component (string, %-decoded)
Paths Object (map of uriTemplate → pathItem)
Output: (pathItem, capture_dict) or (null, {})
Algorithm:
1. Split URL path on '/' into segments.
2. For each (uriTemplate, pathItem) in Paths:
a. Compile uriTemplate into a segment-aligned regex/matcher
(literal segments become literal-match patterns;
single-var segments become capture groups;
{+var} at boundary captures remaining depth).
b. Attempt to match URL segments against the compiled template.
c. If match succeeds, extract captures into a dict {varname: value, ...}
and return (pathItem, captures).
3. If no uriTemplate matches, return (null, {}).
Segment-Alignment Invariant. Each Tier 1 uriTemplate compiles to a matcher that respects segment boundaries: literal text never spans an unescaped /, and variable captures align to segment or remaining-path depth. This alignment ensures that the matcher produces a unique route per path.
⚠ Candidate @0.62: The reverse-parse algorithm is not part of the official RFC6570 specification; it is a Candidate-specific normative artifact. Implementations MUST implement this algorithm (or an equivalent deterministic routing strategy) to comply with this spec. The algorithm is intentionally pseudo-code (not pseudocode), to permit language-agnostic and framework-specific optimizations (regex engines, trie-based route trees, etc.).
| Field | Type | Required | Description |
|---|---|---|---|
{uriTemplate} |
pathItem Object | No | A pathItem keyed by a profile-valid uriTemplate. The key MUST be an RFC6570-compliant string subject to the operator restrictions (Tier 1 only, §2.2). Keys are not regex patterns; they are literal URI templates. Multiple pathItems with overlapping literal prefixes (e.g., /users/me and /users/{id}) are permitted; runtime collision policy is deferred to C003. |
The Paths Object is a map (key-value pairs), not an array. Each key is a uriTemplate. The order of pathItems within the Paths Object is not significant for parsing; the reverse-parse algorithm MUST treat all pathItems as candidates and return zero or one match.
Each uriTemplate is a stable identity component of an operation's signature (C003). The signature of an operation is the combination of (1) HTTP method, (2) uriTemplate, and (3) request body/header schema identity (when multi-method or multi-representation operations are modeled, see C003 for handling). The uriTemplate is never a symbolic name (unlike some resource-oriented designs); it is the literal path pattern itself.
Path collision (two uriTemplates that match the same incoming request URL, e.g., /users/me vs /users/{id} when the incoming URL is /users/me) is a runtime concern, not a parse failure. C003 §3.3 specifies the three-valued verdict and runtime tiebreak (concrete value wins over variable) for operation matching. The path component does not unilaterally determine an operation; the full signature (method, path, request identity) does.
⚠ Candidate @0.74: Resource-oriented modeling (decoupling operation identity from path/method to a separate "resource" hierarchy) is explicitly deferred as a tooling/documentation overlay (C015 §2.3). This spec keeps paths+method identity-primary. Document generators and API frameworks MAY overlay a resource-oriented view on the canonical path+method signatures, but the Candidate spec itself does not ship a decoupled resource construct.
The reverse-parse algorithm populates the PATH per-location schema slot (C004 §4.1). Each variable captured from the uriTemplate is assigned to the corresponding parameter name in the PATH slot of the request's parameterSchema Object. No other per-location slots (query, header, cookie, body) are populated by path parsing.
Example:
paths:
/users/{id}:
requests:
getUser:
method: get
parameterSchema:
path:
type: object
properties:
id:
type: string
responses:
ok:
status: 200
description: Retrieved user
When an incoming request matches /users/12345, the reverse-parse algorithm extracts id=12345 and assigns it to the PATH slot: {id: "12345"}. The parameter schema then validates the captured value against the declared schema (type: string, constraints, etc.).
paths:
/users:
requests:
listUsers:
method: get
summary: List users
operationId: listUsers
responses:
ok:
status: 200
description: OK
/users/{id}:
requests:
getUser:
method: get
summary: Get user by ID
operationId: getUser
parameterSchema:
path:
type: object
properties:
id:
type: string
pattern: '^[0-9]+$'
responses:
ok:
status: 200
/files/{+path}:
requests:
getFile:
method: get
summary: Get file
operationId: getFile
parameterSchema:
path:
type: object
properties:
path:
type: string
responses:
ok:
status: 200
# Note: {+path} captures remaining path depth,
# e.g., /files/docs/report.pdf → path="docs/report.pdf"
/archive;version={ver}:
requests:
getArchive:
method: get
summary: Get archive with version parameter
operationId: getArchive
parameterSchema:
path:
type: object
properties:
ver:
type: string
responses:
ok:
status: 200
# Note: matrix syntax is fully delimited;
# /archive;version=1.0 unmaps to ver="1.0"
In this example:
/users is a literal path with no variables./users/{id} uses a single-segment variable (Tier 1)./files/{+path} uses the leading-slash-form reserved operator (Tier 1), capturing multi-segment depth./archive;version={ver} uses name-bearing matrix syntax (Tier 1).All four uriTemplates are profile-valid and deterministically parseable.
/search?query={q}) or declared separately in a signature.query slot. Query PLACEMENT is surfaced but not finalized in this wave. The full evaluative query-string-to-data-model mapping (Hudlow #100 "query-string-is-not-JSON") is deferred to #108 and beyond./users/me vs /users/{id}), which operation is selected? The collision POLICY (invalid, precedence, priority, strict-mode) is deferred; #16 specifies the best-effort three-valued verdict and runtime tiebreak (concrete > variable).A signature is the composite identity of an operation for the purpose of request matching: mapping an incoming HTTP request deterministically to a single operation in the API description, or identifying a collision (ambiguity). In OpenAPI 3.x, signatures are implicitly path + method. The Candidate v4.0 specification extends this to a multi-aspect tuple that may compose method, URI template (including query parameters), content-type, header values, and request body shape.
Signatures are non-mandatory for tooling. They are exposed at the DOM→ADA (Document Object Model → Abstract Description Abstraction) layer, where both the semantic authoring (DOM) and the artifact's runtime consumption surface (ADA) are optional. The ADA's role is to provide a uniform contract for request-to-operation matching and to surface static collision analysis where feasible.
Matching (correlating an HTTP request to an operation) and correlating (schema-to-schema mapping of request and response bodies across operations) are separated concerns. This section addresses matching only.
A signature MAY compose any subset of the following aspects:
| Aspect | Presence | Notes |
|---|---|---|
| method | Optional | HTTP verb (GET, POST, etc.). Subordinatable; method-only signatures are valid. |
| URI template (RFC 6570 or extended form) | Optional | Includes path and query parameters as a single template per operation. Query and path aspects are not separated in the signature. |
| content-type | Optional | Request Content-Type header value (e.g., application/json); used to discriminate among request body schemas. |
| headers | Optional | Named header values or header presence; used when operations differ by authorization scheme, custom headers, or negotiation directives. ⚠ Candidate @0.6: header aspects depend on RFC 8941 / RFC 9110 header-modeling work (issues #22, #108) not yet concluded. This aspect is admitted but prerequisite-gated. |
| request body shape | Optional | JSON Schema or other schema type expressing request payload structure; used as a last-resort matcher when other aspects do not disambiguate. ⚠ Candidate @0.55: JSON Schema discrimination (e.g., oneOf composition) is demoted to a runtime-only tier and flagged schema-dependent / not-statically-collision-checked by the ADA. The static contract does not guarantee JSON-Schema overlap analysis; see §3.4.2. |
At minimum, a signature composes method and URI template. All other aspects are optional and are selected only when necessary to disambiguate operations or to model legitimate business logic (RPC-style operations, content negotiation, etc.).
⚠ Candidate @0.5: No required per-API declared "signature style" indicator (e.g., a top-level enumeration signaling whether signatures are "path-based" vs "content-negotiated" vs "header-aware"). The signature mechanism is uniform and implicit, driven by the ADA's exposure contract. An optional authoring affordance (such as PR #183's proposed
signaturesarray) MAY be supplied by the DOM and is normalized away by the ADA; it is not authoritative or mandatory.
The ADA MUST normalize each operation's signature by:
{id} in a path) vs. concrete (literal values, e.g., GET, application/json).To match an HTTP request to an operation:
OpenAPI 3.x establishes a precedence rule: concrete (literal) path segments take precedence over variable (parameterized) segments (e.g., GET /users/me matches before GET /users/{id}). The Candidate v4.0 specification carries this precedence forward as a runtime-resolution behavior only, not as a static-detection primitive or a collision-resolution policy.
{id} matching an integer constraint, or a path segment matching both /items/{itemId} and /items/{itemType}) remain undefined in 3.x and are not resolved in v4.0.The ADA MUST expose, for each operation, a collision verdict relative to all other operations. The verdict is three-valued:
| Verdict | Meaning | Example |
|---|---|---|
| provably-disjoint | No possible HTTP request can match two or more operations. Signatures are statically distinct. | GET /users and POST /users (different methods); GET /items/{id} and GET /items/count (different path structure). |
| provable-collision | At least one HTTP request exists that would match two or more operations, and this overlap is statically verifiable. | GET /users and GET /users (identical signatures); GET /items with Content-Type: application/json and GET /items with Content-Type: application/xml when the only difference is schema (both GET /items, but schema discrimination fails—see below). |
| not-statically-determinable | The ADA cannot statically prove disjunction or collision. Overlap may exist at runtime, depending on request payload, header values, or other data-dependent factors. | GET /items with Content-Type: application/json constrained by {"type": "object"} vs. GET /items with Content-Type: application/json constrained by a different schema (JSON-Schema overlap is not generally computable); POST /users with a request body matching oneOf across schemas. |
The collision analysis is a best-effort desideratum, not a mandatory validation gate. Specifically:
JSON-Schema discrimination is demoted to runtime-only. The specification does NOT guarantee static overlap analysis for JSON-Schema constraints. If an operation's signature includes body-shape aspects (parameterSchema constraints), the ADA MAY expose a collision verdict, but MUST flag it schema-dependent / not-statically-collision-checked. Tools MUST NOT assume that distinct schema constraints guarantee disjoint signatures at compile time.
Header-based collisions are prerequisite-gated. Until RFC 8941 / RFC 9110 header-modeling semantics are fully integrated (issue #108), header-based signature aspects are admitted but may be flagged with a capability warning.
Templating system is open. The analysis is bounded by the chosen URI template language (RFC 6570, extended-URI-Template, WHATWG URLPattern). Variable-vs-concrete pattern matching is defined only within the chosen language; inter-language comparison is out of scope.
The ADA's collision verdict is a report, not a policy decision. Specifically:
Surfacing a provable-collision verdict does NOT mandate that the API is invalid. Multiple operations MAY have colliding signatures if the router/runtime implements a collision-resolution policy (see §3.4.4).
Surfacing a not-statically-determinable verdict does NOT mandate additional analysis. Tools MAY choose to apply further heuristics, log a warning, or defer to runtime behavior. The specification does not prescribe a policy for indeterminate cases.
The mechanism is "detect-and-tolerate": the ADA exposes the verdict transparently so that tools and authors can make informed decisions about their API design. The choice of what to do (reject collisions, apply priority rules, use strict-mode, etc.) is left to the API author and the tooling layer, as resolved in frontier issue #187.
⚠ Candidate @0.6: Collision-resolution policy (invalid vs. specificity-order vs. priority-cascade vs. strict-mode) is OPEN and carried to the next frontier stage. The SIG's lean (frontier #186) is "priority-as-last-resort," recorded but not ratified. This section exposes the collision verdict; the policy choice is deferred.
For guidance, the following strategies have been discussed in the SIG:
not-statically-determinable) as a specification error. Require all operations to have provably-disjoint signatures.None of these strategies is standardized in the Candidate v4.0. Implementers SHOULD document their chosen policy.
The operationId field (inherited from OpenAPI 3.x) coexists with the signature mechanism. Specifically:
⚠ Candidate @0.8: The fate of operationId—specifically, whether signatures replace operationId as the sole operation identifier, or whether the two coexist—is unresolved. This specification assumes coexistence.
The following design decisions are explicitly deferred to subsequent frontier resolutions:
parameterSchema or per-location schemas affects how body-shape aspects of signatures are expressed. This is a critical dependency for D1 (JSON-Schema discrimination); if #20 ratifies JSON-Schema-in-the-matcher, D1's ceiling must be revisited.Below is a YAML illustration of a multi-aspect signature scenario, showing how the ADA would expose a collision verdict:
paths:
/items:
requests:
listItems:
method: get
summary: List all items
operationId: listItems
signature:
method: GET
uriTemplate: /items
aspects:
- method
- uriTemplate
collisionVerdict:
status: provably-disjoint
reason: Only GET /items without further path segments
responses:
ok:
status: 200
createItem:
method: post
summary: Create item
operationId: createItem
contentType: application/json
contentSchema:
type: object
properties:
name: { type: string }
signature:
method: POST
uriTemplate: /items
requestContentType: application/json
aspects:
- method
- uriTemplate
- requestContentType
collisionVerdict:
status: provably-disjoint
reason: POST vs. GET on same path
responses:
created:
status: 201
/items/{itemId}:
requests:
getItem:
method: get
summary: Get item by ID
operationId: getItem
parameterSchema:
path:
type: object
properties:
itemId:
type: string
signature:
method: GET
uriTemplate: /items/{itemId}
aspects:
- method
- uriTemplate
collisionVerdict:
status: provably-disjoint
reason: Concrete path /items/{itemId} vs. /items (concrete-over-variable precedence)
responses:
ok:
status: 200
/items/recent:
requests:
getRecentItems:
method: get
summary: Get recent items
operationId: getRecentItems
signature:
method: GET
uriTemplate: /items/recent
aspects:
- method
- uriTemplate
collisionVerdict:
status: provably-disjoint
reason: Concrete /items/recent matches before variable /items/{itemId}
responses:
ok:
status: 200
/notifications:
requests:
notifyJson:
method: post
summary: Send notification (content-type differentiation)
operationId: notifyJson
contentType: application/json
contentSchema:
type: object
properties:
text: { type: string }
signature:
method: POST
uriTemplate: /notifications
requestContentType: application/json
aspects:
- method
- uriTemplate
- requestContentType
responses:
ok:
status: 200
notifyXml:
method: post
summary: Send notification (XML)
operationId: notifyXml
contentType: application/xml
contentSchema:
type: object
signature:
method: POST
uriTemplate: /notifications
requestContentType: application/xml
aspects:
- method
- uriTemplate
- requestContentType
collisionVerdict:
status: provable-collision
collisionResolution: content-type-discrimination
warning: requires router support for content-type routing
responses:
ok:
status: 200
collisionVerdict:
status: provable-collision
collisionResolution: content-type-discrimination
warning: requires router support for content-type routing
Requests in Suluk v4.0 define the structure and constraints of HTTP method invocations on operations. A request MUST specify the HTTP method, content type(s) it accepts, and — at runtime — the schema constraining its parameters and body. Requests are keyed by name in an ordered map at the operation level, enabling optional shorthand when a single request is present.
A request is a named object carrying method context, content negotiation, and parameter/body schema slots. Each request is keyed by a stable NAME (string) in the request collection and resolved to a concrete invocation signature at runtime via per-location parameter schemas and the optional cross-cutting dependency construct.
Properties:
| Property | Type | Required | Description |
|---|---|---|---|
method |
string | YES | HTTP method (uppercase): GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, TRACE. Determines which request(s) are active on the pathItem. |
summary |
string | NO | Short human-readable label for this request variant. Useful when multiple requests exist on a single pathItem. |
description |
string | NO | Longer markdown description of intent, preconditions, or side effects. |
contentType |
string | [string] | NO | IANA media type(s) acceptable in the request body. Single string or array. If absent, the request carries no body (or body is application-determined). |
parameterSchema |
Schema object | NO | Per-location schema slots constraining query, path, header, and cookie parameters. Keyed object with optional query, path, header, cookie sub-schemas, each a JSON Schema 2020-12 object. See §4.3. |
contentSchema |
Schema object | NO | JSON Schema 2020-12 governing the request body. Present only if a body is expected. Applied after Content-Type negotiation. |
crossCuttingDependencies |
Cross-Cutting Construct | NO | Optional envelope for rare value-equality or presence dependencies spanning multiple parameter locations or body. Opt-in; see §4.4. |
Requests are collected in a name-keyed map at the operation level. Each request NAME is a stable identifier (e.g., "application/json", "create-json", "streaming") that tools use to disambiguate and reference the request.
Shorthand (@0.6):
⚠ Candidate @0.6: Shorthand
"post /path"key syntax is permitted as optional sugar at the DOM level; tools MUST desugar it to a standard named request before entering the ADA (abstract description surface). This enables authoring ergonomics in flat keys while preserving the stable, named-map canonical form in interchange. Desugaring rules are deferred to the tooling section.
When a pathItem has exactly one request, tools MAY offer shorthand notation (e.g., a bare "post /users" key instead of a nested requests map), which desugars to { requests: { "post": { method: "POST", ... } } } at the ADA boundary. Authoring tooling MUST NOT rely on shorthand in normative examples or as part of the core DOM->ADA correlation contract.
Parameter schemas are expressed as separate JSON Schema objects, one per location, avoiding reserved-name collision in query/path/header/cookie parameter namespaces.
Structure:
The parameterSchema object (if present) contains these optional keys:
parameterSchema:
query: # JSON Schema 2020-12; constrains query parameters
type: object
properties:
limit: { type: integer, minimum: 1 }
offset: { type: integer, minimum: 0 }
additionalProperties: false
path: # JSON Schema 2020-12; constrains path parameters
type: object
properties:
userId: { type: string, pattern: '^[a-zA-Z0-9]{8}$' }
required: [ userId ]
header: # JSON Schema 2020-12; constrains HTTP headers
type: object
properties:
X-Request-ID: { type: string, format: uuid }
Accept-Language: { type: string }
additionalProperties: true # Headers often include x-* extensions
cookie: # JSON Schema 2020-12; constrains HTTP cookies
type: object
properties:
session: { type: string }
Defaults:
parameterSchema, the runtime behavior is implementation-defined (tools may enforce strict-no-params or permissive-allow-all); tools SHOULD document their choice.additionalProperties: true per C004 #224 (headers and cookies often carry implementation-specific extensions; filtering is conservative).additionalProperties: false is typical for explicit parameter control).⚠ Candidate @0.55: The exact per-location DEFAULT for
additionalPropertiesbehavior when a schema-slot is absent, and the priority of explicit vs. implicit schema-application at runtime, are deferred to implementation profiles and the #127 concrete-templating-system ADR. C004 establishes the DIRECTION (separate slots, no mandatory wrapper); the precise validation-algebra is refinement-deferred.
Relationship to path parameters:
Path parameters are ALWAYS known statically (RFC 6570 uriTemplate). The path slot in parameterSchema MUST constrain only parameters expressed in the operation's uriTemplate; tools SHOULD validate that all path parameters mentioned in the schema are declared in the template, and vice versa.
When needed (rare): if a request expresses a constraint that spans multiple locations — e.g., "the id in the path MUST equal the id in the body" — an optional crossCuttingDependencies object MAY be used. This is NOT the authoring root; it is only invoked when a single-location schema is insufficient.
Structure:
crossCuttingDependencies:
# Presence dependencies (supported in standard JSON Schema 2020-12)
if:
required: [ "X-Custom-Header" ]
then:
required: [ "bodyId" ]
# Value-equality dependencies (DEFERRED to #73)
# Example (not currently valid; requires Relative JSON Pointer):
# - path.id MUST equal body.id
# - Requires custom vocabulary pending #73 resolution
Scope & constraints:
{ parameters: {...}, headers: {...}, body: {...} } from the parsed request instance and evaluates the cross-cutting schema over it.⚠ Candidate @0.55 (deferral): Cross-type value-equality constraints cannot be expressed normatively without the Relative-JSON-Pointer vocabulary (ADR #73). The construct is PRESENT to avoid inventing syntax at authoring time; at validation time, no working value-equality expression exists in the corpus. This deferral is identical to C004's treatment (§2.3) and is a known limitation of the current schema ecosystem, not a Suluk design flaw.
Why optional and opt-in:
C012 #116 (@0.55):
⚠ Candidate @0.55: Inheritance of parameters via a per-level optional
sharedmap is permitted and MUST honor override-vs-accumulate merge semantics (e.g.,queryparameters in a request override path-levelquerydefinitions;headerparameters accumulate additively unless shadowed by request-level name). The concrete per-property merge table is deferred; the direction (single optionalsharedcontainer, resolved before the C003 matcher) is settled at ceiling 0.55. Response-level reach is explicitly deferred.
When a pathItem defines shared parameters across multiple requests, tools MAY support optional per-level shared maps (path level, request level, etc.):
paths:
/users/{userId}:
shared:
parameterSchema:
query:
type: object
properties:
limit: { type: integer, default: 10 }
requests:
get:
# Inherits path-level parameterSchema.query.limit
method: GET
responses:
ok: { status: 200 }
post:
# Inherits path-level parameterSchema.query.limit
# Can override or add parameters
method: POST
responses:
ok: { status: 200 }
Merge semantics:
parameterSchema MUST be merged (allOf) with any inherited path-level parameterSchema before runtime validation.contentType field (optional):
If a request body is expected, contentType specifies the IANA media type(s) the server accepts:
contentType: "application/json"
# or
contentType:
- "application/json"
- "application/x-msgpack"
Relationship to contentSchema:
The contentSchema (JSON Schema) is applied after Content-Type negotiation. Tools MAY bind a schema to a specific content type via headers or media-type parameters (deferred to #127 templating). For now, contentSchema is location-agnostic and applies to any accepted content type.
$schema, $id required in parameter slots, per C004's reserved-name elimination).paths:
/api/orders/{orderId}:
shared:
parameterSchema:
path:
orderId:
type: string
pattern: '^ORD-[0-9]{6}$'
header:
X-Request-ID:
type: string
format: uuid
requests:
get:
method: GET
summary: Retrieve order by ID
description: Fetch full order details including line items and shipping.
parameterSchema:
query:
includeItems:
type: boolean
default: true
includeShipping:
type: boolean
default: true
# Inherits path.orderId and header.X-Request-ID from shared
post:
method: POST
summary: Update order (JSON payload)
description: Modify order details, partial updates allowed.
contentType: application/json
contentSchema:
type: object
properties:
status:
type: string
enum: [ pending, shipped, delivered, cancelled ]
shippingAddress:
type: object
properties:
street: { type: string }
city: { type: string }
additionalProperties: false
parameterSchema:
query:
dryRun:
type: boolean
default: false
sendNotification:
type: boolean
default: true
# Inherits path.orderId and header.X-Request-ID from shared
put:
method: PUT
summary: Replace order entirely (with cross-location constraint)
description: Full replacement; existing order is destroyed and reconstructed.
contentType: application/json
contentSchema:
type: object
properties:
id:
type: string
items: { type: array }
required: [ id, items ]
parameterSchema:
path:
orderId:
type: string
pattern: '^ORD-[0-9]{6}$'
# (overrides shared if explicit; typically inherited)
crossCuttingDependencies:
# Rare case: path ID must match body ID
description: >
The orderId in the path must match the id in the request body.
Currently validates presence; value-equality requires #73.
if:
type: object
properties:
body: { type: object, required: [ id ] }
then:
# Deferred: relative JSON pointer reference to equate path.orderId === body.id
description: "Value-equality constraint pending #73 relative JSON pointer"
"post /path" key syntax and merge algebra.shared merge algorithm applies at pathItem and request; response-level reach is deferred.A response describes an HTTP response message that may result from a request. Responses model the status code, content type, and schema of the response body. Responses are NAME-KEYED MAPS at three scopes: per-request (request-level responses), per-pathItem (pathResponses), and global (apiResponses). The mechanism composes additively across scopes by inheritance, with explicit runtime precedence rules for status/content-type specificity.
Responses may be defined at three levels:
request object, binding specific to that method+contentType pairing.pathItem, applying to all requests on that path UNLESS overridden by request-level definitions.Resolution rule (most-specific-wins): At runtime, a response search follows: request-level → pathResponses → apiResponses. The FIRST response matching the HTTP status code and content-type of the actual response is selected. If multiple responses have the same status at the same scope-depth, they MUST differ by content-type to be unambiguous; if they do not, the tooling-resolution behavior is UNDEFINED (see ⚠ below).
⚠ Candidate @0.62: The runtime status/content-type tiebreak and the scope-depth precedence rank are partially deferred to C003 (the correlation/collision policy). Current prose describes an additive-union + most-specific-match model, but the SIG's live #44 thread (darrelmiller) proposes status-code-scoped precedence as an alternative. The dominant load-bearing piece (additive composition for distinct responses across scopes) is high-confidence; the tiebreak ordering is low-confidence. Source: C012 #17b @0.62.
Each response is a name-keyed entry in the map. The name is the map key and serves as a stable identifier for reference and tooling presentation. The response object itself carries these fields:
| Field | Type | Required | Description |
|---|---|---|---|
status |
string | MUST | HTTP status code or status pattern (e.g., 200, 404, 5XX). Literal numeric codes and wildcard patterns per HTTP semantics are both valid. |
contentType |
string | MAY | Media type of the response body (e.g., application/json, text/plain). Absence implies no response body. |
contentSchema |
JSON Schema | MAY | JSON Schema (2020-12 dialect) describing the structure of the response body, keyed to the contentType. Only valid if contentType is present. |
description |
string | MAY | Human-readable description of this response (e.g., "Resource created successfully"). |
headers |
object | MAY | Map of named header definitions. Deferred pending #108 (header model); indicative shape only. |
order |
integer | MAY | Optional presentation order hint for tooling display. Absence implies insertion order is not significant (C009 hybrid, lowered ceiling). |
Example:
request:
method: POST
contentType: application/json
contentSchema: { $ref: "#/schemas/Speaker" }
responses:
created:
status: 201
contentType: application/json
contentSchema: { $ref: "#/schemas/SpeakerWithId" }
badRequest:
status: 400
contentType: application/problem+json
contentSchema: { $ref: "#/schemas/ProblemDetail" }
pathResponses:
notFound:
status: 404
contentType: application/problem+json
contentSchema: { $ref: "#/schemas/ProblemDetail" }
apiResponses:
serverError:
status: 5XX
contentType: application/problem+json
contentSchema: { $ref: "#/schemas/ProblemDetail" }
⚠ Candidate @0.62: Response-level
parameterSchemainheritance is deferred to the per-location parameterSchema work (C012 #17b, the second half). The field table and composition rules below are INDICATIVE only; the final normative resolution waits on C004 cross-type dependency work (#73). Source: C012 #17b @0.62.
When responses are composed across scope-depth, the parameterSchema (if present at the pathItem or request level per C004) is inherited and composed using JSON Schema allOf:
parameterSchema is allOf'd with any pathItem-level parameterSchema.Example (pathItem-level + request-level composition):
pathItem:
uriTemplate: "/speakers/{speakerId}"
parameterSchema:
type: object
properties:
speakerId: { type: string }
required: [speakerId]
requests:
getSpeaker:
method: GET
parameterSchema:
type: object
properties:
include: { enum: [metadata, history] }
# composed with pathItem parameterSchema via allOf
responses:
ok: { status: 200, contentSchema: { $ref: "#/schemas/Speaker" } }
In this example, the effective request schema is:
{
"allOf": [
{ "type": "object", "properties": { "speakerId": { "type": "string" } }, "required": ["speakerId"] },
{ "type": "object", "properties": { "include": { "enum": ["metadata", "history"] } } }
]
}
Responses are referenced by their stable NAME, never by array index or map-insertion order. A reference to a response in apiResponses might be expressed as:
#/apiResponses/serverError
Or, if a shorthand reference syntax is adopted (C012 #60/61, deferred), a tool might resolve:
#apiResponses.serverError
The exact reference syntax (JSON Pointer vs. shorthand dot-notation) is DEFERRED to the identification/referencing redesign (#26/#49/#72/#73 per C009). The by-name resolution rule is DECIDED now; the wire syntax is deferred.
⚠ Candidate @0.6: Shorthand reference syntax is proposed as sugar (deferred at #60/61); all normative tooling MUST resolve references by name at the DOM→ADA boundary. Source: C012 #60/61 @0.6.
parameterSchema composes at the response level (not just request level) awaits the full #17b/#108 resolution. Current text is indicative only.headers field definition is pending a full header modeling ADR.OpenAPI v4.0 uses JSON Schema 2020-12 as its sole normative schema language for describing structured data. This choice is fixed at the document level and applies to all Schema Objects embedded within the specification.
6.1.1 Dialect Declaration
The JSON Schema dialect MUST be declared explicitly at the document level via the OAS schema keyword (in the root object's metadata). A specification document MAY override the dialect on a per-schema-component basis using the JSON Schema $schema keyword inside an individual Schema Object (e.g., within a component definition), provided the override is static and locally decidable at the DOM-to-ADA boundary.
$schema field (supported for forward-compatibility; discouraged for simplicity)The specific version pin and formalized serialization syntax are DEFERRED (see C013 Consequences).
⚠ Candidate @0.70: The exact byte-grammar for the dialect declaration, JSON-Pointer tolerance semantics, and string-vs-object polymorphism for inline restrictions are deferred to the fragment grammar filing (#73).
6.1.2 Dialect Scope
The declared dialect applies uniformly across:
components/schemas collectionparameterSchema slots (query, path, header, cookie, body)content definitionsNo parallel dialect selector (e.g., an x-schema-dialect property) is permitted.
Schema components (reusable definitions) are housed in a map keyed by stable typed names. The map key IS the canonical single-valued identity of that component and implicitly produces a location-independent anchor.
6.2.1 Typed-Component-Name Identity
Each schema component in the components/schemas map is identified by its key. This key:
components/schemas collection$ref and other referencing mechanismsExample:
components:
schemas:
User:
type: object
properties:
id:
type: string
name:
type: string
ErrorResponse:
type: object
properties:
code:
type: integer
message:
type: string
The User and ErrorResponse keys ARE the component identities; they are never disambiguated by insertion order or array position.
6.2.2 Reconciliation of Anchors
An authored $anchor inside a Schema Object (per JSON Schema 2020-12) names an intra-schema fragment target and MUST NOT contradict the component key. If a schema author includes an anchor, it applies only within that schema's boundary; the component key remains the public identity.
⚠ Candidate @0.70: The byte-grammar for anchor syntax and inline-restriction coexistence with the component key are deferred (#73).
OpenAPI v4.0 supports explicit document import via an Imports Object, enabling multi-file specifications while maintaining local, DOM-to-ADA resolution.
6.3.1 Imports Object Structure
The root-level imports field contains a map of namespace identifiers to import directives:
imports:
common: "https://example.org/schemas/common.json"
auth: "./auth/schemas.yaml"
self: "#"
Each import entry specifies:
#)6.3.2 Import Resolution and Binding
The IMPORTS OBJECT MUST be resolved at the DOM-to-ADA boundary via a single resolution function. The function:
#), external retrieval URLs, and external-base-IRI relative paths (all three MUST be supported)Single-file OAS documents (no external imports) need NOT declare an imports section.
⚠ Candidate @0.70: The formalization of the three-method mandate (IRI-match, retrieval-URL, external-base) is marked as soft-strength pending cross-voice consensus verification. The byte-grammar and serialization form are deferred (#73).
JSON Schema $ref is retained as the in-schema reference mechanism, fenced inside Schema Objects under the declared JSON Schema 2020-12 dialect. A complementary OAS-level reference mechanism (e.g., for referencing components by name without JSON-Pointer syntax) is NOT introduced in this version.
6.4.1 Parsed Distinguishability
The two reference kinds (JSON-Schema $ref and future OAS-level reference, if added) MUST be parse-time-distinguishable by token-plus-slot, not by tree position alone. When a Schema Object includes $ref, the reference is schema-level and uses standard JSON Schema semantics.
Example:
components:
schemas:
OrderItem:
$ref: "#/components/schemas/Product"
Order:
type: object
properties:
items:
type: array
items:
$ref: "#/components/schemas/OrderItem"
Parameter interdependencies are resolved via a three-way split:
6.5.1 Same-Location Dependencies (JSON Schema 2020-12)
All dependencies within a single parameter location are expressed using standard JSON Schema keywords:
dependentSchemas — conditionally apply schema constraints based on property presencedependentRequired — conditionally require fields based on other fieldsif/then/else — condition-based schema variantsallOf, oneOf, anyOf — composition and constraintsExample (query parameters with interdependencies):
requests:
searchOp:
method: GET
parameterSchema:
query:
type: object
properties:
q:
type: string
filter:
type: string
sortBy:
type: string
dependentRequired:
filter: ["q"] # filter requires q to be present
if:
properties:
sortBy: { const: "date" }
then:
required: ["q"]
responses:
ok: { status: 200 }
6.5.2 Cross-Location Value-Equality (Relational Extension)
Dependencies that span locations (e.g., ensuring a path parameter matches a body field) are expressed via a Moonwalk relational EXTENSION vocabulary under the C013 declared dialect frame.
⚠ Candidate @0.58: The exact relational keyword grammar, the parameter-to-instance-data-model deserialization mapping, and query-value reconciliation are deferred to the fragment-grammar filing and the #127 query data-model proposal. Runtime implementations MUST NOT assume deserialization semantics until these details are finalized.
Example (illustrative; syntax not yet finalized):
# Hypothetical relational value-equality declaration (deferred grammar)
paths:
/orders/{orderId}:
requests:
updateOrder:
method: put
parameterSchema: # v4 per-location slots (§4) — not a 3.x parameters[] array
path:
type: object
properties:
orderId: { type: string }
required: [orderId]
contentType: application/json
contentSchema: # v4 flattened body — not 3.x requestBody/content/mediaType
type: object
properties:
id: { type: string }
# Deferred: exact syntax for declaring path.orderId == body.id
x-relational-constraints: # PLACEHOLDER — grammar TBD
- equals: ["path/orderId", "body/id"]
The discriminator serves a limited, non-normative role: it is retained as an optional advisory hint for code generation but removed as a load-bearing validation keyword.
6.6.1 Discriminator as a Codegen Hint
A Schema Object MAY include a deprecated discriminator field (kept for migration only) naming the dispatch property. This field is purely advisory and carries NO normative force:
6.6.2 Variant Selection at Runtime
One-of-N schema selection (previously governed by discriminator) is achieved using standard JSON Schema 2020-12 keywords at RUNTIME only:
oneOf + const constraints on the dispatch property (per-branch):Animal:
oneOf:
- type: object
properties:
type: { const: "dog" }
breed: { type: string }
- type: object
properties:
type: { const: "cat" }
claws: { type: boolean }
propertyDependencies, if/then/else) are noted as candidate mechanisms and deferred to #73 for canonical standardization.6.6.3 Discriminator Mapping Removal
The discriminator.mapping object (a manual enumeration of discriminator values to schema URIs) is removed entirely. Prefer the C009 by-name reference model: reference variants by their stable component name, not by a manual string-to-URI mapping.
⚠ Candidate @0.6: The canonical native keyword for variant dispatch (replacing const-per-branch) is deferred to #73. The advisory
discriminatorfield itself is retained at @0.55 as a migration aid, making this a reversible, low-confidence soft signal; the core variant-selection verdict is stable at @0.62.
Default values are authored inside the schema home — within the JSON Schema 2020-12 default keyword inside each per-location parameterSchema slot — NOT as a separate OAS-level parameter control field.
6.7.1 Schema-Home Defaults
For each parameter (query, path, header, cookie, body), a default value is declared using the JSON Schema default keyword within the parameter's parameterSchema:
requests:
getItems:
method: GET
parameterSchema:
query:
type: object
properties:
limit:
type: integer
default: 10
minimum: 1
maximum: 100
responses:
ok: { status: 200 }
6.7.2 Parameter-Level Default Field (Deferred)
The Parameter Object's default FIELD (a separate top-level field in the parameter definition) is deferred and left OPEN. Future adoption depends on:
Tooling and implementations SHOULD handle intra-location defaults (inside parameterSchema) as the normative source.
⚠ Candidate @0.64: The parameter-level
defaultfield remains unspecified. Authors are directed to use JSON-SchemadefaultinsideparameterSchema. If upstream JSON Schema develops a distinct default-application keyword, this section will be updated by reference.
Requests and responses MAY declare multiple alternative schema languages or validation rules via an opt-in alternative-schema EXTENSION vocabulary (not a core mechanism).
6.8.1 Single Source of Truth per Slot
JSON Schema 2020-12 is the sole core schema language. Within each per-location slot (query, path, header, cookie, body, response), a single parameterSchema or response content schema definition is the source of truth:
requests:
filterOp:
method: GET
parameterSchema:
query:
type: object
properties:
name:
type: string
# Single schema home; no inline mix-within-slot
responses:
ok: { status: 200 }
6.8.2 Alternative Schema Expression (Extension)
Specifications MAY declare alternative schema representations (e.g., Protobuf, GraphQL SDL, WSDL) via a C013 extension vocabulary (the host-spec $vocabulary frame; not a parallel OAS dialect selector):
⚠ Candidate @0.74: The alternative-schema extension vocabulary is ADOPTED in direction but its registry, slot naming, conformance language, and exact mechanism are deferred. Whether the mechanism will be built at all remains subject to a YAGNI review (mechanism may be unused at maturity). The single-source-of-truth per-slot principle is high-confidence (@0.7); the extension framing is contingent (@0.74).
Request and response content is declared via the content object, which maps media types to schemas. Media-type parameters (e.g., charset, boundary) are expressed via a registry-keyed content model (inherited from the #108 header-model decision).
6.9.1 Content Structure
paths:
/users:
requests:
createUser:
method: POST
contentType: application/json
contentSchema: { $ref: "#/components/schemas/User" }
responses:
ok:
status: 200
contentType: application/json
contentSchema: { $ref: "#/components/schemas/User" }
6.9.2 Media-Type Parameters (RFC 6838)
Media-type parameters (charset, boundary, etc.) are expressed via the same content-model registry approach established for header field-models (#108):
fieldModel key or extension slot) is deferred to the content model detailing (#108)⚠ Candidate @0.7: The exact mechanism for expressing media-type parameters within the content model is deferred. The registry approach is adopted; the slot naming and content-model integration are contingent on #108's detailed design.
Response and request headers are modeled via a dedicated header-model reference key (not content-type-style), allowing explicit decoupling of header syntax from content negotiation.
6.10.1 Header Definition
Headers are defined in the response object and referenced by stable name:
paths:
/test:
requests:
getTest:
method: GET
responses:
ok:
status: 200
description: Success
headers:
X-Rate-Limit-Remaining:
description: Remaining API calls
schema:
type: integer
X-Custom-Header:
description: Custom metadata
schema:
type: string
6.10.2 Header Field Models (Registry)
Headers MAY reference a registry of standardized field models for common headers (e.g., RFC 9110 fields, RateLimit headers per IETF draft-ietf-httpapi-ratelimit-headers):
fieldModel) MAY be added alongside the C004 per-location header slot"rfc9110/rateLimit")style: automatic sugar is deprecated in favor of explicit model reference⚠ Candidate @0.6: The exact property name (
fieldModel,headerModel, or similar), the registry URL structure, and the runtime normalization semantics are deferred. Header trailers (RFC 7230 §4.1.2) and cookie placement are surfaced as design considerations but not hardened (@0.42).
Schemas compose via standard JSON Schema mechanisms (allOf, oneOf, anyOf) at the RUNTIME level. Path-item and operation-level inheritance uses an optional per-level shared map carrying name-based merge inheritance down the keyed spine.
6.11.1 Schema Composition (allOf, oneOf, anyOf)
components:
schemas:
Named:
type: object
properties:
name: { type: string }
Timestamped:
type: object
properties:
createdAt: { type: string, format: date-time }
Document:
allOf:
- $ref: "#/components/schemas/Named"
- $ref: "#/components/schemas/Timestamped"
- type: object
properties:
content: { type: string }
6.11.2 Path and Operation-Level Inheritance (Optional shared)
An optional shared map at the pathItem or operation level carries reusable parameter definitions and other constraints down the keyed spine (path-collection -> pathItem -> operation):
paths:
/users/{userId}:
shared:
parameterSchema:
path:
type: object
properties:
userId: { type: string }
requests:
get:
# userId parameter inherited
method: GET
responses:
ok:
status: 200
description: User details
patch:
# userId parameter inherited
method: PATCH
contentType: application/json
contentSchema: { type: object }
responses:
ok:
status: 200
6.11.3 Merge Resolution
The shared wrapper:
⚠ Candidate @0.55: The concrete per-property merge table (which properties override, which accumulate, which are forbidden), response-level reach, tag-inheritance, the reserved key name, and the one-surface-vs-two authoring question are deferred. The @0.55 ceiling reflects contested refutation bites; the no-mandatory-wrapper sub-decision is high-confidence.
| Aspect | Field | Type | Required | Notes |
|---|---|---|---|---|
| Schema declaration | schema |
string | NO | Document-level dialect; default = "2020-12" |
| Component identity | components/schemas/<key> |
object | YES | Key IS identity; no index-based aliasing |
| Imports | imports |
object | NO | Namespace → href map; optional for single-file specs |
| Per-location schema | parameterSchema |
Schema Object | YES (per location) | Houses all same-location dependencies + defaults |
| Default value | parameterSchema/default |
any | NO | JSON-Schema default keyword; parameter-level field deferred |
| Discriminator hint | discriminator |
string | NO | Advisory only; runtime variant selection uses oneOf+const |
| Content media-type | content/<mediaType>/schema |
Schema Object | YES | One schema per media-type |
| Shared inheritance | shared |
object | NO | Per-pathItem or operation; flat-key remains co-valid |
| Header definition | headers/<headerName> |
object | NO | Standard JSON Schema inside; fieldModel key deferred |
| Relational constraint | deferred | deferred | — | Cross-location value-equality; grammar TBD (#24/#100) |
paths:
/orders/{orderId}:
shared:
parameterSchema:
path:
type: object
properties:
orderId:
type: string
pattern: "^ORD-\\d{6}$"
required: [orderId]
requests:
updateOrder:
method: patch
parameterSchema:
query:
type: object
properties:
includeDetails:
type: boolean
default: false
contentType: application/json
contentSchema:
type: object
properties:
id:
type: string
pattern: "^ORD-\\d{6}$"
status:
type: string
enum: [pending, processing, delivered]
items:
type: array
items:
$ref: "#/components/schemas/OrderItem"
required: ["status"]
# Same-location dependency: if status is 'processing', require items
if:
properties:
status: { const: "processing" }
then:
required: ["items"]
responses:
updated:
status: 200
description: Order updated
contentType: application/json
contentSchema:
allOf:
- $ref: "#/components/schemas/Order"
- type: object
properties:
updatedAt: { type: string, format: date-time }
notFound:
status: 404
description: Order not found
conflict:
status: 409
description: Conflict
contentType: application/json
contentSchema:
$ref: "#/components/schemas/ErrorResponse"
components:
schemas:
OrderItem:
type: object
properties:
productId: { type: string }
quantity:
type: integer
minimum: 1
required: ["productId", "quantity"]
Order:
type: object
properties:
id: { type: string }
status: { type: string }
items: { type: array, items: { $ref: "#/components/schemas/OrderItem" } }
required: ["id", "status"]
ErrorResponse:
type: object
properties:
code: { type: integer }
message: { type: string }
required: ["code", "message"]
6.14.1 Normative Conformance
Specifications conforming to OpenAPI v4.0:
parameterSchema per-location slots for all parameter and body schemasshared wrapper for path-item-level inheritance (if supported by tooling)6.14.2 Deferred Mechanisms
The following aspects remain OPEN and are deferred to future waves:
| Item | Deferral Reason | Related ADR |
|---|---|---|
| Fragment byte-grammar and JSON-Pointer syntax | Upstream dependency; needs formalization pass | C013 #73 |
| Relational value-equality vocabulary (cross-location) | Grammar and deserialization model TBD | C014 #24/#100 |
Parameter-level default field |
Awaits JSON-Schema upstream proposal | C015 #113 |
| Alternative-schema extension registry | Contingent on YAGNI review | C014 #122 |
| Media-type parameter slot naming | Depends on #108 content-model detailing | C016 #163 |
| Header field-model registry structure | Registry design and RFC citation TBD | C015 #108 |
| Canonical variant-dispatch keyword | Chooses between propertyDependencies, if/then/else, const | C012 #57 |
| Response-level inheritance and precedence | Distinct from pathItem; deferred separately | C012 #17b |
HTTP fields (headers, cookies, and trailers) are the primary mechanism for carrying metadata, authentication, content negotiation, and request/response directives in HTTP messages. This section defines how Suluk v4.0 represents field schemas, field models, and field-specific constraints.
Per C004 (§5.2), headers and cookies are modeled as per-location schema slots in the request (headerSchema, cookieSchema) and as named Header Objects in responses. Per C015 #108 (§#108), the specification introduces a dedicated fieldModel registry key to resolve the header-field semantics beyond schema alone.
Confidence note: Core header/cookie/trailer structure is high-confidence; the fieldModel reference mechanism sits at @0.6, and trailer placement is deferred.
HTTP field models describe the syntax, semantics, and validation rules for a specific field. The registry is indexed by lowercase-normalized field name (per RFC 9110 §5.1) and may reference:
The registry is read-only at runtime and serves as the ground truth for evaluating field presence, syntax, and multiplicity constraints. Tooling MUST normalize field names to lowercase before lookup.
Example:
content-type,accept,x-request-idare all looked up in lowercase form; the registry entry describes the field's syntax (media-type, list-of-values, single-value, etc.) and any multiplicity rules.
Headers in requests are expressed in two ways, depending on the use case:
The per-location headerSchema slot (per C004) is a plain JSON Schema 2020-12 object applied at runtime to the parsed request headers (as an object with lowercase keys). This is the common case for simple field constraints.
request:
method: GET
# Per-location header slot — simple schema over already-typed slice
headerSchema:
type: object
additionalProperties: true # default: allow implicit headers (#224)
properties:
if-match: { type: string }
accept: { type: string, enum: ["application/json", "application/xml"] }
x-request-id: { type: string, pattern: "^[a-f0-9]{8}$" }
required: ["x-request-id"]
Key constraints:
properties object. Tooling MUST normalize incoming headers to lowercase before validation.additionalProperties: true is the default (C004 #224). Implicit headers (e.g. Host, User-Agent) MUST NOT falsely reject valid requests.fieldModel entry.)A dedicated fieldModel key (C015 #108 @0.6) MAY be added to a header property to reference the field's registered model and enforce field-specific semantics beyond schema.
request:
headerSchema:
type: object
properties:
content-type:
type: string
fieldModel: "content-type" # reference to registry entry
set-cookie:
type: string
fieldModel: "set-cookie" # note: Set-Cookie in requests (RFC 6265)
accept:
type: string
fieldModel: "accept" # structured field: comma-separated media-types
Semantics of fieldModel:
fieldModel is a string value (not a schema keyword) that names an entry in the field-model registry.fieldModel means the schema alone governs validation.⚠ Candidate @0.6: The
fieldModelreference mechanism is contested and sits at lower confidence than the per-location slot structure. The exact registry entry schema, serialization rules for structured fields, and interaction with JSON Schema 2020-12 are provisional. Expect refinement.
The optional style: automatic keyword (per C015 #108) is DOM → ADA sugar only, applying only to the abstract description surface (ADA) that tools consume, not to the on-the-wire format. It indicates that the field's serialization is automatic (determined by the field model) and does not require special deserialization logic.
request:
headerSchema:
type: object
properties:
accept:
type: string
fieldModel: "accept"
style: automatic # ADA sugar: field serialization is automatic
style: automatic is optional; it provides a hint to ADA consumers but does NOT change validation semantics.fieldModel entry, not by style.style as a runtime dispatch mechanism; it is informational only.Note: This differs from OAS 3.x
style(e.g.simple,form), which was a deserialization contract. Here,style: automaticis a metadata tag for the ADA, independent of runtime validation.
Response headers are modeled as a named map of Header Objects, each keyed by lowercase field name.
responses:
"200":
description: Success
headers:
content-type:
description: "Content media type"
schema:
type: string
enum: ["application/json"]
x-rate-limit:
description: "Requests remaining"
schema:
type: integer
fieldModel: "x-rate-limit"
cache-control:
description: "Cache directives"
schema:
type: string
fieldModel: "cache-control" # structured field: comma-separated directives
Header Object structure:
| Field | Type | Constraint | Notes |
|---|---|---|---|
description |
string | OPTIONAL | Describes the header's purpose. |
schema |
JSON Schema | REQUIRED | Plain JSON Schema 2020-12 for the field value. |
fieldModel |
string | OPTIONAL | Registry key for field-specific semantics. |
style |
string | OPTIONAL | DOM → ADA sugar; automatic is the only defined value. |
deprecated |
boolean | OPTIONAL | Marks the header as deprecated. |
required |
boolean | OPTIONAL | Declares whether the header MUST be present. Default: false. |
Semantics:
fieldModel key follows the same registry-lookup semantics as request headers (§7.3.2).Cookies are first-class per-location parameters in requests (per C004 #224) and appear in responses via the standard Set-Cookie response header (§7.6).
Request cookies are modeled in the request's cookieSchema per-location slot, as a plain JSON Schema 2020-12 object.
request:
method: GET
cookieSchema:
type: object
additionalProperties: true
properties:
session-id:
type: string
pattern: "^[a-f0-9]{32}$"
user-prefs:
type: string
required: ["session-id"]
Key constraints:
additionalProperties: true is the default, allowing arbitrary cookie values.Cookie header syntax).⚠ Candidate (deferred): The exact placement of cookies within the signature mechanism (C003) and the interaction with session/correlation tracking are deferred pending resolution of frontier #127 (templating) and #224 refinements.
Response cookies are modeled as Set-Cookie response headers, per OAS 3.2 and RFC 6265.
responses:
"200":
description: Login successful
headers:
set-cookie:
description: "Session cookie"
schema:
type: string
fieldModel: "set-cookie"
The Set-Cookie field model enforces RFC 6265 parsing and the constraint that Set-Cookie headers are never multiplexed (only one per response; multiple cookies require multiple Set-Cookie headers, which are not merged in the headers map).
Note: Modeling multiple cookies in a single response requires multiple response objects (one per status code) or an explicit out-of-band annotation; the headers map is keyed by field name, so
set-cookiecan only appear once. This is a known limitation inherited from OAS 3.2 and deferred for future refinement.
Trailers (per RFC 7230 §4.1.2) are HTTP fields transmitted AFTER the message body in chunked-transfer encoding. Their representation in Suluk v4.0 is deferred pending clarification of:
trailerSchema slot, or in the request/response headerSchema?⚠ Candidate (deferred @0.42): Trailer semantics and placement are explicitly deferred to a future wave. For now, trailers MAY be documented as headers with
x-trailerannotations, or tooling MAY extend the per-location slot model with a dedicatedtrailerSchemaslot. This is an open point pending frontend validation and RFC 7230 §4.1.2 alignment.
When a field's presence or value depends on another field (e.g., If-Match requires ETag in the prior response, or Authorization type depends on the value of a custom X-Auth-Scheme header), the opt-in cross-cutting dependency construct (per C004 §1.2) MAY be used.
request:
method: PUT
headerSchema:
type: object
properties:
if-match: { type: string }
x-auth-scheme: { enum: ["bearer", "basic", "digest"] }
crossDependencies:
# If If-Match is present, x-auth-scheme MUST be bearer (presence-based, standard 2020-12)
allOf:
- if: { properties: { headers: { required: ["if-match"] } } }
then: { properties: { headers: { properties: { x-auth-scheme: { const: "bearer" } } } } }
Note: This construct is separate from the per-location slot structure and is used only when true cross-field constraints cannot be expressed within a single
headerSchema. AbsentcrossDependencies, all field constraints are independent.
Per RFC 9110 §5.1, HTTP field names are case-insensitive. The specification requires:
properties MUST be lowercase.fieldModel: "content-type") MUST use lowercase names.This normalization is NOT optional and applies to all HTTP fields (headers, cookies, trailers, Set-Cookie).
request:
headerSchema:
type: object
properties:
content-type: # lowercase (required)
accept-language: # lowercase (required)
# NOT: Content-Type, Accept-Language
| Object | Location | Parent | Key Constraint |
|---|---|---|---|
headerSchema |
Request | request |
Plain JSON Schema 2020-12; lowercase field names; additionalProperties: true default. |
cookieSchema |
Request | request |
Plain JSON Schema 2020-12; lowercase cookie names. |
Header Object |
Response | responses[status].headers[field-name] |
schema (required), fieldModel (optional), description, required. |
Set-Cookie Header |
Response | responses[status].headers["set-cookie"] |
Per RFC 6265; serialized as Set-Cookie header (not multiplexed in headers map). |
fieldModel |
Request/Response | Field property | String registry key; optional; evaluated at runtime. |
crossDependencies |
Request | request |
Optional JSON Schema with materialized {headers, body, ...} envelope. |
The following are explicitly marked for future refinement or are candidate-level (confidence ceiling noted):
| Item | Ceiling | Status | Notes |
|---|---|---|---|
fieldModel reference mechanism |
0.6 | Candidate | Registry entry schema, structured-field serialization, and JSON Schema 2020-12 interaction are provisional. |
| Trailer placement and semantics | 0.42 | Deferred | Whether trailers use a separate trailerSchema slot, evaluation order, and multiplicity rules are open. |
| Set-Cookie multiplicity in responses | — | Inherited Limitation | Multiple cookies in one response require multiple response objects; inherits OAS 3.2 limitation. |
| Cookie placement in signature mechanism | — | Deferred | Interaction with frontier #127 (templating) and #224 (dynamic-transport-keys) refinements pending. |
| Structured field vocabulary for value-equality | — | Deferred to #73 | Relative-JSON-Pointer relational keywords not yet standardized; blocks cross-field value-equality deps. |
components:
requests:
UserCreate:
method: POST
contentType: application/json
contentSchema:
type: object
properties:
name: { type: string }
email: { type: string, format: email }
required: [name, email]
responses:
"201":
status: "201"
description: "User created"
contentType: application/json
contentSchema:
type: object
properties:
id: { type: string }
name: { type: string }
email: { type: string }
required: [id, name, email]
paths:
/users:
requests:
createUser:
method: POST
summary: "Create a new user"
operationId: createUser
contentType: application/json
# Per-location schema slots
parameterSchema:
header:
type: object
additionalProperties: true
properties:
authorization:
type: string
pattern: "^Bearer [a-zA-Z0-9._-]+$"
x-request-id:
type: string
pattern: "^[a-f0-9]{8}$"
accept:
type: string
enum: ["application/json"]
fieldModel: "accept" # reference field model
required: [authorization, x-request-id]
cookie:
type: object
additionalProperties: true
properties:
session-id:
type: string
pattern: "^[a-f0-9]{32}$"
user-prefs:
type: string
required: [session-id]
path:
type: object
query:
type: object
contentSchema:
type: object
properties:
name: { type: string, minLength: 1 }
email: { type: string, format: email }
required: [name, email]
responses:
"201":
status: "201"
description: "User created"
contentType: application/json
contentSchema:
type: object
properties:
id: { type: string }
name: { type: string }
email: { type: string }
required: [id, name, email]
The field model registry is an opaque mapping from field name (lowercase) to model metadata. The precise schema and registry contents are deferred (C015 #108 @0.6). The canonical sources are:
Example registry entries (illustrative, not normative):
# Illustrative structure (NOT normative)
_field_models:
content-type:
iana_ref: https://www.iana.org/assignments/http-fields/http-fields.xhtml
multiplicity: "single" # max 1 per message
structured_field: false
syntax: "media-type / charset"
accept:
iana_ref: https://www.iana.org/assignments/http-fields/http-fields.xhtml
multiplicity: "multiple" # may repeat
structured_field: true # RFC 8941 list
syntax: "list of media-ranges"
set-cookie:
iana_ref: https://tools.ietf.org/html/rfc6265
multiplicity: "single"
structured_field: false
syntax: "name=value; path=...; domain=...; ..."
This registry is not authored in Suluk documents; it is provided by tooling, RFC-alignment standards, and the extension registry (C013).
Status: frame-level candidate (Suluk fork) · resolved under C013; reconciles C009 (identification-first routing). Deferred: fragment byte-grammar, dialect version pin, JSON-Pointer tolerance, string-vs-object polymorphism UX. Resolves frontier #73/#72/#49/#26 (referencing cluster); does NOT ratify SIG outcomes.
Components is a dynamic-key map of reusable schema and metadata objects. References are links to components and other locatable symbols, split into two independent families:
$ref keyword), confined to Schema Objects under a declared dialect.Imports enable cross-document symbol resolution without requiring the parser to perform I/O; a document may declare the namespace and URI of external symbol sources.
The referencing model is identification-first (per C009): components are identified by their canonical name (the map key), never by array index or insertion order. The exact fragment syntax (#schemas.Foo, #72:Schema, etc.) is deferred; this section specifies the underlying identity and resolution frame.
components:
<component-family-name>:
<component-name>: <schema-or-metadata-object>
Map key structure:
<component-family-name> is a fixed vocabulary of collections (e.g., schemas, requestBodies, responses, etc.); exact roster deferred.<component-name> is a user-supplied string, the canonical stable identity for that component. It MUST be unique within its family within a single OAD (OpenAPI Document).The component key is the primary identity. Under the OpenAPI Candidate v4.0 framework:
Speaker in the schemas family is anchored at an implicit location independent of document structure).$id or $anchor MUST NOT assign a different identity to the component as a whole.⚠ Candidate @0.70: The byte-grammar for canonical anchors (whether
#schemas.Speaker,#72:Speaker,#/$defs/Speaker#Speaker, or another form) is not yet finalized. This section mandates name-based identity semantics; the syntax is deferred to the identification/referencing redesign (#26/#49/#73).
The JSON Schema dialect used within Schema Objects is declared at the document level:
openapi: 4.0
jsonSchemaDialect: "https://json-schema.org/draft/2020-12/schema" # [or override per-schema]
$schema keyword under 2020-12).⚠ Candidate @DEFERRED: The specific version pin (e.g., 2020-12 vs 2024-01 vs a future revision) and the per-schema override policy are deferred to wave B formalization. The frame commits only that dialect is explicitly declared and locally decidable.
Refs inside Schema Objects follow the declared dialect:
components:
schemas:
User:
type: object
properties:
id: { type: string }
role: { $ref: "#/$defs/Role" } # 2020-12 dialect: relative ref within the schema
Role:
enum: [admin, user, guest]
Normative constraints:
$ref keyword, a reserved word in the dialect) and their slot (inside a Schema Object).An OpenAPI reference (distinct from $ref) is a link to a component identified by its canonical name. Its exact syntax is deferred; the semantics are:
<openapi-ref> ::= <component-family-name> ':' <component-name> | absolute-uri
(Exact syntax deferred; illustration only.)
Examples (illustrative):
request:
contentSchema: schemas:Speaker # [or #schemas.Speaker or another form]
response:
default:
contentSchema: schemas:ServerError
links:
nextPage:
target: paths:"{id}/next" # [or #paths.{id}/next, illustration]
pathResponses:
notFound:
$ref: responses:NotFound # [or #responses.NotFound, illustration]
⚠ Candidate @0.50–0.55: Whether OpenAPI refs coexist with a URI-based component alias surface (per the 2025-10-29 OAS-3.2 outcome) is not hardened. The name-keyed primary identity is mandatory; a URI surface is optional and deferred.
An Imports Object declares external symbol sources, enabling cross-document references without requiring the parser to perform I/O:
imports:
commonSchemas: # namespace (user-supplied key)
href: "https://api.example.com/schemas.json" # [or relative URI, or local file path]
# or:
# href: "file:///local/schemas.json"
Map key structure:
commonSchemas, auth, domains).href MAY be an absolute IRI, a relative URI (resolved relative to the document's base), or a file path.href matches the current document's own IRI) OR perform retrieval, but MUST NOT require network/file I/O to accept the declaration.import: entry with href pointing to the document itself) are implicitly valid and useful for documenting internal symbol namespaces.imports:
standard:
href: "https://spec.example.org/common.json"
components:
schemas:
CustomError:
type: object
properties:
detail: { $ref: "standard:Error" } # [or #standard:Error or another form, illustration]
In this example:
standard:Error resolves to the component named Error in the schemas family of the document at the import href.href is the current document, no cross-file lookup is needed.href is external and retrieval succeeds, the Error component from that document is used.self is implicitly reserved and refers to the current document's own components.self is NOT permitted.self is malformed.At the DOM-to-ADA boundary, a single resolution function processes all references:
$ref vs. OpenAPI ref) by token and slot.jsonSchemaDialect).This boundary operation is required for conformant ADA production; it is optional for tooling that works only with the DOM. Single-file OADs (no imports) MUST still support local OpenAPI references by name.
⚠ Candidate @0.50–0.55: Whether the ADA boundary is hard-required or optional, and the exact error/recovery semantics for unresolved imports, are not yet formalized. This section describes the frame; SIG vote will determine enforcement level.
Normative rules:
self namespace is reserved; explicit Imports entries claiming self are forbidden.At lowered confidence (@0.50–0.55):
schemas:Foo + responses:Bar in a single ref token) is deferred.openapi: 4.0
jsonSchemaDialect: "https://json-schema.org/draft/2020-12/schema"
imports:
common:
href: "https://api.example.org/v1/schemas.json"
components:
schemas:
Speaker: # canonical name: Speaker
type: object
properties:
id: { type: string }
name: { type: string }
errors: { $ref: "#/$defs/ValidationErrors" } # JSON-Schema ref (dialect-governed)
$defs:
ValidationErrors:
type: array
items: { $ref: "common:Error" } # [illustration] OpenAPI ref to imported schema
ConferenceProblem:
type: object
properties:
status: { type: integer }
detail: { $ref: "common:ProblemDetail" } # OpenAPI ref to imported schema
responses:
SpeakerCreated: # canonical name: SpeakerCreated
status: 201
contentType: application/json
contentSchema: { $ref: "schemas:Speaker" } # [illustration] OpenAPI ref to local schema
Conflict:
status: 409
contentType: application/json
contentSchema: { $ref: "schemas:ConferenceProblem" }
paths:
"conferences/{confId}/speakers":
requests:
create:
method: POST
contentType: application/json
contentSchema: { $ref: "schemas:Speaker" }
responses:
created: { $ref: "responses:SpeakerCreated" } # [illustration] by-name ref
conflict: { $ref: "responses:Conflict" }
Per C013, the following are explicitly deferred to SIG vote or wave B formalization:
#schemas.Foo, #/$defs/Foo, #72:Foo, or another syntax.#/components/schemas/Foo) remain valid or are retired.contentSchema: "schemas:Speaker" (string) is allowed alongside object form.The identification-first principle (resolve by name, never by index) is non-deferrable and decided now. The wire shape (array vs. map, syntax, order semantics) is latent and revisable based on future SIG decisions.
Security metadata in Suluk v4.0 is externalized by default, with operations referencing security requirements by stable NAME (not by index). This design enforces loose coupling between API operations and their authorization contexts, deferring fine-grained parameter/body-dependent authz decisions to Area-4 detailed design.
Security schemes inherit the OpenAPI 3.2 model: a map of named scheme definitions, each specifying type, flows, and location. Suluk does not extend the 3.2 security-scheme surface.
| Field | Type | Normative | Notes |
|---|---|---|---|
schemes |
Map<string, SecurityScheme> |
MUST | Top-level component; keyed by stable NAME (C009 by-name-never-index). Each scheme follows OAS 3.2 structure (http, apiKey, oauth2, openIdConnect, mutualTLS). |
Example:
components:
securitySchemes:
api_key_auth:
type: apiKey
name: X-API-Key
in: header
description: Stable-name API key reference.
oauth2_implicit:
type: oauth2
flows:
implicit:
authorizationUrl: https://example.com/oauth/authorize
scopes:
read:profile: Read user profile
write:profile: Modify user profile
Operations reference security schemes by stable NAME, stored in a security ARRAY at the operation level. Each element is a map pairing scheme NAME to an array of scopes (for OAuth2/OpenIdConnect) or an empty array for non-scoped schemes.
| Field | Type | Normative | Notes |
|---|---|---|---|
security (operation) |
Array<Map<string, Array<string>>> |
MAY | List of alternative security requirements; all alternatives apply (AND). Absence means no security required. By-name reference per C009. |
Normative Language:
security: [ { api_key_auth: [] } ] MUST include the named scheme's header/query/cookie/path parameter.security: [ { oauth2_implicit: ["read:profile"] } ] MUST validate the OAuth2 token against at least the listed scopes.Canonical Location: Security definitions live in a separate functional area (C011 #141 seven-area decomposition), not embedded in operation definitions. This enforces separation of concerns and allows shared security contexts across operations.
Adoption Pattern:
Operations inherit security from:
security (applies to all operations unless overridden, per OAS 3.0/3.2 convention).security (overrides root; MAY be empty to make an operation public).# Root-level security (applies to all operations)
security:
- api_key_auth: []
paths:
/public-data:
get:
summary: Public endpoint
security: [] # Override: no authentication required
responses:
'200':
description: Public data
/protected-data:
get:
summary: Protected endpoint
# Inherits root security: api_key_auth
responses:
'200':
description: Protected data (API key required)
For the rare case where an operation's authorization depends on runtime values (e.g., a path parameter that selects a tenant, a body field that determines scope eligibility), Suluk permits an operation-local security pointer under the extension vocabulary.
⚠ Candidate @0.68: Fine-grained parameter/body-dependent authz and scope-value reconciliation are not finalized. Hudlow's signature-as-authz-carrier position (which he flags unfinished himself) and scope-term/value-equality vocabulary both defer to Area-4 detailed design and the #24/#100 relational-extension cluster.
Extension Framework:
Implementations MAY define vendor-specific extensions (e.g., x-moonwalk-conditional-authz) to express dependencies, but such extensions are outside core v4.0 and are not portable across tool ecosystems.
The OAS 3.2 security model defines scopes as opaque strings, without a formal grammar for scope-value binding to API parameters or body fields.
⚠ Candidate @0.68: Scope reconciliation across OAuth2/OpenIdConnect schemes and parameter-equality checks (e.g., path ID equals request body ID, both required for authz) are deferred. The #24/#100 ADR panel defers the relational-extension vocabulary (single declared Moonwalk extension keyword for value-equality) to a future fragment-grammar filing. Runtime scope validation remains implementation-defined.
Per C011 #141, security is one of seven modular functional areas. A conforming API specification SHOULD dedicate a separate section or document fragment to:
This separation ensures that security engineers can review and maintain authz constraints independently from endpoint definitions.
Example (Conceptual):
# security.yml (standalone functional area)
components:
securitySchemes:
oauth2_code:
type: oauth2
flows:
authorizationCode:
authorizationUrl: https://auth.example.com/authorize
tokenUrl: https://auth.example.com/token
scopes:
'admin:all': Full admin access
'org:manage': Manage organization
'data:read': Read data
'data:write': Write data
x-authz-rules:
tenant_isolation:
rule: "Request's tenant_id header MUST match token's org_id claim."
data_scope_binding:
rule: "write scope requires body.org_id == token.org_id; read scope requires read permissions."
Operations then reference this by name:
paths:
/orgs/{org_id}/data:
requests:
postData:
method: POST
security:
- oauth2_code: [ 'data:write', 'org:manage' ]
parameterSchema:
path:
type: object
properties:
org_id:
type: string
required: [org_id]
responses:
"201":
status: "201"
description: Data created
"403":
status: "403"
description: Insufficient scope or tenant mismatch
A conforming Suluk v4.0 document:
components.securitySchemes.security overrides for any endpoint that deviates from the root policy.Tooling (code generators, validators, documentation renderers) SHOULD:
components.securitySchemes.A Server object represents a named, stable deployment target. The Candidate v4.0 specification treats servers as thin identity handles—named references that tools and users invoke consistently across environments and time, independent of the runtime URL configuration.
| Field | Type | Description |
|---|---|---|
name |
string | REQUIRED. A unique, case-sensitive identifier for this server. MUST be a valid JSON object key. Server references in Paths Object link-by-name, never by index (per C009). |
description |
string | OPTIONAL. Human-readable purpose and context. Example: "Production API cluster in US-East." |
tags |
[string] | OPTIONAL. Array of tag strings for grouping or filtering (e.g., ["production", "verified"]). |
extensions |
map[string, any] | OPTIONAL. Specification extensions (keys starting with x-); see §13 Extensions. |
Normative Language:
The Servers Map is a JSON object keyed by server name, with Server objects as values. Paths, Requests, and other objects MAY reference a server by name using the servers field (a reference—typically a single server name string or an array of names).
{
"servers": {
"prod-us": {
"name": "prod-us",
"description": "Production US region"
},
"staging": {
"name": "staging",
"description": "Staging environment"
}
}
}
Scope:
servers field) and at Path-Item level or Request level (narrowing the scope for that path or request). Lower scopes override higher scopes.localhost or a synthetic fallback). Explicit declaration is RECOMMENDED for clarity.⚠ Candidate @0.74 (C015 #55): Environment URL configuration—mapping server names to concrete runtime URLs, including per-environment variations (e.g.,
prod-us→https://api-us.example.com,staging→https://staging-api.example.com)—is a deployment-layer responsibility, out of scope for the core Suluk Document.
The Suluk specification defines server identity (a named handle). The mapping from identity to runtime URL is a deployment concern, resolved by:
location field or object that associates server names with concrete URLs.$ENV, substitution), or dynamic discovery.Consequence: A Suluk Document is environment-agnostic; its server names are stable references. Tooling consuming the document is responsible for binding names to environment-specific URLs at runtime.
Suluk Documents MAY be discoverable via the well-known URI defined in RFC 8615. Deployments SHOULD register Suluk Documents at the following locations:
| Location | Semantics |
|---|---|
/.well-known/openapi.json |
Default Suluk Document location (JSON format). |
/.well-known/openapi.yaml or /.well-known/openapi.yml |
OPTIONAL. YAML representation of the same document. |
Normative Language:
/.well-known/openapi to enable tooling discovery. (@0.75–0.85 — per C011 #19)Content-Type: application/json (or application/yaml for YAML variants) and SHOULD be publicly readable (no authentication required) to maximize discoverability.⚠ Candidate @proposal (C008): Document merge and multi-file composition—layering or combining multiple Suluk Documents (e.g., "merge down" a manifest of partial specs) into a single consolidated document—is out of scope for the core Suluk specification.
Rationale: The OpenAPI record (2022–2025) converges on routing composition to:
Multi-file authoring (splitting an API definition across files for organization) is NOT merge; it is a tooling/repository-organization concern independent of the spec. Recursive inclusion (e.g., $ref to a path definition in another file) is already supported via JSON Schema $ref and the Suluk reference model.
Revisability: This decision is held to be reversible. If community tooling consensus coalesces around a specific merge pattern, the spec may adopt it in a future wave. For now, the core remains focused on single-document semantics.
openapi: 4.0.0
info:
title: Server Configuration Example
version: 1.0.0
servers:
- url: https://api-us.example.com
description: |
Production API cluster (US-East region).
Runtime URL bound at deploy time via environment overlay or CI/CD substitution.
- url: https://staging-api.example.com
description: |
Staging environment. Used for pre-release validation.
- url: http://localhost:3000
description: Developer sandbox (localhost).
paths:
/items:
requests:
listItems:
method: get
summary: List all items (production & staging only)
responses:
ok:
status: 200
description: Success
contentType: application/json
contentSchema:
type: array
items:
$ref: '#/components/schemas/Item'
Interpretation:
prod-us, staging, dev./items path is available only on prod-us and staging servers (overriding the document-level scope if one were specified).Servers support specification extensions (keys prefixed with x-) to accommodate tool-specific or domain-specific metadata. Common extensions include:
x-region: Cloud region identifier.x-availability: SLA or availability tier.x-deprecated: Mark a server as deprecated.Extensions are vendor-specific and are not validated by the core Suluk specification.
Cross-References:
Tags are metadata labels attached to operations, used for grouping, categorization, and referencing. In Candidate v4.0, the tags collection is a named map keyed by tag name, allowing tools to reference tags by stable identifier and enabling fine-grained tag definition.
⚠ Candidate @0.68: The deeper tag charset/multi-purpose model and documentation capability are deferred. This section defines the identification and extension mechanism; full tag semantics (format constraints, documentation rendering) remain in a future wave.
The tags object at the root level of an OAS document is a map keyed by the tag name (string). Each key is a unique tag identifier; each value is a Tag Object.
tags: {string -> Tag Object}
Why Map, Not Array: Per C009 (array-vs-map), user-keyed collections in v4 resolve identity by stable name, never by index or insertion order. Tags are referenced by name in operation tags arrays and in the annotation vocabulary (§11.3); a map keyed by tag name makes the reference graph explicit and prevents identity collisions that occur with array-of-named-objects under v4's feature scope (multiple semantic tags per operation, per C009 refutation).
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | MUST | The tag's identifier (matches the map key; MUST be unique and non-empty). |
description |
string | no | Human-readable description of the tag. Supports CommonMark syntax per §11.2. |
externalDocs |
External Documentation Object | no | Link to external documentation for this tag. |
extension |
map{string -> *} | no | Reserved for extension vocabularies (§11.3). Keys follow the x- prefix convention. |
Normative language:
name MUST be identical to its map key.tags map.description field MAY use Markdown (CommonMark 0.30); rendering is tool-dependent.An operation's tags field is a string array, listing the tag names (map keys) that categorize that operation. Each string MUST correspond to a key in the root tags map OR be an undefined tag (for forward compatibility, some tools permit undefined tags; specs SHOULD define all tags upfront).
openapi: 4.0.0
info:
title: Tag Reference Example
version: 1.0.0
paths:
/pets:
requests:
listPets:
method: get
tags:
- "catalog" # references tags["catalog"]
- "read" # references tags["read"]
responses:
ok:
status: 200
description: Success
Candidate v4.0 adopts seven functional areas as the normative modular decomposition of an API specification, per C011 frontier item #141. These areas organize the specification structure and provide a mental model for API designers and tooling.
requests map (per-operation request schemas, keyed by friendly name).responses map (per-operation response schemas, keyed by status or friendly name, per C009).components object, housing reusable schema definitions, security schemes, and other referential anchors (C001: identification-first).components.Use & Audience:
Per C011 frontier item #128, text fields (description, summary, documentation) support configurable rich-text format.
Normative:
textFormat field (string, deferred; candidates: "markdown", "html", "adoc" or other).⚠ Candidate @0.72: Explicit
textFormatroot field and format-handler registry are deferred to C013 extensions. The section documents the design slot and defers enforcement.
Per C015 frontier item #56 (@0.70), context-dependent property semantics are modeled as object-level annotation keywords delivered via the JSON Schema 2020-12 extension vocabulary.
An annotation is a metadata facet attached to a schema object (in any per-location slot: query, path, header, cookie, body), expressing context-specific constraints or behavioral notes that are independent of the schema's own type validation.
Annotations are authored as unknown keywords in JSON Schema 2020-12 objects and are surfaced by validators implementing the annotation-aware dialect. They are grouped by field name when they apply to specific object properties.
Example:
{
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"x-annotation:required-in-POST": true,
"x-annotation:read-only-in-GET": true
}
}
}
Here, x-annotation:required-in-POST and x-annotation:read-only-in-GET are context-specific notes; the property is always a string, but its operational constraint depends on the HTTP method.
Annotations are validated in two passes:
Pass 1: Schema Validation + Annotation Emission
A conformant JSON Schema 2020-12 validator emits annotations and JSON Pointer paths for unknown keywords it encounters, treating them as metadata, not errors. Output: validated instance + annotation map (per RFC 7396 or similar).
Pass 2: Context-Sensitive Enforcement
A second, HTTP-aware validator consumes the annotation map and applies context rules (e.g., "required-in-POST" enforced only when the operation method is POST). This validator MAY reject instances that fail context-sensitive checks, even if Pass 1 succeeded.
⚠ Candidate @0.70: The context-annotation keyword namespace, scope binding (which fields apply), and enforcement semantics are open to refinement. The section defines the mechanism (unknown-keyword-as-annotation); the full vocabulary is deferred to C013 extension work and community-driven adoption.
Context annotations are hosted in the C013 extension vocabulary and are registered under the declared JSON Schema 2020-12 dialect. OAS tools that consume annotations declare explicit support; conformance MUST acknowledge that unknown annotations are silently ignored by base 2020-12 validators.
Normative:
x-annotation:* naming convention to signal OAS extension status.openapi: 4.0.0
info:
title: Pet Store
version: 1.0.0
tags:
catalog:
name: "catalog"
description: "Operations for browsing the pet catalog"
externalDocs:
url: "https://example.com/docs/catalog"
write:
name: "write"
description: "Operations that modify data"
paths:
/pets:
requests:
listPets:
method: get
summary: "List pets"
tags:
- "catalog"
responses:
ok:
status: 200
description: "Success"
createPet:
method: post
summary: "Create a pet"
tags:
- "write"
contentType: application/json
contentSchema:
$ref: "#/components/schemas/Pet"
responses:
created:
status: 201
description: "Pet created"
openapi: 4.0.0
# === Metadata Functional Area ===
info:
title: Payment API
version: 2.0.0
contact:
name: "Support"
url: "https://support.example.com"
# === Paths & Operations Functional Area ===
paths:
/payments:
requests:
createPayment:
method: post
summary: "Create a payment"
tags: ["transactions"]
contentType: application/json
contentSchema:
$ref: "#/components/schemas/PaymentRequest"
responses:
created:
status: 201
description: "Payment created"
# === Schema Components Functional Area ===
components:
schemas:
PaymentRequest:
type: object
properties:
amount:
type: number
minimum: 0.01
currency:
type: string
enum: ["USD", "EUR"]
# === Security Functional Area ===
securitySchemes:
api_key:
type: apiKey
name: X-API-Key
in: header
openapi: 4.0.0
info:
title: Annotations Example
version: 1.0.0
paths:
/users:
requests:
createUser:
method: post
contentType: application/json
contentSchema:
type: object
properties:
email:
type: string
format: email
x-annotation:required-in-POST: true
x-annotation:updatable: false
password:
type: string
minLength: 8
x-annotation:write-only: true
x-annotation:required-in-POST: true
responses:
created:
status: 201
description: Created
listUsers:
method: get
responses:
ok:
status: 200
description: Success
contentType: application/json
contentSchema:
type: object
properties:
email:
type: string
format: email
x-annotation:read-only: true
password:
x-annotation:hidden-from-GET: true
textFormat field & format-handler registry (@0.72): Deferred to C013 extensions; no mandated parsing.Scope: This section specifies a semi-automated transformer U: 3.x-DOM → (4.0-DOM, ReviewLedger) that is mechanical where an earlier Candidate ADR pins a 4.0 target, and systematically FLAGS (never silently drops) where it does not. The transformer resolves Principle 6 ("An automated upgrade process from 3.x to 4.0 will be developed") as a CONFORMANT-PARTIAL upgrader: a deterministic green path PLUS a machine-readable audit ledger citing the governing ADR per lossy construct or deferred decision. Full automation is foreclosed; the honest unit of work is a semi-automated transformer with human-review gates.
Candidate @0.55: This entire resolution is ORIGINATED (sole-witness), inherits the full revisability of every ADR it rests on (C003–C016), and is capped at Originated ceilings (0.5–0.6). No SIG vote exists for the upgrade mechanism. Several target shapes are explicitly DEFERRED (C013 fragment byte-grammar; C005 query-placement; C004 query→data-model deserialization), so the transformer's exact emitted syntax cannot be finalized until those land.
Honest Coverage: The semi-automated transformer achieves ~55–70% mechanical-and-lossless coverage for OpenAPI 3.1, and ~45–60% for OpenAPI 3.0.x. The remainder is flagged lossy or human-assisted via the ReviewLedger. This headline is audit-corrected downward from naive 70–80% estimates; see Consequences (§12.7).
The transformer operates in four stages, each with an explicit output form:
style, explode, allowReserved), discriminator.mapping, parameter.content, header .content, schema .xml.{pointer, flag, adrRef, rationale}.Each non-finalizable construct or lossy case produces a ReviewLedger entry:
{
"pointer": "#/paths/~1pets~1{petId}/requests/listByStatus",
"construct": "style+explode",
"flag": "lossy",
"adrRef": "C005",
"rationale": "parameter style=form, explode=true is FORBIDDEN in path-identity by C005 (multi-segment injection); dropped with receipt; alternation must be manual",
"sourceValue": { "style": "form", "explode": true },
"severity": "warning"
}
Fields:
pointer — JSON Pointer (RFC 6901) to the source 3.x location.construct — human label (e.g., "discriminator.mapping", "parameter.content").flag — one of: lossy, deferred, human_required, collision_candidate.adrRef — governing Candidate ADR (e.g., "C005", "C013").rationale — human-readable reason why this construct cannot be auto-emitted.sourceValue — the 3.x value (for human review).severity — error (transformation fails), warning (lossy but emitted), info (advisory).3.x: Keyed by path string (e.g., "/pets/{petId}"), may include parameters array.
4.0 Mapping:
| 3.x Construct | 4.0 Target | Status | Rationale |
|---|---|---|---|
paths["/pets/{petId}"] |
paths["{petId}"] (uriTemplate) |
Lossless (Tier-M) | C005 Tier-M identity function: OAS 3.x single-segment vars, literal text. Reverse-parse algorithm (C005) inverts the template. |
paths.parameters |
pathItem.parameterSchema + per-location slots |
Partial lossy | C004: per-location schema slots are the target; path-level parameters array is FLATTENED into a single pathItem.parameterSchema (allOf union of all path-level params). Style/explode flagged (see §12.4.7). |
Multi-segment var (e.g., {id*}) |
(forbidden) | Lossy | C005: multi-segment explode is FORBIDDEN in path-identity. Emit flag; human must decide: rewrite path or promote to query. |
Reserved char (e.g., {id:+}) |
(forbidden) | Lossy | C005: reserved-operator vars are forbidden. Emit flag (§12.5.2). |
| Full RFC6570 operators | Tier-M subset only | Lossy/Identity | C005: only literal text + single-segment simple vars + matrix (corrected) + Tier-Q form-query in query component are permitted. All others flagged. |
Candidate @0.55: The uriTemplate REVERSE-PARSE ("URL to template") is mechanically decided by C005's match-safe operator profile only when Tier-M is used. If the 3.x path uses forbidden operators (e.g., prefix truncation), the 4.0 uriTemplate cannot represent it and must be manually rewritten. This is the structural root of URL-to-uriTemplate ambiguity (C005 §1.4).
3.x: paths[path][method] is an Operation object; requestBody nests media types.
4.0 Mapping:
| 3.x Construct | 4.0 Target | Status | Rationale |
|---|---|---|---|
operation |
request (by method + content-type) |
Lossy (name synthesis) | C003: request is keyed by friendly NAME (not operationId). Name is SYNTHESIZED from operationId or method+contentType combo. If operationId absent (common in 3.x), synthesis is deterministic but friendly-name is fabricated. Ceiling lowered @0.55. |
operationId |
request.name (optional, informational) |
Lossy | operationId is GLOBAL in 3.x; request.name is per-pathItem. If operationId is reused across pathItems (non-unique), hoisting to a single global slot requires COLLISION POLICY (C003, open). Conservative: INLINE the operationId as a request field; human decides hoisting. |
requestBody.content["application/json"].schema |
request.contentSchema |
Lossless | C004: single content type per request. If multiple media types map to one request (content negotiation), each gets its own request entry. |
requestBody.content (multiple types) |
Multiple request entries (fan-out) |
Partial lossy (name collision) | If 3.x has POST /pets with both application/json and application/xml in the same requestBody, 4.0 emits TWO named requests. Name collision RISK: if both should be named "createPet", manual disambiguation required. Flagged (§12.5.3). |
requestBody.encoding |
(not preserved) | Lossy | OpenAPI 3.x encoding object controls serialization of multipart/form-data. 4.0 defers wire-protocol serialization to #108 (header model, query-string-is-not-JSON). Flagged lossy. |
Candidate @0.55: Operations→named-requests is LOSSY because the friendly-name synthesis (operationId or derived) fabricates a humanly-readable identifier that may not exist in 3.x, violating the name-must-be-authored principle in some readers. Ceiling is lowered to @0.55 to reflect this tension.
3.x: Parameters are an array; each has in, style, explode, allowReserved.
4.0 Mapping:
| 3.x Construct | 4.0 Target | Status | Rationale |
|---|---|---|---|
parameter (in=query) |
pathItem.parameterSchema.properties[name] OR request.parameterSchema |
Lossless (Tier-M) | C004: per-location schema slots. Query params are flattened into JSON Schema; required array → schema-level required. |
parameter (in=path) |
pathItem.parameterSchema (allOf with uriTemplate vars) |
Lossless | C005 + C004: path params extracted from the uriTemplate reverse-parse + merged with any path-level parameterSchema. |
parameter (in=header) |
pathItem.parameterSchema.properties (under a reserved headers key OR a C004 cross-type envelope if value-equality needed) |
Conditional lossy | C004: header params live in the optional per-location header slot. If header-param is cross-type (e.g., equals a body field), the optional C004 cross-cutting envelope is used (requires Relative JSON Pointer, deferred to #73). Standard headers like Content-Type are handled separately (§12.4.5). Flagged if envelope needed. |
parameter (in=cookie) |
pathItem.parameterSchema (under reserved cookies key OR envelope) |
Conditional lossy | C004: cookies are a closed-vocabulary location. Flagged if envelope needed. |
style, explode, allowReserved |
(not preserved) | Lossy | C005: serialization metadata is omitted from the wire-format path/query. Replacement: constrain parameters to C005 Tier-M (single-segment, no explode, no reserved). If 3.x uses forbidden operators, FLAG (§12.5.2). |
Candidate @0.55: Parameter
style=form, explode=trueon a path parameter is EXPRESSIBLE in RFC6570 (multi-segment explode) but FORBIDDEN in C005 path-identity. The transformer must FLAG this and let a human decide whether to rewrite the path or move the param to query.
3.x: responses keyed by status code (string, e.g., "200", "4XX"); each can have multiple media types.
4.0 Mapping:
| 3.x Construct | 4.0 Target | Status | Rationale |
|---|---|---|---|
response (by status) |
response (by FRIENDLY NAME) |
Lossy (name synthesis) | C009: responses are keyed by user-friendly NAME, not status. Name is synthesized: "ok" for 200, "notFound" for 404, etc. If status is a pattern (e.g., "4XX"), synthesized name is "clientError". Non-deterministic collisions (e.g., two 404 responses with different contents) require human-given names. Flagged collision candidate (§12.5.3). Ceiling @0.55 (same as request-name lossy). |
responses.content (multiple media types) |
Multiple response entries (fan-out) |
Conditional lossy (name collision) | If 3.x has 200: {content: {application/json: {...}, application/xml: {...}}}, 4.0 emits two named responses: e.g., "okJson" and "okXml". Same collision risk as requests (§12.4.2); flagged. Alternatively, emit a SINGLE response with multiple contentType slots (C016 equivalent-media-types affordance, §12.4.5). |
response.default |
apiResponses or pathResponses (with status="default") |
Lossless | The C004 query-slot model includes an optional DEFAULT slot for error responses. Status="default" is synthesized. Flagged advisory (scope TBD). |
response.headers |
Optional per-location header slot (C004) OR cross-cutting envelope (C004) |
Partial lossy | Response headers are a lower-priority modeling element (C008 out-of-scope for core). Flagged deferred (@#108). |
response.links |
(preserved as optional advisory) | Conditional lossy | C012 #58 resolves links as operation-orchestration hints, kept but optional. Removed completely only by human choice (@C012 L35: "removal pole explicitly weighed and rejected"). Flagged advisory. |
Candidate @0.55: The
responsesmap keying by status is LOSSY because identical multiple responses per status (e.g., two different 404 schemas for different content types or request contexts) collide in the friendly-name synthesis and require manual disambiguation.
3.x: Content-Type is nested in requestBody.content[mediaType] and response.content[mediaType].
4.0 Mapping:
| 3.x Construct | 4.0 Target | Status | Rationale |
|---|---|---|---|
requestBody.content[mediaType].schema |
request.contentType + request.contentSchema |
Lossless | C004: each request flattens content-type + schema to top-level fields. If 3.x has multiple content types in a single requestBody, FAN-OUT to multiple requests (collision candidate). |
response.content[mediaType] |
response.contentType + response.contentSchema |
Lossless | Same as request. Multiple media types fan-out or use C016 equivalent-media-types affordance. |
Media-type parameters (e.g., application/json; charset=utf-8) |
(delegated to content/media-type registry) | Deferred | C016: media-type parameters (charset, etc., RFC 6838) are expressed via the C004 content model's registry approach. Exact mechanism deferred to the content model. Flagged advisory. |
content[mediaType].encoding (multipart/form-data) |
(not preserved) | Lossy | Serialization metadata (encoding rules) is deferred to #108 (header model) or dropped. Flagged lossy. |
Candidate @0.6: The fan-out of multiple equivalent content types (
text/jsonvsapplication/json) is optional. C016 allows emitting multiplecontentTypevalues in a single response/request to avoid duplication, but this affordance is lower-priority and flagged informational.
3.x: components.schemas is a map of JSON Schema objects (Draft 7 or 2020-12 in 3.1).
4.0 Mapping:
| 3.x Construct | 4.0 Target | Status | Rationale |
|---|---|---|---|
components.schemas[name] |
components.schemas[name] (by identity) |
Lossless | C009: component identity is the map key (stable NAME). Schema objects are preserved as-is (Stage 0 upgrades dialect if 3.0.x). |
schema.xml |
(not preserved) | Lossy | OpenAPI 3.x xml object controls XML serialization (element name, namespace, etc.). 13 instances in petstore corpus, ZERO ADR target → likely lossy, no 4.0 equivalent. Flagged lossy. Ceiling @0.5 (genuine gap). |
$ref (internal, e.g., #/components/schemas/Pet) |
$ref (by C013 typed-component-name rule) |
Lossless | C013: typed-component-name identity (the map key is the canonical anchor). Refs resolve by stable component NAME. Fragment syntax deferred to C013 byte-grammar. |
$ref (external) |
(import/bundle, or $ref+imported) | Partial lossy | C013: imports are optional. Single-file 3.x documents upgrade cleanly. Multi-file documents require Stage 3 human decision: inline, import, or bundle. Flagged human-required. |
discriminator (object with propertyName + optional mapping) |
(stripped to advisory hint) | Lossy | C012 #57: discriminator is REMOVED as a load-bearing validation keyword. The propertyName hint is retained optionally for advisory purposes; mapping is DROPPED (not preserved). Runtime validation uses standard JSON Schema 2020-12 (const per branch, or if/then/else, or deferred to #73 replacement vocabulary). Flagged lossy. |
discriminator.mapping |
(not preserved) | Lossy | Stripped, not emitted. Flagged lossy. |
oneOf / anyOf / allOf |
(preserved as-is) | Lossless | JSON Schema composition keywords are canonical in 2020-12. |
| 3.x Construct | Status | Flag | Rationale |
|---|---|---|---|
parameter.style |
Lossy | warning | C005: style is non-emittable in 4.0. Query params use form (key-value); path params are single-segment literals or simple vars. Forbidden styles (matrix, label, reserved, etc.) are flagged. |
parameter.explode |
Lossy | warning | C005: explode=true on multi-valued params is forbidden in path-identity. Flagged. |
parameter.allowReserved |
Lossy | info | C005: reserved chars are forbidden in Tier-M. Flagged advisory. |
header.content (media-type nested in header param) |
Lossy | warning | OpenAPI 3.0.x allows header.content[mediaType].schema (nested). No 4.0 equivalent. Flagged lossy. |
parameter.content |
Lossy | warning | OpenAPI 3.0.x allows parameter.content[mediaType].schema. No 4.0 equivalent (C004 handles single schema per location). Flagged lossy. |
requestBody.required |
Lossless | — | Preserved as request.bodyRequired (optional field, defaults false). |
3.x Input:
paths:
"/pets":
parameters:
- name: status
in: query
style: form
explode: true
schema:
type: array
items:
type: string
4.0 Output (Stage 1):
paths:
"": # root path, uriTemplate identity
parameterSchema:
type: object
properties:
status:
type: array
items:
type: string
required: [status]
ReviewLedger Entry (Stage 2):
{
"pointer": "#/paths/~1pets/parameters/0",
"construct": "parameter.style",
"flag": "lossy",
"adrRef": "C005",
"rationale": "style=form, explode=true on array param is form-query-permitted but omitted from 4.0 wire syntax; C005 assumes form for query, honoring the serialization intent",
"sourceValue": { "style": "form", "explode": true },
"severity": "info"
}
3.x Input:
paths:
"/files/{path*}":
parameters:
- name: path
in: path
style: matrix
explode: true
ReviewLedger Entry (Stage 2):
{
"pointer": "#/paths/~1files~1{path*}",
"construct": "uriTemplate + parameter.style=matrix",
"flag": "lossy",
"adrRef": "C005",
"rationale": "multi-segment explode (path*) is FORBIDDEN in C005 Tier-M path-identity; injective reverse-parse not guaranteed; human must rewrite as separate path segments or move to query",
"sourceValue": { "operator": "explode", "name": "path" },
"severity": "error"
}
Stage 3 (Human Decision Required):
Option A: Rewrite path to /files/{dir}/{file} (two separate single-segment params).
Option B: Move to query: /files?path=... (loses path-semantic identity).
3.x Input:
paths:
"/pets":
post:
operationId: createPetJson
requestBody:
content:
application/json:
schema: { ... }
responses:
201: { ... }
post:
operationId: createPetXml
requestBody:
content:
application/xml:
schema: { ... }
4.0 Output (Stage 1 - Fan-out):
paths:
"":
requests:
createPetJson:
method: post
contentType: application/json
contentSchema: { ... }
createPetXml:
method: post
contentType: application/xml
contentSchema: { ... }
ReviewLedger Entry:
{
"pointer": "#/paths/~1pets/post/operationId",
"construct": "operationId_collision_candidate",
"flag": "collision_candidate",
"adrRef": "C003",
"rationale": "Two requests with method=post at the same path differ only by contentType; synthesized names createPetJson / createPetXml assume operationId suffix convention; C003 matcher would collide on method+path; human verify request-naming strategy",
"sourceValue": ["createPetJson", "createPetXml"],
"severity": "warning"
}
3.x Input:
components:
schemas:
Pet:
oneOf:
- $ref: "#/components/schemas/Dog"
- $ref: "#/components/schemas/Cat"
discriminator:
propertyName: petType
mapping:
dog: "#/components/schemas/Dog"
cat: "#/components/schemas/Cat"
4.0 Output (Stage 1):
components:
schemas:
Pet:
oneOf:
- $ref: "#/components/schemas/Dog"
- $ref: "#/components/schemas/Cat"
# discriminator removed; no mapping preserved
ReviewLedger Entry:
{
"pointer": "#/components/schemas/Pet/discriminator",
"construct": "discriminator",
"flag": "lossy",
"adrRef": "C012",
"rationale": "C012 #57 removes discriminator as a load-bearing keyword; mapping is dropped; runtime validation uses JSON Schema 2020-12 const per branch; human verify discriminator hint needed as advisory",
"sourceValue": { "propertyName": "petType", "mapping": { "dog": "...", "cat": "..." } },
"severity": "warning"
}
Status: OPEN GAP
Candidate @0.6: OpenAPI 3.x
callbacksand 3.1webhooksare UNRESOLVED by any ADR and absent from the 4.0 example corpus. This is a genuine frontier gap, not silently dropped.
Transformer Handling:
{construct: "callbacks", flag: "deferred", adrRef: "none", rationale: "No ADR target; gap in the Candidate; routes to a separate Wave-D frontier decision"}Candidate @0.55: C005 resolves the uriTemplate PARSING grammar but DEFERS the query-string-is-not-JSON deserialization detail (C005 §5 / C004 §8). The transformer does not emit query-to-slot mapping detail beyond flattening 3.x parameter.query to the 4.0 per-location
queryslot.
Transformer Handling:
parameterSchema.properties[name].info-level flag.contentType values in a single response/request do not require type-specific query mapping)."Candidate @0.6: C013 DEFERS the exact JSON Schema dialect version pin (2020-12 vs. next-draft). The transformer EMITS the dialect slot but does NOT hard-code 2020-12.
Transformer Handling:
{jsonSchemaDialect: "urn:placeholder"} with a Stage 3 checklist: "Set jsonSchemaDialect version per C013 decision (currently deferred; default to 2020-12 as a conservative interim)."Candidate @0.55: C013 DEFERS the exact fragment-pointer syntax for references (#/components/schemas/Pet vs. pet vs. urn:path). The transformer emits the name-based rule (C013 identity = map key) but does NOT finalize the fragment syntax.
Transformer Handling:
#/components/schemas/Pet).The naive estimate of "70–80% automation" is audit-corrected downward:
Five over-claimed lossless rows are downgraded to lossy-with-receipt:
Definition: A conformant-partial upgrader produces a valid 4.0 document that conforms to all Candidate ADRs where they have decided. Where an ADR defers (byte-grammar, dialect version, collision policy, wire-serialization), the transformer emits a SAFE FORM (inline default, no hoisting, no hard-coded version) and flags the slot for human decision.
Benefits:
Costs:
3.x Input (simplified petstore):
openapi: 3.1.0
info:
title: Swagger Petstore
version: 1.0.0
paths:
/pets:
parameters:
- name: status
in: query
schema:
type: string
style: form
get:
operationId: listPets
responses:
"200":
description: OK
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Pet"
/pets/{petId}:
parameters:
- name: petId
in: path
required: true
schema:
type: string
get:
operationId: getPet
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
"404":
description: Not Found
components:
schemas:
Pet:
type: object
properties:
id:
type: string
name:
type: string
required: [id, name]
4.0 Output (Stage 1 + 2):
openapi: 4.0.0
info:
title: Swagger Petstore
version: 1.0.0
paths:
"{status}": # query-in-uriTemplate (C005 Tier-Q form-query)
parameterSchema:
type: object
properties:
status:
type: string
requests:
listPets:
method: get
responses:
ok:
status: 200
contentType: application/json
contentSchema:
type: array
items:
$ref: "#/components/schemas/Pet"
"{petId}":
parameterSchema:
type: object
properties:
petId:
type: string
required: [petId]
requests:
getPet:
method: get
responses:
ok:
status: 200
contentType: application/json
contentSchema:
$ref: "#/components/schemas/Pet"
notFound:
status: 404
components:
schemas:
Pet:
type: object
properties:
id:
type: string
name:
type: string
required: [id, name]
ReviewLedger (Stage 2):
[
{
"pointer": "#/paths/~1pets/parameters/0",
"construct": "parameter.style",
"flag": "info",
"adrRef": "C005",
"rationale": "Query parameter style=form is preserved as the C005 Tier-Q form-query default; info-level advisory",
"sourceValue": { "style": "form" },
"severity": "info"
}
]
Stage 3 Checklist:
None (all parameters Tier-M, no callbacks, no discriminators, no forbidden operators).
Result: Valid 4.0 document, upgraded from 3.1 with zero human intervention (because this example contains only canonical patterns).
Pseudo-code interface:
function transform3xTo4(doc3x, options = {}) {
const stage0 = normalize(doc3x, options.dialectDefault || "2020-12");
const [stage1, ledger2] = transformStructural(stage0);
const stage3Checklist = detectHumanRequired(stage1, ledger2);
return {
doc4: stage1,
reviewLedger: ledger2,
stage3Checklist: stage3Checklist,
honestCoverage: stage1 ? stage2Ledger.length == 0 ? "100%" : "partial" : "failed"
};
}
If the user provides a collision-policy option:
transform:
collisionPolicy: "prefix-by-contentType" # Alternative: "manual-name-required"
The transformer auto-completes Stage 3 for multi-content-type cases, using the supplied policy (but only if that policy has been ratified by the SIG; Candidate policies are not auto-applied).
Full automation is foreclosed. The transformer is a SEMI-AUTOMATED tool; the name "automated" in Principle 6 is re-scoped to mean "deterministic mechanical green-path + auditable lossy-case flagging," not one-click full automation.
Ceilings are Originated. Every claim in this section inherits the full revisability of its source ADRs (C003–C016). A single new SIG witness (a ratification, or a contradicting ADR) should trigger re-verification.
Deferred items gate finalization. The exact emitted reference syntax, query→data-model deserialization, and the collision/precedence policy are all open (C005, C004, C003). Until those land, the transformer's exact wire-syntax output cannot be finalized; Stage 3 human review is the honest surface area.
Callbacks/webhooks are a genuine gap. Not silently dropped; explicitly flagged as out-of-scope for this resolution and routed to a future Wave-D frontier decision.
Invocations inherit candidate-fork constraints. This transformer is NOT a SIG-ratified tool; it is a Candidate-fork artifact. Deployment should include a disclaimer and route users to the SIG for production guidance.
This section defines the relationship between the Document Object Model (DOM), the Abstract Description Surface (ADA), and the conformance model that tooling consumes. The DOM is the parsed, in-memory structure of a Candidate v4.0 document—one instantiation of the declarative schema. The ADA is the abstract layer that tools inspect to determine operation identity, collision status, and request-response binding; it is not mandatory for either specification authors or tools to produce or consume explicitly, but when tools do perform signature analysis or request routing, they operate on ADA-shaped data.
The Candidate v4.0 spec does not define normative conformance tiers (e.g., Strict/Acceptable/Open); instead, it defines a flat specification with optional constructs. Feature breadth and validation depth are routed to overlays, extensions, and profiles.
The DOM is the in-memory tree produced by parsing a Candidate v4.0 YAML or JSON document. It follows the collection-as-maps structuring model (C009):
| Layer | Structure | Notes |
|---|---|---|
| Root | Document |
Contains openapi, info, servers, paths, components, etc. |
| PathItem | PathItem (keyed by RFC 6570 uriTemplate string) |
Children: pathItem-level parameters, servers, per-HTTP-method operations |
| Operation | Operation (keyed by HTTP method: GET, POST, DELETE, etc.) |
Children: parameters, requestBody, responses, security, tags, etc. |
| Parameter Slot | Per-location map (query, path, header, cookie) within pathItem or Operation | Parameters keyed by name; collected by location per C004 |
| Request Body | RequestBody |
Contains media-type-keyed schema and encoding; participates in signature analysis (C003) |
| Response | Response (keyed by status code or default) |
Contains media-type-keyed schema; scope precedence resolved at runtime per C003/C011 |
Optional Inheritance (shared wrapper):
A pathItem or Operation MAY declare an optional shared map that carries name-based map-merge inheritance down its children (C012 #116 @0.55). Merge semantics (override vs. accumulate) are declared explicitly; inheritance does not apply to the responses field per this Candidate (response-level inheritance deferred to C012 #17b @0.62).
paths:
/users/{id}:
shared:
parameterSchema:
query:
type: object
required:
- api-version
properties:
api-version:
type: string
requests:
getUser:
method: get
operationId: getUser
parameterSchema:
query:
type: object
properties:
fields:
type: string
responses:
ok:
status: 200
deleteUser:
method: delete
operationId: deleteUser
responses:
deleted:
status: 204
Above, both get and delete inherit the api-version parameter.
The ADA is the layer tools inspect to resolve operation identity and collision status. It is not a serialized format; tools construct it from the DOM when needed. The ADA exposes:
A signature identifies an operation by composing zero or more of these aspects:
| Aspect | Type | Participation | Notes |
|---|---|---|---|
method |
HTTP verb | Always | GET, POST, DELETE, etc. |
uriTemplate |
RFC 6570 template string | Always | Path from pathItem key; variable parts treated distinctly from literals |
queryAspect |
Query parameters & literal values | Optional | Variables vs. literals distinguished; affects signature uniqueness (C005) |
contentTypeAspect |
Request Content-Type | Optional | From requestBody mediaTypes; used only if operation is POST/PUT/PATCH |
headerAspect |
Specified request headers | Optional | Deferred: full header signature model depends on C012 #108 (contested) |
bodyShapeAspect |
JSON Schema discriminant properties | Optional | Demoted to runtime last-resort (D1 from C003); not guaranteed statically determinable |
Confidence ceilings:
When two operations share a method and uriTemplate path (but differ in content-type, headers, or body shape), the ADA computes a collision verdict:
type CollisionVerdict
= "provably-disjoint" // Operations are statically distinguishable
| "provable-collision" // Operations definitely overlap (ambiguous)
| "not-statically-determinable"; // Runtime dispatch required (D1 case)
The verdict is a best-effort exposure; it is not a validation gate. The ADA MUST surface the verdict so tools can decide whether to:
What to do about a collision (invalid vs. precedence vs. priority vs. strict-mode) is left OPEN and deferred to C003 resolution step (#20) and the per-operation collision-resolution policy (C003 #c, @>=0.5).
⚠ Candidate @0.5–0.6: Collision analysis and the tiebreaker policy are contested. The frame-level decision (detect-and-tolerate as a reporting mechanism) is high-confidence; the choice of what tooling does with a collision is OPEN.
The ADA distinguishes two concerns:
Matching (request → operation): Given an HTTP request (method, path, headers, body), identify which operation(s) it corresponds to. The signature aspects guide this. Collision analysis reports ambiguity.
Correlating (request ↔ response): Given an operation and a request, determine which response schema applies (status code + content-type). Status-code specificity and media-type specificity use runtime tiebreakers.
These are not unified. The signature (C003) resolves matching; response scoping and precedence (C012 #17b @0.62) resolve correlating.
When tools construct the ADA, they extract signatures as follows:
interface Signature {
// Literal and variable path components
method: string; // "GET", "POST", etc.
uriTemplate: string; // "/users/{id}" (RFC 6570 syntax)
// Optional aspects
queryVariables?: string[]; // Names of distinguishing query params (not values)
contentType?: string; // e.g., "application/json"
headerNames?: string[]; // Header names used for dispatch (DEFERRED)
bodyDiscriminant?: {
propertyName: string;
possibleValues?: string[]; // If statically determinable
};
}
interface OperationIdentity {
operationId?: string; // From the Operation object (optional in 4.0)
signature: Signature;
collisionVerdict: CollisionVerdict;
}
Given two POST operations with overlapping paths:
paths:
/users/{id}/emails:
requests:
addUserEmail:
method: post
operationId: addUserEmail
contentType: application/json
contentSchema:
type: object
properties:
action:
enum: ["add"]
responses:
created:
status: 201
removeUserEmail:
method: post
operationId: removeUserEmail
contentType: application/json
contentSchema:
type: object
properties:
action:
enum: ["remove"]
responses:
ok:
status: 200
The ADA produces:
{
"operations": [
{
"operationId": "addUserEmail",
"signature": {
"method": "POST",
"uriTemplate": "/users/{id}/emails",
"contentType": "application/json",
"bodyDiscriminant": {
"propertyName": "action",
"possibleValues": ["add"]
}
},
"collisionVerdict": "provable-collision"
},
{
"operationId": "removeUserEmail",
"signature": {
"method": "POST",
"uriTemplate": "/users/{id}/emails",
"contentType": "application/json",
"bodyDiscriminant": {
"propertyName": "action",
"possibleValues": ["remove"]
}
},
"collisionVerdict": "provable-collision"
}
]
}
The collisionVerdict is "provable-collision" for both because they share method, path, and content-type; distinguishing them requires runtime inspection of the action property. Tools MAY route requests using the action value or reject this document as ambiguous.
The Candidate v4.0 does not define normative conformance tiers (Strict / Acceptable / Open). Instead:
A conformant Candidate v4.0 document MUST:
name to each parameter and reference it consistently within a scope (C009)shared wrapper's resolution algorithmThe following are optional:
operationId (coexists in Candidate v4.0; fate deferred to C003 step #20)shared parameters (C012 #116 @0.55)shared (DEFERRED to C012 #17b)signature enumeration per operation (D2 from C003: optional authoring affordance only; normalized away by ADA)⚠ Candidate @0.55: The
sharedinheritance wrapper is optional; migration-default remains flat-dict per-parameter. Merge semantics must be declared per-property; the general algorithm is defined normatively, but the specific merge behavior (override vs. accumulate per field) is part of the optional feature and tool-dependent.
Tools and communities MAY define non-normative profiles (e.g., "AsyncAPI Lite", "GraphQL-Interop Profile") that recommend or require specific subsets of optional constructs. Profiles are labeled via x-profile or similar extensions and serve as authoring guidance, not validation rules.
⚠ Candidate @0.6: Completeness levels (#18) and profile vocabulary are deferred to a future step (C012 #76). This Candidate does not specify a labeling vocabulary; communities may adopt their own.
A tool that claims to conform to Candidate v4.0 (e.g., code generator, API gateway, client library) SHOULD:
shared maps if present).Tools are not required to surface the ADA explicitly; internal ADA-like representation is sufficient. However, tools that do expose an ADA-shaped interface (e.g., programmatically queried operation identity or collision analysis) MUST follow the schema defined in §13.4.1.
⚠ Candidate @>=0.85 (ADA concept) / @>=0.5–0.6 (collision tiebreaker policy): The ADA shape is high-confidence. The action a tool takes upon a collision is left OPEN and DEFERRED (C003 #c). This spec defines what the ADA must expose; deployment and tool design decide what to do with the collision verdict.
The following are explicitly left OPEN and DEFERRED pending future work:
| Item | Gated By | Notes |
|---|---|---|
| Collision-resolution policy | C003 step #20 | What to do (invalid vs. precedence vs. priority vs. strict-mode) is a separate decision, not part of this ADA exposure. |
| Header-aspect signatures | C012 #108 (contested) | Full header-model for routing/matching is deferred; currently only path/method/content-type are mature. |
| Response-level inheritance | C012 #17b @0.62 | The shared map does not yet reach responses; response-scope composition is partially deferred. |
| Profile vocabulary & labeling | C012 #76 @0.6 | Non-normative profiles and their naming scheme are DEFERRED. |
| Completeness authoring guidance | C011 #18 (deferred) | Authoring boilerplate, required-minimum-schema fields, and hand-author ergonomics are a separate axis (distinct from tiers) and deferred. |
| operationId fate | C003 step #20 | Whether operationId is mandatory, optional, or deprecated in 4.0 is OPEN; it coexists for now. |
Declared signature array |
C003 D2 @0.5–0.6 | Optional authoring affordance; normalization happens at the DOM→ADA boundary. |
| JSON-Schema body discrimination | C003 D1 @0.5–0.6 | Demoted to runtime last-resort; static guarantees are not made (flagged as contested). |
Serialization and deserialization of the ADA is not part of this specification. Tools construct the ADA from the DOM in memory. The DOM itself is serialized as YAML or JSON per the document rules (C001); the ADA remains an internal/API-level construct.
Desugaring rule: If a document uses optional shorthand (C012 #60/61 @0.6), the DOM parser MUST desugar it into the normative expanded form before constructing the ADA. Example:
# Shorthand (optional)
parameters:
- name: id
in: path
# Desugared (normative)
parameters:
path:
id:
name: id
in: path
Input document snippet:
openapi: 4.0.0-candidate
info:
title: Order API
version: "1.0"
paths:
/orders/{orderId}/items:
requests:
listOrderItems:
method: get
operationId: listOrderItems
parameterSchema:
path:
type: object
properties:
orderId:
type: string
format: int64
query:
type: object
properties:
includeDetails:
type: boolean
responses:
ok:
status: 200
addOrderItem:
method: post
operationId: addOrderItem
parameterSchema:
path:
type: object
properties:
orderId:
type: string
format: int64
contentType: application/json
contentSchema:
type: object
properties:
productId:
type: string
quantity:
type: integer
responses:
created:
status: 201
Resulting ADA (simplified):
{
"operations": [
{
"operationId": "listOrderItems",
"signature": {
"method": "GET",
"uriTemplate": "/orders/{orderId}/items",
"queryVariables": ["includeDetails"]
},
"collisionVerdict": "provably-disjoint"
},
{
"operationId": "addOrderItem",
"signature": {
"method": "POST",
"uriTemplate": "/orders/{orderId}/items",
"contentType": "application/json"
},
"collisionVerdict": "provably-disjoint"
}
]
}
Request routing:
GET /orders/42/items?includeDetails=true → matches listOrderItems (method + path + query-variable presence)POST /orders/42/items with JSON body → matches addOrderItem (method + path + content-type)Neither collision verdict fires because the methods differ; a tool can deterministically route both requests.
(Logically extends §4 Requests and §5 Responses. Resolves the gap surfaced by the upgrade capstone — see [C018].)
⚠ Candidate @0.55–0.6: Originated — the SIG never designed this; it is a clean-room extension of the existing machinery. Provisional.
A webhook is an incoming operation the described API receives but does not host at one of its own paths (the sender owns the URL). Webhooks are a top-level webhooks map keyed by a friendly name (§1, C009), each entry a Request/Response shape (§4–5) carrying a signature (§3).
| Field | Type | Required | Description |
|---|---|---|---|
webhooks |
Map[name → Webhook Object] | MAY | Incoming operations not bound to a server uriTemplate. |
A Webhook Object reuses the Operation shape (§1.4) — method, parameterSchema, contentType/contentSchema, responses — but has no uriTemplate key (the recipient does not own the URL).
webhooks:
newSpeaker:
method: post
contentType: application/json
contentSchema: { $ref: "#/components/schemas/speaker" }
responses:
ok: { status: 200 }
A callback is an out-of-band request the API sends in response to an operation, to a URL supplied at runtime. Each operation MAY carry a callbacks map keyed by a friendly name; each callback is keyed internally by a runtime expression (e.g. {$request.body#/callbackUrl}) whose value is a pathItem-shaped definition (§1.3). The runtime-expression key is resolved at runtime from request/response data — it is not part of the static signature matcher (§3; D1-consistent).
paths:
/subscriptions:
requests:
createSubscription:
method: post
callbacks:
onEvent:
"{$request.body#/callbackUrl}":
requests:
event: { method: post, contentSchema: { $ref: "#/components/schemas/event" } }
⚠ Deferred: the exact runtime-expression grammar (3.x used
{$request.body#/x}); and the broader async/event-driven/streaming space (AsyncAPI overlap) is out of scope for this candidate's HTTP core.
(Logically extends §4 Requests, §14 Callbacks & Webhooks. Resolves the operator-surfaced gap that cost is often incurred not when a route runs, but when a BACKGROUND EVENT fires — a Stripe webhook charges you, a cron job runs, a queue consumer processes. See [C024], [C025], [C026].)
⚠ Candidate @0.58 — ORIGINATED, no SIG witness. The OpenAPI SIG never designed cost annotation; this entire section ORIGINATES the
x-suluk-costvendor facet (aCostModelcarried on a Request) — reusing the C018 webhook machinery and theSulukRateLimit.keystrategy precedent. The Originated ceiling is ~0.58 (single-witness ledger facts, no independent corroboration). Everything here is a vendor extension in thex-suluk-*namespace — it adds no normative async/jobs/events object kind, and does not reopen C018's deliberately-deferred async scope. Ledger: [0cost-trigger.bn], [0cost-jobs.bn], [0cost-reconciliation.bn].
x-suluk-costA cost is described by four orthogonal axes on the x-suluk-cost facet (C024's handrews double-duty rule: never overload one axis with another's meaning):
| Axis | Field | Means | Static or Runtime |
|---|---|---|---|
| How it meters | basis (on each CostComponent) |
per-call, per-token, per-second, … — UNCHANGED by C024 (no per-event member is added) |
Static |
| When/what fires it | trigger (+ triggerRef) |
where the cost accrues — decoupled from the declaring op | Static |
| Who pays | attribution |
the principal charged when a third party fires the event with no live session | Runtime-only |
| Is the amount the real charge | reconciliationBasis (+ amountExpression, amountUnit) |
declared estimate vs the third party's actual invoiced amount | Static enum, runtime amount |
The axes are deliberately disjoint: basis is HOW, trigger is WHEN, attribution is WHO, reconciliationBasis is IS-IT-REAL. A change on one MUST NOT require a change on another.
trigger axis — when/what fires the cost (C024, static)trigger is a static, closed string enum on the CostModel. It is strictly descriptive: it names where a cost accrues and asserts no event-channel, stream, or delivery-protocol semantics — that fence is what keeps it orthogonal to C018's deferred async core.
trigger value |
Cost accrues when… |
|---|---|
synchronous (default) |
this operation's own route runs (backwards-compatible; zero migration) |
webhook-received |
an incoming webhook (§14, C018) fires |
scheduled |
a scheduled / cron job runs |
queue-consumed |
a queue consumer processes a message |
callback-completed |
an out-of-band callback (§14, C018) completes |
synchronous means every pre-C024 declaration is unchanged — no migration is required.triggerRef is a by-name handle (§1, C009) to the webhook / callback / operation whose firing accrues the cost.trigger is locally decidable from the document alone: a linter, a docs renderer, or costAudit decides "this cost is non-synchronous" and "what fires it" with no request issued. (This satisfies the hudlow static-decidability lens @0.78.)x-suluk-cost (and its trigger) at zero new object kind. costAudit / annotateCosts / costTable iterate webhooks alongside paths.webhooks:
paymentSucceeded: # a C018 webhook IS a Request — it carries x-suluk-cost directly
method: POST
contentType: application/json
contentSchema: { $ref: "#/components/schemas/stripeEvent" }
x-suluk-cost:
components:
- source: stripe
basis: per-request
microUsd: 2900 # all money is integer micro-USD (1 USD = 1_000_000 µ$)
estimateMicroUsd: 2900
trigger: webhook-received # NOT synchronous: this cost fires on the inbound event
triggerRef: paymentSucceeded # C009 by-name handle to what fires it
responses:
ok: { status: 200 }
attribution axis — who pays (C024, runtime-only)When a third party fires the event with no live session, the principal cannot be read from the caller. attribution declares a strategy the runtime resolves a concrete principal from, modeled on SulukRateLimit.key:
strategy |
Resolves the principal from… |
|---|---|
session |
the live caller (the existing synchronous path) |
event-expression |
the event payload, at runtime, via a C018 runtime-expression |
job-stamped |
the job's own carried principal |
x-suluk-cost:
components: [ { source: stripe, basis: per-request, microUsd: 2900 } ]
trigger: webhook-received
attribution:
strategy: event-expression
expression: "{$event.body#/customer}" # RUNTIME-ONLY — never enters the static matcher
trust: verified # honored as authoritative only behind a verified signature
expression (a C018 runtime-expression, e.g. {$event.body#/customer}) is runtime-resolved and NEVER enters the static signature matcher — D1-consistent, exactly as C018 walls callback runtime-expression keys (§14.2). This is the same wall the static profile relies on throughout this candidate.trust field (verified | unverified-payload) records whether the attribution input is authentic. An event-expression read off an unverified payload is attacker-controllable.Fail-loud disciplines (costAudit). A background cost MUST surface, never silently bill zero:
| Finding code | Raised when | Severity |
|---|---|---|
unattributed-background-cost |
a non-synchronous cost resolves no principal (no attribution, or event-expression with no expression) — it would bill to the @unattributed sentinel |
warn |
unverified-attribution |
an event-expression attribution's trust is not verified — the payload is attacker-controllable |
warn |
The @unattributed sentinel exists so that an unresolved background cost is billed to nobody but logged loud — never a silent zero. A CostEvent carries a dedupeKey so at-least-once delivery does not double-count a charge.
x-suluk-jobs map — a home for cron/queue work (C025)A scheduled cron tick or a queue-consumed drain has no inbound Request — it cannot live in paths (no uriTemplate) or webhooks (those are incoming HTTP operations, each with a Request). C025 gives it a first-class home: a top-level x-suluk-jobs name-keyed map (§1, C009) of SulukJob entries.
A SulukJob is non-HTTP background work. It carries no Request/Response (there is no HTTP exchange); its static fields are locally decidable, and it carries the same advisory x-suluk-* facets an operation does — notably x-suluk-cost (with a matching trigger) and x-suluk-source.
| Field | Type | Required | Description |
|---|---|---|---|
trigger |
"scheduled" | "queue-consumed" |
YES | the non-HTTP trigger that fires this job |
schedule |
string | NO | for scheduled: a cron expression, statically declared (e.g. "0 0 * * *") |
queue |
string | NO | for queue-consumed: the queue name the consumer drains |
summary |
string | NO | short human-readable label |
description |
string | NO | longer markdown description |
x-suluk-source |
SulukSource | NO | advisory provenance — where in source this job was projected from |
x-suluk-cost |
CostModel | NO | the job's declared cost, read by @suluk/cost |
x-suluk-jobs:
nightlyReindex:
trigger: scheduled
schedule: "0 0 * * *" # static cron string — locally decidable
summary: Rebuild the search index
x-suluk-cost:
components: [ { source: compute, basis: per-second, microUsd: 14 } ]
estimateMicroUsd: 50400
trigger: scheduled # matches the job's own trigger
attribution: { strategy: job-stamped }
invoiceDrain:
trigger: queue-consumed
queue: invoices # the queue the consumer drains
x-suluk-cost:
components: [ { source: stripe, basis: per-request, microUsd: 2900 } ]
trigger: queue-consumed
attribution: { strategy: job-stamped }
@suluk/cost walks jobs: eachJob feeds a unified costLoci (paths + webhooks + jobs) into costAudit / costTable, so a job's cost is declared, audited (the same fail-loud unattributed-background-cost discipline), and tabled like any other locus.@suluk/reference rolls a job's (deferred) cost into costRollup — it counts toward both the total and the deferred tally.Disjoint from webhooks (the contrarian lens, C025). A webhook is an incoming HTTP operation (has a Request, lives in webhooks); a job is non-HTTP (a cron tick / queue drain, no Request). The two loci do not overlap and do not duplicate.
Whose job (the handrews lens, C025). Cost and jobs stay a vendor extension. core gains only a structural SulukJob shape — no new normative control keyword — reusing C009 named maps and the x-suluk-* namespace. The DOM→ADA request matcher never reads x-suluk-jobs (D1).
reconciliationBasis axis — declared estimate vs actual charge (C026)C024/C025 let a cost declare it accrues on a background event and who pays — but the recorded amount was still the operator's declared estimate. The real charge a third party makes when the event fires differs: proration, tax, partial refunds, chargebacks. C026 adds the fourth orthogonal axis to close this:
reconciliationBasis: "declared-estimate" (default) | "payload-reconciled".payload-reconciled cost reads the actual charged amount from the event at runtime via amountExpression (a C018 runtime-expression, e.g. {$event.body#/amount}), interpreted in a declared amountUnit (micro-usd default | cents for Stripe → ×10 000 | usd → ×1 000 000).CostEvent.totalMicroUsd then becomes the real invoice line (proration/tax/refund included), and CostEvent.reconciled is set true.x-suluk-cost:
components: [ { source: stripe, basis: per-request, microUsd: 2900 } ]
estimateMicroUsd: 2900 # the GUESS
trigger: webhook-received
attribution: { strategy: event-expression, expression: "{$event.body#/customer}", trust: verified }
reconciliationBasis: payload-reconciled
amountExpression: "{$event.body#/amount_received}" # RUNTIME-ONLY — reads the ACTUAL charge
amountUnit: cents # Stripe charges in cents → ×10_000 to µ$
reconciliationBasis enum + amountUnit) is locally decidable (hudlow); the amountExpression is runtime-only — never the static matcher, exactly as attribution.expression is walled (§15.3).@suluk/cost runtime: reconciledAmount + eventCostEvent apply it — the actual charge replaces the estimate.Fail-loud (costAudit). A payload-reconciled cost with no amountExpression raises reconciliation-incomplete (warn): the actual charge can't be read, so it falls back to the estimate — surfaced, not silent.
The lifecycle of one background charge, from declaration to recorded event:
x-suluk-cost: a trigger (when), an attribution.strategy (who), and a reconciliationBasis (is-it-real). costAudit checks all three statically and emits the fail-loud findings above.attribution (session / event-expression / job-stamped); an event-expression is honored as authoritative only when trust: verified.reconciliationBasis: payload-reconciled, reconciledAmount reads amountExpression in amountUnit and uses the actual charge; otherwise the estimateMicroUsd stands.eventCostEvent emits a CostEvent: principal (or @unattributed), trigger, dedupeKey (so at-least-once delivery does not double-count), reconciled (true iff the amount is the third party's actual charge), breakdown, and totalMicroUsd.Reference-docs propagation is free. Everything rides the Request-attached (or job-attached) x-suluk-cost facet that Scalar / Swagger already render (and that survives a 3.1 downgrade). @suluk/reference walks paths + webhooks + x-suluk-jobs, rolls deferred costs into costRollup ({ priced, undeclared, totalMicroUsd, deferred }), and shows a charged on: <trigger> badge (with the triggerRef in parentheses when present).
There is no 3.x antecedent — cost annotation, deferred-job description, and reconciliation are Originated for this candidate. The mapping is therefore additive-only:
| OpenAPI 3.x | Suluk v4.0 | Notes |
|---|---|---|
| (no cost concept) | x-suluk-cost on a Request |
Originated; a vendor extension that survives the 3.1 downgrade as an unrecognized x-* key |
Vendor x-* on a webhook |
x-suluk-cost.trigger: webhook-received + triggerRef |
reuses the C018 webhook-as-Request shape; zero new object kind |
| (no cron/queue description) | x-suluk-jobs map of SulukJob |
Originated; no normative async kind — drops whole on a 3.1 downgrade, nothing else moves |
| (no actual-charge linkage) | reconciliationBasis: payload-reconciled + amountExpression |
Originated; the static enum survives the downgrade, the runtime expression is opaque to 3.1 |
A 3.1 downgrade drops x-suluk-jobs whole and reduces every x-suluk-cost to an unrecognized vendor key; the underlying paths and webhooks survive as the ordinary operations they always were. No normative content is lost because none was added.
Everything in this section is Originated at ~0.58 (single-witness ledger, no SIG ratification) — treat every claim as provisional, capped, and revisable.
⚠ Candidate @0.58 — whole section is Originated. Cost annotation, the trigger/attribution/reconciliationBasis axes, and the x-suluk-jobs map are clean-room (no SIG design). Each is a vendor extension asserting no normative async/event-channel semantics; none adds a normative jobs/events object kind. ([C024]/[C025]/[C026]; [0cost-trigger.bn] cost_trigger_vendor_ext_not_normative_kind @0.6, [0cost-jobs.bn] cost_jobs_no_async_reopen_no_webhook_dup @0.6.)
⚠ Candidate @0.55 — attribution is runtime-only, NOT statically enforced. attribution.strategy is DECLARED; the runtime resolves the principal. The attribution.expression (and the C026 amountExpression, and the idempotencyKey) are C018 runtime-expressions that NEVER enter the static matcher — they are reused as runtime values only, never promoted into the static signature. The C018 runtime-expression grammar itself remains DEFERRED (§14.2). ([C024]; [0cost-trigger.bn] cost_attribution_runtime_strategy_out_of_matcher @0.55.)
⚠ DEFERRED — the no-session Principal model (Open-Decision #5). The event-expression and job-stamped strategies (charging when there is no live caller) sit at a lower ceiling pending the Principal-model. What a resolved principal is across the no-session strategies is not settled here. ([C024]; [0cost-trigger.bn].)
⚠ DECLARED, not enforced — the fail-loud findings are advisory audit output. unattributed-background-cost, unverified-attribution, and reconciliation-incomplete are costAudit warnings (severity warn), surfaced by tooling — they are not schema-enforced gates and do not block a document. The @unattributed sentinel bills to nobody but logs loud; an unverified attribution is surfaced, not rejected; a reconciliation-incomplete cost falls back to the estimate. ([C024]/[C026]; [0cost-trigger.bn] cost_background_audit_fail_loud @0.58, [0cost-reconciliation.bn] cost_reconciliation_orthogonal_fail_loud @0.6.)
⚠ Candidate @0.58 — reconciliationBasis closes, but does not certify, the actual-charge gap. payload-reconciled makes CostEvent.totalMicroUsd the real invoice line at the C024-tracking Originated ceiling. The richer reconciliation surface C024 originally flagged — proration / tax / refund / chargeback as separate modeled dimensions — is folded into the single amountExpression-read amount, not separately enumerated. ([C026]; [0cost-reconciliation.bn] cost_reconciliation_basis_static_amount_runtime @0.58.)
Source ADRs: C024, C025, C026. Ledger:
0cost-trigger.bn,0cost-jobs.bn,0cost-reconciliation.bn.
x-suluk-agents)(Layers ON TOP of §4 Requests, §14 Webhooks, and the C025 jobs vendor map — a composition manifest over EXISTING operations, never a new operation kind. See [C027].)
⚠ Candidate @0.52 — ORIGINATED, clean-room, witness-thin. The SIG never specified an agent layer; this whole section is constructed by burhan from the C025
x-suluk-jobsprecedent and a three-vendor industry cowpath (Strands' agent-as-tool, the Claude Agent SDK's name-keyedagentssubagent map, OpenAI Agents' handoffs). It leans on the D1 matcher-invariance guarantee, which the ledger caps at 0.139 (d1_is_soft_guardrail_cap_0139, sole-witness soft guardrail), so the burden on anything touching the matcher is higher, not lower. Resolved by a 15-voice persona council (workflowwf_9e8712c7-871, 2026-06-12): near-unanimous (11 support-with-conditions, 3 reframe, 0 oppose). The headline "tiering solves the context problem" claim was adversarially refuted and reframed — see §16.8. Source: [C027]; ledgerplan/facts/0agents-d1.bn(claimsagents_toplevel_vendor_map,d1_agent_selector_safe,agents_no_request_value_selector,agents_determinism_declared_not_enforced,agents_recursion_lint_bounded_frontdoor,agents_ceiling_originated_low_context_reframed) +plan/facts/0agents-d1-witness.bn(d1_matcher_invariance_test_green,agents_cyclic_block_no_matcher_perturbation).
x-suluk-agents is (and what it is not)An agent is an LLM-orchestrated unit composed over an API: a routing-oriented description plus deterministic routes (by-name $refs into existing operations) and model-bearing skills (instruction bundles), optionally calling sub-agents. x-suluk-agents is an OPTIONAL top-level map that describes such compositions; it asserts no event channel, loop, or delivery protocol.
x-suluk-agents is a vendor extension in the x-suluk-* namespace (riding the C025 x-suluk-jobs precedent EXACTLY), NOT a normative kind. It is additionalProperties-legal under the document's existing [ext: x-${string} ] catch-all, so:
The four nouns are disjoint loci (pinned in CONTEXT.md): an agent (this section) is an LLM-orchestration manifest; a module (C021) is a contract-merge fragment; a job (C025) is non-HTTP background work; a webhook (§14) is an incoming operation. A route is a by-name $ref into an existing operation, never a parallel namespace.
| Field | Type | Required | Description |
|---|---|---|---|
x-suluk-agents |
Map[name → SulukAgent] | MAY | Composition manifest layered on top of the API. Sibling to x-suluk-jobs. The map key is the agent's stable wire-level identity (the emitted MCP-tool / OpenRouter-function id; C009 by-name, never by index). |
SulukAgent objectA SulukAgent carries no Request/Response and is NEVER consulted by the request→operation matcher (§16.4, D1).
| Property | Type | Required | Description |
|---|---|---|---|
description |
string | YES | Routing-oriented — the field the serving LLM selects on. A lint rejects empty/one-word values. |
scope |
[string] | NO | Static resource:action authz; the agent's complete reachable surface is statically enumerable from the document. |
skills |
Map[name → SulukSkillRef] | NO | Instruction bundles. Presence of model is the hard static skill-vs-route discriminator (§16.3). |
routes |
Map[name → SulukRouteRef] | NO | Deterministic routes: by-name $refs into existing operations. No model field, ever. |
agents |
Map[name → SulukAgentRef] | NO | By-name sub-agent refs (never inline). |
maxDepth |
number | conditional | REQUIRED whenever agents is non-empty (enforced by a lint, not the schema). A typed leaf is maxDepth 0 with agents {}. |
trustBoundary |
"untrusted" |
NO | Marks a tier whose retrieved / lower-tier content MUST NOT escalate scope or upgrade a figure's provenance. |
contextBudget |
{ tokens, basis: "estimate" } |
NO | Advisory per-tier context budget (basis is always estimate); fail-loud, never silent-zero. |
thinking |
{ maxRounds, budget? } |
NO | Within-agent iteration cap (C029). DECLARED-not-enforced; never read by the matcher. Out of scope for this section — see §17 (C029). |
x-suluk-agents:
conin:
description: >-
Construction-intelligence agent: turns messy project documents into
decision-grade cost/cashflow deliverables over a deterministic spine.
scope: [ "deliverables:generate", "library:read" ]
skills:
instructions:
provenance:
source: "https://conin.example/v1/instructions"
contentHash: "sha256-8f3c…"
version: "2026-06-12"
model: [ "anthropic/claude-opus-4", "openai/gpt-5" ]
tier: resident
routes:
generateDeliverable:
operationRef: "#/paths/v1~1deliverables/requests/generate"
guarantee: same-in-same-out
tier: resident
searchLibrary:
operationRef: "#/paths/v1~1library~1search/requests/search"
tier: cold-tail
agents:
retrieval:
ref: "#/x-suluk-agents/conin-retrieval"
maxDepth: 1
A SulukSkillRef is an instruction bundle. The PRESENCE of a model need is what makes an entry a skill (the system-text path) rather than a deterministic route — this is the load-bearing static partition, computable with zero requests.
Skill text is a provenance pointer, never inlined mutable prose. The served instructions are the single source of truth; a projected SKILL.md is GENERATED from provenance.source, and the contentHash binding makes drift tool-detectable and fail-loud (the one feature multiple council voices independently called genuinely missing today).
| Property | Type | Required | Description |
|---|---|---|---|
model |
[string] | NO¹ | Explicit OpenRouter model-id preference list, cheap→capable (the opt-out path). Structural-only — never read by the matcher. |
modelProfile |
enum | NO¹ | A named selection profile (tool-reliable | cheap-fast | balanced | max-reasoning | long-context | vision) resolved against the model catalog. |
modelPrefer |
{ intelligence?, cost?, speed?, context? } (0–3) |
NO¹ | Escape-hatch preference weights over the four author-facing axes. |
modelRequire |
{ needsStructured?, inputModalities?, minContext?, zdr? } |
NO | Explicit hard requirements the author adds. |
modelResolve |
"pinned" | "router" | "latest" |
NO | How the model is resolved from the survivor set (default pinned). Author surface only; never read by the matcher. |
tier |
"resident" | "cold-tail" |
NO | Static serving partition (§16.6). Absent ⇒ resident. |
whenToUse |
string | NO | Routing-oriented precondition prose (runtime-advisory; never a request-value selector — D1, §16.4). |
trust |
"author-declared" | "retrieved" |
NO | A retrieved skill MUST NOT escalate scope or upgrade a figure's provenance. |
scope |
[string] | NO | Static resource:action authz for this skill. |
provenance |
{ source, contentHash, version? } |
NO | Single source of truth + staleness binding. SKILL.md is generated from source, hashed to detect drift. |
¹ A skill expresses what it needs (via model/modelProfile/modelPrefer/modelRequire), not necessarily a frozen id; @suluk/models picks the best current model. The model-resolution surface (modelProfile/modelPrefer/modelRequire/modelResolve, and the zdr requirement) is governed by C029/C030 and is out of scope for this section beyond noting the seam: it is structural-only, never read by the matcher.
The discriminator is mechanical: an entry under
skillscarries (or implies) amodelneed; an entry underroutesnever carries amodel. That single, static, request-free distinction is the genuinely novel true property the construct ships (§16.8).
A SulukRouteRef is EXCLUSIVELY a by-name operationRef $ref into an EXISTING paths[*] / webhooks / x-suluk-jobs operation — never an inline re-declaration. Inlining is forbidden because it forks C009 operation identity and strands the operation on a 3.1 downgrade.
| Property | Type | Required | Description |
|---|---|---|---|
operationRef |
string | YES | A by-name $ref into an existing operation (e.g. #/paths/v1~1deliverables/requests/generate). Resolve-linted; a dangling ref fails burhan-converge. |
guarantee |
"same-in-same-out" | "idempotent" | "safe" |
NO | DECLARED determinism intent (advisory, unverifiable-by-schema). NOT enforced (§16.8). |
tier |
"resident" | "cold-tail" |
NO | Static serving partition (§16.6). Absent ⇒ resident. |
scope |
[string] | NO | Static resource:action authz for this route. |
A route carries NO model field, by construction. That absence is the hard static route-vs-skill discriminator.
x-suluk-agents (the load-bearing safety rule)⚠ Candidate @0.55: this is the central safety claim (
d1_agent_selector_safe), sole-witness in0agents-d1.bn, strengthened by an executable witness (below).mizan_verify_claimreturns no bcmea violation with a recommended cap equal to the declared ceiling (0.55) and above D1's 0.139 floor.
The DOM→ADA request→operation matcher is UNTOUCHED by agents. Normatively:
buildAda MUST NOT read any x-suluk-agents field. It iterates Object.entries(doc.paths) only (ada.ts:31-39) — never an x-* sibling — so a top-level x-suluk-agents map is invisible to it by the same construction that already keeps x-suluk-jobs out.matchRequest reads only op.request.method + the compiled path-template (ada.ts:69-85); computeSignature reads only method / contentType / contentSchema / uriTemplate. None of the agent fields (description, whenToUse, model, tier, scope) exist on Request or feed the signature.buildAda(docWithAgents) deep-equals buildAda(docWithoutAgents).There is deliberately NO field anywhere in SulukAgent / SulukSkillRef / SulukRouteRef that references request / DOM / header / body / query values. Selection — which skill, route, or sub-agent fires — is runtime-advisory prose, walled exactly as C018 walls callback runtime-expressions and C024 walls attribution.
A selector/strategy field that gated routing on request data would be INADMISSIBLE — forbidden outright, not resolved by precedence (agents_no_request_value_selector). The #20 parameterSchema static-matcher tripwire is declined identically here, by removal-by-design: a BLOCKING linter rejects any agent field whose value pattern-matches a C018 runtime-expression or a JSON-Pointer into request locations.
D1 by module boundary, not discipline. The agent layer is parsed by a separate @suluk/agents package that @suluk/core's buildAda / matchRequest provably never imports; a build-time import-boundary test backs the invariant. core gains only the structural SulukAgent shape — no new normative control keyword.
Executable witness. test/agents-d1-invariance.test.ts (3 pass, 8 expect() calls) is a maintained regression tripwire: buildAda(docWithAgents) deep-equals buildAda(docWithoutAgents), and matchRequest resolves every operation identically with vs without the block (d1_matcher_invariance_test_green). Critically, a deliberately CYCLIC, maxDepth-less agents block ALSO leaves the matcher's ADA unchanged (agents_cyclic_block_no_matcher_perturbation) — recursion well-foundedness is an author/install lint concern, provably irrelevant to the matcher. The day anyone makes the matcher read an agent field, this test fails loud.
A SulukAgentRef is a by-name reference into the same x-suluk-agents map (never an inline agent — inlining would fork C009 identity).
| Property | Type | Required | Description |
|---|---|---|---|
ref |
string | YES | A by-name $ref like #/x-suluk-agents/<key> into the finite x-suluk-agents map. |
Recursion is well-founded by LINT, not by schema (JSON Schema cannot express acyclicity):
maxDepth is REQUIRED whenever agents is non-empty. A typed leaf is maxDepth 0 with agents {}. A BLOCKING author/install linter enforces this.builder/src/compose.ts planComposition topo-order + cycle flag, C021), not new invention.matchRequest / MCP tool-call — never an in-matcher graph traversal. So the matcher never recurses (this is what §16.4.1 point 3 and the cyclic-block witness prove).INTERSECTION(child, caller), never union — no escalation.⚠ Deferred — recursion beyond one parent→child hop. Conin runs ONE flat agent with ZERO nesting; even modeling its retrieval tier as a sub-agent is a first nesting we authored, not one we observed. So recursion ships as cycle-lint + depth-lint + schema slot ONLY — no cockpit / cost / serving machinery — until a second real nested (or non-Conin) agent exists. See §16.9.
Two pure-function projections are the conformance test (mirroring how SPEC.md projects from the ledger, C022): one contract projects to (a) a Claude plugin (plugin.json + .mcp.json HTTP-MCP-OAuth + a generated SKILL.md) and (b) an OpenRouter / OpenAI-compatible manifest. Both are deterministic given a contentHash-pinned instructions snapshot, and the map key is the stable wire-level tool/function id on both.
No credentials cross the seam (OpenRouter keys, the plugin OAuth token). Serving and execution are a post-projection adapter concern, upholding C020 (no-credentials) / C023 (the L3 line).
Tier-trim serving (DECLARED capability; mandatory-vs-advisory is OPEN — §16.9). Each route and skill carries an optional tier:
tier |
Where it appears | Meaning |
|---|---|---|
resident (default; absent ⇒ resident) |
the serving adapter's default tools[] / tools/list |
available without an extra round-trip |
cold-tail |
revealed via a discover_tools meta-tool on demand |
withheld from the default surface to shrink the cheap/lower tier's tool count |
The @suluk/agents adapter CAPABILITY is built: projectOpenRouter puts resident routes in the default tools[] and the cold-tail behind a discover_tools meta-tool, and residentSurface / assertDefaultServedResident audit it. This is the mechanism the tiering thesis needs — but it must be actively performed by the adapter; the schema never enforces it.
The one conforming implementation, Conin, is non-conformant on day one. These are tracked as named conformance-failure fixtures, never presented as a clean 1:1 mapping:
| Fixture | What fails | Why |
|---|---|---|
dangling operationRef |
run_core_primitive is MCP-only with no REST path → a route $ref resolves to nothing |
a route MUST $ref an existing operation (§16.4); resolve-lint fails burhan-converge |
| id-casing mismatch | snake_case MCP tool ids (search_library) vs camelCase v4 operationIds |
the map key MUST be the stable wire-level id on both projection targets (§16.6) |
| full-catalog over-serve | Conin's public MCP tools/list ships the full catalog today |
zero context saved in the served path; the cold-tail is not yet withheld (§16.6 / §16.8) |
The projections being deterministic and the failures being named fixtures (rather than hand-waved) IS the conformance contract.
⚠ Candidate @0.52: each item below is a recorded reframe or DECLARED-not-enforced marker. If a paragraph in this section asserts any of them as enforced or delivered, that is a defect in the projection.
agents_determinism_declared_not_enforced). No vendor extension can stop a cheap model calling a retrieval tool to invent a number; guarantee: same-in-same-out asserts intent (mirrors C026 PROVISIONAL). A runtime "SOURCED-only-from-a-deterministic-route" gate is a deferred adapter obligation, not a schema guarantee.agents_ceiling_originated_low_context_reframed). Adversarial verification in Conin's code found tiering relocates + duplicates context (conserved, not destroyed): the parent still needs broad context to route, and Conin's public MCP tools/list ships the full catalog today (zero context saved in the served path). The honest, load-bearing claim is: tiering makes context allocation EXPLICIT and per-tier cost AUDITABLE — a CONDITIONAL reduction a conforming serving adapter must actively perform (resident in the default tools/list, cold-tail behind discover_tools). The genuinely novel, true property is the static model-presence partition (skill vs route), computable with zero requests.Everything below is explicitly deferred / advisory / DECLARED-not-enforced, recorded here so nothing above is mistaken for shipped normative machinery. (Source: [C027] §Deferred; honest ceiling ~0.52, Originated, tracking C024/C025, leaning on D1 @0.139.)
projectOpenRouter / discover_tools / residentSurface / assertDefaultServedResident; tier carries the partition — the first half of the reopen-trigger has fired), but whether the standard MANDATES it, and Conin's server-side over-serve fix, remain open.guarantee; it does not enforce determinism.guarantee (same-in-same-out | idempotent | safe) is advisory intent, unverifiable by schema (mirrors C026 PROVISIONAL).description, whenToUse, and the model-resolution surface are never read by the matcher (D1, §16.4.1).costCeiling) is a future sibling x-suluk-policy construct (projected separately under C028), or out of scope for a single-contributor fork.requiresHuman / resumable); memory scope / reset-boundary — are DEFERRED. The iterative-loop bound (thinking / maxRounds) is now SHIPPED in §17.2 (C029); only stopCondition stays FORBIDDEN/deferred (per C029 — reopens on a second non-Conin agent with a non-budget terminator). models[] is adopted; the rest may be forked by the first non-Conin agent.INTERSECTION(child, caller) is the rule; graceful-failure mechanics are unspecified.tier enum, and whether a learned value is admissible at all, is OPEN — tier MUST stay a static enumerable partition or be dropped.contentHash drift detection beyond fail-loud signalling, and the SKILL.md generation grammar, are refinement-deferred — the binding direction (generated from source, hashed) is settled; the byte-grammar is not.There is no OpenAPI 3.x precedent for an agent layer; this is an Originated extension, not a migration target.
| OpenAPI 3.x | Suluk v4.0 | Notes |
|---|---|---|
| (none) | x-suluk-agents map |
No 3.x equivalent. Severable on downgrade. |
an operationId |
a route operationRef $ref |
A 3.1 downgrade DROPS the agents map whole; the referenced operations survive unchanged as the ordinary operations they always were. |
custom x-* orchestration vendor keys |
SulukAgent skills / routes / agents |
Three industry frameworks (Strands, Claude Agent SDK, OpenAI Agents) share the two-tier shape; this paves the cowpath, it does not invent one. |
(Logically extends §16 — the x-suluk-agents composition map (C027). This section adds the operator-owned governance overlay that NARROWS an agent's self-declaration, the within-agent thinking envelope, and the model-resolution seam to @suluk/models. Resolves the C027 deferred questions — operator governance (open-Q #6), the thinking axis, and the model-pick delegation — see [C028], [C029], [C030].)
⚠ Candidate @0.52–0.76 — ORIGINATED, governance-over-an-agent-layer. Every construct here is constructed by burhan from principles; the SIG never designed an agent layer, let alone an operator overlay on one. C028 and C030's keying claims sit at the bottom of the band because they are originated-on-originated (governance over the C027 agent layer, which itself leans on the soft D1 0.139 floor). C030's verified ZDR-router path is the high point (~0.72–0.76, one live probe). Treat the whole section as provisional and revisable; the honest ceilings are restated inline. Source: ADRs [C028], [C029], [C030]; ledger
plan/facts/0policy-d1.bn,0thinking-bound.bn,0delegation.bn.
All three constructs ride the x-suluk-agents/x-suluk-jobs move EXACTLY: optional, additive, no new normative kind, and — critically — never read by the request→operation matcher (the D1 invariant of §3). A document carrying none of them validates byte-identically to one that does. This invariance is the load-bearing safety property and is independently witnessed by executable tests (see §17.4).
x-suluk-policy — Operator Governance Overlay (C028)An operator policy is a static, locally-decidable overlay that an operator (not the agent author) sets to NARROW what an agent self-declares. It is a top-level vendor map, keyed by operator/fleet name — one policy spans many third-party agents — riding the catch-all [ext: x-${string}] exactly as x-suluk-agents does.
| Field | Type | Required | Description |
|---|---|---|---|
["x-suluk-policy"] |
Map[operator-name → SulukPolicy] | NO | Operator-owned governance overlay; absent ⇒ no governance, agents serve self-declared (C028). |
SulukPolicy shapeEvery field is STATIC, a set/enum/min/membership operation, and NARROW-ONLY. No field may reference a request/DOM/header/body value — the #20 parameterSchema tripwire is declined here too (D1).
x-suluk-policy:
acme-fleet: # keyed by OPERATOR / fleet name (the operator owns it)
appliesTo: [ research-agent ] # by-name refs into x-suluk-agents keys; absent ⇒ all agents
scopeAllowlist: [ "docs:read" ] # effective scope = INTERSECT(agent.scope, scopeAllowlist)
agents: { deny: [ shell-agent ] } # deny/allow sub-agent keys
tools: { allow: [ search ] } # deny/allow route (tool) keys
retrievalTools: { deny: [ web-fetch ] } # the untrusted tier's blast radius, specifically
capTier: resident # pin the MAX tier; a cold-tail skill is downgraded (+flagged)
modelAllowlist: [ "openai/gpt-4o-mini" ] # effective model[] = INTERSECT(skill.model, modelAllowlist)
maxDepthCap: 1 # effective maxDepth = min(agent.maxDepth, maxDepthCap)
forbidNesting: false # true ⇒ effective maxDepth 0
costCeiling: # DECLARED, NOT ENFORCED — see §17.5
amount: 500
amountUnit: micro-usd # "micro-usd" | "cents" | "usd"
basis: per-request # OPTIONAL free-form
enforcedBy: adapter # REQUIRED: "adapter" | "runtime"
appliesTo binds BY AGENT NAME (#/x-suluk-agents/<key>), NEVER a request predicate. Empty or absent ⇒ the policy governs all agents in the document.scopeAllowlist, agents/tools/retrievalTools {deny,allow}, capTier, modelAllowlist, maxDepthCap, forbidNesting) are narrowing axes: applying the policy can only REMOVE capability an agent self-declared.The override algebra is a total, order-independent MEET. policyConstrain(agent, policy) reuses the shipped intersectScope:
effective = INTERSECT(operatorPolicy, agentSelfDeclaration)
The effective agent MUST NOT exceed either input on any axis (scope, tier, model, depth, tools). The precise claim is MEET / never-exceeds-either — not "the operator is always narrower": there is no operator node above the root agent today, so the policy genuinely adds the ceiling rather than tightening an existing one.
policyConstrain result that would grant capability beyond the agent's self-declaration is a lint hard-fail (policy-widening), the same class as the shipped scope-escalation lint — never a precedence-resolved warning.agentManifest (so the C021 signature covers the operator's caps), and a policy-aware over-serve auditor (assertServedSubsetGoverned → policy-denied-served) rejects serving anything the policy denied.request-value-selector rejection (the D1 floor), policy-applies-dangling / policy-applies-malformed, policy-unsatisfiable (a modelAllowlist leaving a skill no model, or a deny removing every tool), policy-widening, and the cross-facet cap-below-estimate (the operator under-budgeted costCeiling.amount vs the author's own x-suluk-cost estimate).Normative grammar:
SulukPolicy MUST NOT reference any request/DOM/header/body value; appliesTo MUST bind by agent name only (D1; d1_policy_selector_safe).policyConstrain MUST be a MEET — the effective agent MUST NOT exceed the operator policy OR the agent self-declaration on any axis (policy_monotone_narrowing_meet).costCeiling.enforcedBy MUST be present whenever costCeiling is present (see §17.5).⚠ Candidate @0.52 (
policy_toplevel_vendor_map,policy_monotone_narrowing_meet): ORIGINATED, originated-on-originated — governance over the C027 agent layer, which leans on the soft D1 0.139 floor. The MEET soundness rests onanalyzeScopes/intersectScopeusing first-reaching-path intersection on a DAG — sound for the shallow one-hop shapes shipped today, to be re-audited before any recursion-reopen. There is zero second user: Conin is author == operator, so the override has nothing to override; the verdict survives only because reserving a security floor is a one-way door and the static subset reuses shipped infra at near-zero cost. An independent D1 witness would lift the whole tower.
A SulukAgent MAY declare a thinking envelope — a static cap on within-agent iteration (reason→tool→reason in the same completion, context accreting until a response). It is a sibling of contextBudget/maxDepth on the agent.
x-suluk-agents:
research-agent:
description: Researches and summarizes a topic across sources.
thinking:
maxRounds: 6 # static int >= 1; REQUIRED when `thinking` present
budget: { tokens: 40000, basis: estimate } # OPTIONAL; reuses the contextBudget/C024 vocab verbatim
| Field | Type | Required | Description |
|---|---|---|---|
thinking.maxRounds |
integer (≥ 1) | YES (when thinking present) |
A ceiling on internal reason→tool→reason rounds in one completion. |
thinking.budget |
{ tokens: number, basis: "estimate" } |
NO | A cost ceiling on the round-bundle; reuses the contextBudget/C024 vocabulary verbatim. |
maxRounds and budget. Document facts, locally decidable, read by the context analyzer and the linter — never by buildAda/matchRequest/computeSignature.stopCondition is FORBIDDEN. Any stopCondition / stopConditionKind enum or loop-process descriptor is rejected by a blocking lint (thinking-process-declared). Such a field would model runtime control flow a generator can only echo — the #20 parameterSchema tripwire in an enum costume — and is D1-FORBIDDEN, not precedence-resolved. The bound is a number a tool acts on; the process is a runtime predicate.
Consumed, not decorative. The field exists because the context analyzer reads it: it folds round-accretion into the worst-case load
peakTokens = base + (budget.tokens ?? (maxRounds − 1) × residentToolTokens)
so that no-fitting-model / context-over-budget / model-too-small fire honestly on a multi-round agent (fixing the single-shot blindspot that silently under-counts an agent which thinks 6 rounds), and thinking-context-growth reports the peak. A lint enforces maxRounds present-and-positive.
Orthogonal to recursion (not redundant with maxDepth). A self-delegating sub-agent + maxDepth is delegation-down (a new completion, scope intersected, context reset per hop, bounding call-graph nesting depth, and tripping the cycle-linter). Thinking is the inverse: the same completion, context accretes per round, no scope change, bounding within-agent iteration breadth at fixed depth (Conin's 6 rounds are at depth 0). Orthogonal bounds, like depth vs fan-out.
Normative grammar:
thinking.maxRounds MUST be present and ≥ 1 whenever thinking is present (thinking_bound_static_field).stopCondition / stopConditionKind / loop-process descriptor under thinking; tools MUST reject it (thinking_declares_bound_forbids_process).thinking fields MUST NOT be read by the matcher; a thinking-stripped document MUST yield a byte-identical Ada (D1; d1_thinking_bound_safe).thinking ⇒ an opaque single pass (the zero-migration default).⚠ Candidate @0.52 (
thinking_bound_static_field): ORIGINATED at a held-low ceiling — one real cowpath (Conin's hand-rolled 6-round cap), zero second looped agent, leaning on the D1 0.139 floor. The council was UNANIMOUS (11/11 bound-only), but unanimity over a single witness is still single-witness.
A skill declares NEEDS, not a frozen model id, and @suluk/models selects. The author surface on SulukSkillRef is a single set of fields; none is read by the matcher (the C027 seam).
| Field | Type | Description |
|---|---|---|
model |
[string] |
EXPLICIT OpenRouter ids, cheap→capable — the opt-out path. |
modelProfile |
enum | A named selection profile: tool-reliable | cheap-fast | balanced | max-reasoning | long-context | vision. |
modelPrefer |
{ intelligence?, cost?, speed?, context? } (each 0|1|2|3) |
Escape-hatch preference weights over the four author-facing axes. |
modelRequire |
{ needsStructured?, inputModalities?, minContext?, zdr? } |
HARD requirements the author adds beyond what's derived. |
modelResolve |
"pinned" | "router" | "latest" |
How the model is resolved from the survivor set; default pinned. |
@suluk/models keeps the survivor set (the moat, all contract-time): checkFilters (governance FAIL-CLOSED + the C028 modelAllowlist terminal MEET + the analyzer minWindowRequired gate + caps), the OpenRouter facts catalog with the weekly normalizeOpenRouter pull, the snapshotHash pin, the "why this model / filter-trace" explainer, and the fail-loud empty-set path. The per-request cost/quality/capability pick within that fenced set is delegated to OpenRouter's openrouter/auto, fenced by allowed_models = the ENUMERATED survivor ids (never a wildcard — the MEET stays terminal; widening is inadmissible) and cost_quality_tradeoff = deriveCQT(profile) set explicitly.
modelResolve — governance-gated and mechanicalpinned (default): target = { kind: "pinned", model: ranked[0].id }, pickPinned: true — a reproducible, bisectable, endpoint-bindable served id. Required for agent loops, forced-tool-choice, and any governed skill.router (opt-in, UNGOVERNED only): target = { kind: "router", model: "openrouter/auto", allowedModels: survivors, costQualityTradeoff }, pickPinned: false. A governed skill — one under any operator policy — that declares router FAILS LOUD at contract time (delegation_governance_gate_force_pin): a runtime pick cannot bind an endpoint region/retention and is non-reproducible across dates.latest: a best-effort ~author/family-latest alias — defers the version to request time; not reproducible (recorded in the explainer); never the default.A governed skill MUST pin. Region has no OpenRouter endpoint knob (one model id fans out to many endpoints; allowed_models fences the model id, not the served endpoint), so any operator-governed skill force-pins; Suluk does not ship a governed-router path.
Reproducibility is honestly DOWNGRADED on the router/latest path: snapshotHash pins the survivor SET + the CQT knob; pickPinned: false marks the served id logged-not-pinned. For a region-residency / SOC2 audit ("which concrete model on date X?") the answer is pin — the auditable fence MUST NOT be laundered as reproducible selection (delegation_reproducibility_downgraded_honest).
modelRequire.zdr: true requires zero-data-retention serving. Confirmed by a live probe (2026-06-13, delegation_zdr_auto_combination_verified_router_path_shipped): POST /chat/completions with { model: "openrouter/auto", provider: { zdr: true } } returns 200 with a real completion (google/gemini-2.5-flash-lite), with and without allow_fallbacks: false — provider preferences do combine with the auto-router.
x-suluk-agents:
zdr-agent:
description: Summarizes uploaded documents under zero-data-retention serving.
skills:
summarize:
modelRequire: { zdr: true } # resolves to the ROUTER regardless of modelResolve
# → target = { kind: "router", model: "openrouter/auto", provider: { zdr: true } }, pickPinned: false
zdr skill resolves to the router ({ kind: "router", model: "openrouter/auto", provider: { zdr: true } }) regardless of modelResolve — there is no per-model ZDR fact to pin against, so ZDR is runtime-only via provider.zdr; pickPinned: false.provider object exposes zdr (a hard "restrict to ZDR endpoints" constraint), but no region/residency field — so region/license-governed selection MUST pin permanently (no OpenRouter endpoint field exists; delegation_zdr_field_exists_region_absent).Normative grammar:
modelResolve defaults to pinned; a skill governed by any operator policy MUST resolve to pinned — router/latest MUST fail loud at contract time (delegation_governance_gate_force_pin).router target's allowedModels MUST be the enumerated survivor ids — never a wildcard (the C028 modelAllowlist MEET stays terminal).modelRequire.zdr: true MUST resolve to the router with provider: { zdr: true }; ZDR combined with an operator policy MUST fail loud (delegation_zdr_auto_combination_verified_router_path_shipped).delegation_zdr_field_exists_region_absent).⚠ Candidate @0.76 (
delegation_hybrid_keep_set_delegate_pick): the section's high point, capped by the originated keying claims (~0.6) and the honest ZDR caveat. The live probe proves theauto+provider.zdrcombination is accepted and routes; a single 200 does NOT prove strict exclusion of non-ZDR endpoints (the same model returned with and withoutzdr— flash-lite is ZDR-eligible on Google). OpenRouter's doc asserts the restriction; the probe confirms acceptance.
None of x-suluk-policy, thinking, or the modelResolve/modelRequire/modelProfile/modelPrefer fields is ever read by the request→operation matcher. buildAda iterates doc.paths only; matchRequest reads only method + the compiled path-template; computeSignature reads only method/contentType/contentSchema/uriTemplate. A document with these constructs stripped yields a byte-identical Ada. This is witnessed independently and executably:
test/policy-d1-invariance.test.ts — buildAda byte-identical with vs without a deny-all x-suluk-policy block (d1_policy_selector_safe).test/agents-d1-invariance.test.ts — buildAda/matchRequest byte-identical with vs without a thinking-bearing block (d1_thinking_bound_safe).modelResolve is skill metadata, never read by the matcher.The policy parser ships in @suluk/agents, which @suluk/core provably never imports (core-boundary.test.ts).
The following are DECLARED / RESERVED / advisory only — none is enforced or shipped as a runtime, and none may be read as a normative guarantee:
costCeiling is DECLARED, NOT ENFORCED (C028, policy_costceiling_declared_not_enforced). The schema declares the operator's third number (cap / estimate / actual, on three distinct owners: operator x-suluk-policy / author x-suluk-cost / C026 reconciled). It cannot enforce it; enforcedBy (REQUIRED) names the runtime adapter, and a cap-breaching run is a NAMED conformance failure. Anyone reading C028 as "Suluk enforces your budget" has been misled. (C026 PROVISIONAL honesty, restated at the point of definition.)costCeiling enforcement runtime (admission-gate / terminate-at-spend kill-switch, plus any cost-metering/billing engine and a @suluk/policy execution path) is RESERVED — built-by-nobody. Reopen-trigger: a real fleet operator running ≥ 2 agents authored by a party OTHER than the operator (a second x-suluk-policy entry pointing at agents not in the operator's own authorship, OR a conforming adapter wiring a runtime meter).thinking.maxRounds is DECLARED, NOT ENFORCED (C029). It caps re-entries; it does NOT enforce termination or determinism. Only the serving adapter (Conin's existing 6-round loop) clamps to it; budget.basis: estimate is never laundered as a bill.stopCondition vocabulary is FORBIDDEN and DEFERRED (C029). Reopens only when a second, non-Conin agent has a real internal loop that terminates on something other than a round/budget count (a witnessed non-budget terminator). Until then it is unwitnessed decoration and a blocking lint rejects it.costCeiling enforcement runtime.BUCKETING_RULES/applyTierOverlay/weekly Class-B leaderboard treadmill); tiers are kept only as optional coarse filter floors. The facts catalog, normalizeOpenRouter, snapshotHash, and the governance-fact columns are NOT deleted — the filters and the pin depend on them.endpoints[] sub-list is RESERVED, NOT BUILT (C030, delegation_keying_endpoints_reserved_no_representative_region). The catalog stays MODEL-keyed; per-endpoint axes (region/retention/latency) await a real fleet needing per-endpoint region governance OpenRouter cannot express, plus endpoint gov data. A "representative" gov.region cell is FORBIDDEN — it would degrade fail-closed → fail-OPEN at the endpoint layer (forged in-region attestation); gov.region/dataRetention stay per-model UNKNOWN/fail-closed.latest resolution path is non-reproducible (C030) — recorded in the explainer, never the default.delegation_other_routers_orthogonal_or_niche): fusion (an ensemble) is IGNORED for selection (link out, do not absorb — adjacent to suluk sub-agents + §17.2 thinking); pareto is a future router target only for taskShape === 'coding' + empty-policy (governance-blind); free/bodybuilder are link-out niche. None is ever a default profile.⚠ Candidate @0.55–0.62 — PROVISIONAL BUILDABLE DEFAULTS, NOT RATIFIED GRAMMARS. This appendix pins concrete, implementable defaults for four surfaces that C003/C005/C013 deferred at the byte-grammar level but that tool authors need to build interoperable tooling. They do NOT override those deferrals; they are the default the spec RECOMMENDS until a ratified grammar lands (#26/#49/#72/#73, #100/#108, signature-key residual #7). Each is fully revisable. Source: ADR C019.
Base-document reconciliation prerequisites (this appendix assumes these are applied to the normative body): (1) one canonical OpenAPI reference emitter
#/components/<type>/<name>— the no-prefix#/schemas/...and dot/colon forms in §5/§8 are illustrative only; (2) §6.4's "an OAS-level reference mechanism is NOT introduced" clause is struck (§8 introduces it, per C013 #49); (3) per-location slots are namedparameterSchema.{query,path,header,cookie,body}(the §7headerSchema/cookieSchemaspelling is an alias to be reconciled); (4) §2.4's URL input is split on unescaped/BEFORE percent-decoding each segment.
Two reference surfaces, disjoint by host context, distinguished by token + slot (never tree-depth, C013 #49):
| Surface | Where it appears | Token | Canonical emitter form | Resolution |
|---|---|---|---|---|
| OpenAPI Reference Object | non-Schema reuse: responses, requests, links, parameters, securitySchemes, tags, examples | {$ref: ...} (+ optional summary/description) |
#/components/<type>/<name>, #/paths/<esc-uriTemplate>/..., #/tags/<name> |
same-document JSON-Pointer, by NAME (C009) |
JSON-Schema $ref |
inside any Schema Object: contentSchema/body, parameterSchema.*, components/schemas/*, $defs/items/properties/allOf/… |
$ref (2020-12 keyword) |
#/components/schemas/<name> or #<name> (implicit anchor) |
the declared 2020-12 dialect evaluator |
Tie-break (the body/contentSchema ambiguity): a $ref in any slot whose declared type includes Schema Object is always the JSON-Schema keyword, regardless of companion keys — never the Reference Object.
Pointer escaping: tokens are RFC6901-escaped ONLY (~0=>~, ~1=>/); the #/... is a JSON Pointer and is NOT re-percent-encoded as a URI fragment — braces stay literal. Worked example: pathItem key /pets/{petId} is referenced as #/paths/~1pets~1{petId} (not …~1%7BpetId%7D).
RESOLVE(ref, host, doc):
SCHEMA_OBJECT_SLOTS = { contentSchema, body, parameterSchema.{query,path,header,cookie,body},
components.schemas.*, and anything transitively inside one }
kind = host ∈ SCHEMA_OBJECT_SLOTS ? JSON_SCHEMA_REF
: host has "$ref" (+ only summary/description) ? REFERENCE_OBJECT : error
ABNF: ref = [import-ns ":"] [doc-uri] "#" json-pointer ; json-pointer per RFC6901
import-ns = 1*(ALPHA/DIGIT/"-"/"_") ; C013 #72, byte-grammar DEFERRED
if import-ns present: doc = resolve_import(import-ns) // location only; no mandatory I/O
if JSON_SCHEMA_REF and ref == "#<name>": return schema whose implicit anchor == <name>
unescape tokens (~1->'/', then ~0->'~')
if pointer == /components/<type>/<name>:
t = doc.components[type][name]; if absent: error(missing) // by KEY; MUST NOT fall back to positional
return t
if pointer == /paths/<tmpl>/...: return walk(doc.paths[unescape(tmpl)], rest)
if pointer == /tags/<name>: return doc.tags[name]
if JSON_SCHEMA_REF: return dialect.resolve(ref, host)
The ADA computes a tooling-internal matcher/dedup/collision key (not a DOM field, C003(a)). Fixed aspect order (method, path, queryKS, ctypeSet, headerAS, bodyId):
CANONICAL-SIGNATURE(request, pathItem):
method = uppercase(request.method)
path = pathItem.key // LITERAL uriTemplate (C009 map key) — NOT var-name-erased
queryKS = sorted_dedup(Tier-Q query var-names) // key-set, values excluded (C005 D3)
ctypeSet = sorted_dedup(map(strip_params∘lowercase, asArray(request.contentType))) or ['*'] // params stripped (C016)
headerAS = sorted_dedup(lowercase(name) for identity-participating headers) or ['*'] // C015/#224; best-effort #108
bodyId = $ref ? canonical_ref_string : inline ? '#inline' : '*' // SENTINEL, never a structural hash
key = 'M='+method+'|P='+path+'|Q='+join(',',queryKS)+'|C='+join(',',ctypeSet)+'|H='+join(',',headerAS)+'|B='+bodyId
Normalization (byte-identical across emitters): sets sorted by Unicode code point + deduped; non-participating aspect => '*'; path preserves var spelling (name-erasure is overlap-test-only).
COLLISION(a,b): // C003 three-valued verdict, report-not-gate
ctypeSet: MEDIA-DISJOINT(no a∈A,b∈B co-satisfy; ranges & C016-equivalents are NON-disjoint) ? provably-disjoint : UNDECIDED
bodyId: a or b == '#inline' -> UNDECIDED // D1: JSON-Schema discrimination is runtime last-resort
headerAS: #108 incomplete -> UNDECIDED
other: both concrete & cannot co-satisfy -> provably-disjoint
any UNDECIDED -> not-statically-determinable ; else provable-collision
Collision policy (invalid vs precedence vs priority vs strict-mode) stays OPEN (C003).
Deserialize the raw request into the JSON instances the parameterSchema.{query,header,cookie} slots validate. Coercion is slot-driven, type-category (integer+number => JSON number; integer-ness left to validation).
QUERY (form-style): strip '?'; split on '&'; split each pair on FIRST '='; DECODE AFTER splitting ('+' -> SP)
repeated key -> array (wire order); single key -> scalar string
bare key '?flag' (no '=') -> true iff slot type boolean, else ''
absent slot -> all-strings instance, NO coercion
HEADER: lowercase names (C015/#224 MUST); comma-split/combine ONLY for fields the §7.2 fieldModel registry
marks list-of-values (RFC9110 §5.6.1) — non-list fields (date, user-agent, quoted commas) preserved VERBATIM
COOKIE: parse 'cookie' header (RFC6265) into name->string; coerce if slot present
UNKNOWN keys (additionalProperties:true is the C004 default): included in wire form, uncoerced
COERCE: numeric -> Number(v) if JSON-number; boolean -> true/false from true|1 / false|0; array -> wrap singleton;
union/oneOf/anyOf -> coerce iff a branch permits numeric/boolean, else leave string
DETERMINISM: identical raw input + slot schema + fieldModel registry => identical JSON instance
⚠ Only form-style is pinned here;
deepObject/spaceDelimited/pipeDelimitedare left for the ratified #100/#108 grammar.
Three operator tiers (C005) + validate/compile/reverse-parse:
| Tier | Operators | Use |
|---|---|---|
| 1 MATCH-SAFE | literal; {id} (one segment, excl. / ? #); {+path} = CAPTURE_REST (terminal, multi-segment); ;name={id} (matrix) |
path identity |
| 2 QUERY-ONLY | {?a,b}, {&x,y} |
query, parsed as order/repetition-insensitive key-set |
| 3 FORBIDDEN | {x*}, {+x*}, non-boundary {+x}, {#x}, {.x}, {x:2}, regex, list/comma-scalar var |
author error / not-statically-determinable |
VALIDATE-PROFILE: '{+var}' is Tier-1 IFF it is the entire final segment AND terminal AND single scalar
'/files/{+path}' -> Tier1 ; '/files/prefix{+path}' -> Tier3 ; '/a/{+path}/b' -> Tier3
REVERSE-PARSE(urlPath, Paths):
urlSegs = split(urlPath, unescaped '/').map(pctDecodeSegment) // SPLIT first, decode each segment
// '%2F' stays inside a segment -> '/files/a%2Fb' is 2 segments, not 3
candidates = templates whose COMPILE matches urlSegs (CAPTURE_REST consumes the tail)
if 0 -> (null,{}); if 1 -> it; else sort by specificity DESC -> top (+ emit C003 verdict)
specificity (over the TEMPLATE, not the matched portion):
CAPTURE_REST always ranks LAST; else count LITERAL segments, tie-break per-segment literal>var left-to-right
Worked: URL /files/readme.txt against {/files/{+path}, /files/{id}/versions, /files/readme.txt} routes to the literal (CAPTURE_REST loses); /files/a/b routes to /files/{+path} (path=a/b). Overlap (/users/me vs /users/{id}) is a C003 verdict + runtime concrete-over-variable tiebreak, never document order (C009).
⚠ Slash-bearing single-value path params remain INEXPRESSIBLE in this profile (the price of deterministic reverse-parse, C005).