Skip to content

fix(compilers/openapi): read what a draft spells otherwise - #1467

Merged
OmarAlJarrah merged 107 commits into
mainfrom
fix/openapi-older-draft-keywords
Nov 6, 2026
Merged

OmarAlJarrah merged 107 commits into
mainfrom
fix/openapi-older-draft-keywords

Conversation

@OmarAlJarrah

@OmarAlJarrah OmarAlJarrah commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

Summary

A 3.1 or 3.2 schema can name a JSON Schema draft before 2020-12 as its dialect. Until now every
keyword such a draft spells otherwise was read as 2020-12 reads it. This reads each in the schema's
dialect, adds rows for drafts 6 and 7 and 2019-09, and closes a reporting gap the work exposed.

The first seven commits each build and pass on their own:

  1. Rows for drafts 6 and 7 and 2019-09. Each keyword row cites its sentence in the draft's text,
    and each dialect states whether it ignores the keywords beside a $ref.
  2. Bound shapes judged at every schema. The rule table leaves the shape of minimum, maximum,
    multipleOf and the exclusive bounds to the schema lowering, which reported a malformed one only
    where it read the schema's constraints. On main, {not: {exclusiveMinimum: true}} in a 3.1
    document compiles with no diagnostic, and so does a malformed bound under if,
    propertyNames or contains, beside an enum, or beside type: array. The check now hands
    each such value to its caller (oas.Options.Owned). The load phase judges it once the bindings
    say which dialect the schema at that position is read in, for the source and for every object a
    reference reaches. The lowering still reads and drops what it cannot use, and reports neither.
  3. The draft readings.
    • items written as a list is a tuple in drafts 4, 6 and 7 and 2019-09, its rest the
      additionalItems beside it. In any other dialect a list is openapi/invalid-items-list,
      judged by the load phase through the same hand-over, and items reads as absent.
    • dependencies is kept as a validation-only keyword in drafts 4 to 7.
    • Draft 4's id names a resource and gives a $ref its base, and is kept verbatim as $id
      is. An identifier naming a fragment alone declares a plain name in drafts 4 to 7, and
      definitions holds schemas, decided as $defs is. An $id's format is judged in the
      schema's dialect, so draft 7's $id: '#leaf' is accepted where 2020-12's is refused.
    • A keyword beside a $ref, which drafts 4 to 7 ignore, is applied as 2020-12 applies it and
      warned of as openapi/later-version-construct at its key, bar $schema, extensions and the
      identifier (item 22).
    • The binder and the load phase share one rule for which $schema chooses a dialect: a schema
      no other holds, or one rooting a resource by an identifier spelled as its embedder spells one
      (JSON Schema 2020-12 §9.3.1) or as its own dialect does. A $schema naming no dialect,
      which no absolute URI names, chooses none in either. Before this, the binder switched dialect
      at any $schema.
    • A schema is read in the dialect of where it is (lowering.SchemaRulesAt), so a node an alias
      places under two roots reads each one's, as the constraint recorded on openapi: keywords a draft before 2020-12 spells otherwise are read as 2020-12 reads them #1457 asks, by every
      reader since item 32.
  4. An anchor's name in its dialect's grammar. Drafts 6, 7 and 2019-09 spell a plain name as a
    letter, then letters, digits, -, _, : and ., where 2020-12 admits a leading _ and no
    :, and draft 4 states none (item 30). A 2019-09
    $anchor: a:b, or draft 7's $id: '#a:b', was refused or never declared, and a $ref naming it
    went unresolved. $anchor and $dynamicAnchor are now judged in their schema's dialect, as an
    $id is, and the binder declares an anchor by that dialect's grammar.
  5. Draft 4's id wherever $id is read. Once the binder reads id as a base, every other
    reader of $id must read it too.
    • A document fetched over the network that names draft 4, by a $schema or its
      jsonSchemaDialect, is refused for an id naming a location on disk, as any document is for
      an $id reaching a local file (openapi: a document fetched over HTTP can name a local file, and it is read #929). Without this, a remote draft 4 document could rebase a
      relative $ref onto the disk. The id is judged as the binder reads it
      (bind.IdentifierURI): cut at #, trimmed and resolved by RFC 3986, so ' FILE:///x/#' is
      refused too, as is a single-letter scheme, a Windows drive's. A relative id stays on the
      network, and in a document naming no dialect that reads id, no id refuses anything.
    • A draft 4 schema's $id is kept and said to be read for nothing, rather than "read only for
      reference resolution". The identifier keyword comes from the rows
      (oas.SchemaRules.Identifier), one reading the binder's rule shares.
    • A document of schemas naming a draft at its root reads its tuple there as below it.
    • A schema in a draft's dependencies is bound, as one in 2020-12's dependentSchemas is.
  6. An items list read off the raw node in the two predicates that relied on the parser's
    model holding the false schema for one, which no docs/parser-library.md entry states.
  7. The fetched-id judgement made the binder's own, as the previous item describes.

Then #1459 and main are merged in, the first as its last head, whose tree main landed, and:

  1. The binder passes over a $schema naming no dialect. fix(compilers/openapi): read a schema in the dialect it names #1459's final round had the load
    phase skip one, as though it were not written, while this branch's binder read that schema as
    the table reads the value, in the minor's own dialect. In a document defaulting to draft 4, a
    $schema: 'rel/x' beside id then left the id undeclared, and a $ref naming it went
    unresolved and was fetched where external references are allowed. The binder now asks
    (bind.DialectRule.NamesNone), and the load phase meets no such root.
  2. No more allocation per schema than main. Reading id had dialectAt make room for a
    diagnostic per keyword on every schema, about 30 MB on the Cloudflare schema, and the binder's
    frame, which now carries a dialect, made each child slice a walk built 40% larger. The former
    starts from no slice again, and a walk appends a node's children to its stack.
  3. A fetched id refuses its document only where a draft reads it. As first written, the
    check refused a remote 3.1 document whose example wrote {id: 'u:42'}, a one-letter scheme read
    as a drive's, which main reads (exit 0 became 1). It now asks the dialects the document names,
    as item 5 states.
  4. The drafts' conditions name their operands, as the styleguide's rule 1.8 asks.
  5. Messages word what a draft declares. See below.
  6. A draft's $ref sibling is warned of only where applying it reads otherwise. Over 560
    public JSON Schema files, 10 of the 49 warnings named a key whose reading applying it leaves
    alike: the definitions or $defs a root writes beside the $ref naming one of them, and keys
    no row defines, such as nullable, or a $id in draft 4, which spells its identifier id. Only a field the draft's rows define is warned of now,
    OpenAPI's own among them, bar $schema, definitions and $comment, and since item 22 the
    identifier.
  7. A schema past the depth bound is reported once. A tuple nested past the schema walk's bound
    drew a second error at its absent rest, which the document does not write; 2020-12's
    prefixItems did so on main too, at the absent items. A schema the document does not write
    now lowers as the top type before the depth check.
  8. Draft 4's id is kept without a list per schema, about 3.7 MB on a document of 20,000
    draft 4 components.
  9. A 2020-12 anchor in a draft reads by 2020-12's grammar. The binder held every anchor in a
    draft 4 to 7 schema to the drafts' plain-name grammar, though neither defines $anchor or
    $dynamicAnchor, so draft 7's $anchor: _x declared nothing and a $ref to it went
    unresolved, where main resolved it; validation judged such an anchor by no grammar. The drafts'
    grammar now applies to drafts 6 and 7's fragment identifier and 2019-09's $anchor, and an
    anchor keyword the dialect defines none of is read and judged as 2020-12 reads it (openapi: a 3.0 document's prefixItems lowers to a tuple with no diagnostic #820).
  10. Texts that had the lowering report a malformed bound say the load phase does, and
    docs/ir/boundaries.md names the drafts' dependencies among the validation-only keywords.
  11. The schema fuzzer is seeded with the drafts' readings: a draft 7 tuple, a draft 4 id with
    a fragment identifier, and a 2019-09 anchor holding a colon, each compiling clean so the
    oracles run on it. The dialect-keywords status names the formats the load phase judges in a
    schema's dialect beside the shapes.
  12. A 2019-09 $id naming a fragment is refused again. Item 3 judges an $id's format in its
    schema's dialect, and 2019-09's row gave $id the URI-reference format, so a 2019-09
    $id: '#foo', which its text forbids ("MUST NOT contain a non-empty fragment") and main
    refused by 3.1's rule, drew no finding. The row now cites that sentence and holds $id to the
    format 2020-12's row does. Draft 4's id row gains the URI format its text states, which no
    check reads until openapi: a schema read in a JSON Schema draft is held to its minor's keyword rows #1466, and docs/ir/boundaries.md names id among the resource keywords.
  13. A draft's tuple as a multipart part is read through its entries. The part reader fix(compilers/openapi)!: read binary idioms as file content #1435
    brought in with main read an array's elements from prefixItems and items, where the
    parser's model holds false for a draft's list of items, so a draft 7 part
    {type: array, items: [{}, {}], additionalItems: false} was no file where the same tuple
    written with prefixItems was. It now reads a draft's entries and additionalItems
    (schema.ListedTuple). Every other reader of items and prefixItems, found by a sweep,
    already tells a list from one schema.
  14. What a tuple's malformed entry lowers as is stated, under Limitations below.
  15. An identifier beside a draft's $ref names nothing. Drafts 4, 6 and 7 ignore every keyword
    beside a $ref, but the binder read an $id there, or draft 4's id, as 2020-12 does. On the
    JSON Schema test suite's "$ref prevents a sibling $id from changing the base uri", a draft 7
    $ref resolved to the string its sibling $id rebased it onto, where the draft names a number,
    and a draft 7 {$id: other.json, $ref: '#/definitions/A'} went unresolved. Where a dialect
    ignores a $ref's siblings (bind.DialectRule.IgnoresRefFields), an identifier beside one now
    gives it no base, roots no resource and declares no plain name, and a schema writing a $schema,
    an identifier and a $ref inside a schema read in such a draft roots none either. The identifier
    is not warned of as applied, and its info says it is read for nothing. 2019-09 and 2020-12 still
    read it as the $ref's base.
  16. The $ref-sibling warning names the dialect as read. It said the schema names the dialect
    ignoring the keyword, though the dialect is as often its document's jsonSchemaDialect or an
    enclosing root's $schema.
  17. A draft tuple's entry that is no schema is reported. {items: [5], additionalItems: 5} in a
    draft 7 schema compiled with no finding, where {prefixItems: [5], items: 5} draws two errors: the
    load phase's check of the schemas built for the entries skipped any the parser read as a boolean.
    Each entry and rest is now reported as its 2020-12 twin is, an entry by the check of what the
    parser built there, and the rest as a field beside its items list
    (oas.SchemaRules.TupleRestFinding): an error where no schema spells it, and for null the
    warning a null field draws, read as left out.
  18. A draft tuple below another schema lowers as written. The parser decodes a schema as the
    false schema where a keyword beneath it fails to decode under its own name, though the position
    writes a mapping (docs/parser-library.md#failure-beneath-itself, pinned by a canary). A draft's
    list of items is such a keyword, so a valid tuple as an array's items,
    {type: array, items: {type: array, items: [{type: string}]}}, lowered as a list of a closed
    empty model, and an array of objects one of whose properties is a tuple lost the whole object, as
    a public draft 7 friends.json did. The typed builder now decodes each such schema again from its
    mapping, in place, so every reader of the model sees it, up to typed.MaxMends of them one below
    another; one past them is the top type with an openapi/degraded-construct error, as the schema
    depth bound gives. openapi: a keyword failing to decode beneath itself turns the outer schema false #1299's invalid inputs now lower as their rule says too:
    {type: array, items: {type: string, items: null}} is a list of strings.
  19. A draft's spelling of a keyword reads as its twin in keyword lists. A string body on
    application/octet-stream writing draft 4's id, a draft's definitions or 2019-09's
    $recursiveAnchor was no file, where $id, $defs and $dynamicAnchor leave one, and a draft's
    dependencies in an inline allOf branch kept the branch verbatim, where dependentRequired
    folds onto the position. schema.TwinKeyword names the 2020-12 keyword a spelling stands for in
    the writing schema's dialect, for the file-body reading and the allOf fold, and the
    validation-only reader keeps a draft's dependencies at each branch folding into a position too.
    Union branches already read the spellings alike. $schema and $vocabulary make a body no file
    in every dialect, main included: openapi: a $schema or $vocabulary in a body schema makes it no file #1521.
  20. main's remembered base walk reads each schema's dialect. fix(compilers/openapi): bound costs that outgrow the document #1470 answers Table.Base from a
    walk it remembers. The merge has that walk carry the dialect and whether a schema holds each
    position, stepping and entering as resources.holding does, and its differential test now
    covers two fixtures naming dialects. test(archtest): hold calls into unimported packages to a ratchet #1504, merged next, counts the calls a package makes
    through a function it is handed into one it does not import: this branch adds four, of kinds
    its entries keep under skills: say when a function value may cross into another package #1477's rule, bind.Config.Dialect, a dialect's rows built only once a
    schema names it, and oas.Options.Owned, a traversal callback, with the Locate that places
    an Owned's finding, so those counts rise with entries saying so.
  21. A base in another document is read in its dialect before any walk of it. The binder read
    bases through the dialects schemas name only once it had found one naming any, a test on the
    whole run, and a $ref that a source $ref's chain meets in another document is resolved before
    that document is read. Where the source names no dialect, a draft 7 document's $id beside that
    $ref rebased it, and a draft 4 document's root id gave it no base, so its {$ref: 'a.json'}
    beside an entry identified a.json went unresolved, on 953c11b5a too. The test is now per
    document (identifiers.readsDialects).
  22. A schema reached from another document is read in its own document's dialect. A reading of
    a schema's dialect by its node, which folding an inline allOf branch and reading a body as a
    file make, gave a node no $schema root holds the rows of the document whose content the
    lowering was in. A draft 7 document's dependencies in an allOf branch the source reaches
    through a $ref kept its branch verbatim as though the conjuncts disagreed, and a draft 7
    source's definitions in a body schema another document reaches read as an unknown keyword, so
    the body was no file. The load phase now notes another document's own rows by node where they
    are not the source's, and the reading falls back to the source's rather than the context's
    (load.Dialects.Of, lowering.SchemaRulesOf).

Then main is merged in (2123d296e), touching none of the branch's files, and:

  1. A draft's fragment identifier is declared by its draft's grammar. Drafts 6 and 7 require an
    identifier naming a fragment alone to open with a letter; draft 4 states no grammar, its id
    being any URI. The binder held all three to the plain-name grammar and nothing judged it, so a
    draft 4 id: '#_inner' or '#1x' declared nothing and a $ref to it went unresolved, and a
    draft 7 $id: '#_x' compiled with no finding where main reported it. The draft 6 and 7 $id
    rows now cite that sentence and give the identifier a format the load phase judges in the
    schema's dialect (oas.PlainNameURI), and the binder declares a fragment alone by the grammar
    the rows give (bind.DialectRule.PlainFragments), in draft 4 any fragment no JSON pointer
    spells, read as a $ref's fragment is. A $ref fragment no JSON pointer spells is looked up as
    a plain name, as every dialect reads one, and keeps "its fragment is no JSON pointer" where no
    schema declares it and no grammar admits it.
  2. 2019-09's $ref row quotes its sentence whole, "a URI-Reference" where it read "a URI".
  3. An aliased schema is read in each place's dialect by every reader. Folding an inline allOf
    branch, reading a body as a file and reading an exclusive bound asked a schema's dialect of its
    node, which an alias shares, so a node under a draft 4 root and a 2020-12 schema read one
    dialect at both, chosen by the roots' names. A branch's dependencies then folded at the
    2020-12 place, where no reader kept it, and was lost with no finding; a body writing id was a
    file there; and a valid draft 4 bound was dropped while a malformed 2020-12 one stayed beside
    its error. The typed bindings now place each schema by the model it holds
    (typed.Bindings.SiteOf), which decoding builds afresh for every path to a node
    (docs/parser-library.md#alias-expansion), and those readers ask that place's rows. The index
    is kept only where a schema names a dialect by $schema, as otherwise each node reads its own
    document's. This lifts the limit that the fold read a conjunct's bounds by its node.
  4. A draft tuple's rest beside a list no tuple holds is kept as its twin is. Where a draft's
    list of items lowers to no tuple, as with no type: array, its additionalItems was kept as
    an unknown keyword under out_of_scope, the reason the IR keeps for runtime and SDK policy,
    while its 2020-12 twin items beside prefixItems is a degraded lowering. The shape census now
    holds it with its list under one finding (docs/compilers/openapi.md#shape-applicator); beside
    an items that is no list it is still an unknown keyword.
  5. Two findings say what the writer can do: an items list where the dialect reads no tuple
    suggests prefixItems or naming a draft, or in a 3.0 document what 3.1 writes (item 37), and a
    schema past the decoding bound suggests moving the schemas below it into a component, which
    reads them.
  6. The limits tracked by openapi: a schema read in a JSON Schema draft is held to its minor's keyword rows #1466 and openapi: a 2019-09 schema's $recursiveRef is kept verbatim, never expanded #1475 are stated at the code that has them, judgeSource
    and dynamicRefName, and missingArrayItems says why it reads an items list off the raw node.
  7. A type list's draft tuple is read where the schema writes it. A schema whose type list
    makes it a union lowers each variant at a pointer of its own, where the load phase builds no
    tuple entry, so the array variant of a draft's {type: [array, object], items: [...], additionalItems: ...} held the top type for each entry and its rest, and since item 33 its
    additionalItems was kept nowhere. The typed bindings now also place by its model each schema
    writing a list of items, whatever the dialects named, and the tuple reads its entries and rest
    where the schema writing them is placed, a lookup shared with item 32's readers.
  8. The items-list advice follows the version. A 3.0 document can write neither prefixItems
    nor a $schema, so its finding says 3.0 writes no tuple, which 3.1 writes as prefixItems.
  9. A plain name no grammar admits keeps the merge-bound reason where the document's
    identifiers were read only up to that bound, as a draft 4 id past it may declare the name.
  10. The rules name the tests pinning the draft readings, and the rule for an additionalItems
    beside a list says it holds in every dialect.
  11. The plain-name format's description and two comments are reworded.
  12. The finding about a draft's fragment identifier says it names a plain name, and the text is
    pinned; item 46 words it as the sibling formats word theirs.
  13. The items-list advice is keyed on prefixItems alone, as every row set defining it
    defines $schema too.
  14. The typed bindings' list-of-items reader takes bind's name for its twin, listsItems.
  15. The tuples rule says what a type list and a stray rest keep: a type list's array variant
    is the tuple, leaving nothing on the union, and an additionalItems beside a list its schema's
    dialect reads as no tuple is kept as at a position lowering to no tuple, now pinned at 2020-12
    and 3.0. Two
    comments that said the typed bindings place nothing where no schema names a dialect now say
    they place each schema writing a list of items, which their test pins.
  16. Four test doc comments no longer open with their first words twice, and a fifth finishes its
    sentence. A check refusing such a line is archtest: refuse a doc comment line holding a second comment marker #1562.
  17. The plain-name format reads as its siblings do: a URI reference whose fragment, where
    written alone and no JSON pointer, is a name of a letter, then letters, digits, -, _, :
    and ..
  18. The comment on the items-list advice names the rows it reads.
  19. Four lines of the documents this change writes are rewrapped at their width, no word
    changed.

Departure from #1457's expected result

#1457 expects V's description, beside a draft 4 $ref, "not read as V's own". This applies it
and warns instead. That follows the decision #871 recorded for the same construct in a 3.0 schema,
whose $ref also ignores its siblings: lower it as the later version reads it, the reading that
loses least, and say so (docs/compilers/openapi.md#later-version-construct). A consumer can set a
warned keyword aside, and could not rebuild the lowering from a verbatim copy. The identifier is the
one sibling not applied (item 22): applying it changes what the $ref names, and ignoring it, as
the draft does, loses nothing, as it is kept verbatim either way.

Visible change on input that names a draft

A draft's definitions and the entries of its tuple now hold schemas whose $refs are bound, so a
relative $ref there resolves against the root's identifier, as one in 2020-12's $defs already
does. Under an $id naming a URL it is fetched where external references are allowed: of the 587
public JSON files writing $schema below, 6 test fixtures whose $id names an example.com URL
for files on disk went from exit 0 to exit 1 this way, as each does on main written with $defs.
#1051 is the remedy, commented there. A seventh, an OpenAPI 3.0 meta-schema, embeds draft 4's
meta-schema under that meta-schema's own id while keeping the definitions its $refs name at
its root, where draft 4 reads them against that id, so they name nothing.

A draft's additionalItems beside a list of items at a position lowering to no tuple is kept
under degraded_lowering with the list, with one openapi/degraded-construct info naming both,
rather than under out_of_scope with an openapi/unknown-schema-keyword info (item 33). A draft 4
id naming a fragment no plain name spells, such as '#1x', now declares it (item 30).

Visible changes on input that was already invalid

  • A malformed bound is reported at the keyword, with line and column, rather than at the schema.
    The code is unchanged.
  • An items list where no dialect takes one is openapi/invalid-items-list at every minor,
    rather than openapi/validation/validation-type-mismatch. The spurious openapi/false-schema
    info beside it is gone, since the parser's false there was never written.
  • A $ref naming a plain name no schema declares reports that no schema declares it, naming a
    draft's fragment identifiers among the declarations, rather than "its fragment is no JSON
    pointer".
  • A bound written by an overlay is attributed to the overlay, as validation findings are, where
    the lowering attributed it to the source.
  • A draft 6 or 7 $id naming a fragment alone that is no plain name, such as '#_x', is refused
    for that, where main refused every $id naming a fragment.
  • A $ref naming a plain name no grammar admits, such as '#1x', in a document whose identifiers
    were read only up to the merge bound, reports that bound rather than "its fragment is no JSON
    pointer" (item 38).
  • An additionalItems beside a 2020-12 items list, which is reported, is kept under
    degraded_lowering, as a draft's beside a list no tuple holds is since item 33, rather than as an
    unknown keyword.

Reworded messages

Each said $id or $anchor where a draft's id or fragment identifier is now read too:

  • The rebased-branch message says "an identifier" rather than "an $id".
  • Two schemas declaring one plain name list "an identifier naming that fragment alone" beside
    $anchor and $dynamicAnchor, and two declaring one URI say "as their identifier".
  • A $dynamicRef left unexpanded below a resource, and a $defs pointer only a resource's own
    definitions would answer, say "an identifier".

Limitations, stated in docs/compilers/openapi.md

  • The rule table's check still holds a draft's schema to its minor's keyword rows, bar the shapes
    above, so a keyword only a draft defines, such as dependencies or draft 4's id, is held to
    none, and so is a definitions entry no $ref reaches: openapi: a schema read in a JSON Schema draft is held to its minor's keyword rows #1466. A tuple's entries and rest are
    judged since item 24, as 2020-12's are.
  • A keyword 2020-12 added, such as prefixItems, is read as 2020-12 reads it in a draft's schema:
    commented on openapi: a 3.0 document's prefixItems lowers to a tuple with no diagnostic #820, the same mechanism as its 3.0 case.
  • 2019-09's $recursiveRef is kept verbatim and never expanded as its $dynamicRef is: openapi: a 2019-09 schema's $recursiveRef is kept verbatim, never expanded #1475.
  • Within a fetched document naming draft 4, the id check reads no grammar, so an example's id
    naming a location on disk refuses it too (docs/compilers/openapi.md#references).
  • A schema the parser reads as false though it writes a mapping is decoded again no more than
    typed.MaxMends deep, one below another, since each decoding reads all below it: one past them
    is the top type, reported (docs/compilers/openapi.md#whole-schema-fallbacks).
  • A $schema naming a draft by another URI than its meta-schema declares, such as
    https://json-schema.org/draft-07/schema or http://json-schema.org/draft/2019-09/schema# (1 and
    6 of the 560 files), names no dialect the table has rows for, as fix(compilers/openapi): read a schema in the dialect it names #1459 has it: it reads as the
    minor's own, with openapi/unread-dialect.

Closes #1457
Closes #1299

Test plan

  • New tests in each package, each holding its own coverage of the code it adds:
    • oas: the hand-over and FormatFinding.
    • bind: tuples, draft identifiers and definitions, embedded roots, walks entering roots,
      DialectAt, and a $schema naming no dialect.
    • load: the judge, alias dedupe, a reached unmodelled schema, and which fetched id refuses its
      document.
    • lowering: SchemaRulesAt.
    • operation: a draft's tuple as a multipart part.
    • schema: tuple lowering, ListedTuple, the census, $ref siblings, a union branch, and an
      allOf fold under id.
    • naming: tuple steps.
    • e2e: TestOlderDrafts_*, and TestOwnedShapes_AreReportedWhereverTheCheckReadsASchema over
      the gap's sites and a reached document.
    • A schema-drafts conformance case with its canary rows classified.
  • Mutation, on the merged tree: the 52 defects first planted, re-planted where the merges and the
    later commits moved their sites. 49 turn a named test red, and so does a fiftieth once the case
    pinning an on-host id names draft 4, which item 10 had left judging nothing; 2 sites no longer
    exist, rewritten by items 4 and 5. 16 more for items 8, 10, 13, 14 and 16 each turn one red,
    one only after a case was added for it. 9 more for items 19 and 20: 8 turn one red, one only
    after a case was added for it, and dropping draft 4's id format is equivalent, as no check
    reads that row (openapi: a schema read in a JSON Schema draft is held to its minor's keyword rows #1466).
  • Differential against main (4ac3031d7), comparing exit code, IR and diagnostics per spec:
    • Over the 231 YAML and JSON files under testdata bar goldens, 3 differ: the new case, and the
      reworded rebased-branch and $dynamicRef messages.
    • Over 1,840 real-world OpenAPI 3 specs, 11 differ, every exit code unchanged. 8 are the
      items-list code move. 3 are bound errors now placed at the keyword, one of which also gains
      two reports, on a 3.0 array's bounds the lowering never read.
    • Over the 587 public JSON files writing $schema, each reached from a 3.1 document's $ref,
      401 differ. 11 that exited 1 now exit 0, on readings the drafts give that main refused: 9
      tuples, 2 fragment $ids and 2 plain-name anchors. 7 that exited 0 now exit 1, those under
      "Visible change on input that names a draft".
  • At 039e5a9a7, 560 public JSON Schema files, 380 of them draft 7, 29 draft 4 and 13 2019-09, each reached
    from a 3.1 document's $ref, against main: 399 differ. Each difference is one of these: an
    unread-dialect warning gone (386 files) and unknown-keyword infos gone (175), a tuple accepted
    (14 files lose a type-mismatch error) and a draft 7 $id naming a fragment (2 lose a format
    error), $ref siblings warned of (item 13), an items list refused in a file read as 2020-12
    (4), and the bound $refs above.
  • Against the previous head (cf29b1a62), over 2,077 OpenAPI files, 249 under testdata and
    1,828 real-world specs, and the 560 JSON Schema files: none differs in exit code, IR or
    diagnostics, while the two binaries differ on the probes for items 19 and 20.
  • The harness's oracles, determinism, order invariance and pass idempotence among them, over each
    of the 560 files beside a 3.1 wrapper: 468 pass every oracle and 92 stop at an error diagnostic.
    Against main, 9 that stopped now pass, and 7 that passed now stop, each at a definitions
    $ref that a sibling file the copy lacks, or an example.com $id, leaves unresolved. Six large
    3.1 specs, their jsonSchemaDialect set to each draft in turn, pass them too, bar one stopping at
    the same Parameter Object error in every dialect. Against the same specs read in 2020-12 they
    differ only by the $ref-sibling warnings drafts 4 to 7 draw, beside description, minLength
    and maxLength.
  • FuzzLowerSchema for four minutes from seeds of the drafts' readings: 2.5 million executions,
    no failure.
  • Shapes the corpora hold none of, against main: a tuple of 100,000 entries costs no more than
    one written with prefixItems, linear in its entries; tuples nested past the depth bound report
    once; an alias bomb through tuple lists is refused as before. A document of 20,000 draft 4
    components costs 1.0% to 1.1% more instructions retired, medians of three runs at GOGC=off,
    whether each names the draft or its jsonSchemaDialect does: the readings the draft adds, of
    which handing its bounds to the load phase is 0.2%.
  • Golden spec (Cloudflare openapi.json @9c77db52): morphic validate output identical, 1,969
    lines, and the compiled IR byte-identical. Cost at GOMAXPROCS=1 GOGC=off, over four paired runs
    against main: 0.12% to 0.22% more instructions retired, against a base of 87.95G to 88.15G,
    and 0.7% less peak memory, which with the collector off is what the compile allocates.
  • morphic-harness testdata: the same outcome for every spec as main, bar the new case, which
    main stops at the error its items list draws and the head passes through every oracle.
  • Probes, each compiled on main and the head: a remote document served over HTTP with an
    example's id (read on both) and a draft 4 one whose id names the disk (refused on the head);
    a draft 4 $schema: 'rel/x' beside id; draft 7 duplicate fragment identifiers and draft 4
    duplicate ids; a $dynamicRef under a draft 4 id; tuples reached through a component $ref,
    a path item $ref and a document of schemas; a 2019-09 $id: '#foo' (refused on both); a draft
    7 tuple as a multipart part beside its prefixItems twin; and a draft tuple's entries and rest
    written as 5, 'x', [1] and null, unreached and reached by a $ref.
  • Items 22 to 24, on the head they land at:
    • 20 defects planted at their sites each turn a named test red, two only after a case was added
      for them: writesRef reading a $ref written as a mapping as one, and isTupleRest comparing
      no documents.
    • Differential against the previous head (953c11b5a) over 2,658 inputs, the 231 YAML and JSON
      files under testdata bar goldens, the 1,840 real-world specs and 587 JSON files writing
      $schema, each beside a 3.1 wrapper: 23 differ, no exit code among them. 20 only reword the $ref-sibling warning, 2 are a real-world draft 7
      schema writing $id beside its $ref, whose types are unchanged and whose identifier is no
      longer warned of, and 1 is the conformance case, which now writes an id beside a $ref.
    • Golden spec: morphic validate output and the compiled IR byte-identical to the previous
      head's, at 88.17G to 88.29G instructions retired against its 88.18G to 88.24G.
    • The harness's oracles pass on the conformance case and on the test suite's sibling-$id case
      in drafts 4, 6 and 7.
    • Coverage of oas, bind, annotation, schema and load by their own tests: 100%.
  • Items 25 to 28, on the head merged with main (4ac3031d7):
    • 34 defects planted at their sites each turn a named test red, three only after a case was added
      or rewritten for them: decoding a written false again, pinned by a count of decodes, and the
      run-wide dialect test and a fast path for an unread document, pinned by a draft spelling its
      identifier $id.
      A second mend before the landings were built turned none red, as the one after them mends
      what was recorded before, so it was removed.
    • Differential against the head before each, over the same inputs: item 25 changes 1 file, the
      public friends.json, whose items model now has its three properties; item 26 changes the
      order of 2 infos in the new case; item 28 changes none.
    • The JSON Schema Test Suite's groups for drafts 4, 6, 7 and 2019-09 (suite @245a45abc), each
      written to its own file with its draft's $schema and reached from a 3.1 document: all 1,014
      compile with no error but refRemote.json's refused network fetches, and 969 pass every
      harness oracle, the other 45 stopping at those fetches. Its "$ref prevents a sibling $id from
      changing the base uri" lowers its $ref to the number drafts 4, 6 and 7 name.
    • A malformed draft tuple at 14 positions, from a component to a webhook and a callback, draws the
      errors its prefixItems twin draws at each.
    • Hostile cost of item 25, at GOMAXPROCS=1 GOGC=off: a chain of items 1,000 deep ending in a
      list costs 6.8G instructions retired against the previous head's 1.4G, and would cost 40G at
      500 deep with no bound.
    • Golden spec: morphic validate output and the compiled IR byte-identical after each of items
      25, 26 and 28, its cost within the noise of the head before.
    • FuzzLowerSchema for three minutes on the final head: 1.44 million executions, no failure.
    • Coverage of every touched package by its own tests: 100%.
  • Item 29, on the head merged with main (ab953d453):
    • 6 defects planted at its sites each turn a named test red: governing no other document's nodes,
      governing a document's root before the roots it holds, falling back to the context's rows,
      governing a document whose rows are the source's and noting scalars, those two changing only
      what is stored, and walking past lists, which the list-alias case counts.
    • Differential against the previous head (5c36e79a8) over the same 2,658 inputs: none differ.
      The new end-to-end cases, one per direction, each lower as the previous head did not.
    • Cost at GOMAXPROCS=1 GOGC=off, three runs alternating with the previous head's: a 3.1 source
      reaching 20 schemas of a 3.0 document of 20,000 retires a median 3.86G instructions (3.85G to
      3.89G) against 3.79G (3.79G to 3.80G), 1.8% more, and peaks at 224 MB against 219 MB, at a
      byte-identical IR. Noting scalars too, as the walk did, cost 6% and 9%, so it notes none.
    • Golden spec @57b5196: the compiled IR byte-identical to the previous head's, and its 1,969
      diagnostics too, among them 9 openapi/unresolved-security-scheme errors main draws alike,
      since that revision names schemes it does not define.
  • Items 30 to 35, on the head merged with main (2123d296e):
    • 26 defects planted at their sites each turn a named test red, four only after a case was added
      for them: holding draft 6's or draft 7's $id row to a plain URI, which leaves validation and
      the binder agreeing on a reading the draft refuses, indexing the bindings before decoding
      again, and indexing them only for a source naming a dialect, not another document. A check that the
      bindings place a schema in a document the compile lists survives: no input reaches it, and it
      keeps the fallback schema.ListedTuple keeps. A check of a fragment's UTF-8 before looking a
      name up was equivalent, as no dialect declares a name no UTF-8 spells, and was removed.
    • Differential against the previous head (e9cfa2de5) over 2,664 inputs, the 237 YAML and JSON
      files under testdata bar goldens, the 1,840 real-world specs and the 587 JSON files writing
      $schema beside a 3.1 wrapper: 12 differ, each an openapi/invalid-items-list error gaining
      item 34's suggestion, no exit code and no other diagnostic among them.
    • The JSON Schema Test Suite's 265 groups of items, additionalItems, ref, refRemote,
      definitions, dependencies and anchor for drafts 4, 6, 7 and 2019-09: 20 differ, each an
      additionalItems beside a list in an untyped schema, now degraded_lowering; none draws an
      error but refRemote's refused fetches.
    • The harness's oracles pass on probes of items 30, 32 and 33: aliased folds and bodies in both
      orders of the roots' names, draft 4 fragment identifiers, and a stray rest in an untyped schema,
      beside a $ref, in a property and in a union.
    • Golden spec @305b054ec: the compiled IR and morphic validate output byte-identical to the
      previous head's. Cost at GOMAXPROCS=1 GOGC=off, three runs alternating: 94.12G to 94.29G
      instructions retired against 94.18G to 94.32G, and 6,028 MB peak against 6,027 MB. A document of
      20,000 components each naming draft 4, where the index is kept, retires 68.09G to 68.13G
      against 67.92G to 68.02G (0.25%) and peaks at 4,533 MB against 4,490 MB (1.0%), at a
      byte-identical IR.
    • Coverage of bind, bind/typed, load, oas and schema by their own tests: all 7,201
      statements.
  • Items 36 to 40, on the head merged with main (2123d296e):
    • 8 defects planted at their sites each turn a named test red, all but one only through a case
      these items add: reading a variant's tuple at its own pointer, placing no list of items where
      no dialect is named, the placement lookup ignoring the document it finds, or skipped by either
      of its two readers, 3.1's advice given in a 3.0 document, the merge-bound reason dropped, and
      the decoding bound's advice dropped.
    • Differential against the previous head (cb7798bdc) over the same 2,664 inputs: 7 differ, each
      a 3.0 document's openapi/invalid-items-list error taking item 37's advice. The JSON Schema
      Test Suite's 265 groups: none differ, none draws an error but refRemote's refused fetches.
    • The harness's oracles pass on a draft tuple as a type list's variant, named by $schema and by
      jsonSchemaDialect, with a tuple among its entries.
    • Golden spec @305b054ec: the compiled IR and morphic validate output byte-identical. Cost at
      GOMAXPROCS=1 GOGC=off, three runs alternating with the previous head's: 94.13G to 94.18G
      instructions retired against 94.02G to 94.25G, and 6,030 MB peak against 6,030 MB.
    • Coverage of bind, bind/typed, load, oas and schema by their own tests: all 7,216
      statements.
  • Items 41 to 44: 4 defects planted at their sites each turn a named test red, three through a case
    these items add (letting a List hold a stray rest, the format's old wording, placing no list of
    items where no dialect is named) and the fourth, giving 3.1's advice everywhere, through item
    37's 3.0 case. Differential against the previous head (8cecaadd2) over the same 2,664 inputs:
    none differ. Coverage of bind, bind/typed, load, oas and schema by their own tests: all
    7,216 statements.
  • Items 45 to 47: the format's new wording is pinned by the test item 41 added, which turns red on
    the wording before it.
  • Over the whole change, its comments, doc sentences and messages were swept for the defect class
    item 45 fixes and its neighbours: a second comment marker on a line, a word written twice, an
    unpaired backtick, two spaces inside a sentence, a cited identifier the tree no longer declares,
    a new declaration whose doc comment does not open with its name, a comment ending mid-sentence,
    and a line past 100 characters. Only the lines item 48 rewraps remained, bar two of 101, as
    docs/compilers/openapi.md on main keeps 64 lines past 100. The
    harness over testdata gives every spec main has the outcome main gives, and the new case
    passes every oracle.
  • Ran locally: the touched packages' tests, each new commit's build and tests on its own,
    internal/archtest, golangci-lint on the touched packages and subjects.py. CI ran the gate on
    cb7798bdc, 8cecaadd2, 105e7142d and 8d05168d1, both jobs green on each, and runs it on the
    head.

OpenAPI 3.0 ignores the keywords written beside a schema's $ref. The
compiler applies them as 3.1 does and warns of each, but then read each
keyword in a different dialect: `nullable` as JSON Schema 2020-12 reads
it, which defines no such keyword, `exclusiveMinimum` as 3.0 reads it,
and 3.0's requirement of `items` beside `type: array` not at all. The
warning claimed a 3.1 reading, which was false for `exclusiveMinimum`.

Keywords beside a 3.0 $ref are now read as 3.0 defines them, the only
reading under which what the author wrote means what it says: a
`nullable` there adds null to a `type` written beside it, in the null
reading and in what admission reads a schema to admit; with the policy
off it is no undefined keyword, so it is not kept verbatim; and a
`type: array` there with no `items` draws openapi/missing-array-items.
The warning now says the keyword is applied as in 3.1 and read as in
3.0.

The reason openapi/inferred-nullable gives for a nullable beside a $ref
no longer says no version gives it an effect. A 3.1 one names the
dialect, a 3.0 one with no type beside it says it writes none, and one
whose null the conjunction removes names what removed it: an allOf
beside it, or what the $ref lands on.
The rule table in internal/oas states, per minor, each fact the schema
lowering branches on: that `nullable` is a Schema Object field only in
3.0, that 3.0's exclusive bounds are booleans and 2020-12's numbers, and
that 3.0 requires `items` beside `type: array`. The lowering read none
of those rows. It asked lowering.Ctx.Is30, which parsed the document's
version, so each fact was stated twice, in two packages, with nothing
holding the two to each other.

oas.SchemaRules is the Schema Object's rows at one minor, built once
when a context is made, and Ctx.SchemaRules replaces Is30. The lowering
now asks whether the dialect defines `nullable`, what shape
`exclusiveMinimum` takes, and whether a schema whose `type` is `array`
must write `items`: the rule's condition is read from its row too, so
the lowering's test of `type` is the table's. SchemaRules has no
accessor for its minor, so a holder of one cannot branch on it, and a
version the table does not cover answers with rows defining nothing, as
Is30 answered false.

The one output change is the reason openapi/inferred-nullable gives in a
2020-12 schema, which now says the dialect it is read in defines no
nullable keyword rather than quoting the declared version.
A document a reference reaches beside the source was read at the
source's minor, whatever it declared. A 3.0 root reaching a 3.1 response
refused the response's numeric exclusiveMinimum, a 3.1 root reaching a
3.2 response required the description 3.2 dropped, a 3.0 schema reached
from 3.1 lost its nullable and its required items, and Document.Sources
recorded every such document as read at the source's minor.

Each document is now read at the minor it declares: its schemas in that
minor's dialect, its reached objects held to that minor's table, and its
Document.Sources entry naming it. A document declaring no version, such
as a bare schema file, or one the table does not cover, such as 3.3.0
or a Swagger 2.0 file, is read at the source's minor as before, and the
compile still stands.

load.Other carries the Schema Object's rows at its minor, and
lowering.Ctx.SchemaRules answers with those of the document its scope
reads. The null reading crosses documents only at a $ref, so each alias
and the end of a chain are read in their own document's dialect, and
the walk returns to its own once it has read them. What admission reads
crosses documents through declaration sets as well, so it reads each
schema in the dialect of the document the bindings place it in.

The load phase now builds the rule table once and shares it between
judging the source, judging what references reach, and reading each
document's rows, where it built the table twice before.
That no lowering asks the OpenAPI version a document declares, and
reads what a construct means at that minor from the rule table instead,
was a sentence in the lowering specification that nothing enforced. A
lowering that read the version again would state a fact the table
already states, and nothing would hold the two to each other.

The versionreadings analyzer, which make vet runs, refuses every reading
of the version outside compilers/openapi/internal/load and
compilers/openapi/internal/oas, in production files: the parser
library's version field and GetOpenAPI method of its document type,
promoted or not, oas.ParseMinor and load.SupportedMinor. The library
names other fields OpenAPI too, a walk's among them, so the field is
the one its document type declares, compared as an object rather than
by name. Detection's own probe of a source's version is no use of the
library's type, so routing a source by its version stays allowed.

The plants cover each form a use takes, the two exempt packages, a
look-alike type, a test file and a package outside the module.
Two sites the change reads its dialect at had no test that told a wrong
reading from the right one.

The agreement of an allOf's value bounds reads each conjunct in its
document's dialect. A numeric exclusiveMinimum is a bound from 3.1 on,
so two writers of one meet and fold; in 3.0 the lowering reports and
drops it, so its writers state nothing and stay apart.

The schema readers ask the rows of a schema's document for every schema
they read across the bindings, most often the source's. Those come
without a lookup by name, which would parse the name as a URL on each
miss; an allocation count holds them to it.
Reading each schema in its own document's dialect gave admission two
methods, past the 20 internal/archtest allows a type under compilers/.
Neither needed the walker's state. Which dialect a schema is read in
follows from the context and where the bindings place the schema, so it
is a function of those, definesNullableAt. What a schema writes takes
that dialect as an argument, so each reader says which one it reads in:
the schema's own document's across the bindings, c's for a schema of
c's document.
The document-minor table carried a check function per case, which rule
6.2 of the styleguide reads as a red flag; each case now states the
bounds and the missing-items warnings it expects. The helper takes the
nullable-widening policy rather than a bare bool, the two-minor test
preallocates what it collects, an allocation count reads its answer
outside the counted function, and a comparison of rows holding an
unexported field compares their identity.
testifylint refuses assert.True over a comparison and asks for
assert.Equal, but assert.Equal compares deeply, so rows built twice
would pass it. The claim is that a minor's rows are built once, so the
test checks identity itself and reports a miss with t.Error.
Three places still said the keywords written beside a 3.0 schema's $ref
are read as 3.1 reads them, which stopped being true when they began to
be read as 3.0 defines them: the IR's unmodeled-content chapter, the
comment on the Schema Object's row in the rule table, and the reason the
validation canary gives for its ref-siblings class. Each now says they
are applied as 3.1 applies them and read as 3.0 defines them. The
nullable rule's paragraph is rewrapped where an earlier edit left a
long line.
OpenAPI 3.1 has a root Schema Object's `$schema` choose the JSON Schema
dialect it is read in, and a document's jsonSchemaDialect stand in for
it where none is written. Both were kept verbatim and ignored, so a
draft 4 schema's `minimum: 5, exclusiveMinimum: true`, which means
x > 5, was refused as a malformed 3.1 bound.

The rule table now has rows by dialect as well as by minor: the OAS
dialect and JSON Schema 2020-12 read as the minor's own keywords, and
draft 4's keywords stand in for them where a schema names it. A dialect
changes the keywords alone; the fields and rules OpenAPI adds to a
Schema Object stay the minor's. The binder notes the `$schema` each
schema no other holds writes, a document of schemas naming its dialect
at its root, and the load phase hands the lowering the rows each such
root governs by the nodes of its tree, with each document's
jsonSchemaDialect as the default. The exclusive-bound reading asks the
rows of the schema it reads by its node, so a schema a `$ref` names is
read in its own root's dialect whichever declaration lowers first.

A dialect the table has no rows for, a later draft or one of the
author's, is read as the minor's own, with an openapi/unread-dialect
warning at the `$schema` or jsonSchemaDialect naming it. A 3.0
document defines neither keyword, so nothing changes there.
A draft before 2020-12 still has the keywords it spells otherwise read
as 2020-12 reads them, the table has no rows for drafts 6, 7 and
2019-09, and a $schema at the root of a resource an $id embeds chooses
nothing. The dialect rule's Status and the code that draws each line
now name the issue tracking it.
A `$schema` or jsonSchemaDialect that is no absolute URI, such as a
number or a relative path, is already an openapi/validation finding of
the rule table, which formats both rows as absolute URIs. It also drew
an openapi/unread-dialect warning at the same place, two findings for
one defect. SchemaRules now reports a dialect as unread only where an
absolute URI names one the table has no rows for; a malformed value
reads as the minor's own, as before, and is reported once.

The dialect rule also states what a node an alias places at several
positions reads: the dialect of the first root whose tree holds it, at
every position. No reading today tells one valid node's two dialects
apart, since a valid exclusive bound in one is malformed in the other.
The parser library holds a 3.1 schema to its version's 2020-12
meta-schema whatever dialect the schema names, so it refuses the new
conformance case's draft 4 bounds; the validation oracle now gives that
disagreement a reason of its own. The annotation package's own tests
read the info of each dialect keyword, `$vocabulary`'s included, which
only the end-to-end case reached, and the new tests are written as the
linters ask.
openapi/unread-dialect joins the diag package's list of every code it
declares, and the load phase spells the jsonSchemaDialect pointer
itself rather than reaching the ids package, which its layer may not
import.
OpenAPI 3.1.1 has a `$schema` choose the dialect of any Schema Object
that is a schema resource root, and JSON Schema roots a resource of its
own at a subschema whose $id names another URI. Only a Schema Object no
other holds, or a schema file's root, chose one, so a draft 4 property
with an $id beside its `$schema` was refused for its boolean bound.

The binder now counts a schema whose $id roots a resource as a dialect
root too, and the load phase governs roots innermost first, so a nested
root keeps its own dialect for what it holds while the rest of the
outer root reads the outer's. An $id naming its own document roots no
resource and still chooses nothing.
A 3.1 or 3.2 schema naming draft 6, draft 7 or 2019-09 drew an
openapi/unread-dialect warning and was read as 2020-12, as the rule
table had rows for draft 4 alone among the earlier drafts. Each now has
its keyword rows, every one citing its sentence in that draft's core or
validation text, and each dialect states whether it ignores the
keywords beside a `$ref`, as drafts 4, 6 and 7 do and 2019-09 does not
(SchemaRules.IgnoresRefFields).
The rule table leaves the shape of minimum, maximum, multipleOf and the
exclusive bounds to the schema lowering, which reads them off the raw
node. The lowering reported a bound no decimal spells, and an exclusive
bound in the other form than its dialect gives it, only where it read
the schema's constraints. A schema it never reads that way went
unreported: one under not, if, propertyNames or contains, one beside an
enum, or an array's value bounds. `{not: {exclusiveMinimum: true}}` in a
3.1 document compiled with no diagnostic.

The check now hands each such value to its caller (oas.Options.Owned),
already placed as its own findings are. The load phase judges each once
the bindings say which dialect the schema is read in, by where the
schema is, for the source and for every object a reference reaches, and
reports it at the keyword. The lowering still reads and drops what it
cannot use, and reports neither.
A 3.1 or 3.2 schema naming a JSON Schema draft before 2020-12 was read
as 2020-12 reads it wherever a keyword is spelled otherwise there. Each
is now read in the schema's dialect:

- items written as a list is a tuple in drafts 4, 6, 7 and 2019-09, its
  rest the additionalItems beside it. Elsewhere a list is reported as
  openapi/invalid-items-list by the load phase at every schema the check
  reads, and items reads as absent.
- dependencies is kept as a validation-only keyword in drafts 4 to 7.
- Draft 4's id names a resource and gives a $ref its base, a fragment
  alone declares a plain name in drafts 4 to 7, and definitions holds
  schemas. An $id's format is judged in the schema's dialect.
- A keyword beside a $ref, which drafts 4 to 7 ignore, is applied as
  2020-12 applies it and warned of as a later version's construct.

The binder and the load phase now share one rule for which $schema
chooses a dialect: a schema no other holds, or one rooting a resource by
an identifier spelled as its embedder or its own dialect spells one. A
schema is read in the dialect of where it is, so a node an alias places
under two roots reads each one's.
The JSON Schema drafts before 2020-12 spell a plain name as a letter,
then letters, digits, '-', '_', ':' and '.', where 2020-12 admits a
leading '_' and no ':'. A 2019-09 `$anchor: a:b`, or draft 7's
`$id: '#a:b'`, was refused or never declared, and a $ref naming it went
unresolved, while a 2019-09 `$anchor: _x` was accepted.

The check now hands `$anchor` and `$dynamicAnchor` to the load phase,
which judges each name in its schema's dialect as it does an `$id`, and
the binder declares an anchor by its schema's dialect's grammar. A
$ref's plain-name fragment is looked up under either grammar, since only
a declared name resolves, and the message for one no schema declares
names a draft's fragment identifiers too.
…rom-table

Two conflicts. internal/archtest's allowlist for lowering keeps main's
nodeview and yaml with the branch's oas, and its comment says both.
lowering's version test keeps the branch's assertions under main's new
BinaryMedia parameter, which the branch's other New calls now pass too.

main's multipart part reading (#1435) asks Ctx.Is30, which this branch
removes: a part writing no type is a file from 3.1, whose Encoding
Object defaults its contentType to application/octet-stream. Until the
table states that default, it asks whether the dialect defines nullable,
which answers the same at every minor.
lowering.New built the whole rule table a second time per compile to
read the source's Schema Object rows, while the load phase already held
one. The load phase now reads the source's rows off the table it judged
the source against, through the dialects that reads each reached
document, so a document read at the source's minor shares them, and
hands them over as load.Document.Rules. A context reads them from what
WithLoaded carries; one given no load phase still reads the version its
document declares.

A compile now builds the table once. BenchmarkCompile_Petstore, three
runs each at GOMAXPROCS=1: 1,442,131 to 1,319,664 B/op and 12,664 to
12,113 allocs/op.
A multipart part writing no type is a file from OpenAPI 3.1, whose
Encoding Object defaults its contentType to application/octet-stream
(3.1.1 and 3.2.0, "The default values for `contentType` are as
follows": a schema with type absent takes application/octet-stream;
3.0.4's table has no such row). That reading asked the version, and the
merge of main kept it by asking whether the dialect defines nullable.

The table now states it: a Field carries Defaults, each a value for one
case at the minors its span holds, and the Encoding Object's contentType
defaults a schema writing no type to application/octet-stream from 3.1.
oas.EncodingRules is the Encoding Object's rows at one minor, as
SchemaRules is the Schema Object's, and load.Rows pairs the two, so a
document's rows, the source's included, come from one table per
compile. lowering.EncodingRules reads them in the document whose content
the context reads, and the part is then judged as naming that media
type.

The table's own tests hold each default to its citation, to minors its
field is defined at, and to one value per case at each minor.
The rule that no lowering reads the version a document declares was a
go/analysis analyzer of its own in internal/lintrules. forbidigo
expresses it: two patterns refuse the parser library's version field and
accessor, and oas.ParseMinor, outside the OpenAPI compiler's load phase
and rule table, which an exclusion excuses, and outside tests, as the
analyzer did. A member promoted through an embedding of the library's
document is the one spelling forbidigo does not see, and nothing embeds
it. A struct literal writing the version, which the analyzer refused as
a read, is no use forbidigo matches.

load.SupportedMinor, read nowhere outside load, is unexported, so the
compiler confines it and no pattern is needed.

The plants move to scripts/lint-planted.sh's tree: look-alikes of load
and oas under internal/lintrules/testdata/planted read the version and
draw nothing, and one of a lowering draws each refusal, a context's
c.Doc.GetOpenAPI() among them. The analyzer, its tests and its plants
go, and internal/lintrules and cmd/morphic-vet return to main's.
A 3.0 $ref site writing type: array and no items drew
openapi/missing-array-items, which says only the schema's other keywords
constrain its elements, even where the $ref lands on a schema writing
items: {$ref: X, type: array} over X: {type: array, items: {type:
string}} lowers to an alias over X, a list of strings, and the warning
was false. The same conjunction spelled {allOf: [{$ref: X}, {type:
array}]} drew none, as an inline allOf branch restating what a
refinement names is not reported.

The keywords beside a $ref are now held to the rule with what they
conjoin with: the site is reported only where admission's reading of
the whole site, its aliases and allOfs included, finds no items. An
items it finds holds however much of the rest it read, and a reading cut
short before reaching one leaves the site reported as written.
Three statements had drifted from what the compiler does.
diag.LaterVersionConstruct said a keyword beside a 3.0 $ref is lowered
as in a 3.1 document, a line after saying it is read as 3.0 defines it,
and later-version-construct opened the same way. openapi-versions said a
version that is not major.minor.patch is reported, which holds for the
source alone: a document a reference reaches is read at the minor such
a version names, with no finding, since only what a reference reaches
is judged. TestDocumentMinor_EachDocumentIsReadAtTheMinorItDeclares now
pins that case.

TestSupportedMinor_AllocatesNothing pinned a cost by the lowering asking
the version of every schema, which it no longer does: the version gate
runs once per document, so the test goes. TestParseMinor_AllocatesNothing
keeps its pin under the reason that remains, Check and CheckFormat
asking it of every object the load phase judges.
nulls.lands reads each alias of a $ref chain, and its end, in the
dialect of the document holding it, and set the read's own back by hand
after the last. A return added between the two would have left the rest
of the read in another document's dialect. The restore is now deferred,
so it holds on every way out; the closure does not escape, and the
deferred call costs no allocation.
A Default held on every Field grew each of the table's Field rows by a
slice header, and the table is built once a compile: the Petstore
compile allocated about 11 KB more for one default the Encoding Object
states. Defaults now sit on their Object beside its Rules, each naming
its field, as a rule does, and the table's tests hold each to a field
its object defines at every minor the default holds at.

BenchmarkCompile_Petstore, three runs each at GOMAXPROCS=1: 1,332,877
to 1,322,027 B/op, 12,118 allocs/op either way.
missing-array-items' rule said a $ref site is reported where nothing it
conjoins with writes an items, while the reading behind it, admission's,
holds the elements to no items for items: true, and none for an items
only a union's branches write. Both still draw the warning, and the rule
and missingArrayItemsBesideRef now say so; TestLower_MissingArrayItems
pins each.

The table's descriptions name the defaults it now states: the oas
package doc, the validation rule, and openapi-versions, whose reached
document's Encoding Objects take its minor's defaults. Three comments
are made exact: the load phase's dialects reads the source and each
reached document, not the source first; the forbidigo rule names tests
among what its exclusions excuse; and the test exclusion says a test
may read a fixture's version.
A reading of a schema's dialect by its node, which folding an inline
allOf branch and reading a body as a file both make, answered for a node
no `$schema` root holds with the rows of the document whose content the
lowering was reading, not those of the document holding the node. A
draft 7 document's `dependencies` in an allOf branch the source reached
through a $ref kept its branch verbatim, as though the conjuncts
disagreed, and a draft 7 source's `definitions` in a body schema another
document reached was an unknown keyword, so the body was no file.

The load phase now notes, for each node of another document that no root
governs, that document's own rows where they are not the source's, and
the reading by node falls back to the source's rows rather than the
context's, so it answers alike from every document.
Every reading of a schema's dialect by its node asks it of a schema's
mapping, and the only schema a scalar can be is a boolean, which writes
no keyword, yet the walk noting each node's rows noted every key and
value too. Now that it covers the whole of another document whose rows
are not the source's, that was most of its cost: a 3.1 source reaching
20 schemas of a 3.0 document of 20,000 compiled with 6% more
instructions retired and 9% more peak memory than before the walk
covered it, and with about 2% more of each once scalars are left out.

Lists are still noted, so a list that aliases place many times is
walked once.
Drafts 6 and 7 name a subschema by an identifier that is a fragment
alone, and require it to open with a letter, then letters, digits, '-',
'_', ':' and '.'. Draft 4 states no grammar for one: its `id` is any
URI, and `#foo` names what the URI it makes names. The binder held all
three to the drafts' plain-name grammar, and nothing judged it, so a
draft 4 `id: '#_inner'` or `'#1x'` declared nothing and a `$ref` to it
went unresolved, while a draft 7 `$id: '#_x'` compiled with no finding
where main reported it.

The draft 6 and 7 `$id` rows now cite that sentence and give the
identifier a format, PlainNameURI, that the load phase judges in the
schema's dialect. The binder declares a fragment alone by the grammar
its dialect's rows give (bind.DialectRule.PlainFragments): the
plain-name grammar there, and in draft 4 any fragment no JSON pointer
spells, read as a $ref's fragment is. A $ref fragment no JSON pointer
spells is looked up as a plain name, as every dialect reads one, and
keeps its "no JSON pointer" reason where no schema declares the name
and no grammar admits it.
The row cut "a URI-Reference" to "a URI" at a line break of the draft's
text, which then said something the draft does not.
Folding an inline allOf branch, reading a body as a file and reading an
exclusive bound asked a schema's dialect of its YAML node, which an
alias shares between places. A node aliased under a draft 4 root and a
2020-12 schema then read one dialect at both, chosen by the roots'
names: a branch's `dependencies` folded at the 2020-12 place, where no
reader kept it, so it was lost with no finding; a body writing `id` was
a file at the 2020-12 place; and a valid draft 4 bound was dropped
while a malformed 2020-12 one was kept beside its error.

The typed bindings now place each schema by the model it holds
(typed.Bindings.SiteOf), which decoding builds afresh for every path to
a node, and those readers ask the rows of that place, in whichever
document. The index is kept only where a schema names a dialect by
`$schema`: otherwise each node reads its own document's, which its node
tells. This also lifts the limit that the allOf fold read a conjunct's
exclusive bounds by its node.
Where a draft's list of `items` lowers to no tuple, as with no
`type: array`, the list was kept as a degraded lowering and the
`additionalItems` beside it as an unknown keyword, under out_of_scope,
the reason the IR keeps for runtime and SDK policy. Its 2020-12 twin,
`items` beside `prefixItems`, is kept as a degraded lowering beside its
list.

The shape census now counts `additionalItems` beside a list of items as
homed by a Tuple alone, so it is kept with its list under one finding.
Beside an `items` that is no list it is the rest of no tuple and stays
an unknown keyword.
An `items` list where the schema's dialect reads no tuple now suggests
`prefixItems` or naming a draft that reads one, and a schema past the
bound on decoding again suggests moving the schemas below it into a
component, which reads them.
The exclusive-bound check spelled out the boolean-scalar test the
package declares as isBoolean.
The rule table holding a draft's schema to its minor's rows (#1466) and
2019-09's `$recursiveRef` never expanded (#1475) were stated only in
docs/compilers/openapi.md; judgeSource and dynamicRefName now say so.
missingArrayItems says why it reads an `items` list off the raw node.
A schema whose type list makes it a union lowers each variant at a
pointer of its own, below the schema's. A draft's tuple there read its
entries and rest at that pointer, where the load phase builds none, so
the array variant of `{type: [array, object], items: [...],
additionalItems: ...}` held the top type for each entry and for its
rest, and the census, seeing a Tuple, then left `additionalItems` kept
nowhere at all.

The typed bindings now also place by its model each schema writing a
list of items, whatever the dialects named, and the tuple reads its
entries and rest where the bindings place the schema writing them. That
lookup is now shared with the readers of a schema's dialect (placedAt),
whose crossing into another document a new case pins.
An `items` list where the dialect reads no tuple suggested
`prefixItems` or naming a draft by `$schema`, neither of which a 3.0
document can do. A 3.0 document's finding now says that 3.0 writes no
tuple, which 3.1 writes as `prefixItems`. Each finding's advice, and
the one past the bound on decoding again, is now pinned.
A plain name no grammar admits was reported as naming no JSON pointer
even where the document's identifiers were read only up to the merge
bound, though a draft 4 `id` past the bound may declare it, while a
name a grammar admits reported that bound. Only a name no schema
declares, with every identifier read, is now reported as no pointer.
The references, tuples, shape-applicator and dialect-keywords rules
now list the tests that pin what they say of the drafts, and the rule
for an `additionalItems` beside a list of items says it holds in every
dialect, as the census reads it.
The PlainNameURI format's description, used in the finding about an
identifier, reads as one clause, and two comments say what they meant.
The finding about a draft 6 or 7 `$id` naming a fragment alone that is
no plain name said the reference "names a letter, then ...". It now
says it names a plain name, as the sibling formats word theirs, and the
message is pinned.
Every row set defining `prefixItems` also defines `$schema`, so asking
for both decided nothing the first did not.
The typed bindings' reader of whether a schema writes `items` as a list
is now listsItems, the name bind gives its twin.
The tuples rule said a position lowering to no Tuple keeps its list,
though a type list's array variant is the tuple and leaves nothing on
the union; it now says so apart, and says where an `additionalItems`
beside a list no dialect reads is kept, which a 2020-12 and a 3.0 case
now pin. Two comments said the typed bindings place nothing where no
schema names a dialect, though they place a schema writing a list of
items, which their own test now pins.
Four test doc comments opened with their first words twice, joined by a
second comment marker, and one left "is" unfinished where "placed" had
been dropped.
The PlainNameURI format's description glossed the name with a colon,
where the AnchorName and PlainName descriptions say "a name of a letter,
then ...". It now says its fragment is such a name, as they do.
The comment on itemsListRemedy named one row set twice and said of a
single keyword that 3.0's rows define none.
Three lines the draft readings added to docs/compilers/openapi.md and
docs/ir/boundaries.md ran to 102, 115 and 156 characters, and one
stopped short mid-paragraph; each is rewrapped at 100, no word changed.
@OmarAlJarrah
OmarAlJarrah merged commit 2a0996a into main Nov 6, 2026
4 checks passed
@OmarAlJarrah
OmarAlJarrah deleted the fix/openapi-older-draft-keywords branch November 6, 2026 14:43
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