fix(fields): parse LocationField coordinates as strict whole-string numbers - #6766
Merged
Merged
Conversation
…umbers
A bare `parseFloat` stops at the first character it cannot read and returns
what it got, so each half of the typed pair was accepted as if it were whole:
`"12abc, 34"` emitted `{ lat: 12, lng: 34 }` — a coordinate nobody typed.
Unlike objectui#6714, the platform validator cannot be the oracle: every one
of those truncations is a pair `valueSchemaFor({ type: 'location' })` accepts,
so nothing downstream could ever object. Measured on b76ca67 through a real
ObjectForm, `dataSource.create` was handed `{"lat":12,"lng":34}` with
`aria-invalid="false"` and no diagnostic. The class is wider than the card's
three: `0x10` truncates to `0`, and `"12.5 N, 34 E"` drops the hemisphere.
Each half is now tested against `parseFloat`'s own grammar, ANCHORED — not a
stricter notion of a number invented in the widget — so every form that is a
number today still is: negatives, a leading `+`, surrounding whitespace,
exponent forms, and a bare decimal point on either side. The refusal is
announced through objectui#6716's `refusalError` machinery and names the half
it could not read; a third silent refusal would have re-opened the defect
#6716 had just closed.
Two boundaries drawn deliberately: text with no number at the front keeps the
pre-existing format sentence, and `Infinity` carries no residue so it is still
refused by #6714's range arm. Degree/hemisphere notation is not parsed, per
the maintainer ruling of 2026-08-29.
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
|
os-sales
marked this pull request as ready for review
August 29, 2026 08:53
This was referenced Aug 29, 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.
Fixes #6715
The defect
LocationFieldread each half of the typed pair with a bareparseFloat, which stops at the first character it cannot read and returns what it got. So"12abc, 34"emitted{ lat: 12, lng: 34 }— a coordinate nobody typed.The platform validator cannot be the oracle here, and that is what separates this card from objectui#6714. Every one of those truncations is a pair
valueSchemaFor({ type: 'location' })ACCEPTS: well-formed, in range, and wrong. #6714's999, 999was at least a value the contract refuses, so something downstream could in principle have objected. A truncation is a value the contract blesses.Reproduced first, on the base commit
b76ca6764Driving a real
ObjectForm(create mode, atype: 'location'field, a fakeDataSource), typing and submitting:The last rows show the size of the class, and neither was on the card:
0x10truncates to0— objectui#6272's|| 0in the Gulf of Guinea, arriving through a different door."12.5 N, 34 E"— the PASTE shape triage guessed the real user route to be — drops the hemisphere. A12.5 Spaste would have been stored as+12.5, on the wrong side of the equator, with nothing said.The ruling this implements
⛔ Degree/hemisphere notation parsing is deliberately NOT ruled and is NOT built here.
12°N,12.5 N,12° 30' Nall stay refused. The last describe block of the widget pin is the guard that it was not smuggled in, and the refusal message deliberately does not advertise a notation this widget will not read.What changed
One file:
packages/fields/src/widgets/LocationField.tsx.The numeric test is
parseFloat's own grammar, ANCHORED — not a stricter notion of a number invented in the widget:parseFloatreads the longest PREFIX matching this grammar and discards the rest; the anchors turn "there is a number at the front" into "the whole text IS that number".parseFloatstill supplies the value, so nothing about the reading itself moved.⛔
Number()is not the test, although it looks like the same idea: it reads'0x10'as16,'0b11'as3and the empty string as0. A hex literal is not a coordinate notation, and none of those readings is what was typed.ParsedDraftgains a fourth outcome,residue, andhandleChangea third arm that announces through the samesetRefusalErrormachinery objectui#6716 landed rather than a new one — that sequencing is why this card was held. A third silent refusal would have re-opened the defect #6716 had just removed, on a third input class. The message names the half it could not read:Nothing in the refusal/draft machinery was restructured, renamed or simplified.
The boundary, stated explicitly
This is the whole card, so it is stated here rather than left to the regex. Each half of the pair is judged in this order:
abc,NaN,here,--1,(12)12abc,1.2.3,12deg,0x10,0b11,0o17,1_000,1e,1.2e,12 34,1-2,12°,12.5 N12,12.5,.5,30.,-12,+12,1e3,1E3,1.5e-3,1.e3,-.5Infinity,+Infinity,-InfinityTwo of those rows are deliberate decisions rather than fallout:
Infinitycarries no residue —parseFloatreads the whole word — so the new gate has nothing to say about it and it goes on to objectui#6714's range arm, which is where that card put it and what its docblock in this same file still describes. A strictness that swallowed it here would have silently invalidated a minutes-old explanation while keeping every "no emission" pin green. There is a pin for exactly this.Whitespace is unaffected: the halves were already trimmed, so leading and trailing space around each half and around the pair still work.
Tests
Two new files, 59 new tests.
packages/fields/src/__tests__/LocationField.strictNumeric.test.tsx— 45 tests: the card's three inputs, the wider truncation class, the accepted-forms table above, the objectui#6716 arms pinned as undisturbed, and the ruling's degree/hemisphere fence.packages/plugin-form/src/ObjectForm.locationResidue.test.tsx— 14 tests: the same refusals measured where the defect is actually observable, at what a realObjectFormhands todataSource.create. Each case asserts the PREMISE from the spec first — that the old reading produced a pair the platform accepts — so the file states why no downstream check could have caught it.Ablation, against the committed implementation
ec938a22bThe strictness was reverted by weakening the grammar to
/^[\s\S]*$/(the const stays referenced, so the mutation reachesdisttoo), with the restore trapped onEXIT INT TERMat an absolute path and pinned toHEAD.The mutation was confirmed on disk by grepping for the injected text and for the removed grammar — never by the editor's exit code — and again in
distafter each leg's rebuild. The restore is proven by observed state:git diff HEADis zero bytes and the file's blob hash equals that path's HEAD blob hash, both non-empty.It discriminates. Mutated:
Test Files 2 failed | 4 passed (6),Tests 27 failed | 77 passed (104). Every one of the 27 is a residue pin. Zero clean-pair pins moved, and neither objectui#6714's nor objectui#6716's files moved at all — the "still accepts every WHOLE-STRING number" table, the range file and the refusal-diagnostic file stayed green throughout. The end-to-end failure reads exactly like the defect:Restored:
Test Files 6 passed (6),Tests 104 passed (104).Union, on the final commit
ec938a22bType-check, both packages, chained with
&&so the verdict covers both: exit 0. Both packages spell ittype-check, and both echoedtsc --noEmit && tsc -p tsconfig.test.json, so neither silently matched zero scripts. The new test files were confirmed to be real program inputs with--listFiles(1 hit each) — a packagetsconfig.jsonhere excludes**/*.test.tsxfrom the build program, so "type-check is clean" would otherwise have said nothing about them.Gates, each captured by redirect-then-capture, never after a pipe — all exit 0:
check:spec-symbols·check:spec-floors -- --cross-check·check:control-bytes·check:i18n-keys·check:i18n-dead-keys·check:readme-exports·check:self-import·check:phantom-deps·check:side-effects-array·check:vi-mock-specifiers·check:doc-fences·check:doc-snippets·check:doc-types·check:esm-specifiers·check:sdui-registration-pins·check:published-dist·check:eager-closure·lint:coverage·type-check:coverage·check-changeset-no-majorQuoting the two that matter most, in their own words:
check:spec-floorsandcheck:readme-exportsfirst ran on a partly-built tree and reported their own unmet precondition (no-artifact ... produced no build output to judge, andthe population COLLAPSED -- this run proves nothing). Those runs are recorded as NOT MEASURED, not as red; the full workspace was then built and both returned a real green, withcheck:readme-exportsreporting0 unbuiltand 37 of 40 packages read.Lint: a declared narrowing
eslint .was run for the two packages this diff touches — 304 files judged, 0 errors (fields197,plugin-form107, counted from--format json) — rather than all 46 packages.Three readings make that a measurement rather than a gap: the population is
lint:coverage's own census,✅ lint coverage: 46/46 packages linted, 0 with outstanding errors (0 total); the file counts come from eslint's own JSON output, not from a guess about which files count; and the config cannot carry the diff outward —eslint.config.jsis untouched by this branch and configures no type-aware linting (noprojectService, noparserOptions.project), so a change inside two packages cannot move any untouched package's verdict. CI runs the full farm regardless.Scope
buildValidationRulesgets nolocationbranch. objectui#6744 owns that question, andpackages/plugin-form/src/ObjectForm.locationRange.test.tsxwas treated as read-only; its no-branch docblock stays true, and there is a pin for it.@object-ui/fieldsexports nothing new;WHOLE_NUMBER_TEXT,COORDINATE_LABELS,ResidueHalfandrefusedResidueMessageare all module-local.content/docs/releases/untouched. A changeset is included.Out of scope, recorded not fixed
CurrencyFieldandPercentFielduse the sameparseFloatreading with no whole-string guard, relying entirely on browsertype="number"sanitization, which happy-dom (this package's test environment) does not implement. Measured, not reproduced as a user-visible defect.Generated by Claude Code