Skip to content

feat: reconcile the value-shape vocabulary — STRING, NUMBER, BOOLEAN, JSON - #57

Merged
justinmerrell merged 3 commits into
mainfrom
feat/value-shape-vocabulary
Aug 19, 2026
Merged

feat: reconcile the value-shape vocabulary — STRING, NUMBER, BOOLEAN, JSON#57
justinmerrell merged 3 commits into
mainfrom
feat/value-shape-vocabulary

Conversation

@justinmerrell

Copy link
Copy Markdown
Contributor

What changes

The value-shape vocabulary on schema.type becomes STRING, NUMBER, BOOLEAN, JSON in both the component and blueprint families. INTEGER is withdrawn. Alongside it, pattern and enum are forbidden on a JSON value and format is confined to STRING — two restrictions the schema's own description fields already claimed and nothing enforced. New component §6.3 Value schemas publishes the vocabulary in prose for the first time, with three requirement IDs; blueprint §5.3 cites it rather than restating it.

Why

Closes #54 — but not as filed, and the difference is the point.

The issue asked for a two-file rename of INTEGERNUMBER, citing a "§10" that defines the vocabulary. No such section exists in this repository. Component §10 is Known debt; blueprint and listing §10 are Security considerations. The §10 it cites belongs to musher-dev/platform.

The vocabulary lived only in a schema enum, which CONTRIBUTING ground rule 1 makes informative — so type: NUMBER was rejected here, and applying the rename as filed would have broken CI in exactly the two ways the issue described, mirrored back:

  • structural-036 (a pass case) would fail with ERR_INVALID_VALUE;
  • semantic-012 would die structurally before reaching the ERR_INCOMPATIBLE_TYPE it exists to pin.

Verified against main before touching anything:

{ "ok": false, "phase": "structural",
  "diagnostics": [ { "code": "ERR_INVALID_VALUE",
                     "path": "/spec/contract/inputs/gamePort/schema/type" } ] }

Scope was 7 corpus hits, not 2, plus 3 stale uses in blueprint prose and the two schema modules themselves. So this is a specification/platform divergence of the class ADR 0005 and ADR 0010 handle, and it is settled the same way: decided on the merits, recorded, then schema, prose and corpus moved together.

ADR 0013 carries the reasoning — six decisions, each decidable on its own, plus the two silences it deliberately leaves open (a default is not checked against the constraints beside it; semanticType is not paired with type).

Why now

git tag -l is empty, published.json reads "releases": {}, and #1/#2/#3 are still open, so ADR 0005 §1's pre-publication window is opencheck:compat reports "No releases to replay yet", which is the mechanical form of the same fact. Adding a member stays free forever; withdrawing one is free now and never again.

On INTEGER vs keeping both

Keeping INTEGER alongside NUMBER was the cheaper change — purely additive, not breaking. It was rejected on what it would have obliged the contract to answer: is 5432.0 an INTEGER; does an INTEGER output satisfy a NUMBER input given §4.2 permits no widening in either direction; what would a bound mean on each. Under that no-widening rule, two near-synonymous numeric shapes make independently authored components reject each other over a difference no consumer can observe. Integrality is now written as a constraint: type: NUMBER with pattern: '^-?[0-9]+$', which says strictly more about a port than type: INTEGER ever did (the value schema has never carried minimum/maximum).

Controlled-vocabulary justification (GOVERNANCE.md)

type is an ADR 0003 placement-one vocabulary, so the two additions justify themselves against the terms that exist:

  • NUMBER — replaces INTEGER rather than joining it. It is JSON's own numeric kind and the only numeric term the transport can distinguish.
  • JSON — no existing term covers a structured value. STRING would carry the document, but a consumer that will parse what it receives and one that will not are asking for different things, and nothing in the contract could tell them apart.

Compatibility

  • Breaking — a previously valid document now fails.

Two ways: any document using type: INTEGER, and any pairing format with a non-STRING type. No example and no fixture carried the second, so it cost nothing now and would have cost a major version later.

