Skip to content

feat(ir)!: give Payload a Required field and Parameter a Provenance - #437

Open
fuad-daoud wants to merge 2 commits into
stack/2-redeclarationfrom
stack/3-ir-fields
Open

feat(ir)!: give Payload a Required field and Parameter a Provenance#437
fuad-daoud wants to merge 2 commits into
stack/2-redeclarationfrom
stack/3-ir-fields

Conversation

@fuad-daoud

Copy link
Copy Markdown
Collaborator

Closes #421 and #423. Breaking — two IR field additions.

Payload.Required *bool (#421)

requestBody.required survived only as an inverted sentinel: Unmodeled["openapi:required"] = false written when the body is not required, nothing written when it is. A consumer had to know an OpenAPI-specific key and read its absence as true.

ir/unmodeled.go classifies no_ir_home as "a gap expected to close, not a boundary" — this is that gap. Tri-state on purpose: absent stays distinguishable from false, for a format that does not express body optionality. 563 occurrences across GitHub and Stripe.

Downstream this is not merely a missing feature: dexpace/spaceapi renders the literal heading Request body (required) off this boolean, so on any IR not produced by the OpenAPI compiler it would print a confidently wrong statement.

Parameter.Provenance (#423)

ir.Parameter was the only lowered node without one, and params.go carried a comment saying so. Two consequences: x-sunset promotion was not wired at the parameter position, and after mergeParameters merges path-item parameters into every operation, nothing recorded that a parameter was inherited rather than declared.

An inherited parameter now carries the path item's own declaration pointer, not the operation's.

BREAKING CHANGE: Payload gains required; Parameter gains a non-omitempty provenance. A 0.3.0 consumer reading openapi:required now finds nothing and reads every body as required.


Stack 3 of 8. Base stack/2-redeclaration — review and merge bottom-up. Every commit here passed make gate when it landed, and the full gate was re-run on the top of the stack. Run it as GOTOOLCHAIN=go1.26.3 make gate; this machine's Go 1.27 fails it for reasons unrelated to any change (#431).

🤖 Generated with Claude Code

https://claude.ai/code/session_01SYeBgDsskwnyitLgPGCPn1

fuad-daoud and others added 2 commits September 5, 2026 20:19
Request-body optionality survived only as an inverted sentinel: the OpenAPI
compiler wrote Payload.Unmodeled["openapi:required"] = false when a body was
not required and wrote nothing when it was, so recovering the fact meant
knowing an OpenAPI-specific key and reading its absence as true. A consumer
that reads typed fields alone saw every body as required — 563 times across
GitHub's and Stripe's published specs. ir/unmodeled.go grades no_ir_home as
"a gap expected to close, not a boundary", and this is that gap.

ir.Payload now carries Required *bool. The pointer is the point: a format
that expresses body optionality treats an unstated body as optional, so
folding "the format is silent" onto the same value as "the document says no"
would lose the distinction a non-OpenAPI compiler needs. Response and message
payloads leave it nil, because only a request body can be omitted.

The OpenAPI compiler always sets it, since OpenAPI's own default makes an
undeclared `required` mean false rather than unstated, and it no longer
writes the openapi:required entry or the info diagnostic that announced the
degradation — the fact is modeled now, so neither describes anything.

ir-design.md is normative on the field shapes, so §7.2's Payload and §14's
OpenAPI lowering summary are updated with it.

BREAKING CHANGE: a consumer reading Payload.Unmodeled["openapi:required"]
must read Payload.Required instead; the Unmodeled entry and its
openapi/degraded-construct info diagnostic are no longer emitted. The
per-reason reachability test moves its no_ir_home witness to a parameter's
allowEmptyValue, which still has no typed home.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYeBgDsskwnyitLgPGCPn1
ir.Parameter was the last lowered node carrying no Provenance, and two
things followed from that.

A parameter's vendor extensions were stranded. ir-design §12 rule 4 says
a node with no provenance is not promoted into, because a promotion that
cannot be marked Inferred cannot be audited — so the parameter position
was the one ir.Deprecation carrier PromoteDeprecation was not wired at,
and a deprecated parameter's x-sunset sat unread beside an empty
Deprecation. It is wired now, and extension-promotion.yaml gains the
parameter row so the sweep fails at that carrier rather than being
covered by a neighbour.

Parameter origin was erased. mergeParameters merges a path item's
parameters into every operation on the path, and nothing afterwards
recorded that a given parameter was inherited rather than declared. The
stamp uses the pointer internal/operation already threads per parameter
for the interning fix (#36, #107): an operation's own entry points under
that operation, a $ref'd one at the component it names, and a path-item
one at the path item — one declaration named by every operation that
inherits it, which is what tells the two apart.

BREAKING CHANGE: ir.Parameter gains a Provenance field, serialized
without omitempty like every other node's. Every golden carrying a
parameter moves, and a consumer decoding the IR sees a new object on
each one.

Closes #423

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYeBgDsskwnyitLgPGCPn1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants