Summary
lower() elects one keyword family per position (const → enum → allOf, then the type set) and lowers it. #35 covered the competing families the election passes over. A second, narrower class remains: keywords the elected lowering simply never reads. They are neither modelled nor kept, and nothing is reported.
Two shapes reproduce today:
# 1. type beside allOf — lowerAllOf builds a Model (an object); a non-object
# type set is never read.
S:
allOf: [{$ref: '#/components/schemas/Base'}]
type: string
# → Model{Base: Base}; `type: string` gone, zero diagnostics.
# 2. format beside const — hoistLiteral produces a Literal, which has no
# Encoding field, and unhomedKeywords only flags `format` when no type is
# declared beside it.
T: {const: 5, type: integer, format: int32}
# → Literal{5}; `format: int32` gone, zero diagnostics.
Why this is separate from #35
#35's mechanism is "which of several competing families wins", and the answer is a property of the dispatch order alone — the losing families are always losing, whatever the winner produced. This one is a different question: did the lowering that won actually consume this keyword? Answering it needs a per-winner, value-dependent rule rather than a keyword list:
allOf + type: object is extremely common and loses nothing (a Model is an object), so a rule keyed on node kind alone would emit a bogus Unmodeled entry and diagnostic on a large share of real specs. Only a non-object type set is lossy.
const + a consistent type is likewise redundant rather than lossy; only a contradictory one, or a format the Literal cannot carry, loses anything.
unhomedKeywords/applicatorHome is the existing home for "the node has nowhere to put this", and its contract is deliberately keyed on node kind so it cannot drift from lower()'s dispatch. Extending it here would break that contract, since a *ir.Model reached through lowerAllOf and one reached through lowerModel differ in whether type was read.
So the fix wants its own design decision about what "consumed" means for each winner, taken on its own terms rather than as a side effect of the co-declared-family work.
Expected
Whatever the position elects, a keyword the elected lowering does not read is kept verbatim under Unmodeled with a degraded-construct diagnostic, and one that is read stays silent — in particular allOf + type: object must produce neither an entry nor a diagnostic.
Summary
lower()elects one keyword family per position (const→enum→allOf, then the type set) and lowers it. #35 covered the competing families the election passes over. A second, narrower class remains: keywords the elected lowering simply never reads. They are neither modelled nor kept, and nothing is reported.Two shapes reproduce today:
Why this is separate from #35
#35's mechanism is "which of several competing families wins", and the answer is a property of the dispatch order alone — the losing families are always losing, whatever the winner produced. This one is a different question: did the lowering that won actually consume this keyword? Answering it needs a per-winner, value-dependent rule rather than a keyword list:
allOf+type: objectis extremely common and loses nothing (a Model is an object), so a rule keyed on node kind alone would emit a bogusUnmodeledentry and diagnostic on a large share of real specs. Only a non-object type set is lossy.const+ a consistenttypeis likewise redundant rather than lossy; only a contradictory one, or aformatthe Literal cannot carry, loses anything.unhomedKeywords/applicatorHomeis the existing home for "the node has nowhere to put this", and its contract is deliberately keyed on node kind so it cannot drift fromlower()'s dispatch. Extending it here would break that contract, since a*ir.Modelreached throughlowerAllOfand one reached throughlowerModeldiffer in whethertypewas read.So the fix wants its own design decision about what "consumed" means for each winner, taken on its own terms rather than as a side effect of the co-declared-family work.
Expected
Whatever the position elects, a keyword the elected lowering does not read is kept verbatim under
Unmodeledwith a degraded-construct diagnostic, and one that is read stays silent — in particularallOf+type: objectmust produce neither an entry nor a diagnostic.