Per ADR 0005 §1 and GOVERNANCE.md § Compatibility review, the window waives the v<N> directory and the migration note while a family is unpublished. It waives nothing else: this needs CODEOWNERS approval, and both feature commits carry a BREAKING CHANGE: trailer.

task changes — 12 changes can reject a document that validates today
### blueprint/v1
- spec.parameters.*.schema.enum — maxItems — → 0
- spec.parameters.*.schema.format — type — → "null"
- spec.parameters.*.schema.pattern — type — → "null"
- spec.parameters.*.schema.type — enum removed INTEGER
- spec.parameters.*.schema.type — enum added NUMBER, JSON

### component/v1
- spec.contract.inputs.*.schema.enum — maxItems — → 0
- spec.contract.inputs.*.schema.format — type — → "null"
- spec.contract.inputs.*.schema.pattern — type — → "null"
- spec.contract.inputs.*.schema.type — enum removed INTEGER
- spec.contract.inputs.*.schema.type — enum added NUMBER, JSON
- spec.contract.outputs.*.schema.enum — maxItems — → 0
- spec.contract.outputs.*.schema.format — type — → "null"
- spec.contract.outputs.*.schema.pattern — type — → "null"
- spec.contract.outputs.*.schema.type — enum removed INTEGER
- spec.contract.outputs.*.schema.type — enum added NUMBER, JSON

#### Requirements
- COMP-VAL-001, COMP-VAL-002, COMP-VAL-003 — requirement added

Conformance

Nine new fixtures; five existing ones migrated with their declared diagnostics unchanged (NUMBERSTRING exactly as INTEGERSTRING, so every mismatch survives).

Case Expected Pins
component/structural/039-integer-value-type fail — ERR_INVALID_VALUE COMP-VAL-001
component/structural/040-number-value-type pass COMP-VAL-001
component/structural/041-json-value-type pass COMP-VAL-001
component/structural/042-json-value-with-pattern fail — ERR_INVALID_TYPE COMP-VAL-002
component/structural/043-json-value-with-enum fail — ERR_INVALID_VALUE COMP-VAL-002
component/structural/044-format-on-a-non-string-value fail — ERR_INVALID_TYPE COMP-VAL-003
blueprint/structural/024-integer-parameter-type fail — ERR_INVALID_VALUE COMP-VAL-001
blueprint/structural/025-json-parameter-with-pattern fail — ERR_INVALID_TYPE COMP-VAL-002
blueprint/structural/026-json-parameter pass COMP-VAL-001

structural-039 is the one that matters for review: it passed before this branch and fails after, which is what makes the withdrawal executable rather than declared. The blueprint cases are not redundant — the two families hold separate $defs, so a component fixture proves nothing about a blueprint document.

Zero new diagnostic codes, zero registry rows, zero UNCOVERED entries, and no change to tools/ — the runner compares schema.type by string equality and maps Ajv keywords to codes generically, so the vocabulary was never encoded there.

Checklist

  • task check passes locally (exit 0; 155 cases, 36/40 requirements pinned, Ajv/Blaze agree on 143 subjects with 0 disagreements)
  • schemas/dist/ regenerated with task bundle and committed (never edited by hand)
  • Conformance fixtures added for every behavioural change, each citing a clause
  • Normative prose updated in the affected spec.md
  • Commit messages are Conventional and correctly scoped
  • Commits are DCO signed off

Follow-ups

  1. musher-dev/platform PR #1997 carries the vocabulary but not the pattern/enum/format restrictions — the one half where this repository is ahead. Its pin for the two fixtures can be lifted once this lands.
  2. minimum/maximum on a value schema is a separate design surface, and additive, so it stays available after the window closes.

…ue vocabulary

The value-shape vocabulary was published nowhere. Component §6.1 and §6.2
discussed `schema` and `type` at length without ever saying what `type` may
contain, while every neighbouring vocabulary — `suppliedBy`,
`platformDefault.source`, `valueFrom` — got a prose table. The only statement of
membership was a schema `description` string, which CONTRIBUTING ground rule 1
makes informative. The meaning of `type` was defined in a field this repository
declares non-normative.

