The same value constraint reaches two different channels depending on which lowering built the
model, and the diagnostic that explains the second case states a reason that is not true.
Reproduction
openapi: 3.1.0
info: {title: T, version: "1"}
paths: {}
components:
schemas:
Base: {type: object, properties: {id: {type: string}}}
ViaModel: {type: object, properties: {a: {type: string}}, minProperties: 2}
ViaAllOf: {allOf: [{$ref: '#/components/schemas/Base'}], minProperties: 2}
$ morphic compile mp.yaml -skip-validate | jq -c '.types["t/openapi/components/schemas/ViaModel"] | {kind, constraints, u: (.unmodeled|keys)}'
{"kind":"model","constraints":{"exclusiveMin":false,"exclusiveMax":false,"uniqueItems":false,"minProps":2},"u":null}
$ morphic compile mp.yaml -skip-validate | jq -c '.types["t/openapi/components/schemas/ViaAllOf"] | {kind, constraints, u: (.unmodeled|keys)}'
{"kind":"model","constraints":null,"u":["openapi:minProperties"]}
info openapi/degraded-construct mp.yaml#/components/schemas/ViaAllOf: this position lowered to a
node of kind "model", which has no home for minProperties declared beside it; kept verbatim
under Unmodeled
Both positions lower to an ir.Model. One puts minProperties in Constraints.MinProps; the
other puts it in Unmodeled and reports that the node has nowhere to put it — while the node
beside it demonstrably does.
Why it happens
lowerModel reads the position's constraints and fills ir.Model.Constraints. lowerAllOf
builds its ir.Model without doing so, so the field is left nil. The census then runs, finds
no value in Constraints, and concludes the keyword is homeless — a conclusion drawn from
what the lowering happened to fill rather than from what the node can hold.
Nothing is lost, so this is not a repeat of #268 (fixed by #348): that work is what keeps the
keyword at all. What is wrong is where it lands and what is said about it.
Impact
A consumer reading Model.Constraints gets minProps for one spelling of the same
constraint and must also scan Unmodeled for the other, with no signal telling it which to
expect. The diagnostic actively misleads: it attributes the placement to a missing field.
This is distinct from #10, which is about reconciling constraints declared on the allOf
branches. Here the constraint is declared at the composing position itself.
Expected
lowerAllOf fills the Constraints of the model it builds from the position's own schema, as
lowerModel does, so minProperties reaches the same field either way and the census reports
homelessness only for a keyword the node genuinely cannot hold.
The same value constraint reaches two different channels depending on which lowering built the
model, and the diagnostic that explains the second case states a reason that is not true.
Reproduction
Both positions lower to an
ir.Model. One putsminPropertiesinConstraints.MinProps; theother puts it in
Unmodeledand reports that the node has nowhere to put it — while the nodebeside it demonstrably does.
Why it happens
lowerModelreads the position's constraints and fillsir.Model.Constraints.lowerAllOfbuilds its
ir.Modelwithout doing so, so the field is left nil. The census then runs, findsno value in
Constraints, and concludes the keyword is homeless — a conclusion drawn fromwhat the lowering happened to fill rather than from what the node can hold.
Nothing is lost, so this is not a repeat of #268 (fixed by #348): that work is what keeps the
keyword at all. What is wrong is where it lands and what is said about it.
Impact
A consumer reading
Model.ConstraintsgetsminPropsfor one spelling of the sameconstraint and must also scan
Unmodeledfor the other, with no signal telling it which toexpect. The diagnostic actively misleads: it attributes the placement to a missing field.
This is distinct from #10, which is about reconciling constraints declared on the
allOfbranches. Here the constraint is declared at the composing position itself.
Expected
lowerAllOffills theConstraintsof the model it builds from the position's own schema, aslowerModeldoes, sominPropertiesreaches the same field either way and the census reportshomelessness only for a keyword the node genuinely cannot hold.