Skip to content

feat(ir)!: type the secrecy and stability a document states - #1229

Merged
OmarAlJarrah merged 31 commits into
mainfrom
feat/ir-extension-stated-facts
Nov 2, 2026
Merged

OmarAlJarrah merged 31 commits into
mainfrom
feat/ir-extension-stated-facts

Conversation

@OmarAlJarrah

@OmarAlJarrah OmarAlJarrah commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

Summary

An operation's stability tier, a parameter's redaction and most of what a deprecation can say reached the IR only as verbatim Unmodeled entries, so every consumer would derive them from a vendor key its own way. An operation deprecated by a key alone read as live, and a value marked sensitive read as plain. This gives each fact a typed home and reads it by the promotion policy, with the entry kept beside it and the node marked extension-promotion.

IR, 0.27.0

  • Operation.Stability and OperationGroup.Stability: a closed alpha | beta | stable, empty where a source states none and never read as stable. A group's tier applies to members stating none, read through the group. irverify reports any other value (ir/unknown-stability).
  • Parameter.Secret, the per-use form Property.Secret already is. A carrier never hoists an alias, so where a parameter's schema owns no node a Sensitive scalar would have to be minted at a pointer a $ref can name. format: password at a parameter that owns no node now lands there, inherited from a $ref target as at a property, and keeps no openapi:format entry.
  • docs/ir/boundaries.md#access states the rule that separates TypeCommon.Access from client shaping: a fact true for every target is IR, and one SDK's surface is not. So protobuf's export/local and Smithy @internal are facts, the second with no operation field yet (no_ir_home, a gap and not a boundary), and TCGC @access is out_of_scope on types and operations alike.

Promotion policy

  • ExtensionPromotions.Targets is now ordered sources per target. 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. This ends two keys resolving by how they sort.
  • A source can read a member of an object value, so one key can fill two fields.
  • Three new targets. TargetDeprecation creates the Deprecation the members fill, at every carrier a keyword can. TargetStability fills the tier. 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, and only those.
  • Reading moves off lowering.Ctx, which was one method short of the type cap when this began (608a5a4), to a Promoter holding the policy.

A case no issue names. Annotations merge from a $ref's target onto its use field by field, but a promotion read only its own node: Old: {deprecated: true, x-sunset: D} kept its date while {$ref: Old} lowered to deprecation: {}. A property, header or parameter now reads what the target's mapped keys state beneath its own, read once per target however many uses name it, since a key's value can be as large as the document. A key on the target counts as the target's deprecated, so a deprecated beside the $ref decides over it, as docs/compilers/openapi.md#ref-use-site says of the keyword. A target lends a member only where it is deprecated itself: a date on a live target annotates nothing there or at a use, whatever the use says of itself. That is what keeps the 3.1 $ref in step with the 3.0 allOf decorator an emitter folds, which reads the Deprecation the target's node holds. Two spellings still part, a member key beside the allOf (#1220) and a deprecated: false on the decorator (#1258). A target's misfit is reported once, at the target.

Where a lent fact is recorded. #1219 records a promotion on the node keeping the extension, so that an auditor finds the marker beside the entry explaining it. A fact a $ref's target lends a use is the one case this records twice: the target keeps the entry and lists extension-promotion, and the use lists it too, though it keeps no entry. A use is read without resolving its target (docs/ir/annotations.md#what-a-reference-carries-down), so an unmarked use would state a guessed deprecation or redaction as declared, and its type reaches the entry, which a marker on an ancestor, the design #1219 rejected, does not. On the golden spec 78 properties are marked this way, and each one's type target holds the key and the marker itself. Rule 3 of docs/ir/unmodeled.md#promotion, extension-promotion and the history entry say so.

A folded field is marked for what it keeps. An allOf fold takes Secret from any declaration, so a later declaration's promoted redaction has to bring its marker. A property's marker covers every fact a promotion read into it, so the fold cannot take the whole marker: a declaration redacted by format: password, marked for a promoted date the field drops with that declaration's deprecation, would mark a field keeping no promoted fact. Each declaration tells the merge which heuristics set its Secret, and the fold joins that alone (schema.TestAllOf_RedeclarationMarksAPromotedSecretOnly).

