feat: reconcile the value-shape vocabulary — STRING, NUMBER, BOOLEAN, JSON - #57
Merged
Conversation
…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>
This was referenced Aug 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changes
The value-shape vocabulary on
schema.typebecomesSTRING,NUMBER,BOOLEAN,JSONin both the component and blueprint families.INTEGERis withdrawn. Alongside it,patternandenumare forbidden on aJSONvalue andformatis confined toSTRING— two restrictions the schema's owndescriptionfields 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
INTEGER→NUMBER, 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 tomusher-dev/platform.The vocabulary lived only in a schema
enum, which CONTRIBUTING ground rule 1 makes informative — sotype: NUMBERwas rejected here, and applying the rename as filed would have broken CI in exactly the two ways the issue described, mirrored back:structural-036(apasscase) would fail withERR_INVALID_VALUE;semantic-012would die structurally before reaching theERR_INCOMPATIBLE_TYPEit exists to pin.Verified against
mainbefore touching anything: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
defaultis not checked against the constraints beside it;semanticTypeis not paired withtype).Why now
git tag -lis empty,published.jsonreads"releases": {}, and #1/#2/#3 are still open, so ADR 0005 §1's pre-publication window is open —check:compatreports "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
INTEGERvs keeping bothKeeping
INTEGERalongsideNUMBERwas the cheaper change — purely additive, not breaking. It was rejected on what it would have obliged the contract to answer: is5432.0anINTEGER; does anINTEGERoutput satisfy aNUMBERinput 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: NUMBERwithpattern: '^-?[0-9]+$', which says strictly more about a port thantype: INTEGERever did (the value schema has never carriedminimum/maximum).Controlled-vocabulary justification (GOVERNANCE.md)
typeis an ADR 0003 placement-one vocabulary, so the two additions justify themselves against the terms that exist:NUMBER— replacesINTEGERrather 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.STRINGwould 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
Two ways: any document using
type: INTEGER, and any pairingformatwith a non-STRINGtype. 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 aBREAKING CHANGE:trailer.task changes— 12 changes can reject a document that validates todayConformance
Nine new fixtures; five existing ones migrated with their declared diagnostics unchanged (
NUMBER≠STRINGexactly asINTEGER≠STRING, so every mismatch survives).component/structural/039-integer-value-typeERR_INVALID_VALUECOMP-VAL-001component/structural/040-number-value-typeCOMP-VAL-001component/structural/041-json-value-typeCOMP-VAL-001component/structural/042-json-value-with-patternERR_INVALID_TYPECOMP-VAL-002component/structural/043-json-value-with-enumERR_INVALID_VALUECOMP-VAL-002component/structural/044-format-on-a-non-string-valueERR_INVALID_TYPECOMP-VAL-003blueprint/structural/024-integer-parameter-typeERR_INVALID_VALUECOMP-VAL-001blueprint/structural/025-json-parameter-with-patternERR_INVALID_TYPECOMP-VAL-002blueprint/structural/026-json-parameterCOMP-VAL-001structural-039is 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
UNCOVEREDentries, and no change totools/— the runner comparesschema.typeby string equality and maps Ajv keywords to codes generically, so the vocabulary was never encoded there.Checklist
task checkpasses locally (exit 0; 155 cases, 36/40 requirements pinned, Ajv/Blaze agree on 143 subjects with 0 disagreements)schemas/dist/regenerated withtask bundleand committed (never edited by hand)clausespec.mdFollow-ups
musher-dev/platformPR #1997 carries the vocabulary but not thepattern/enum/formatrestrictions — the one half where this repository is ahead. Its pin for the two fixtures can be lifted once this lands.minimum/maximumon a value schema is a separate design surface, and additive, so it stays available after the window closes.