fix(fields): LocationField reads and writes the spec's { lat, lng } - #6667
Conversation
`LocationField` was the one `type: 'location'` surface reading
`value.latitude` / `value.longitude`, each behind `|| 0`, and emitting
`{ latitude, longitude } | null`. Measured against the pinned
`@objectstack/spec@17.2.0`, `valueSchemaFor({ type: 'location' })` REJECTS
that shape (`invalid_type` at `[lat]`, `[lng]`) and ACCEPTS `{ lat, lng }`,
so a spec-canonical record rendered `0, 0` in the edit box — a valid
coordinate in the Gulf of Guinea — while the same record rendered correctly
in detail views and on the map, which read `lat`/`lng` first.
The widget now reads and writes `LocationValue`, re-exported from
`@objectstack/spec/data` rather than re-declared, and reads a pair only when
both `lat` and `lng` are finite numbers. The `|| 0` defaults are gone with
the rename, so a half-stored pair no longer invents its missing coordinate.
BREAKING (stored data): a record in the deprecated `{ latitude, longitude }`
spelling now renders EMPTY in the edit surface until it is re-saved or fixed
at the data layer. It keeps rendering in detail views and on the map. No
compatibility fallback exists anywhere in this change — the maintainer ruled
the bare flip explicitly over a dated read-side shim.
`GeolocationField` is untouched: `geolocation` is not a member of the spec's
closed `FieldType` union and its value schema accepts both spellings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
✅ 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
|
✅ ACCEPT — flipped ready, auto-merge onReviewer: Gate reading: all 29 check runs read, returned count compared against The one thing that had to be checked on the card, not in the commitThis PR ships a breaking change to stored data: a record in the retired The ruling is there and it is unambiguous. Maintainer, 2026-08-28, live director session (summon #2), batch #2 item 3, verbatim 「6272 A1 其他同意」 — A1 adopted explicitly over this seat's recommended A2, with no compatibility fallback of any kind. The ruling comment records that the empty-render consequence "was presented in terms and accepted — it is part of the ruling, not an oversight for a later card to 'discover'." ⇒ ⛔ The absence of a shim here is the ruling being obeyed. A future reader who "fixes" it by adding a read-side fallback would be overturning a maintainer decision, not repairing an omission. The dev pinned that: File surface — matches what the ruling authorized, item for itemThe ruling authorized the out-of-fence surface as a condition of A1. The diff is exactly it: The changeset is scored ⭐ My own split premise was falsified here, and the dev reported it instead of absorbing itI fenced The dev took one line — the ⭐ The reusable form: before splitting a docs page off as "not load-bearing", ask which gate compiles or parses it. A compiled snippet is code with prose around it. The ablation is the discriminating kindAgainst the pre-fix widget: 11 failed / 14 passed — 9 of the new suite plus 2 of the moved Emissions are validated through Honesty items I am ratifying
Follow-up Generated by Claude Code |
Fixes #6272
Implements the maintainer's 2026-08-28 ruling on that card — option A1, the bare flip, chosen explicitly over the recommended A2 read-side shim.
LocationFieldnow reads and writes@objectstack/spec'sLocationValue({ lat, lng, altitude?, accuracy? }), re-exported from@objectstack/spec/datarather than re-declared.Why the widget was the side that moved
Measured on this branch point (
origin/mainat813bf832), against the pinned@objectstack/spec@17.2.0through the contract itself —valueSchemaFor({ type: 'location' }):{ latitude, longitude }invalid_typeat[lat],[lng]{ lat, lng }{ lat, lng, altitude }So this widget was the one
locationsurface producing a shape the platform's own validator refuses.LocationCellRenderer(packages/fields/src/index.tsx) andObjectMapreadinglat/lngfirst is correct by contract, not tolerance, and neither is touched.Symptom table, re-confirmed rather than inherited
The card's table was measured at
ef2a3bd8dand re-measured by an earlier seat at14ef9f5. Re-measured here at813bf832with a throwaway probe (deleted before the commit; working tree verified clean afterwards):{ latitude: 30.2741, longitude: 120.1551 }30.2741, 120.1551{ lat: 30.2741, lng: 120.1551 }0, 030.2741, 120.1551{ lat: 30.2741, lon: 120.1551 }0, 0"30.2741,120.1551"[30.2741, 120.1551]0, 0null{ latitude: 30.2741 }30.2741, 0{ lat: 30.2741 }0, 0{ lat, lng, altitude: 5 }0, 030.2741, 120.1551Produce side re-confirmed single-shaped before the change — measured emission
[{"latitude":30.2741,"longitude":120.1551}]— so this is a rename at both ends of one widget, not a tolerance question. One drift from the earlier table, reporting granularity only: the "empty" rows render theEmptyValueplaceholder glyph, so theirtextContentreads as an en dash rather than the empty string. Same behaviour, measured throughtextContenthere.The pin is unchanged at
17.2.0; no bump was attempted.BREAKING, and it is the ruling rather than an oversight
A record stored in the deprecated
{ latitude, longitude }spelling — including one this widget itself wrote before this change — now renders EMPTY in the edit surface, where it used to render its coordinates. It keeps rendering correctly in detail views, list cells and on the map. Re-saving through this widget, or fixing the value at the data layer, restores it. That cost was presented in terms and accepted; no compatibility fallback of any kind was added.The
|| 0defaults are gone with the rename, so a half-stored pair no longer invents its missing coordinate ({ lat }alone used to read0, 0;{ latitude }alone read30.2741, 0). A stored{ lat: 0, lng: 0 }still renders0, 0, because that is now the only way those digits can appear.No-fallback audit of the whole diff
Every remaining
latitude/longitudeoccurrence in the changed files was enumerated and classified:packages/fields/src/widgets/LocationField.tsx— 8 occurrences, all in the doc comments explaining the retirement plus one human-facing placeholder string ('latitude, longitude', the words a user reads for the coordinate order, deliberately unchanged — the key spelling changed, the coordinate names did not). Zero in any read or write path.packages/plugin-detail/src/__tests__/InlineFieldInput.composite.test.tsx— remaining occurrences are all inside thegeolocationdescribe block, which is deliberately a different case.??,||alias chain, or second key lookup exists anywhere in the diff.The three plugin-detail assertions
packages/plugin-detail/src/__tests__/InlineFieldInput.composite.test.tsxpinned the OLD shape on both the read and the produce side and could not survive the flip. All three moved in this same commit, confined to thelocationdescribe block (lines 165-210 of the pre-change file):{ latitude: 37.7749, longitude: -122.4194 }became{ lat: 37.7749, lng: -122.4194 }— the rendered pair37.7749, -122.4194is unchanged, so the assertions still say what they said;expect(next).toEqual({ latitude: 40.7128, longitude: -74.006 })becametoEqual({ lat: 40.7128, lng: -74.006 });locationandgeolocationsit in that one file in two different spellings.The
geolocationblock below it is untouched, and the ablation below shows two of those three assertions failing against the pre-fix widget — they really were load-bearing in both directions.One increment beyond the dispatched fence, declared
content/docs/fields/location.mdxwas fenced OUT of this card and split to #6660. That split rested on a premise this run measured false: the page'stsxsnippet is compiled bycheck:doc-snippets, and once the widget's props carry the spec value type, the snippet'suseStatetype parameter stops being assignable. Measured, after building what the gate needs (it first reportedPRECONDITION NOT MET, which is not a verdict):So that page is load-bearing for the flip after all, exactly as the three plugin-detail assertions are. One line changed — the
useStatetype parameter on line 101 — which is the whole of what the gate forces. Everything else #6660 lists (:53,:116,:134-140, the prose and the plaintext/jsonc blocks) is untouched and #6660 remains open and unclaimed for it.Deliberately NOT in this change
GeolocationField—geolocationis not a member of the spec's closedFieldTypeunion andvalueSchemaForaccepts both spellings for it, so it sits outside this contract.packages/fields/src/__tests__/AddressCellRenderer.test.tsxandDetailSection.addressDisplay.test.tsx— already{ lat, lng }and spec-conformant.packages/app-shell/src/utils/paramValueShape.ts— held on the serial constraint.altitude/accuracythrough an edit. The widget drops them, as it always has; that is filed separately as finding(fields):LocationFielddrops the spec's optionalaltitude/accuracywhen a user edits the coordinate pair #6664 rather than folded in here.Evidence
Every heavy command ran in the foreground through the shared verify lock; each verdict below is the gate's own line, with exit codes captured before any pipe.
Tests (union re-run at final commit
614b208a):pnpm exec vitest run --maxWorkers=2 packages/fields/ packages/plugin-detail/—Test Files 225 passed (225),Tests 2898 passed (2898), lockVERDICT command-exit 0.pnpm exec vitest run --maxWorkers=2 examples/schema-catalog/— the two edited demo schemas are rendered by that suite through the realSchemaRendererand form renderer:Test Files 14 passed (14),Tests 1835 passed (1835), lockVERDICT command-exit 0.packages/fields/src/__tests__/LocationField.specShape.test.tsx— 12 tests. It asserts the emission against the spec schema itself (valueSchemaFor(...).safeParsesucceeds), not only against a hand-written expected object, and pins the ruled empty-render so a reintroduced fallback fails it.Ablation — the pre-fix widget put back under the new tests. Restored the pre-change blob with
git checkout 813bf832 -- packages/fields/src/widgets/LocationField.tsx; mutation proven on disk before the run (git hash-objectequal to the pre-fix blob9d9db58, anchored grep: injectedvalue.latitude= 1, removedonChange({ lat, lng })= 0). ResultVITEST_EXIT=1,Tests 11 failed | 14 passed (25)— 9 of the new suite plus 2 of the moved plugin-detail assertions. Restoration proven by observation, not by an exit code:git diff HEADempty,git statusclean, hash back toa1c38fb, and both anchored greps back to their fixed-tree values. The script carried an absolute-pathtrap ... EXIT INT TERM. No rebuild leg was needed: the test imports the widget by relative path and the root vitest config aliases every workspace package tosrc, so nothing in this ablation resolved throughdist.Type-check —
pnpm --filter @object-ui/fields --filter @object-ui/plugin-detail run type-check: both echoedtsc --noEmit && tsc -p tsconfig.test.jsonand printedDone; lockVERDICT command-exit 0. Both packages include their tests intype-check, so the new suite's compile-time pin (the exportedLocationValueis type-identical to the spec's) is enforced there.Gates, each quoting its own verdict line:
check:spec-symbolsspec symbol derivation: 1315 files scanned against 4959 spec export namescheck:doc-snippetsSemantic phase: 267 of 267 block(s) judged, 0 failed, controls (resolution / sentinel / positive / undeclared) all behavedcheck:doc-fencescheck:doc-typesEvery documented component type is registered.check:control-bytesscanned 5500 tracked text file(s), so both new files were in the populationcheck:phantom-depsEvery in-scope import is declared by the package that publishes it.check:self-import,check:esm-specifiers,check:vi-mock-specifiers,check:designer-field-key-parityNo changeset declares a major bump.eslint . --no-inline-config --format jsonjudged 3877 files; all four changed TypeScript files are in that population with 0 errors (11 warnings, all pre-existing-styleno-explicit-anyin test files)Not measured, and reported as such rather than as a pass:
check:eager-closureexits 2 withNo eager-closure report ... This is a broken gauge, not a passing budget— it needs anapps/consolevite build this run did not do.check:readme-exportsexits 1 on46 self-import(s) could not be judgedbecause several packages outside this change's build closure have nodiston disk; of the 332 self-imports it did judge,0 wrong-path, 0 fabricated. Neither can be moved by this diff in the failing direction: the new export is type-only (erased, so it cannot enter a runtime chunk) and it is an addition, andcheck:readme-exportsonly flags names a README imports that its package does not export. CI runs both with everything built.The changeset is marked
minor, notmajor, per AGENTS.md 版本号策略 — this repo never publishesmajoroutside an@objectstackmajor sync, andscripts/check-changeset-no-major.mjsenforces it. The break is real and is stated in FROM/TO terms in the changeset body.Generated by Claude Code
Generated by Claude Code