That is survivable for three self-evident primitives. It stops being survivable
when a member arrives whose meaning is not self-evident, and `JSON` is that
member: every value here is carried as text, so a JSON value's string form is an
encoded document, and one value has many spellings.

New §6.3 Value schemas states the vocabulary, the string transport, and three
requirement IDs. `INTEGER` goes because `NUMBER` is JSON's own numeric kind and
covers whole numbers and reals alike; an author who needs whole numbers writes
`pattern: '^-?[0-9]+$'` rather than reaching for a second type. Keeping both
would have obliged the contract to say whether `5432.0` is an INTEGER and
whether an INTEGER output satisfies a NUMBER input under §4.2's no-widening
rule — two near-synonyms that make independently authored components reject each
other over a difference no consumer can observe.

`pattern` and `enum` are forbidden on a JSON value: both decide membership on
the spelling, so either would accept one author's formatter and reject
another's. `format` is confined to STRING, which its own description already
claimed and nothing enforced.

Six fixtures arrive with it. structural/039 passed before this commit and fails
after, which is what makes the withdrawal executable rather than declared.
structural/036 is migrated for the same reason in reverse.

Filed as issue #54 with the divergence inverted — it cited a §10 that does not
exist here, and the rename it asked for would have broken CI in exactly the two
ways it described. See ADR 0013.

BREAKING CHANGE: `INTEGER` is withdrawn from `schema.type`; use `NUMBER`, with
`pattern: '^-?[0-9]+$'` where whole numbers are required. `format` MUST now be
null where `type` is not `STRING`. ADR 0005 §1's pre-publication window applies:
no `v<N>` directory and no migration note, since no version has been tagged.

Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
An authored parameter's `schema` is the same block a component input carries,
but this family holds its own `$defs` for it — so the component-side change
leaves blueprint parameters accepting a token the component module has
withdrawn, and nothing in CI would say so. structural/024 exists because a
fixture over there proves nothing about a document over here.

The vocabulary itself is not restated. ADR 0003 §2's rule against mirroring a
published vocabulary applies to a sibling spec.md as much as to an external
surface, and §7 already uses that construction for diagnostics: §5.3 names the
block component §6.3 defines and says this family enforces it on the same terms.

§4.2's three worked examples named INTEGER, which no longer exists. Rewriting
them also sharpens the argument they were making — "which string a formatter
picks" is exactly the difference between `5432`, `5432.0` and `5.432e3`.

A new paragraph says JSON is not a top type, because it is the member most
likely to be read as one. A STRING output does not satisfy a JSON input, or the
reverse: a consumer that will parse what it receives and one that will not are
asking for different things, and the wire is the last place that difference is
visible. Without it, the first author to try the wire reads the rejection as a
bug.

See ADR 0013.

BREAKING CHANGE: `INTEGER` is withdrawn from a parameter's `schema.type`; use
`NUMBER`. `pattern` MUST be null and `enum` MUST be empty where `type` is
`JSON`, and `format` MUST be null where `type` is not `STRING`. ADR 0005 §1's
pre-publication window applies: no `v<N>` directory and no migration note, since
no version has been tagged.

Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
Issue #54 arrived asking for a two-file rename and citing a §10 that does not
exist in this repository. What it had found was a real divergence with the
direction reversed: the platform had adopted JSON's own value kinds, and this
contract still carried INTEGER — enforced by an enum whose membership was stated
nowhere but an informative `description` field.

The ADR decides the six questions that answers, each on its own terms so a
maintainer can decline one without unpicking the rest, and records the two it
declines to answer — a default is not checked against the constraints beside it,
and `semanticType` is not paired with `type`.

It also records why the timing is not incidental. Adding a member stays free
forever; withdrawing one is free only while no family has been tagged, and
ADR 0005 §1's window closes at the first tag.

Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
@justinmerrell
justinmerrell merged commit fbab4ba into main Aug 19, 2026
6 checks passed
@justinmerrell
justinmerrell deleted the feat/value-shape-vocabulary branch August 19, 2026 12:27
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.

Conformance corpus still uses type: INTEGER — should be NUMBER per §10

1 participant