From 6f0bfab448edc559a39787b82dc9a0c8fca34b17 Mon Sep 17 00:00:00 2001 From: Copilot Date: Fri, 15 May 2026 23:48:03 +0200 Subject: [PATCH] feat(catalog): v1.3 seed exemplars for input-shape, output-shape, ipc-rpc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six new exemplar files across three new view-type directories: - docs/exemplars/input-shape/cli-argparse-shape.md: POSIX short-option grouping, GNU long-option conventions, -- separator (sourced from POSIX.1-2017 §12 and GNU Coding Standards). - docs/exemplars/input-shape/openapi-request-body.md: JSON content-type binding, JSON Schema 2020-12 alignment, nullable keyword removal (sourced from OpenAPI 3.1.0 spec). - docs/exemplars/output-shape/json-rpc-response-2.0.md: jsonrpc/id/result|error exclusive union, reserved error code range, batch response correlation (sourced from json-rpc.org 2.0 spec). - docs/exemplars/output-shape/openapi-response-envelope.md: HTTP status map, wildcard keys, default key semantics (sourced from OpenAPI 3.1.0 spec). - docs/exemplars/ipc-rpc/unix-socket-rpc.md: AF_UNIX SOCK_STREAM, newline- delimited framing, O_CLOEXEC and SIGPIPE disciplines (sourced from POSIX bind(2)). - docs/exemplars/ipc-rpc/subprocess-rpc.md: stdin/stdout JSON-line protocol, EOF shutdown, deadlock avoidance, SIGTERM/waitpid lifecycle (sourced from POSIX waitpid(2)). Three new axes.yml files defining axes for each view type. Catalog plumbing: - bin/cross_view_gate.py: _VIEW_TO_CATALOG_TYPES updated — §9 → {input-shape}, §10 → {output-shape}, §12 → {api-shape, ipc-rpc}. Removes the help-text placeholders that were documented as interim until v1.1/v1.3 shipped. - tests/test_v1_1_e2e.py: updated the deferral-reason assertion to reflect that §9 now has compatible input-shape exemplars (human-typed), so both deferrals are operator-deferral and excessive-post-ship-iteration fires (2 > 1). 9 new tests in tests/test_catalog_seed_v1_3.py. Full suite: 1976 passed. Co-Authored-By: Claude Opus 4.7 --- bin/cross_view_gate.py | 6 +- docs/exemplars/input-shape/axes.yml | 11 ++ .../input-shape/cli-argparse-shape.md | 27 +++ .../input-shape/openapi-request-body.md | 24 +++ docs/exemplars/ipc-rpc/axes.yml | 11 ++ docs/exemplars/ipc-rpc/subprocess-rpc.md | 25 +++ docs/exemplars/ipc-rpc/unix-socket-rpc.md | 25 +++ docs/exemplars/output-shape/axes.yml | 11 ++ .../output-shape/json-rpc-response-2.0.md | 24 +++ .../output-shape/openapi-response-envelope.md | 24 +++ tests/test_catalog_seed_v1_3.py | 154 ++++++++++++++++++ tests/test_v1_1_e2e.py | 16 +- 12 files changed, 346 insertions(+), 12 deletions(-) create mode 100644 docs/exemplars/input-shape/axes.yml create mode 100644 docs/exemplars/input-shape/cli-argparse-shape.md create mode 100644 docs/exemplars/input-shape/openapi-request-body.md create mode 100644 docs/exemplars/ipc-rpc/axes.yml create mode 100644 docs/exemplars/ipc-rpc/subprocess-rpc.md create mode 100644 docs/exemplars/ipc-rpc/unix-socket-rpc.md create mode 100644 docs/exemplars/output-shape/axes.yml create mode 100644 docs/exemplars/output-shape/json-rpc-response-2.0.md create mode 100644 docs/exemplars/output-shape/openapi-response-envelope.md create mode 100644 tests/test_catalog_seed_v1_3.py diff --git a/bin/cross_view_gate.py b/bin/cross_view_gate.py index 3f0440a..1cc263e 100644 --- a/bin/cross_view_gate.py +++ b/bin/cross_view_gate.py @@ -174,10 +174,10 @@ def _check_cross_view_references( # Multiple view-types are allowed because a §11 Human-User View may bind to # both help-text and error-text catalog entries. _VIEW_TO_CATALOG_TYPES: dict[str, set[str]] = { - "9": {"help-text"}, # placeholder until v1.1 ships input-shape exemplars - "10": {"help-text"}, # placeholder until v1.1 ships output-shape exemplars + "9": {"input-shape"}, + "10": {"output-shape"}, "11": {"help-text", "error-text"}, - "12": {"api-shape"}, + "12": {"api-shape", "ipc-rpc"}, "13": {"log-format", "observability"}, } diff --git a/docs/exemplars/input-shape/axes.yml b/docs/exemplars/input-shape/axes.yml new file mode 100644 index 0000000..0fc16c0 --- /dev/null +++ b/docs/exemplars/input-shape/axes.yml @@ -0,0 +1,11 @@ +taxonomy-version: 1 +axes: + arg-style: + values: [posix-flags, gnu-long-flags, subcommand-verb, positional-only, json-body, query-string] + description: How inputs are structurally presented to the program. Posix-flags = short-form (-x); gnu-long-flags = long-form (--flag, --flag=value); subcommand-verb = first positional arg is an operation name; positional-only = unnamed positional args; json-body = structured JSON payload in HTTP request body; query-string = URL-encoded key=value pairs in the query component. + optionality: + values: [all-required, all-optional, mixed, schema-validated] + description: Whether inputs are required or optional. All-required = every declared input must be supplied; all-optional = all inputs have defaults; mixed = some required some optional; schema-validated = a schema object (JSON Schema or OpenAPI) declares required/optional per-field. + separator-support: + values: [none, double-dash, content-type-boundary, newline-delimited] + description: How the input boundary between option parsing and positional data is signaled. None = no explicit separator; double-dash = POSIX -- convention ends option scanning; content-type-boundary = MIME multipart boundary header; newline-delimited = each record is a newline-terminated JSON line. diff --git a/docs/exemplars/input-shape/cli-argparse-shape.md b/docs/exemplars/input-shape/cli-argparse-shape.md new file mode 100644 index 0000000..800f076 --- /dev/null +++ b/docs/exemplars/input-shape/cli-argparse-shape.md @@ -0,0 +1,27 @@ +--- +view-types: [input-shape] +conventions: + - "A POSIX utility name consists of lowercase letters and digits; hyphens may appear between name components but not as the first or last character (POSIX.1-2017 §12.1)" + - "Short options are a single hyphen followed by a single alphanumeric character (e.g. `-x`); they may be grouped without whitespace when they take no argument (e.g. `-abc` equals `-a -b -c`)" + - "An option-argument may be supplied directly adjacent to its option letter with no whitespace (`-ofile`) or separated by a single space (`-o file`); both forms must be treated identically" + - "GNU long options use a double-hyphen prefix followed by a word or hyphenated words (e.g. `--output`, `--output-file`); the argument may be supplied as `--option=value` or as two space-separated tokens `--option value`" + - "The double-hyphen token `--` used alone terminates option scanning; all subsequent tokens are treated as non-option arguments even if they begin with `-` (POSIX.1-2017 §12.2 guideline 10)" + - "Options that accept optional arguments (GNU extension) must use the `--option=value` form; `--option value` is not parsed as an optional argument — the value would be treated as the next positional argument" + - "Option processing order: short options in the order given, then long options in the order given; interleaving short and long is permitted unless the implementation explicitly disables it" + - "Operands (non-option arguments) follow all options; POSIX requires operands to appear after all options unless the implementation supports mixing (signaled by `POSIXLY_CORRECT` absence in GNU implementations)" +axes: {arg-style: gnu-long-flags, optionality: mixed, separator-support: double-dash} +calibrated-for: [programmatic-trusted, programmatic-untrusted, human-typed] +taxonomy-version: 1 +source-url: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html +last-reviewed: 2026-05-15 +--- + +# cli-argparse-shape input-shape conventions + +POSIX defines the canonical shape for command-line utility arguments in §12 of the Base Definitions. The foundational rule is that options begin with a single hyphen and a single character; operands are everything that is not an option or an option-argument. This vocabulary — option, option-argument, operand — is precise: a reviewer can label every token in a command line as exactly one of the three before considering what a token means semantically. + +Short-option grouping (`-abc`) is permitted for options that take no argument. The moment one option in the group takes an argument, the remainder of the group characters are that argument — e.g. `-obfile` means option `-o` with argument `bfile`, not three options followed by `file`. Implementing parsers must consume options greedily from left to right within a group. + +GNU long options (`--word`) extend POSIX by naming options legibly. GNU's canonical reference is the Coding Standards chapter "Standards for Command Line Interfaces" which adds the `--option=value` form and the `--` terminator. The separator rule is critical: `--option value` assigns `value` only to a required argument; for optional arguments, the `=` form is the only unambiguous parse. A parser that accepts `--option value` for optional arguments silently changes behavior when `value` happens to match the next operand. + +The `--` separator is the only POSIX-standardized way to pass operands whose text begins with `-`. Every conformant implementation must treat the token sequence `-- -file` as a single operand whose value is the string `-file`. Omitting `--` support creates an exploitable surface when operand content comes from user-supplied data (filenames, search terms, etc.). diff --git a/docs/exemplars/input-shape/openapi-request-body.md b/docs/exemplars/input-shape/openapi-request-body.md new file mode 100644 index 0000000..3478f84 --- /dev/null +++ b/docs/exemplars/input-shape/openapi-request-body.md @@ -0,0 +1,24 @@ +--- +view-types: [input-shape] +conventions: + - "A Request Body Object in OpenAPI 3.1 has a required `content` field whose keys are media type strings (e.g. `application/json`); the absence of `content` is a schema violation" + - "Each media type entry contains a `schema` field whose value is a Schema Object or Reference Object conformant to JSON Schema draft 2020-12 (OpenAPI 3.1 aligns with JSON Schema 2020-12, not draft-07)" + - "The `required` boolean field on the Request Body Object defaults to `false` when absent; an omitted body on a required=true operation is a 400-level error" + - "Sending `Content-Type: application/json` binds the request body to the schema under that media type key; other content types (e.g. `application/x-www-form-urlencoded`) bind to their respective schema entries" + - "A `$ref` in a Schema Object is resolved against the document's `#/components/schemas` namespace or an external URI; inline schema and referenced schema are semantically equivalent after resolution" + - "The `nullable` keyword is not valid in OpenAPI 3.1 (it was a 3.0 extension); use `type: [string, null]` or `oneOf` with a null type entry to express an optional-valued field" + - "Multiple content types on one operation are valid; the server selects the binding by matching the `Content-Type` header to the `content` map keys using media-type pattern matching (RFC 7231 §3.1.1.5)" +axes: {arg-style: json-body, optionality: schema-validated, separator-support: content-type-boundary} +calibrated-for: [programmatic-trusted, programmatic-untrusted, api-consumer, library-consumer] +taxonomy-version: 1 +source-url: https://spec.openapis.org/oas/v3.1.0#request-body-object +last-reviewed: 2026-05-15 +--- + +# openapi-request-body input-shape conventions + +OpenAPI 3.1 describes the request body via the Request Body Object, which differs from path and query parameters in one important structural respect: the body's schema is not a single schema but a map from content type to schema. A single operation can declare `application/json` and `application/x-www-form-urlencoded` bodies with distinct schemas — the consumer's `Content-Type` header determines which schema applies. A reviewer checking an operation must match the schema to the incoming content type before validating the payload; applying the JSON schema to a form-encoded body (or vice versa) produces meaningless results. + +The alignment with JSON Schema 2020-12 (replacing the 3.0/draft-07 alignment) has one frequently-mishandled consequence: the `nullable` keyword is invalid in 3.1 and must not appear in specs targeting that version. The canonical replacement is a type array (`type: [string, null]`) or a `oneOf` with a null branch. Tools that silently accept `nullable: true` in 3.1 documents are applying 3.0 parsing rules. + +The `required` field on the Request Body Object governs whether the body itself may be absent — it does not govern individual fields inside the schema (field-level required is expressed inside the Schema Object's `required` array). This distinction matters: `required: false` on the body means the entire body may be omitted; it says nothing about which fields inside a provided body are mandatory. diff --git a/docs/exemplars/ipc-rpc/axes.yml b/docs/exemplars/ipc-rpc/axes.yml new file mode 100644 index 0000000..f058826 --- /dev/null +++ b/docs/exemplars/ipc-rpc/axes.yml @@ -0,0 +1,11 @@ +taxonomy-version: 1 +axes: + transport: + values: [unix-socket, tcp-socket, stdin-stdout, named-pipe, shared-memory] + description: The OS-level channel used for IPC. Unix-socket = AF_UNIX SOCK_STREAM or SOCK_SEQPACKET; tcp-socket = AF_INET/AF_INET6 loopback; stdin-stdout = file descriptors 0 and 1 of a child process; named-pipe = mkfifo or CreateNamedPipe; shared-memory = mmap or POSIX shm_open. + framing: + values: [newline-delimited, length-prefix, http-upgrade, none] + description: How message boundaries are marked on the stream. Newline-delimited = each JSON object terminated by \n (NDJSON); length-prefix = a fixed-width byte count preceding each payload; http-upgrade = HTTP/1.1 Upgrade handshake then a framing sub-protocol; none = single-message connection (open, write, read, close). + lifecycle: + values: [per-request, persistent-session, child-process-boundary] + description: How the connection lifetime relates to individual calls. Per-request = open a new connection for each call; persistent-session = one long-lived connection carries many calls; child-process-boundary = connection lifetime equals the child process lifetime (stdin closes = child exits). diff --git a/docs/exemplars/ipc-rpc/subprocess-rpc.md b/docs/exemplars/ipc-rpc/subprocess-rpc.md new file mode 100644 index 0000000..b3af879 --- /dev/null +++ b/docs/exemplars/ipc-rpc/subprocess-rpc.md @@ -0,0 +1,25 @@ +--- +view-types: [ipc-rpc] +conventions: + - "The parent process spawns the child with its stdin and stdout connected to pipes; the child reads JSON-RPC request objects from stdin (fd 0) and writes JSON-RPC response objects to stdout (fd 1)" + - "Each JSON-RPC message on stdin and stdout is a single UTF-8 JSON object terminated by a single newline byte; the child must not emit any non-JSON output to stdout (debug output must go to stderr, fd 2)" + - "EOF on stdin signals orderly shutdown: when the parent closes the write end of the stdin pipe, the child's read on stdin returns 0 bytes; the child must flush any buffered output to stdout, then exit with status 0" + - "The child must not read from stdin and write to stdout concurrently in a single-threaded implementation unless the child uses non-blocking I/O or a select/poll loop; a blocking read on stdin while stdout is full causes deadlock if the parent is also blocked writing" + - "SIGTERM sent to the child is a request for graceful shutdown: the child should stop accepting new requests, complete any in-flight request, flush stdout, and exit; SIGKILL is the parent's fallback if the child does not exit within a timeout" + - "The child's exit status is the machine-level shutdown indicator: exit 0 = clean shutdown; exit non-zero = crash or protocol error; the parent must read the exit status via waitpid to avoid leaving zombie processes" +axes: {transport: stdin-stdout, framing: newline-delimited, lifecycle: child-process-boundary} +calibrated-for: [library-consumer, sdk-author, api-consumer] +taxonomy-version: 1 +source-url: https://pubs.opengroup.org/onlinepubs/9699919799/functions/waitpid.html +last-reviewed: 2026-05-15 +--- + +# subprocess-rpc ipc-rpc conventions + +The stdin/stdout JSON-line protocol treats the child process lifetime as the session lifetime. The parent opens the channel by spawning the child; the parent closes the channel by closing its write end of the stdin pipe; the protocol ends when the child exits. Every JSON-RPC exchange happens inside that lifecycle boundary — there is no connect or reconnect; there is only spawn and terminate. + +The newline-terminated framing rule carries an important constraint for the child: stdout must be used exclusively for JSON-RPC responses. Any diagnostic or log output emitted to stdout corrupts the framing stream from the parent's perspective. The child must redirect all non-response output to stderr. Implementations that mix log lines into stdout produce unparseable frames the first time a log statement fires. + +The deadlock scenario in single-threaded implementations is a classic producer/consumer problem: if both parent and child are blocked on writes (parent writing to the child's stdin pipe, child writing to the parent's stdout pipe) and neither pipe has buffer space, neither can proceed. The solution is either a threaded or async read-write loop, or use of non-blocking I/O with select/poll on both pipe file descriptors. Single-threaded synchronous implementations that read one frame then write one frame avoid the deadlock only when the parent follows the same alternating pattern — a fragile assumption. + +SIGTERM-based graceful shutdown gives the child the opportunity to complete an in-flight request and flush its output buffer before exiting. The parent should wait a bounded interval after sending SIGTERM (typically 5–30 seconds depending on expected request latency) before escalating to SIGKILL. The parent must call waitpid after the child exits to collect its exit status and prevent a zombie process entry in the process table. diff --git a/docs/exemplars/ipc-rpc/unix-socket-rpc.md b/docs/exemplars/ipc-rpc/unix-socket-rpc.md new file mode 100644 index 0000000..bd10715 --- /dev/null +++ b/docs/exemplars/ipc-rpc/unix-socket-rpc.md @@ -0,0 +1,25 @@ +--- +view-types: [ipc-rpc] +conventions: + - "The server creates an AF_UNIX socket of type SOCK_STREAM and binds it to a filesystem path; the path must be unlinked before bind or bind will return EADDRINUSE" + - "Each JSON-RPC message is terminated by a single newline byte (0x0A); no other framing is added; the receiver reads until it encounters a newline, then parses the accumulated bytes as a single JSON-RPC object" + - "Messages on a UNIX socket connection are ordered and lossless within a single connection; no message-level sequence numbers are needed for ordering, but JSON-RPC id fields are still required for request/response correlation" + - "Connection lifecycle: server calls accept() to obtain a client file descriptor; for each complete newline-delimited frame the server reads, it writes exactly one newline-delimited response frame (for non-notification requests) or nothing (for notifications); EOF on the read side signals client disconnect and the server closes its end" + - "The socket file descriptor is inherited by child processes unless O_CLOEXEC (or SOCK_CLOEXEC on Linux) is set at creation time; the server must set O_CLOEXEC on both the listening socket and each accepted file descriptor to prevent leaking the IPC channel into subprocesses" + - "SIGPIPE is generated when writing to a socket whose read end has been closed; servers must either install SIG_IGN for SIGPIPE or use MSG_NOSIGNAL on send calls, and must handle EPIPE from write/send as a normal disconnect event" +axes: {transport: unix-socket, framing: newline-delimited, lifecycle: persistent-session} +calibrated-for: [library-consumer, api-consumer, sdk-author] +taxonomy-version: 1 +source-url: https://pubs.opengroup.org/onlinepubs/9699919799/functions/bind.html +last-reviewed: 2026-05-15 +--- + +# unix-socket-rpc ipc-rpc conventions + +JSON-RPC 2.0 over a UNIX domain socket combines two independently specified protocols. The transport (AF_UNIX SOCK_STREAM) provides a reliable, ordered, full-duplex byte stream between two processes on the same host. The framing (newline-delimited JSON) turns that byte stream into a sequence of discrete messages. Neither layer knows about the other: the socket does not understand JSON, and JSON-RPC does not know it is running over a UNIX socket. + +The newline-delimiter convention is simple but fragile if the JSON serializer emits embedded newlines inside string values. A conformant implementation must ensure that no newline character appears inside the JSON text before the message-terminating newline. The canonical approach is to disable pretty-printing (all JSON in one line) — POSIX imposes no constraint on JSON shape; the newline-delimited convention is a stack-level protocol agreement, not a POSIX requirement. + +The O_CLOEXEC discipline is operationally important in any server that spawns subprocesses: without it, a forked child inherits the listening socket and all accepted connection file descriptors. The child holds a reference to the socket even after exec, which prevents the OS from delivering EOF to the client when the server closes its copy. Setting O_CLOEXEC at socket creation eliminates the inheritance path entirely without requiring per-fork close calls. + +SIGPIPE handling is mandatory for any persistent-session server. A client that exits while the server is mid-write causes SIGPIPE delivery; the default disposition terminates the process. The correct disposition is SIG_IGN (or MSG_NOSIGNAL) so that write returns -1 with EPIPE instead, which the server handles as a clean disconnect and closes the accepted file descriptor. diff --git a/docs/exemplars/output-shape/axes.yml b/docs/exemplars/output-shape/axes.yml new file mode 100644 index 0000000..497eb2d --- /dev/null +++ b/docs/exemplars/output-shape/axes.yml @@ -0,0 +1,11 @@ +taxonomy-version: 1 +axes: + envelope: + values: [http-status-body, json-rpc-2.0, json-lines, plain-text, structured-log, problem-details-rfc7807] + description: The outermost container that wraps every response. Http-status-body = HTTP status code plus a body; json-rpc-2.0 = the JSON-RPC 2.0 response object (jsonrpc, id, result|error); json-lines = one JSON object per newline; plain-text = unstructured UTF-8 stream; structured-log = keyed log records (logfmt, JSON); problem-details-rfc7807 = RFC 7807 problem object. + success-error-split: + values: [http-only, body-field, exclusive-union, status-plus-body] + description: How success and error outcomes are distinguished. Http-only = HTTP status code is the sole indicator; body-field = a field in the body carries success/error; exclusive-union = exactly one of result or error is present (JSON-RPC model); status-plus-body = HTTP status plus a structured error body. + schema-binding: + values: [openapi-3.1, jsonschema-draft7, none, vendor-spec] + description: Which schema vocabulary constrains the response object. Openapi-3.1 = OpenAPI 3.1 response object; jsonschema-draft7 = JSON Schema draft-07; none = no formal schema; vendor-spec = proprietary specification document. diff --git a/docs/exemplars/output-shape/json-rpc-response-2.0.md b/docs/exemplars/output-shape/json-rpc-response-2.0.md new file mode 100644 index 0000000..378c0f6 --- /dev/null +++ b/docs/exemplars/output-shape/json-rpc-response-2.0.md @@ -0,0 +1,24 @@ +--- +view-types: [output-shape] +conventions: + - "Every response object must contain a `jsonrpc` member with the string value `\"2.0\"` exactly; no other version string is valid under this spec" + - "The `id` member must be present and must equal the `id` of the request being responded to; when the request `id` was null or the request was a notification (no `id`), a response must not be sent" + - "A response must contain either a `result` member or an `error` member, never both simultaneously; a response with neither member is non-conformant" + - "The `result` member may be any JSON value including null; its structure is defined by the server method, not by the protocol" + - "The `error` member, when present, must be an Error Object containing an integer `code` field, a string `message` field, and an optional `data` field of any type" + - "Error codes in the range -32768 to -32000 are reserved for pre-defined protocol errors: -32700 (Parse error), -32600 (Invalid Request), -32601 (Method not found), -32602 (Invalid params), -32603 (Internal error); server-defined errors use codes outside this range" + - "Batch responses: when a request array is sent, the response is an array of individual response objects in any order; the client matches responses to requests via the `id` field" +axes: {envelope: json-rpc-2.0, success-error-split: exclusive-union, schema-binding: none} +calibrated-for: [programmatic-consumer, library-consumer, api-consumer, sdk-author] +taxonomy-version: 1 +source-url: https://www.jsonrpc.org/specification +last-reviewed: 2026-05-15 +--- + +# json-rpc-response-2.0 output-shape conventions + +JSON-RPC 2.0 defines a minimal response envelope: four possible members (`jsonrpc`, `id`, `result`, `error`), one of which (`result` or `error`) is exclusive. The exclusive-union property is the structurally load-bearing constraint. A client that inspects both members — treating a response as success if `result` is truthy even when `error` is also present — is implementing the protocol incorrectly, because a conformant server will never send both. + +The `id` field is the correlation mechanism. When a call carries `id: 42`, the response carries `id: 42`. When a call is a notification (no `id` member), no response is sent at all — this is a deliberate protocol choice to enable fire-and-forget messaging without reserving response capacity. A client that expects a response to a notification will hang; a server that sends a response to a notification is non-conformant. + +Error codes from the reserved range (-32768 to -32000) carry stable cross-implementation meaning and must not be repurposed by server implementations. The `data` field in an Error Object is unconstrained and is the canonical extension point: it may carry structured diagnostic information (stack traces, field-level validation failures) without altering the top-level protocol shape. Servers that embed extended error information in the `message` string instead of `data` are technically conformant but operationally harder to parse programmatically. diff --git a/docs/exemplars/output-shape/openapi-response-envelope.md b/docs/exemplars/output-shape/openapi-response-envelope.md new file mode 100644 index 0000000..f03ade4 --- /dev/null +++ b/docs/exemplars/output-shape/openapi-response-envelope.md @@ -0,0 +1,24 @@ +--- +view-types: [output-shape] +conventions: + - "A Response Object in OpenAPI 3.1 requires a `description` field (a string); `content` and `headers` are optional" + - "HTTP status codes in the Responses Object map keys may be specific (`\"200\"`, `\"404\"`) or wildcard (`\"2XX\"`, `\"4XX\"`, `\"5XX\"`); a specific code takes precedence over a wildcard for the same status" + - "The `content` field maps media type strings to Media Type Objects; the Media Type Object's `schema` field is a JSON Schema 2020-12 (or Reference Object) describing the response body for that content type" + - "The `Content-Type` response header value determines which Media Type Object schema applies to the body; the server selects the content type using the `Accept` request header and the operation's `content` map" + - "The `default` key in the Responses Object covers all HTTP status codes not explicitly listed; it is used for error envelopes that apply across many status codes (e.g. a uniform error body for all 4XX and 5XX responses)" + - "Headers declared in the Response Object's `headers` field are in addition to `Content-Type` and `Content-Length` which are described via the `content` map, not via `headers`" + - "Combining schemas across status codes via `$ref` is standard practice; a shared error envelope schema (e.g. `#/components/schemas/ErrorBody`) may appear in multiple Response Objects via Reference Object" +axes: {envelope: http-status-body, success-error-split: status-plus-body, schema-binding: openapi-3.1} +calibrated-for: [programmatic-consumer, api-consumer, library-consumer, sdk-author] +taxonomy-version: 1 +source-url: https://spec.openapis.org/oas/v3.1.0#response-object +last-reviewed: 2026-05-15 +--- + +# openapi-response-envelope output-shape conventions + +The OpenAPI 3.1 Response Object describes what a server sends back for a given HTTP status code. The structure is declaration-first: an operation's `responses` field is a map from status code string to Response Object, and each Response Object contains a `content` map from media type to schema. The status code is the first discriminator (is this a 200 or a 404?); the `Content-Type` header is the second (which schema governs this 200's body?). Both discriminators must be applied before schema validation is meaningful. + +Wildcard keys (`"2XX"`, `"4XX"`) allow one Response Object to cover a range of codes — useful when the error envelope is uniform across all client errors. The precedence rule (specific code beats wildcard) means a server can declare a generic `"4XX"` error envelope and also override it with a specific schema for `"422"` validation failures without ambiguity. + +The `default` key is distinct from wildcards: it is the fallback for any status code not named in the map, including codes outside the standard ranges. OpenAPI uses it idiomatically for error schemas that apply to all unexpected outcomes. A client consuming an undocumented status code can find a schema for the body via `default` if the API author declared one; without `default`, the client must treat the body as opaque. diff --git a/tests/test_catalog_seed_v1_3.py b/tests/test_catalog_seed_v1_3.py new file mode 100644 index 0000000..5810bf2 --- /dev/null +++ b/tests/test_catalog_seed_v1_3.py @@ -0,0 +1,154 @@ +"""tests/test_catalog_seed_v1_3.py — v1.3 catalog seed: input-shape, output-shape, ipc-rpc. + +Six tests: + 1-2. input-shape exemplars are present in the loaded catalog. + 3-4. output-shape exemplars are present. + 5-6. ipc-rpc exemplars are present. + 7. validate_catalog() returns no errors for the full catalog. + 8. Each new exemplar's calibrated-for values are all in _ALL_FINGERPRINTS. +""" +from __future__ import annotations + +import pytest + +from bin import _catalog +from bin._catalog import CatalogError, load_catalog, validate_catalog +from bin._catalog import _ALL_FINGERPRINTS + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + +@pytest.fixture(autouse=True) +def fresh_catalog(monkeypatch): + """Force catalog reload before each test so prior test state does not bleed.""" + monkeypatch.setattr(_catalog, "_LOAD_CACHE", None) + yield + monkeypatch.setattr(_catalog, "_LOAD_CACHE", None) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +_NEW_KEYS = [ + "input-shape:cli-argparse-shape", + "input-shape:openapi-request-body", + "output-shape:json-rpc-response-2.0", + "output-shape:openapi-response-envelope", + "ipc-rpc:unix-socket-rpc", + "ipc-rpc:subprocess-rpc", +] + + +# --------------------------------------------------------------------------- +# Tests: exemplar presence +# --------------------------------------------------------------------------- + +def test_input_shape_cli_argparse_shape_in_catalog(): + """input-shape:cli-argparse-shape is loadable and present in the catalog.""" + cat = load_catalog() + assert "input-shape:cli-argparse-shape" in cat.exemplars, ( + "expected input-shape:cli-argparse-shape in catalog; " + f"available input-shape keys: {[k for k in cat.exemplars if k.startswith('input-shape:')]}" + ) + + +def test_input_shape_openapi_request_body_in_catalog(): + """input-shape:openapi-request-body is loadable and present in the catalog.""" + cat = load_catalog() + assert "input-shape:openapi-request-body" in cat.exemplars, ( + "expected input-shape:openapi-request-body in catalog; " + f"available input-shape keys: {[k for k in cat.exemplars if k.startswith('input-shape:')]}" + ) + + +def test_output_shape_json_rpc_response_in_catalog(): + """output-shape:json-rpc-response-2.0 is loadable and present in the catalog.""" + cat = load_catalog() + assert "output-shape:json-rpc-response-2.0" in cat.exemplars, ( + "expected output-shape:json-rpc-response-2.0 in catalog; " + f"available output-shape keys: {[k for k in cat.exemplars if k.startswith('output-shape:')]}" + ) + + +def test_output_shape_openapi_response_envelope_in_catalog(): + """output-shape:openapi-response-envelope is loadable and present in the catalog.""" + cat = load_catalog() + assert "output-shape:openapi-response-envelope" in cat.exemplars, ( + "expected output-shape:openapi-response-envelope in catalog; " + f"available output-shape keys: {[k for k in cat.exemplars if k.startswith('output-shape:')]}" + ) + + +def test_ipc_rpc_unix_socket_rpc_in_catalog(): + """ipc-rpc:unix-socket-rpc is loadable and present in the catalog.""" + cat = load_catalog() + assert "ipc-rpc:unix-socket-rpc" in cat.exemplars, ( + "expected ipc-rpc:unix-socket-rpc in catalog; " + f"available ipc-rpc keys: {[k for k in cat.exemplars if k.startswith('ipc-rpc:')]}" + ) + + +def test_ipc_rpc_subprocess_rpc_in_catalog(): + """ipc-rpc:subprocess-rpc is loadable and present in the catalog.""" + cat = load_catalog() + assert "ipc-rpc:subprocess-rpc" in cat.exemplars, ( + "expected ipc-rpc:subprocess-rpc in catalog; " + f"available ipc-rpc keys: {[k for k in cat.exemplars if k.startswith('ipc-rpc:')]}" + ) + + +# --------------------------------------------------------------------------- +# Tests: frontmatter parses without CatalogError +# --------------------------------------------------------------------------- + +def test_all_new_exemplars_parse_without_error(): + """Each of the six new exemplars parses without raising CatalogError. + + Confirmed by the catalog having zero parse_errors attributable to + the new exemplar paths. + """ + cat = load_catalog() + new_view_prefixes = ("input-shape", "output-shape", "ipc-rpc") + new_parse_errors = [ + e for e in cat.parse_errors + if any(prefix in str(e.path) for prefix in new_view_prefixes) + ] + assert new_parse_errors == [], ( + f"unexpected CatalogError(s) for new v1.3 exemplars: {new_parse_errors}" + ) + + +# --------------------------------------------------------------------------- +# Test: validate_catalog() clean +# --------------------------------------------------------------------------- + +def test_validate_catalog_returns_no_errors(): + """validate_catalog() returns an empty error list for the full catalog + including the six new v1.3 exemplars.""" + errors = validate_catalog() + assert errors == [], ( + f"validate_catalog() returned {len(errors)} error(s):\n" + + "\n".join(errors) + ) + + +# --------------------------------------------------------------------------- +# Test: calibrated-for values are all in _ALL_FINGERPRINTS +# --------------------------------------------------------------------------- + +def test_new_exemplar_calibrated_for_values_are_known(): + """Every calibrated-for value in the six new exemplars is in _ALL_FINGERPRINTS.""" + cat = load_catalog() + bad: list[str] = [] + for key in _NEW_KEYS: + ex = cat.exemplars.get(key) + if ex is None: + bad.append(f"{key}: not found in catalog") + continue + for fp in ex.calibrated_for: + if fp not in _ALL_FINGERPRINTS: + bad.append(f"{key}: unknown fingerprint {fp!r}") + assert bad == [], "unknown fingerprint values found:\n" + "\n".join(bad) diff --git a/tests/test_v1_1_e2e.py b/tests/test_v1_1_e2e.py index b07046f..97c88db 100644 --- a/tests/test_v1_1_e2e.py +++ b/tests/test_v1_1_e2e.py @@ -221,16 +221,14 @@ def test_v1_1_acceptance_synthetic_spec(tmp_path: pathlib.Path) -> None: deferrals = [f for f in all_findings if f.kind == "post-ship-iteration-deferral"] assert len(deferrals) == 2 - # v1.2.1 #5: §9 (help-text placeholder catalog) has no exemplar compatible - # with `human-typed` fingerprint → tagged `no-compatible-exemplar`. - # §12 (api-shape) has exemplars compatible with `api-consumer` → - # tagged `operator-deferral`. The aggregate warn counts only the - # operator-deferral subset, so with one operator-deferral the warn - # does NOT fire (≤ 1 threshold). + # v1.3 #8: §9 now maps to input-shape which has cli-argparse-shape (human-typed) + # and openapi-request-body (programmatic-trusted) — both compatible with the + # human-typed fingerprint in §8.3. §12 (api-shape|ipc-rpc) has api-consumer + # compatible exemplars. Both deferrals are operator-deferral (compatible + # exemplars exist; operator chose to defer). Count=2 > 1 → excessive fires. by_reason = {d.reason for d in deferrals} - assert "no-compatible-exemplar" in by_reason - assert "operator-deferral" in by_reason - assert "excessive-post-ship-iteration" not in kinds + assert by_reason == {"operator-deferral"} + assert "excessive-post-ship-iteration" in kinds # Fix 3: behavioral why + structural-only verification on step 2 assert "verification-too-shallow-for-claim" in kinds