fix: enforce the §2 unknown-property rule below the envelope - #10
Merged
Conversation
spec.md §2 requires unknown properties to be rejected at every level, but additionalProperties: false was present on only SeedComponentMetadata. The 21 Component*Request defs were open, so a misspelled optional key anywhere below the envelope validated clean and fell back to its default. The gap is an artifact of the bootstrap import (ADR 0001): platform Seed* models set Pydantic extra="forbid"; reused *Request wire models inherit the default extra="ignore". Map-shaped schemas (contract inputs/outputs, workload endpoints/volumes, build arguments) are untouched — their keys are chosen by the document author. Refs #6 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
The six Blueprint*Request defs carried no additionalProperties, so a misspelled optional key inside a parameter or a connection ref validated clean. The Seed* defs were already closed, which is why the first level below the envelope appeared strict while everything under it was not. Refs #6 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
structural/003-unknown-field only injects a key at the root, which is why the gap below the envelope survived the bootstrap import. structural-005 injects the typo deeper: a probe field on component, a parameter's ui block on blueprint, and an array element on listing. Against the pre-change schemas the component case validated cleanly and the blueprint case produced no ERR_UNKNOWN_FIELD. The listing case passes either way — listing was already fully strict, so it is a regression pin rather than proof of a fix. Refs #6 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
Nothing checked this before, which is how 27 open defs survived the import. Any subschema declaring type: object and properties must now also declare additionalProperties: false. Map-shaped schemas declare no properties and are unaffected, so the rule needs no exemption list. walkObjects now carries a JSON Pointer so the diagnostic can name the offending subschema. Refs #6 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
All four unknown-field fixtures cite #envelope, but §2 never named the code they assert — the binding came only from the §8 registry. State it inline, as the specVersion sentence two lines down already does, and say explicitly that the rule covers optional fields, where ignoring a typo substitutes the default. Refs #6 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
This was referenced Aug 9, 2026
7 tasks
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
additionalProperties: falsenow appears on every record-shaped$defsin all three families, so the published bundles enforce the §2 rule that unknown properties are rejected at every level rather than only at the root of the envelope. 27 defs were closed — 21 incomponent, 6 inblueprint;listingneeded no change because all four of its defs were already strict. A new lint rule stops the gap reopening, andstructural-005-nested-unknown-fieldfixtures pin the behaviour in each family.Map-shaped schemas are deliberately left open:
contract.inputs/outputs,workload.endpoints/volumes,build.arguments, and blueprint'scomponents/parameters/connectionsuseadditionalPropertiesas a map value schema and their keys are chosen by the document author.Why
Closes #6.
specifications/component/v1/spec.md§2 has always required this:The bundles did not implement it — the constraint was present on only 5 of 35
$defs. The split is an artifact of the bootstrap import (ADR 0001 → "Debt accepted at bootstrap"), not a decision: platformSeed*models set Pydanticextra="forbid", while reused*Requestwire models inherit the defaultextra="ignore".listingwas fully strict only because all four of its defs happen to beSeed*models.The practical effect was silent substitution of defaults. This validated clean before this PR:
The probe then ran with the default 10s delay, so a slow-booting service was marked unready and restart-looped with no diagnostic pointing at the document. The higher-stakes instances were in
ComponentInputRequest, where a misspelledisRequiredsilently made a required input optional and a misspelledsuppliedBysilently changed where a value came from.Fixed spec-side rather than by re-importing from
musher-dev/platform:schemas/src/is authored here,check:driftonly compares src→dist, and ADR 0001 follow-up #2 is to invert the generator so the platform verifies against the published bundle. A follow-up on the platform to give request-body models a strict base is still worth filing.Compatibility
Flagging this honestly rather than filing it under "Correction", since documents that validated yesterday will fail today.
Two things that should inform the review:
The normative contract does not change. §2 already required rejection at every level, and
spec.mdis normative while schemadescriptions are explanatory. What changes is that the bundle finally implements the prose. Any document this newly rejects was already non-conforming; it was being accepted by a schema that failed to encode its own specification.v1is pre-stable and nothing has shipped. No tags, no releases,release-pleasemanifest all0.0.0, and all threespec.mdfiles declareStatus: Draft (pre-stable). The §3 guarantee is anchored tov1.0.0— "A document that validated againstv1.0.0MUST validate against every laterv1.x.y" — andv1.0.0does not exist yet, so there is no consumer the guarantee currently protects. The only published surface is the movingv1alias, which README andsite.tsboth describe as not a stable target.Worth noting for sequencing: the compatibility rules in
GOVERNANCE.mdhave no written pre-stable exemption, even though ADR 0001 andspec.md§5/§10 repeatedly schedule breaking work "before v1 is declared stable". Issues #8 and #9 are also tightenings and will raise the same question. If it should be written down rather than argued per-PR, that is the ADR to open.This should land before release PRs #1–#3. Merging any of them tags
1.0.0and makes that family's validation surface immutable.Checklist
task checkpasses locallyschemas/dist/regenerated withtask bundleand committed (never edited by hand)clausespec.md— schemadescriptions are explanatory, not normativegit commit -s)Verification
task ci:test: 3 bundles match their sources, 7 examples validate, 18 conformance cases pass. Format, types, shellcheck and actionlint clean. The catalog staleness gate is clean.Fail-before/pass-after, with the schema changes reverted and the new fixtures left in place:
component/structural-005expected to fail but validated cleanlyblueprint/structural-005declared diagnostic ERR_UNKNOWN_FIELD at /spec/parameters/siteTitle/ui was not producedlisting/structural-005listingwas already strictThe
listingfixture is a regression pin, not proof of a fix, and its commit message says so. It is included for parity across families.The new lint rule was confirmed to bite by deleting one closure: it failed with
/$defs/ComponentProbeRequest declares properties but not "additionalProperties": false. Each of the five commits was checked out individually and passestask ci:teston its own.The
componentfixture is the issue's own reproduction, so its green run is the closing evidence for #6.