Where the two stability keys disagree, the order decides. x-stability is read first, whichever tier each key names, so x-stability: stable wins over x-fern-availability: beta. On the golden spec that reads the less stable statement: all 47 disagreements are beta against generally-available. It is right where the text can tell (2 descriptions say beta, and none of the 2,636 operations marked generally available alone does). It over-reports where the mark sits on one method of a resource and not its neighbours, as GET and PUT of /accounts/{account_id}/access/apps/{app_id} carry it and DELETE does not, so a consumer gating on the tier hides such an operation. The info on each disagreement and the kept entries are what let it audit that, and no other order is shown to do better: reversing it loses the two the text confirms.

Left open, each stated in the code or docs/compilers/openapi.md

This stacks on nothing and holds main @7f849127, and #1340 stacks on it. #1219, #1226 and #1302 took 0.24.0, 0.25.0 and 0.26.0, so this takes 0.27.0, which #1269 (@fbebf329), #1325 (@894e65bb) and #1333 (@0f044362) claim as well. The constant merges between them in silence and only the generations row and the history entry conflict, so each that lands after another moves to the next free version, keeping the rows and entries main has. A trial merge of this head against each open PR: #1269 conflicts in params.go, docs/compilers/openapi.md, docs/ir/version-history.md, ir/irversion_test.go and the secret-format golden, and merges clean with main; #1325 conflicts in docs/ir/version-history.md and ir/irversion_test.go, the version claim; #1333 conflicts there and in params.go, and merges clean with main; and #1278 conflicts in docs/compilers/openapi.md, as it does with main. #1199, #1227, #1233, #1235, #1277, #1301, #1311, #1322, #1327, #1329, #1331, #1332, #1334, #1335, #1336, #1337, #1339, #1340 and #1351 merge clean. #1333 reads a parameter's elected schema under the context of the document its media type sits in, and the conflict is one return line: with it resolved toward #1333, a parameter and a header whose content names a $ref'd media type, in this document or another, read their schema's target as one written in place does, and are marked for it. #1269 rewrites fillParamSchemaAnnotations in compilers/openapi/internal/operation/params.go around this change's Secret, the ref-use-site rule in docs/compilers/openapi.md and the secret-format golden, and has a carrier read through a refinement to its target, so the later of the two has referentOf read the same target.

