Skip to content

feat(ir)!: state tuple openness and bounds from prefixItems - #734

Open
fuad-daoud wants to merge 3 commits into
mainfrom
feat/ir-tuple-openness-bounds
Open

fuad-daoud wants to merge 3 commits into
mainfrom
feat/ir-tuple-openness-bounds

Conversation

@fuad-daoud

Copy link
Copy Markdown
Collaborator

Summary

A JSON Schema tuple written with prefixItems currently reports its openness the wrong way round, and drops the bounds beside it.

  • A bare prefixItems (no items) is silent. The source's open tail — every position past the fixed head is unconstrained — reaches the IR in no form at all, and the node is byte-identical to a genuinely closed one.
  • items: false is reported as open. It lowers to the same Tuple, keeps Unmodeled["openapi:items-after-prefix"], and emits "items after prefixItems is an open tuple". The closed form is described as the open one.
  • minItems/maxItems/uniqueItems beside a tuple are homeless. ir.Tuple has no Constraints field, so the census keeps them verbatim under Unmodeled and reports the keyword as having no home.

This corrects the lowering to the JSON Schema 2020-12 reading, and gives a tuple the bounds field List already has.

Change

ir.Tuple gains Constraints *Constraints (the same field and tags as List), and the tail past the positional head is decided by a single helper over three states:

Source Unmodeled["openapi:items-after-prefix"] Diagnostic Constraints
items absent value true, at the tuple's own pointer info degraded-construct, says open from bounds
items: true verbatim, at /items same info from bounds
items: {schema} verbatim, at /items same info from bounds
items: false none none from bounds

An absent items behaves as the empty schema true in 2020-12, so its entry states the effective tail; the declaration pointer distinguishes it from a written items: true. items: false is exactly what the head states, so it keeps nothing and is reported for nothing. When items is absent but unevaluatedItems is present, nothing is synthesized — the §4.7 keeper already holds that keyword verbatim, and its raw value is the tail.

A tuple's tail is therefore closed exactly when it carries no openapi:items-after-prefix entry, and open exactly when it carries one (unconstrained when the value is true, typed when it is a schema). "Closed" says nothing follows the head; it does not mean "exact": with no minItems the same source still accepts a shorter array, so the length comes from Constraints, never from the head alone.

ir.Tuple's prose loses "fixed-arity" for the same reason — prefixItems requires none of its positions, so a head of two names two element types and never says two elements are required. The TypeSpec and Erlang rows carry the forward note that a genuinely fixed tuple must lower with Constraints.MinItems = len(Elems) and no tail entry, or every position becomes optional in silence.

Breaking

IRVersion moves to 0.7.0. This adds a JSON key (constraints) to every tuple node and moves minItems/maxItems/uniqueItems off unmodeled onto it, so a consumer pinned to 0.6.0 that read a tuple's collection bounds out of unmodeled now finds nothing there. The stamp move is the whole of the break; every prefixItems tuple's JSON changes.

Test plan

  • make gate green; coverage is exactly 100% (7598 of 7598 statements).
  • Table-driven schema test over the five states (absent, true, false, typed, bounds), each asserting Elems, Constraints, the full Unmodeled map and whether the info fired.
  • Conformance fixture tuples-prefixitems.yaml holds one schema per state (closed first, so a lowering that confused closed with open is visible in the single-order golden), with assertTuples asserting the tail entry's value, reason and pointer per schema.
  • unhomed-keywords turns ItemsBoundOnTuple into a control (items: false, maxItems: 2): it loses the openapi:maxItems entry and gains constraints.
  • Four mutations planted and reverted, each reddening its named test: the old early return for absent items; dropping the false arm; dropping the tuple arm from the census; not calling the bounds reader.
  • irverify corpus and the unpreservable suite pass unchanged; unwitnessed.golden.txt is untouched, so Tuple.Constraints is witnessed by the corpus.
  • Harness order oracle clean on the new fixtures.

Merge ordering

This branch and the webhook-name branch both set IRVersion to 0.7.0 from the same base. Whichever merges second must rebase and take 0.8.0, re-anchoring ir/document_test.go's three literals and regenerating its goldens.

ir.Tuple gains Constraints, the field List already carried, so a tuple's
minItems/maxItems/uniqueItems move off Unmodeled and onto the node. A tail past
the positional head is one entry under openapi:items-after-prefix whatever its
source: an absent items keeps the literal true at the tuple's own pointer, items:
true and a typed tail keep what they wrote at their own /items pointer, and items:
false keeps nothing and is reported for nothing. items absent beside
unevaluatedItems is left to the §4.7 keeper, whose raw value is the tail.

The head is a positional head of fixed length whose positions are not individually
required; an instance's length is bounded by Constraints, never restated by Elems.
IRVersion moves to 0.7.0: a 0.6.0 consumer that read a tuple's collection bounds
out of unmodeled now finds nothing there.
Resolve the conflicts with main, move the 0.7.0 history entry into
ir-design §2.1 where main now keeps it, and bring the doc comments this
branch adds under the 100-word cap main now enforces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCzyGPq9DQ7RLfagZm2JB5
Main took irVersion 0.7.0 for group IDs and idSpaces, so this change moves to 0.8.0: the constant, its ir-design.md history entry, the neighbour rows of TestCompatibleVersion and every golden.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant