Skip to content

feat(ir)!: give Deprecation a RemovalDate; open Enum from x-extensible-enum - #439

Open
fuad-daoud wants to merge 2 commits into
stack/4-errorcasefrom
stack/5-promotion
Open

feat(ir)!: give Deprecation a RemovalDate; open Enum from x-extensible-enum#439
fuad-daoud wants to merge 2 commits into
stack/4-errorcasefrom
stack/5-promotion

Conversation

@fuad-daoud

Copy link
Copy Markdown
Collaborator

Closes #417 and #427. Breaking — one IR field addition.

Deprecation.RemovalDate (#417)

x-sunset was promoted into RemovalVersion, a field documented as "the version in which the entity is scheduled for removal". But x-sunset carries an ISO date by convention — it echoes RFC 8594's Sunset header, which is a date by definition.

Option 1 from the issue: a distinct field. A version and a date are two facts, not two spellings of one. A document may state both ("gone in 3.0.0", "gone on 2026-08-01") and neither is derivable from the other without a release calendar the IR does not have — so a single field carrying which spelling it holds would have to drop whichever it read second. A consumer tells them apart by which key the value arrived under, with no re-parsing.

RemovalDate is the source's own text, neither parsed nor normalized: no source format defines the field, so none defines its format.

Behaviour worth flagging: no default key names RemovalVersion any more, so the corpus stops witnessing it and it gains a line in unwitnessed.golden.txt. Inventing an x-* spelling for a removal version would be a convention morphic made up.

x-extensible-enumEnum.Closed (#427)

The IR already had exactly the field this extension is the OpenAPI spelling of, and the compiler hardcoded Closed: true, so the extension had no effect and survived only as a generic vendor extension. Correctly not marked breaking: the field's meaning is unchanged, only the compiler's computed value.

BREAKING CHANGE: Deprecation gains removalDate and x-sunset routes there. A consumer reading a removal date off removalVersion now finds it empty.


Stack 5 of 8. Base stack/4-errorcase — 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 7, 2026 23:21
x-sunset echoes RFC 8594's Sunset header, which is a date by definition,
but the default promotion mapping read it into Deprecation.RemovalVersion
— a field whose name, doc comment and sibling all say version. A consumer
deciding whether removing a deprecated operation is breaking compares a
sunset against a release date, and could not tell which spelling it had
been handed without re-parsing the string.

Take issue #417's option 1: a distinct RemovalDate beside RemovalVersion,
with x-sunset promoting to the date. A version and a date are two facts,
not two spellings of one — a document may state both ("gone in 3.0.0",
"gone on 2026-08-01"), and neither is derivable from the other without a
release calendar the IR does not have. A single field carrying which
spelling it holds (option 2) would have to drop whichever fact it read
second, so it costs losslessness to buy nothing a second field does not
already give: the field a value arrives in is what says which fact it is.

Deliberately out of scope, and stated in ir-design.md and at the reading
site: RemovalDate is the source's own text, neither parsed nor
normalized. No source format defines the field, so none defines its
format; and the key→field mapping is caller policy, so a key pointed at
the date field is the caller's statement that it holds a date. Morphic
records which fact was stated and leaves the calendar to the consumer.

BREAKING CHANGE: Deprecation gains removalDate, and x-sunset now fills it
instead of removalVersion. A consumer reading removalVersion for a sunset
reads an empty field until it moves. No default key names RemovalVersion
any more — a document stating a removal version names its own key, per
promotion rule 1 — so the corpus stops witnessing that field and it joins
unwitnessed.golden.txt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYeBgDsskwnyitLgPGCPn1
ir.Enum has carried a Closed bool since the IR was written, and the OpenAPI
compiler set it true at both construction sites unconditionally. So the one
key the format has for saying an enum is open, x-extensible-enum, survived
only as a generic vendor_extension entry, and every consumer reading typed
fields saw a closed enum whatever the document said. Open versus closed
decides whether a generator emits a fallback member and whether a differ
calls an added value breaking, so this was a wiring gap, not a modelling one.

Add TargetEnumOpen to the promotion vocabulary, map x-extensible-enum onto it
by default, and apply it in attachDeclaredAnnotations beside the deprecation
promotion — the point at which a declaration's extensions have reached the
node's map, which is what makes "the extension survives its own promotion"
structural here as it is there. Every promotion property holds unchanged: the
entry stays put with its vendor_extension reason, the node records
extension-promotion in Provenance.Inferred, and a disabled policy writes
nothing.

The target names the fact rather than the field, which the rest of the
vocabulary does not. Openness is the only half of that bool a document ever
declares — a schema's `enum` is closed by definition — so a target named for
Closed could only ever be written false and would read as its own opposite at
every mapping naming it. For the same reason the key's presence is the
statement rather than its value: the established spelling writes the member
list as the value, and a list of members says nothing about openness the key
naming it has not already said. A boolean is the one shape that does state it
alone, so an explicit `false` is read as written rather than inverted.

Deliberately out of scope, and stated in ir-design.md and at the reading site:
a document writing x-extensible-enum *instead* of `enum`, with the members in
the extension, lowers to no ir.Enum at all and there is no node to open.
Minting one would be reading a member list out of a vendor key rather than
promoting a field; the entry survives verbatim for a consumer that wants to.

The corpus can now witness the matrix's open-enums row, so its
matrixRowsUncovered reason is deleted rather than left to go stale, and
extension-promotion.yaml gains the three enums that pin the three answers the
reading has: the convention opens one, an explicit false declines to, and an
enum naming no such key is untouched.

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