fix(fields): validate a stored location value on an edit form - #6811
Merged
Conversation
`buildValidationRules` is the producer of the host-side `error` prop that every field widget's published objectui#3222 slot reads, and it had no branch for `location`. A coordinate already in the record that violated the spec's range was therefore never validated on an edit form: the control rendered it, nothing marked it invalid, and submitting re-wrote it unchanged. It now compiles a `validate.location` entry that adjudicates a PRESENT value against `valueSchemaFor(field, 'stored')` — the platform's own value-shape contract (ADR-0104 D1), the same schema the engine's record validator checks a stored `location` against. The bounds are not restated in objectui and the message is built from the schema's own issues, so neither can drift from the spec (AGENTS.md #0.1). Absence stays `required`'s business: the spec's schema describes a present value and refuses null/undefined outright, so the rule asks core's `isMissingForRequired` rather than inventing a second definition of "empty". A field-authored `validate` composes under its own key instead of being replaced. Landed on a hard precondition from the maintainer ruling of 2026-08-29: 28 stored location values across every measurable dataset (app-showcase seed, qa dogfood field-zoo matrix, objectui schema-catalog) adjudicated by that same schema — 28 accepted, 0 refused. That zero means "zero within measurable scope"; customer deployments are not measurable here. The two sibling pins that asserted the branch's ABSENCE are rewritten to assert the property that actually survives: a refusal produces no value, so the host rule is handed `undefined` and the input-time announcement stays the widget's (objectui#6714/#6716 unchanged). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
This was referenced Aug 29, 2026
Both said `buildValidationRules` "still has no `location` branch". This branch gives it one, so shipping that prose in the same package would leave a contradiction for the next reader — the failure mode the docblock exists to prevent. Each sentence was a compound claim and only half of it is falsified: - "still has no `location` branch" -> false as of this branch - "this card does not give it one" -> still true (#6716 / the refusal-diagnostic card did not add it; objectui#6744 did) So the true half is kept verbatim and only the false half is corrected, with a pointer to the card that added the branch and a note that it serves the STORED case and never these refusal arms — which is the property the surrounding paragraph is actually about, and which this branch confirms rather than contradicts. Prose only: no assertion, no behaviour, no export, no changeset. Neither file is in PR #6801's file list (verified against its 14 files). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
This was referenced Aug 29, 2026
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
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.
Fixes #6744
The defect
buildValidationRulesis the producer of the host-sideerrorprop that every field widget's published objectui#3222 slot reads, and it had no branch forlocation. So a coordinate that was already in the record and violated the spec's range was never validated on an edit form: the control rendered it, nothing marked it invalid, and submitting re-wrote it unchanged.This is a different defect from #6714 / #6716, which are about a refusal at INPUT time. A refusal means
onChangenever fires, so the typed text never becomes a form value and this rule is handedundefined. The two do not overlap and neither replaces the other.The hard precondition came first
The maintainer ruling of 2026-08-29 (director session batch #4) made landing conditional on counting existing out-of-range coordinates across everything measurable, and forbade shipping a hard block on a non-zero reading.
Reading: 0 refused out of 28 stored location values. Zero within measurable scope.
Taken with the platform's own validator rather than a hand-written range: every stored
locationvalue in every measurable dataset was run throughvalueSchemaFor({ type: 'location' }, 'stored').showcase_account.hqx14,showcase_task.locationx10,showcase_field_zoo.f_locationx1)packages/qa/dogfood/test/field-zoo.matrix.tsexamples/schema-catalog/src/schemas/fields-location/Controls from the same call, so the zero is a reading and not a broken probe:
{ lat: 999, lng: 999 }was refused withtoo_bigat both keys, and{ lat: 37.7749, lng: -122.4194 }was accepted.A second, independent sweep over 12,465 files across both repos for coordinate literals (both the spec
lat/lngspelling and the retiredlatitude/longitudeone) found 44 out-of-range values. Every one of them is a test negative fixture, a source docblock, docs prose, or a changeset. Zero in any dataset.Also counted, because the rule adjudicates the whole value and not only its range: stored location values the spec refuses for any other reason (a retired
{ latitude, longitude }record, a numeric string). That class is also zero within measurable scope. It is the same 28-value adjudication above, which used the full schema.Boundary, stated as the ruling requires. Customer deployments are not measurable from a development container. This zero means "zero within measurable scope". It is never evidence that no such coordinate exists.
The shape
buildValidationRulesnow compiles avalidate.locationentry that adjudicates a PRESENT value againstvalueSchemaFor(field, 'stored').LocationField's existingisSpecAcceptedLocationandrefusedRangeMessagealready follow.locationagainst this same schema under ADR-0104 D1 (packages/objectql/src/validation/record-validator.ts), warn-first until a deployment'sos migrate value-shapesscan certifies zero violations and rejecting afterwards. The form now surfaces that verdict where the person who can correct it is standing, instead of inventing a verdict of its own.required's business. The spec's schema describes a present value and refusesnullandundefinedoutright, so the rule asks core'sisMissingForRequired, the repo's single presence contract and the same predicate the form renderer's ownrequiredvalidator calls. That is what keeps a create form with an untouched location field valid.validatecomposes under its own key rather than being replaced, spelled the same way the form renderer already normalisesrules.validatewhen it addsrequired.WeakMapcaching mirrors the platform's ownshapeSchemaFor, asvalueSchemaFor's contract requires of runtime consumers.Pins, in both directions
New:
packages/plugin-form/src/ObjectForm.locationStoredRange.test.tsx(13 cases), driving a realObjectForm.{ lat: 999, lng: 999 }on an edit form flipsaria-invalidto"true", renders the spec's own complaint, and callsdataSource.updatezero times. Repeated across each bound (91, 0/-91, 0/0, 181/0, -181).aria-invalid="false"; the spec's optionalaltitude/accuracykeys survive; an absent location does not block the edit.LocationFieldemits out-of-range coordinates the spec rejects #6714 is untouched.type: 'location'value spelled{ lat, lng }reads as0, 0inLocationFieldwhile the same value renders correctly throughLocationCellRenderer#6272's read guard is deliberately not wired to the range).The expected message is BUILT from the schema's issues in the test rather than typed out, so a bound that moves in the spec cannot leave a stale literal passing here.
Reverse verification
The branch was deleted from the committed tree and the pins re-run.
@object-ui/fieldsis aliased topackages/fields/srcby the rootvitest.config.mts(line 279), so the ablation acts on source and needs no rebuild for either leg.1ca93832, on-disk afterffc00029, deleted-anchor count 1 to 0, injected-marker count 1.1ca93832, marker count 0,git diff HEADempty.Two sibling pins were rewritten, not deleted
ObjectForm.locationRefusal.test.tsx(#6716) andObjectForm.locationResidue.test.tsx(#6715) each asserted thatbuildValidationRulescompiles NO rule for a location field. That was a scope fence for those cards, and this card is the one that answers it. Both now assert the property that actually survives and that a future edit could still break: the rule exists, and a refusal hands itundefined, so the input-time announcement stays the widget's.ObjectForm.locationRange.test.tsx's docblock was updated for the same reason.Verification
Green union re-run at
aa8372ad4, the final commit, after the docblock correction:pnpm exec vitest run packages/fields/ packages/plugin-form/ packages/components/src/renderers/form/— 253 files, 3142 tests passed.pnpm --filter @object-ui/fields type-checkandpnpm --filter @object-ui/plugin-form type-check— both green. All four edited or added test files are in plugin-form'stsconfig.test.jsonproject (verified by--listFiles, one hit each), so that green actually covers them.check:control-bytesOK (5646 files) -check:spec-symbolsOK -check:phantom-deps"Every in-scope import is declared by the package that publishes it" -check:self-importOK -check:esm-specifiersOK -check:i18n-keysOK -check:side-effects-arrayOK -check:vi-mock-specifiersOK -check:designer-field-key-parityOK -check:action-forward-parityOK - changeset presence and no-major both OK.check:control-bytesre-run ataa8372ad4: "OK (scanned 5646 tracked text file(s); skipped 85 binary)".Not measured here, and left to CI:
check:readme-exports— after building the two changed packages it reports zero findings for either of them, but its repo-wide population needs every package'sdistand the gate prints "the population COLLAPSED, this run proves nothing" without it. This PR adds no export.check:sdui-registration-pinsandcheck:eager-closure— both need a console build. The gate itself calls its no-build path "exit 2, not a pass". Worth a reviewer's eye on the eager-closure number: this PR adds a runtime import of@objectstack/spec/datato the fields barrel. Its exposure is bounded, because that module is already an eager runtime import in@object-ui/core(server-owned-value.ts,unmaterialized-fields.ts,filter-tokens.ts) and in@object-ui/types(zod/form.zod.ts), so no new vendor chunk is introduced. Budget headroom onmainis 45,996 bytes.The two docblocks this branch falsified are corrected here
Both said
buildValidationRules"still has nolocationbranch", which this branch makes false. Leaving that prose in the same package would be a contradiction for the next reader, so it is corrected in the same PR rather than deferred.Each sentence was a compound claim and only half of it is falsified, so only that half was rewritten:
widgets/LocationField.tsx:356buildValidationRulesstill has nolocationbranch, and this card does not give it one."buildValidationRulesHAS alocationbranch as of objectui#6744 - for that STORED case, never for these refusal arms - and this card did not give it one."__tests__/LocationField.refusalDiagnostic.test.tsx:32locationbranch and this card does not give it one."The "this card did not give it one" half is true in both places and is kept verbatim: #6716 and the refusal-diagnostic card did not add the branch, this one did. The surrounding paragraph in each file - that a branch installed there sees
undefinedin both refusal arms while firing correctly for a stored pair - is confirmed by this change, not contradicted, and was left untouched.Prose only: no assertion, no behaviour, no export, no changeset. Neither file is in PR #6801's file list, verified against its 14 files (it touches CurrencyField, GeolocationField, NumberField, PercentField, numberBadInput and two NumberInputWidgets tests).
Scope
Locked to
location, per the ruling. Whether other field types have the same stored-value gap inbuildValidationRuleswas not surveyed and is a separate card.Session:
session_01CRJge11jso9TpXRWFt1Z49Generated by Claude Code