Test plan

  • The tree as it lands holds main @7f849127, and CI runs the gate on it. Its compiler is d38453f's, as every commit since changes a test, a document, a comment or the values irverify walks to its findings, or merges main's test: cut the work make gate does that no check reads #1313 and its bump of go.yaml.in/yaml/v3 (build: bump go.yaml.in/yaml/v3 from 3.0.5 to 3.0.6 in the go group #1342). --verify of the golden spec writes the same document and the same 1,862 diagnostics on either side of each, and the stability, cross-document and unquoted-date probes give the same output. So the golden table below is re-derived at d38453f against main @b206dc99, and every row of the IR is unchanged from the earlier measurement; the diagnostic totals moved with main, by the same 47 infos, and none is removed. sweep.py over the 1,954 specs of a real-world corpus and the 227 of testdata, against b206dc9: exit codes 0 for 1,661 and 1 for 520 on both sides, and 5 differ past the version stamp, the 4 conformance cases this change writes and one real-world parameter writing format: password, which now lands in Secret with its no_ir_home entry and info gone. Mutants at the sites the merges resolved, each red at the test named: the security scheme's promotion dropped (TestConformance), Parameter.Secret dropped from the promotion table (TestExtensionTarget_EveryTargetIsInThePromotionTable), OperationGroup given a Provenance (TestPromotionTable_EveryNodeCanRecord), and at the fold's marker, the merge passing no marker for a promoted Secret and the fold joining a declaration's whole marker (both at TestAllOf_RedeclarationMarksAPromotedSecretOnly, the second at TestFold_ThePromotionMarkerFollowsTheSecretTaken too). The bullets after this one stand at the revision each names.
  • After merging main @608a5a46 (827e7ed), where only the version claim conflicted and no golden or census line needed regenerating: make fmt vet lint lint-planted build fuzz bench-smoke each exit 0. Coverage with -timeout 1500s reached all 16,972 statements only over two runs together: at a load average near 200, canary children reported hung and two harness socket tests failed on a long temp directory (test: two harness tests fail when TMPDIR is long #1295), and each package passes alone. Every row of the golden table below is unchanged, --verify exits 1 on the same 9 errors, and the diagnostics differ from main's by the same 47 infos. morphic-harness over testdata gives 170 ok and 49 error-diagnostic, the extra one each being fix!: lower file bodies, enums, unions and streams as declared #1191's fixtures. 43 mutants at the sites the merge touched, beside a no-op control that survived: 37 went red. Three survivors were promotion sites no test held: the join at Promoter.Stability, which on 10 golden operations would read an inferred stream as declared, and the entry a parameter's and a header's promotion read from a content map holding several. TestStreaming_MarkerListKeepsAPromotionBesideBoth now states a tier, and TestPromotion_AContentCarrierReadsTheEntryItElects declares text/plain before application/json; each goes red under its mutant, at the parameter and the header alike, and a no-op control stays green. A fourth survivor was a published row of generations rewritten with this change's hash, which nothing enforces (test: a published IRVersion row can be rewritten with ./ir green #1314). The other two are equivalent. The sweep, grids, order reversals, cost figures and mutant sets below were not rerun on the merge, and stand at 346c6b6.
  • make fmt vet lint lint-planted build fuzz bench-smoke on this tree, each exit 0, and the command of make coverage with -timeout 1500s for its 300s, which -race runs of ir and pass could not meet on a loaded machine: "Coverage gate passed: all 16873 statements covered."
  • Golden spec, cloudflare/api-schemas @319dc756, main @b206dc99 against this branch @d38453f2. morphic compile --verify exits 1 on both for the same 9 unresolved-ref errors.
main branch
diagnostics 1,815 1,862
info degraded-construct 1,439 1,486
Unmodeled entries 25,567 25,567
type nodes 41,697 41,697
operations stable / beta / alpha / unset 0 / 0 / 0 / 3,763 2,638 / 545 / 7 / 573
operations deprecated 332 469
operations with a message / a removal date 0 / 0 217 / 53
parameters with a message 0 5
properties with a message 0 41
properties secret 2 172
nodes sensitive / with a message 0 / 0 36 / 44
morphic compile --verify openapi.json >ir.json 2>diags.txt
wc -l diags.txt
jq '[.. | objects | .stability? // empty] | group_by(.) | map({(.[0]): length})' ir.json

All 47 new diagnostics are the disagreement above. The golden IR was also rebuilt, at 346c6b6, from main's by a script applying these rules, field by field: it differs from the branch's in nothing but the source path and those 47 diagnostics. Each x-sensitive: true entry among the 140 kept at a carrier has its field set and its marker, and the 2 others are the oneOf above.

  • At 346c6b6, a corpus of 1,954 specs through sweep.py, the script of the morphic-verifying-changes skill, plain and with --verify: no exit code changes, 1,242 error diagnostics on both sides, and one spec differs past the version stamp, a parameter writing format: password that now lands in Secret. The corpus writes none of the new default keys, so this shows the change touches nothing else. The golden spec is what reaches it.
  • go run ./cmd/morphic-harness: over testdata, 169 ok and 48 error-diagnostic, the fixtures written to draw one. The golden spec stops at its 9 unresolved-ref errors, so a copy declaring the three schemes they name ran through every oracle and came back ok, which puts the two-order and idempotence oracles over what this promotes.
  • Conformance: new extension-stability and extension-redaction, with extension-promotion and secret-format extended, and stability, redaction and deprecation witnessed in matrix.md. testdata/unwitnessed.golden.txt gains OperationGroup.stability alone, which no OpenAPI construct sets.
  • A two-order diff in e2e, since the order oracle reads no operation or parameter, and a test that runs each use-site case at all three carriers and by both spellings of a parameter and a header.
  • 57 mutants planted at each new branch, carrier and word in a throwaway copy, beside a no-op control that survived: every one went red, each read from the test that fired. Along the way 7 survivors showed a guard nothing needed, which is gone, one showed the enum carrier passing on no report, which has a test, and one showed a target's document left out of what a use remembers, which has a test too.
  • A second set of 37 behind each test the rules cite and each argument and call the change adds: 35 went red, with a no-op control surviving. Two are equivalent, as a filled member is never empty and alpha is a valid tier. One survived at first, a break for a continue in ExtensionsNamed that dropped the keys after one that cannot be serialized, which has a test.
  • A third set at the arguments PromoteUse takes, beside a no-op control that survived. Reading a parameter's or a header's own schema in place of the one elected from schema and content survived at first, so every use-site case now runs through content too, and both go red. c for dc at a header is equivalent, as both read one bindings table: a header and a parameter declared in another document over a schema there compile alike on both builds.
  • Two grids computed from the rule text alone, not from this code, each through schema at 3.0 and 3.1 and through content at 3.1, the first also at 3.2: 2,808 carriers crossing the keyword, key and member on each side of a $ref, and 648 crossing format: password with x-sensitive on the object, the schema and the target. None differs here, and on main 810 and 288 of the 3.1 schema rows do.
  • Invalid inputs are all an info and the entry kept, none refused: x-stability: 3, an unknown word, legacy, x-sensitive: "yes", a disagreement, and on a deprecated node x-cfDeprecation: text and x-forge-sunset: {date: 7}, which a live node has no field to read into. Empty text states nothing, so it fills no field, marks nothing and does not stand before a later source.
  • A grid of 7 targets against 10 annotations beside a $ref and beside an allOf decorator, with the fold computed from the IR: a date on a live target no longer parts them, and the 20 of its 70 pairs that do are the two left open above, a member key (openapi: a promoted key on an allOf reads apart from its $ref spelling #1220) and deprecated: false (openapi: a decorator's deprecated: false reads apart from one beside a $ref #1258), neither of which the golden spec writes.
  • Order, beyond the e2e diff: reversing every mapping of a spec holding each target at each carrier, and each allOf, changes nothing but the allOf index in two pointers, diagnostics compared as a sorted multiset. A build planted so the first reader's answer wins differs at dozens of carriers. Reversing every mapping of the golden spec itself changes the stability, redaction, deprecation and marker of none of its 106,713 operations, parameters, headers, types and properties, and its diagnostics match but for the order a message lists keywords in.
  • Cost: on an 84 KB spec with a 20,000 item key on a target 500 uses name, instructions retired fall from 58G at the previous head to 1.09G, against main's 0.90G. checkStability in irverify prunes to the path checkStreamingModes prunes to, named operationPath now that it serves both, and TestOperationPath_ListsEveryCarrier counts Stability's carriers, so a field of that type off the path fails there, as a third one planted with the shape regenerated does. Shown every value, the check cost a verified golden compile about 0.5% (118.88G against 118.25G instructions pruned, two runs each, identical output) and Verify alone on the golden IR 3.9% (medians of 1,109 ms against 1,067 ms over ten alternated runs, at 6cb8cef). Each of four mutants goes red: walking every value, pruning everything, and the path losing nested groups or Operation. irverify: two walk checks of one operation field are shown every value #1267 proposes the same table for the two other checks of one operation field, and names it and its tests as they were before the rename. On a spec of 8,000 $ref carriers with the collector off, three alternated runs give medians of 13.7G on main and 13.5G here, inside each side's spread. On the golden spec the same runs give 90.80G on main (90.61 to 90.95) and 91.10G here (90.93 to 91.37), 0.3% more.

Breaking

  • IRVersion moves to 0.27.0, and a consumer pinned to 0.26.0 refuses every document this writes, whether or not it sets stability or a parameter's secret.
  • A consumer reading openapi:format on a parameter no longer finds it, and reads Parameter.Secret.
  • Deprecation is now set from a key alone: 137 golden operations that read as live now read as deprecated, as do uses of a target a key deprecates.
  • Sensitive and Secret are now set from x-sensitive: 36 golden nodes and 170 properties that read as plain now ask for redaction.
  • compilers/openapi: ExtensionPromotions.Targets changes from map[string]ExtensionTarget to map[ExtensionTarget][]ExtensionSource, DefaultExtensionPromotions returns that shape, and a caller stating a mapping restates it by target.

Closes #578
Closes #581
Closes #655

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.
@OmarAlJarrah

Copy link
Copy Markdown
Member Author

Findings from merging main @608a5a46 into this branch at 346c6b6

This merge is local and not pushed, so the PR still shows as conflicting. The change survives it, but the merge turned up three findings, and the PR body needs updating once a merge goes on.

Findings

1. No test holds the join at Promoter.Stability. Replacing prov.Inferred = ir.JoinInferred(prov.Inferred, ExtensionPromotionHeuristic) with an assignment at that site leaves the suite green. The code is right today, but 10 operations in the golden spec depend on that line. Compiled with the assignment, all 10 go from streaming-media-type,extension-promotion to extension-promotion while still streaming, so a stream inferred from a media type reads as declared. TestStreaming_MarkerListKeepsAPromotionBesideBoth catches the same change at the deprecation site only, because its operation states no tier. Adding x-stability: beta to that operation covers it: the test passes as it stands today, and fails with the assignment (expected "group-path-prefix,streaming-media-type,extension-promotion", actual "extension-promotion").

2. No test ties a carrier's promotion to the content entry #1191 elects. electTypeSpelling now takes the lowest key, not the first. fillParamDetail and the header path pass elected.js, which is correct: with text/plain declared before application/json, Secret and Deprecation follow application/json at both carriers, whichever entry holds the target. But passing the first declared entry instead leaves the suite green at both, as no test has entries that differ in what they promote. Such a map draws an openapi/validation error, so this reaches only an invalid document and is low priority. content-several-entries is the natural place for a case.

3. checkStability walks every value, while checkStreamingModes now prunes. #1191 landed the sibling check with streamingPath, and that check's comment records 0.8% per verified compile for an unpruned walk. Stability sits only on Operation and OperationGroup, both already on that path.

Golden spec, compile --verify with GOMAXPROCS=1 GOGC=off, two alternated runs Instructions retired
Merge as it stands 118.88G (118.80 to 118.96)
Returning streamingPath[v.Type()] in place of true 118.25G (118.07 to 118.44)

That is about 0.5%, from a small sample. The pruned build's golden IR and diagnostics are byte-identical, and ./ir/irverify passes on it. The cost of pruning is real, though: this check's comment says the by-type walk holds a new carrier at once, and TestStreamingPath_ListsEveryCarrier counts only StreamingMode's carriers, so sharing the path needs a matching count for Stability. This is a decision to make either way, since the two checks now sit side by side with opposite designs.

Outside this change: nothing enforces "a row on main is never edited". Putting this branch's old hash on the 0.23.0 row leaves ./ir green, so a wrong resolution of this very conflict would pass.

The merge

It conflicts only on the version claim, in docs/ir/version-history.md and ir/irversion_test.go. #1191 took 0.23.0, so this change moves to 0.24.0. main's row and entry stay as they are, the history entry's three "pinned to 0.22.0" become 0.23.0, and the new row is:

{version: "0.24.0", format: 1, shape: "9bafa2deb68d14c36771cb98a76474130e781e9e7f5090214633a6a8c2ae48c0"},

That hash holds only while nothing else lands first. #1226 also claims 0.24.0 and #1219 still claims 0.23.0, so whichever lands later takes the next version.

No golden or census line needed regenerating. Across the 22 files both sides changed, every line count matches branch + main - base, apart from those version lines and the reflect and require imports both sides added. The shape golden's diff is this change's additions against main, and main's StreamingNone against the branch.

What was checked on the merge

  • Gate: make gate did not pass as one command on this machine, so this is the step-by-step result.
    • fmt, vet, lint (0 issues), lint-planted, build, fuzz and bench-smoke all passed.
    • Coverage ran with -timeout 1500s, as the test plan does. ir, load and archtest took 706s, 766s and 344s under a load average near 200.
    • Two packages failed that run, and neither failure comes from the code. canary children reported hung at that load, and the package passes alone in 11.7s. Two harness socket tests failed because the temp directory used for that run made the socket path too long, and they pass with the default one.
    • morphic-covergate reported 3 of 16,972 statements uncovered, all in internal/harness/path.go. Rerunning that package covers all three and passes the coverage gate alone. So 100% rests on those two runs together, not on one green run.
  • Golden spec: every row of the Test plan table is unchanged, and compile --verify still exits 1 on the same 9 unresolved-ref errors. The IR differs from main's only in the fields this change owns, and the diagnostics only by the same 47 infos.
  • Oracles: the golden copy declaring the three missing schemes comes back ok. Over testdata, morphic-harness gives 170 ok and 49 error-diagnostic, the extra one each being fix!: lower file bodies, enums, unions and streams as declared #1191's new fixtures.
  • What main changed around this code: the following all behave as both sides state.
    • An enum keeps its ValueType beside a redaction, and no openapi:format entry returns.
    • A non-exclusive enum union takes Sensitive.
    • A file body's schema node keeps Sensitive.
    • A stream's frame is unchanged.
  • Planted defects: 43 at the sites the merge touched, beside a no-op control that survived. 37 went red and 6 survived. Three survivors are findings 1 and 2, and go test ./... stays green with all three planted together. A fourth is the published row above. The other two aren't gaps: the join at the redaction site is equivalent today, because no other marker reaches those carriers, and dropping a second witness from the redaction row is fine, because a row needs only one.

Not rerun on the merge: the 1,954-spec sweep, the two grids, the order reversals, the cost figures and the mutant sets the Test plan lists. Those claims still stand at 346c6b6.

PR body, once a merge is pushed

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.
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.
@OmarAlJarrah
OmarAlJarrah merged commit be889c6 into main Nov 2, 2026
4 checks passed
@OmarAlJarrah
OmarAlJarrah deleted the feat/ir-extension-stated-facts branch November 2, 2026 12:15
OmarAlJarrah added a commit that referenced this pull request Nov 2, 2026
#1229 listed, under extension-promotion's status, that a flattened member
reads no redaction off a declaration it overrides. FlatProperty.Secret
now accumulates over every declaration, so the item no longer holds.
OmarAlJarrah added a commit that referenced this pull request Nov 2, 2026
#1229 took IRVersion 0.27.0, so this change moves to 0.28.0: main's row
and history entry stand, and this change's follow under the new number,
with the shape hash of the merged tree. fillParamType returns the
elected spelling, as #1229 has it, and reads its schema in the context
declaring it, as this change has it.
OmarAlJarrah added a commit that referenced this pull request Nov 2, 2026
Brings in #1229 and #1325.

- Both bump IRVersion, to 0.27.0 and 0.28.0, so this branch's bump
  moves to 0.29.0. Its row takes main's new shape and keeps this
  branch's names and meaning; the version history entry moves with it.
- #1229 gives ir.Parameter a Secret, so a parameter's redaction has a
  home and xml is the one keyword left with none. fillParamSchema keeps
  xml for each voice and the schema, and the wrapper that also kept
  format: password goes. The secret-format golden is regenerated: it
  differs from main's by this branch's cases, and from this branch's by
  the parameter's Secret.
- #1229 reads a target's mapped keys onto a carrier at the end of a
  $ref only. A refinement is a use of what it names, so
  {allOf: [{$ref: T}], description: d} read T's deprecated keyword
  but not its x-stability, where {$ref: T, description: d} read both.
  KeysLentTo now reads the keys along the chain the keywords are read
  along (Use.Referents), nearest first: the nearest link stating a
  deprecation, by a keyword or a key, decides it and lends its members,
  and any link asking for redaction asks it of the use. A position
  owning a node lends its carrier none, as it lends no keyword.
  Promoter.Referent keys what it remembers by the schema's identity, as
  an inline link has no binding position.
  TestPromotion_ARefinementLendsWhatItRefines pins it.
- TestPromotion_ADecoratorMatchesTheRefItStandsFor assumed an allOf of
  one $ref over a string is a decorator. It is a refinement here, so
  the test now holds its carriers to the $ref spelling's, and keeps the
  fold comparison over object targets, where it is still a decorator.
- #1258's example, deprecated: false on such an allOf over a string,
  now reads as beside the $ref, so its status names the object case
  that still parts, and extension-promotion's #1220 entry names what is
  left of it.
OmarAlJarrah added a commit that referenced this pull request Nov 2, 2026
#1229, merged in fd37a88, gives ir.Parameter a Secret and holds a
parameter's format: password there rather than under openapi:format.
TestParameter_ABranchSpeaksAsItsSchemaDoes still expected the verbatim
entry for a branch writing it, so it failed on the merge. It now holds
the branch's redaction to Secret and the entry's absence, beside the
readOnly and xml the parameter still keeps verbatim at the branch.
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