ADR-0028: Schema construction enforces its grammar at the Add site#84
Merged
Merged
Conversation
The Schema grammar's rules — Field non-empty, leaf Selector non-empty, list-container Selector non-empty — lived in the fold (SchemaContentParser), so an invalid Schema constructed cleanly and failed only at parse time. The two arms were asymmetric: a leaf-list with no Selector got swallowed by the per-leaf catch and silently dropped the field; an object-list with no Selector aborted the whole parse. Moved enforcement to the construction site: Schema.Add validates every child at the add call (the exact line the user wrote the bad element). Added Schema.ListOf(field, selector, ...children) static factory that bundles the IsList + Selector + Children triple a user previously had to remember together. The fold's existing checks stay as belt-and- suspenders for the mutation-after-Add path; they are no longer the primary line of defence. The previously-misleading ListSchemaWithoutSelectorThrows test (which asserted silent swallow-and-log, contradicting its own name) actually throws now, with a sibling pinning the same fast-fail for the object-list arm — the two arms are now uniform. Guardrail: 0 errors, 126 tests pass (107 unit + 19 satellite), AOT smoke ALL PASS. Surface-additive on Schema; one narrowly-breaking behaviour edge for code that constructed pathological Schemas. Design, the five rejected alternatives (fluent-builder DSL replacement, init-only properties, sibling-type model split, parameterless-ctor internalisation, documentation-not-enforcement), and the implementation status: docs/adr/0028-schema-construction-guards.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
a35f062 to
90f0373
Compare
pavlovtech
added a commit
that referenced
this pull request
May 23, 2026
…uards ADR-0028: Schema construction enforces its grammar at the Add site
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.
ADR-0028 — Schema invariants move from the fold to the construction site
The Schema is the user-facing DSL — the very first thing every consumer constructs. Its grammar rules (Field non-empty, leaf Selector non-empty, list-container Selector non-empty) lived in the fold (
SchemaContentParser), so an invalid Schema constructed cleanly and failed only at parse time, asymmetrically: a leaf-list with no Selector got swallowed by the per-leafcatch (Exception)and silently dropped the field; an object-list with no Selector aborted the whole parse. The pre-0028 testListSchemaWithoutSelectorThrowsdocumented the symptom — its name said Throws and its body asserted silent unset.Moved enforcement to
Schema.Add(SchemaElement)— validates Field non-empty, leaf Selector non-empty, list-container Selector non-empty (a non-list nested Schema is exempt; the fold never reads its own Selector by design). The throw site is the exact line the user wrote the bad element — best possible error location.Added
Schema.ListOf(string field, string selector, params SchemaElement[] children)static factory that bundles the IsList + Selector + Children triple in one call, with construction-time validation of field+selector. The old shape (new Schema(field) { Selector = ..., IsList = true, Children = { ... } }) keeps working; the factory is the recommended path going forward.The fold's existing
Field is nullguard and threeRequireSelectorcalls stay in place, annotated as belt-and-suspenders for the one remaining path (mutation of a SchemaElement after Add — records here use{ get; set; }, not init-only). They are no longer the primary line of defence.Design, the five rejected alternatives (fluent-builder DSL replacement / init-only properties / sibling-type model split / parameterless-ctor internalisation / documentation-not-enforcement) with load-bearing reasons, and the implementation status:
docs/adr/0028-schema-construction-guards.md.Narrowly breaking
Selector, child with emptyField) now throwsArgumentExceptionat theAddcall instead of failing at parse time (or silently). TheListSchemaWithoutSelectorThrowstest was changed to assert the construction-time throw (ConstructingALeafListWithoutASelectorThrowsAtTheAddSite); a sibling pins the same for the object-list arm.Schema.ListOfis purely new; no public method removed.Guardrail
Whole-solution build 0 errors · 107 unit tests (94 pre-0028 + 1 replaced + 1 new sibling + 12 new
SchemaConstructionTests) · 19 satellite tests (Sqlite/Puppeteer/Mongo/Cosmos/AzureServiceBus) · Native-AOT smoke ALL PASS. Integration tests deferred to CI (livealexpavlov.dev+ real Puppeteer, slow by design).🤖 Generated with Claude Code