Skip to content

docs(emitters): lower redaction and stability tiers - #1340

Open
OmarAlJarrah wants to merge 37 commits into
mainfrom
docs/emitters-redaction-stability
Open

OmarAlJarrah wants to merge 37 commits into
mainfrom
docs/emitters-redaction-stability

Conversation

@OmarAlJarrah

@OmarAlJarrah OmarAlJarrah commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Summary

Two facts the IR states that no document told an emitter what to do with. Both concern what an SDK shows rather than what it sends, so one document answers them (docs/emitters/refine/redaction.md). This builds on #1229, now on main, which gives Parameter a Secret and operations a Stability.

Redaction (#895).

Stability (#1222).

  • OpPlan.Stability resolves the tier once: the operation's own, else its nearest group's, else none. An unstated tier is never read as stable.
  • The floor every target keeps: an alpha or beta operation's documentation names its tier, and the docs badge every stated tier.
  • Go adds a doc-comment paragraph and gates nothing. A build tag per tier would split the package's API by tag and double every build.
  • Kotlin adds @AlphaApi (RequiresOptIn at ERROR) and @BetaApi (WARNING), with the tier also in KDoc, since Swift sees no annotation.
  • No target leaves an alpha operation out, since hiding operations is the filter pass's job, so the SDK and its docs hold the same ones.
  • MarkStability joins the step list, both lowering tables answer the new rows, and docs/ir/operations.md now points here instead of at this issue.

On the golden spec (spec @319dc756) 3,190 of 3,763 operations state a tier, as #1229's own document records.

T-45 pins redaction and T-46 stability.

Closes #895
Closes #1222

Test plan

  • Every link added resolves to a file and heading present on this branch, docs/ir/operations.md#stability among them.
  • TestEmitterDocs_EveryTargetAnswersEveryConstruct's rule: each new shared row has an answer in both target tables, read by eye against the test's parser (first cell, non-empty rest).
  • CI: the doc tests.

Merge notes. #1229 has landed, and this now targets main. Its merge of #1229's final head and of main resolved one conflict, in docs/ir/operations.md: this PR's link replaces #1229's "not stated yet (#1222)". The design note there that cited #895 now links the plan's redaction reading. It is independent of the emitter-design stack (#1329 ← … ← #1339), which builds on main. Whichever lands second resolves three small overlaps:

The insertions here avoid every other line the stack edits.

The IR could say an operation was deprecated, and could not say how
stable it is: a tier survived only as a verbatim Unmodeled entry, so each
emitter would derive it from a vendor key its own way. A parameter had no
redaction field at all, so `format: password` at a parameter stayed under
Unmodeled while a property took Secret from the same schema.

Operation and OperationGroup gain Stability, a closed alpha | beta |
stable that is empty where a source states none, and irverify reports a
tier the IR does not declare. Parameter gains Secret, the per-use form
Property.Secret already is: a parameter never hoists a node, so a
Sensitive scalar would have to be minted for it.

The OpenAPI compiler now lowers `format: password` at a parameter that
owns no node to Parameter.Secret, inherited from a $ref target as at a
property, and keeps no openapi:format entry beside it.

docs/ir/boundaries.md#access states the rule that separates
TypeCommon.Access from client-shaping `@access`.

BREAKING CHANGE: IRVersion moves to 0.23.0. A consumer pinned to 0.22.0
cannot decode a document that sets either field, and one reading
openapi:format on a parameter no longer finds it.
Two keys mapped to one field resolved by how the keys sort, and nothing
was reported: the policy was a map from key to target, which could not
state an order, so the walk sorted the keys and the last one won.

Targets is now a map from target to an ordered list of sources, and the
list is the precedence: the first source stating a value fills the
field, and a later one stating another is reported at its own key. A
source can name a member of an object value, so one key that holds two
facts can fill two fields.

Reading moves off Ctx to a Promoter holding the policy. Ctx was one
method short of the cap on a type's methods, and promotion reads nothing
of it but the policy. The policy's lists are copied with the map, which
maps.Clone alone left shared with the caller.

BREAKING CHANGE: ExtensionPromotions.Targets changes from
map[string]ExtensionTarget to map[ExtensionTarget][]ExtensionSource, and
DefaultExtensionPromotions returns the new shape.
An extension stating a tier, a redaction or a deprecation reached only a
verbatim Unmodeled entry, so an operation deprecated by a vendor key alone
read as live, and a value marked sensitive read as plain.

Three targets join the promotion policy. TargetDeprecation creates the
Deprecation the member targets fill, at every carrier a keyword can.
TargetStability fills Operation.Stability from a closed set of words.
TargetRedaction sets Sensitive on a node and Secret on a property, header
or parameter, where `format: password` lands. The default mapping gains
the keys the golden spec writes for each, and for a deprecation's message
and removal date, two of them objects read by member.

A key mapped to both deprecation and stability reports one defect once:
the word `deprecated` and a value neither can hold are the deprecation
reader's to answer for.
Secret folds by OR, so a redeclared field takes it from a later
declaration as readily as from the first, while the field keeps the
first declaration's provenance. A redaction a promotion read on a later
declaration would then carry no marker saying a heuristic made it, which
the promotion contract requires of every node it writes.

The fold joins the marker of each redacting declaration, which gives the
same answer in either branch order.
Annotations merge from a $ref's target onto the property, parameter or
header naming it, field by field, but a promotion read only the node it
ran on. So `Old: {deprecated: true, x-sunset: D}` kept its date on the
node while `{$ref: Old}` lowered to an empty Deprecation, and a key that
deprecates a target left its uses live, though an allOf decorator over
the same target reaches the emitter deprecated through the fold.

A carrier now reads the target's mapped keys beneath its own. A key on
the target counts as the target's `deprecated`, so a `deprecated` beside
the $ref decides over it as it does over the keyword. Only the keys the
policy maps are looked up, and the target reports its own misfits, so a
use reports nothing a declaration already has.

Promotion moves to the caller of each carrier, after the object around a
header's or parameter's schema has spoken: that object decides what is
deprecated, and so what a member can fill.

A key on an allOf branch, on a path item, a tag or a content entry, or
inside a oneOf kept verbatim is still read by nothing, since a promotion
reads the top level of a carrier's Unmodeled. The rule's Status says so,
with the issues that track each.
A use read its target's mapped keys afresh, so a key's value was read
once per use. A value as large as the document times as many uses as the
document names it is their product: 84 KB with a 20,000 item key and 500
uses retired 58G instructions where main takes 0.9G. What a target lends
is now read once per target and shared by every copy of the context,
which puts the promoter behind one pointer.

Three readings change with it.

A target lends a member only where it is deprecated itself. A date on a
live target annotates nothing there, and a use deprecated on its own read
it anyway, while a 3.0 decorator over the same target could not: the fold
that stands for it reads the Deprecation the node holds, and the node has
none. The two spellings of one use now agree on what the target lends.

Empty text states nothing. A field holding "" reads as unset, so such a
key filled nothing, left a marker saying it had, and stood in front of a
later source with something to say.

Where the two stability keys disagree the order decides and not the
tiers, which is what the code did and the text did not say. That it reads
the less stable statement holds of what the golden spec writes.
Secret merges from a $ref target onto a parameter as it does onto a
property, so the three comments listing what a use site holds of its
target were one short, and the one in ir/typeref.go says everything else
is read from the target.

A schema between a use and the end of its chain lends nothing because
that is what the keyword does, which #1029 tracks as a defect, so the
rule cites it rather than settling it. And a key written on both a
parameter or a header and its schema shares one entry (#396), so the
schema's is not read. That is a limit on the rule that false clears no
redaction, and it belongs where the rule is.
A limitation belongs at the code or rule that has it, and these sat in
the PR text or nowhere: a key a parameter and its schema both write
(#396), a stability key's boolean (#654), the facts the default leaves
(#656), a key inside a verbatim oneOf (#661), a target read past the
schema a use names (#1029), and what no refine step reads yet (#895,
#1222).

The use-site rule gains the Status it lacked, and the test holding a
decorator to its $ref says which two spellings it does not cover, one of
them newly filed as #1258. The cost test stops quoting a size it never
measured.

The promoter holds a memo now, so its comment says what makes that safe.
What a target lends is remembered by position, and two documents can hold one pointer, so the use read second must not take the first one's answer. No promotion test read a target in another document.
A parameter was said never to hoist a node of its own, though a schema with an enum does, and the sentence went on to count the parameters that own one. What holds is that a carrier never hoists an alias, so a parameter whose schema owns no node has none to hold a Sensitive.
The fold joins the marker of a later redaction, but the rule said only
that a merged field names the heuristic behind its deprecation, and its
pin was not cited. Name both.

The version history records what each bump changed, and a lowering that
starts filling a member is a change of meaning the shape cannot show.
This bump has one: keys now state a deprecation and a redaction, and a
use reads its target's. Record it beside the two shape changes.

The list in ir.Constraints of what a $ref's use reads was the last of
its kind to omit redaction.
The conformance assertion walks parameters, headers and properties but
said two carriers, and the file comment above the promotion tests still
called the reading one function and every target a field with no other
spelling, though a Promoter holds it and `deprecated` is a keyword.
The comment on ExtensionTarget gives a silent policy as the reason the
vocabulary is closed, while a mapping naming an unknown target compiles
with no report. That predates this change and is filed as #1281; say so
at the type and in the rule's status, so neither reads as enforced.
A parameter and a header elect their schema from `schema` or `content`,
and PromoteUse takes the elected one. Every use-site case wrote `schema`,
so reading the object's own schema instead left all of them green. Run
each case at both spellings.
#1191 landed 0.23.0 first, so this change keeps the row and the history
entry main holds and appends its own under 0.24.0. Its history entry now
names 0.23.0 as the generation a pinned consumer holds.
Two promotion sites had no test that a wrong answer would turn red.

Promoter.Stability joins its marker onto an operation's Inferred list.
Assigning it instead dropped the streaming and grouping markers on the
ten golden-spec operations that state a tier and stream by media type,
so an inferred stream read as declared, with the suite green. The
marker-list test now states a tier as well as a deprecation reason.

A parameter or header whose content map holds several entries promotes
from the entry the election takes, the lowest key. Promoting from the
first declared entry instead left every test green at both carriers.
The new test declares text/plain first and application/json second,
with the target on each in turn.
#1219 took 0.24.0 and #1226 0.25.0, so this change moves to 0.26.0,
keeps both rows and history entries, and appends its own.

#1219 turned the field list of docs/ir/unmodeled.md#promotion into a
table held to the code both ways. It gains the fields this change
fills: a Deprecation itself at each carrier, Operation.Stability and
Parameter.Secret, with TypeCommon named among the nodes keeping a
deprecation's or a Secret's extension, as a $ref's target lends both
to a use. TestExtensionTarget_EveryTargetIsInThePromotionTable now
names every field a target fills rather than one, since TargetRedaction
fills three. OperationGroup joins Variant and EnumMember as a node rule
4 names, and TestPromotionTable_EveryNodeCanRecord holds it there.

#1219's rule 3 records a promotion on the node keeping the extension.
A field a $ref's target lends a use is the one case this change records
twice: the target keeps the entry and its marker, and the use lists the
marker too, since a use is read without resolving its target and its
type reaches the entry. Rule 3, extension-promotion and the 0.26.0
history entry say so.

#1217 ended fillPropertyAnnotations in PreserveCarriedKeywords; the
promotion this change moved to PromoteUse stays out of it. #1226
rewrote lowerSecurityScheme's return, which now calls the Promoter.
The two new conformance goldens gain the unresolved auth namespace
every OpenAPI document declares since #1226.
#1302 took 0.26.0 for a change of meaning, with the shape of 0.25.0,
so this change moves to 0.27.0 and keeps its shape hash. Its history
entry now names 0.26.0 as the version an older consumer is pinned to.
An allOf fold took a later declaration's whole marker whenever that
declaration was secret. A property's marker covers every fact a
promotion read into it, so a declaration redacted by format: password
whose promoted removal date lost to an earlier deprecation marked the
field, which then kept neither promoted fact.

Each declaration now tells the merge which heuristics set its Secret,
empty where a keyword declared it, and the fold joins that alone.
The fold's marker was held only from e2e, which the coverage gate does not
count for compilers/openapi/internal/schema, so the line naming a promoted
Secret's heuristic stood uncovered. The case moves beside the other allOf
redeclaration tests, where both of its sites redden under a mutant.
A message #1219 wrote on main named PromoteDeprecation, which this change
replaced with Promoter.Deprecation, and the merge kept it.
A use a $ref's target lends a promoted fact to lists the marker, though
the entry the heuristic read is the target's, not its own. The field's
definition named only the node's own Unmodeled.
No refine step read TypeCommon.Sensitive, so an emitter following the
design redacted by Property.Secret alone and printed in the clear every
value the IR marks through its type, and no document said what a target
does with an operation's stability tier.

The plan now records, per type, whether it is sensitive along its Base
chain and which flattened members are redacted, accumulating Secret over
every declaration of a member as requiredness does. A redacted value
shows as [REDACTED] in every text rendering, a collection as the marker
alone, and a raw body holding one is never logged; the wire is
untouched. OpPlan.Stability resolves an operation's tier through its
groups once. Every target documents an alpha or beta tier, Kotlin also
requires an opt-in, and nothing reads an unstated tier as stable.
checkStability was shown every value of a document, though its type sits
only on Operation and OperationGroup. That cost a verified compile of the
golden spec about 0.5%, and Verify alone on its IR about 4% (spec
@319dc756). It now prunes to the path checkStreamingModes already prunes
to, renamed operationPath as it serves both, and
TestOperationPath_ListsEveryCarrier counts Stability's carriers, so a
field of that type off the path fails there rather than passing unread.
readStability's comment called a key's boolean the deprecation target's
statement, which reads as both values. Only true is: a false names no
tier and is reported here, as the extension-promotion rule states and
TestPromoter_OneKeyTwoTargetsReportsOnce holds.
The plan read a member's declarations itself because the flattening
dropped a redaction an overridden declaration asked for. #1355 gives
ir.FlatProperty a Secret that accumulates as Required does, so
TypePlan.Redacted reads that instead.
Base automatically changed from feat/ir-extension-stated-facts to main November 2, 2026 12:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant