fix(fields): refuse location coordinates the spec rejects - #6717
Merged
Conversation
`LocationField` accepted any pair of FINITE numbers as a coordinate, while
`@objectstack/spec`'s `LocationValueSchema` also constrains their range
(`lat` -90..90, `lng` -180..180). Typing `999, 999` emitted
`{ lat: 999, lng: 999 }`, which `valueSchemaFor({ type: 'location' })`
refuses with `too_big` at both keys - the producer direction of the
contract-first failure class (AGENTS.md #0.1).
Measured before choosing the disposition: nothing downstream rejects or
repairs the value. A real `ObjectForm` submit handed
`place: { lat: 999, lng: 999 }` straight to `dataSource.create`, with
`aria-invalid="false"` and no error text anywhere. So the widget is the only
place a refusal can work, and the fix refuses the emission - extending the
rule this widget already applies to text that is not a coordinate pair from
format to range.
The bounds are not restated in the widget. A hand-copied `-90..90` would be a
second contract free to drift, so the emission is put to `LocationValueSchema`
itself. That also covers the whole emitted object (so `altitude`/`accuracy`
carried across an edit are held to the contract too) and refuses `Infinity`,
which the finiteness gate let through.
Reading is deliberately unchanged: a record already holding an out-of-range
pair still renders, so the person who can correct it can still see it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
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 #6714
LocationFieldaccepted any pair of finite numbers as a coordinate, while@objectstack/spec'sLocationValueSchemaalso constrains their range (lat-90..90,lng-180..180). Typing999, 999emitted{"lat":999,"lng":999}— a valuevalueSchemaFor({ type: 'location' })refuses withtoo_bigat both keys. Producer direction of the contract-first failure class (AGENTS.md #0.1), open to every user who edits a location field, since typing the coordinates is this field's only interaction.The pre-measurement triage required, and what it decided
The ruling made one reading a precondition of choosing the arm: does anything downstream reject or repair the value before storage? It was measured by driving a REAL
ObjectForm(create mode, atype: 'location'field, a fakeDataSource) and typing the card's999, 999:Neither. Nothing rejects it and nothing repairs it:
sanitizeFormDatafilters KEYS (server-managed, computed, read-only) and never inspects a value;buildValidationRuleshas nolocationbranch, so itsmin/maxrules only ever carry an author-declared bound on a scalar; andvalueSchemaForhas no runtime call site anywhere in the repo — it appears only in tests and comments. The payload goes straight todataSource.create.Per the ruling's own branch — 若下游不拒 ⇒ 越界坐标会落库,那么「拒绝发射」是唯一能防住脏数据的那条 — that pushes to refuse the emission, and this PR takes that arm. It extends a rule this widget already applies to text that isn't a coordinate pair from format to range: the typed pair is simply not written and the prior value stands. Same branch, same comment, no new UI and no new mechanism.
The measurement is kept as a pin (
packages/plugin-form/src/ObjectForm.locationRange.test.tsx) rather than discarded, so the fact that made the arm correct is the thing that fails if it ever stops being true.The bounds are not restated in the widget
A hand-copied
-90..90in the widget would be a second contract free to drift from the spec — the shape #0.1 bans — so the emission is put toLocationValueSchemaitself.@objectstack/specis already a runtime dependency of@object-ui/fields(file-value.tsimportsisFileIdTokenfrom the same subpath), and the schema is a memoized lazy schema, so this costs onesafeParseof a 2-4 key object.Two consequences of asking the schema rather than testing two bounds by hand, both deliberate:
altitude/accuracycarried across an edit (finding(fields):LocationFielddrops the spec's optionalaltitude/accuracywhen a user edits the coordinate pair #6664, which landed on this file as PR fix(fields): carry the spec's optional location keys across a coordinate edit #6713) are held to the contract too — a no-op today, and the guard that keeps it one;Infinityis refused as well.parseFloat('Infinity')isInfinityand!isNaN(Infinity)istrue, so the pre-existing format gate let it through;z.number()does not. Same defect class, no extra branch.Per the dispatch's pointer, the new predicate sits beside
isFiniteNumberrather than in a parallel validator, andisFiniteNumberkeeps its job unchanged.Reading is deliberately unchanged.
isLocationValueis the READ guard and stays range-free, so a record that already holds an out-of-range pair still renders in the box — blanking it would hide the dirty data from the only person who can correct it. #6272's empty render was for a value whose SHAPE this widget cannot read; this shape is readable, it is only not writable.Anti-vacuity: red before, green after
Reverse-verified from the committed state, by restoring the merge-base widget (
98188c284) into the tree and re-running. The mutation was proven on disk before anything was measured — guard-marker count 2 to 0, andgit hash-objectequal to the merge-base blobaac3b3cfand different from the HEAD blob28f88fa2; atrap ... EXIT INT TERMwith absolute paths held the restore leg. The tests resolvepackages/fields/srcthrough the root vitest alias table (notdist), so no rebuild is involved and the source mutation is what ran.The 11 failures are exactly the two new suites;
LocationField.optionalKeysandLocationField.specShapestayed green under the ablation, so the pin is targeted rather than a blanket breakage. Restoration proven afterwards:git diff HEADempty, on-disk hash back to28f88fa2, marker count back to 2.Every case is judged by the spec's own refusal, never by a range copied into the test — including the inclusive bounds
90, 180and-90, -180, which are real places an off-by-one would have made untypable.Verification
All on
232813b45(the commit this PR ships), run after the final commit.Test Files 4 passed (4)/Tests 42 passed (42)type-check(fields + plugin-form)tsc --noEmit && tsc -p tsconfig.test.json,Donecheck:changeset-presence3 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)check:control-bytesOK (scanned 5573 tracked text file(s); skipped 85 binary)check:spec-symbolscheck:phantom-deps/check:self-import/check:esm-specifiers/check:vi-mock-specifierstype-check:coverage45/46 via type-check+41/41 packages compile their testslint:coverage46/46 packages linted, 0 with outstanding errorsBoth new test files were confirmed present in their package's
tsconfig.test.jsonprogram viatsc --listFiles(1 hit each) — thetype-checkpass really does cover them.Lint was narrowed, and here is the evidence it measured what it claims.
eslint --no-inline-configon the three changed files:filesLinted=3,errors=0,warnings=8, all@typescript-eslint/no-explicit-any— the same class and convention as the pre-existing siblingLocationField.optionalKeys.test.tsx(0 errors, 4 warnings). The file count is read from--format json, not counted by hand. The narrowing cannot have hidden anything in untouched files because type-aware linting is not enabled ineslint.config.js(noprojectService, noparserOptions.project), so this diff cannot move any untouched file's verdict. Repo-widepnpm lintis CI's run.Two gates read NOT MEASURED locally, and neither is a verdict on this diff.
check:spec-floorsandcheck:readme-exportsboth fail on an unbuilt workspace and say so in their own output (produced no build output to judge/the population COLLAPSED, both namingpnpm buildas the remedy); their 12 and 7 findings name only packages and READMEs this diff never touches.check:spec-floorsis nonetheless the gate most implicated here, since this PR adds a runtime spec import, so its substantive question was answered directly instead:@objectstack/spec@17.0.0does carryLocationValueSchema(verified by unpacking 17.0.0 — 1 reference indist/data/index.d.ts, 3 indist/data/index.mjs), so@object-ui/fields' declared^17.0.0floor is honest and no floor bump is needed. CI builds the workspace and will run both gates for real.Changeset
.changeset/6714-location-range-refusal.md, scored patch on@object-ui/fields. Reasoning: user-visible behaviour changes (an input previously accepted is now refused), so the empty-frontmatter "declares no release" form would be wrong — but nothing is added to the public surface. No new export, no new prop, no new option; the widget narrows what it writes to what the platform already required, and a value it now declines to emit is one storage would have been wrong to hold.@object-ui/plugin-formgains only a test file and rides thefixedgroup.Out-of-scope findings, filed not fixed
The
parseFloatleniency on these same two lines was fenced out by both the card and the ruling and is not touched here. It and one neighbouring gap are recorded as their own cards:LocationFieldaccepts a partly-numeric coordinate, emitting a plausible wrong location #6715 —parseFloat('12abc')is12, so"12abc, 34"emits a plausible, spec-VALID, wrong location. Filed unassigned, unlabelled for triage.LocationFieldrefuses input silently — the validation slot it wires has no producer #6716 — both refusals (format, and now range) are silent: the widget wires the 字段 widget 的错误提示键:spec 声明error,objectui 渲染errorMessage(declared ≠ enforced) #3222 validation slot buterroris host-produced and nothing produces one for this field. Filed unassigned withfinding. Explicitly not a request to reverse this PR's arm.Dedup before filing: repo-scoped REST list of open issues updated since 2026-08-18 (239 issues), grepped for
parseFloat/LocationField/coordinate/latitude/aria-invalid/ silent-refusal wordings. Only #6714 matched, where both appear as excluded halves.Generated by Claude Code