Skip to content

fix(fields): refuse location coordinates the spec rejects - #6717

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-6714-location-range-refusal
Aug 29, 2026
Merged

fix(fields): refuse location coordinates the spec rejects#6717
os-sales merged 1 commit into
mainfrom
claude/issue-6714-location-range-refusal

Conversation

@claude

@claude claude Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Fixes #6714

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} — a value valueSchemaFor({ type: 'location' }) refuses with too_big at 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, a type: 'location' field, a fake DataSource) and typing the card's 999, 999:

create payload            {"title":"HQ","place":{"lat":999,"lng":999}}
spec verdict on it        REJECT  too_big@[lat], too_big@[lng]
aria-invalid on control   "false"
visible field error text  "Place"   (the label — no error rendered)
create call count         1

Neither. Nothing rejects it and nothing repairs it: sanitizeFormData filters KEYS (server-managed, computed, read-only) and never inspects a value; buildValidationRules has no location branch, so its min/max rules only ever carry an author-declared bound on a scalar; and valueSchemaFor has no runtime call site anywhere in the repo — it appears only in tests and comments. The payload goes straight to dataSource.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..90 in the widget would be a second contract free to drift from the spec — the shape #0.1 bans — so the emission is put to LocationValueSchema itself. @objectstack/spec is already a runtime dependency of @object-ui/fields (file-value.ts imports isFileIdToken from the same subpath), and the schema is a memoized lazy schema, so this costs one safeParse of a 2-4 key object.

Two consequences of asking the schema rather than testing two bounds by hand, both deliberate:

Per the dispatch's pointer, the new predicate sits beside isFiniteNumber rather than in a parallel validator, and isFiniteNumber keeps its job unchanged.

Reading is deliberately unchanged. isLocationValue is 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, and git hash-object equal to the merge-base blob aac3b3cf and different from the HEAD blob 28f88fa2; a trap ... EXIT INT TERM with absolute paths held the restore leg. The tests resolve packages/fields/src through the root vitest alias table (not dist), so no rebuild is involved and the source mutation is what ran.

RED  (merge-base widget)   Test Files  2 failed | 2 passed (4)
                           Tests      11 failed | 31 passed (42)
GREEN (this branch)        Test Files  4 passed (4)
                           Tests      42 passed (42)

The 11 failures are exactly the two new suites; LocationField.optionalKeys and LocationField.specShape stayed green under the ablation, so the pin is targeted rather than a blanket breakage. Restoration proven afterwards: git diff HEAD empty, on-disk hash back to 28f88fa2, 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, 180 and -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.

check verdict line
the four suites above Test Files 4 passed (4) / Tests 42 passed (42)
type-check (fields + plugin-form) both echoed tsc --noEmit && tsc -p tsconfig.test.json, Done
check:changeset-presence 3 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)
check:control-bytes OK (scanned 5573 tracked text file(s); skipped 85 binary)
check:spec-symbols exit 0
check:phantom-deps / check:self-import / check:esm-specifiers / check:vi-mock-specifiers exit 0
type-check:coverage 45/46 via type-check + 41/41 packages compile their tests
lint:coverage 46/46 packages linted, 0 with outstanding errors

Both new test files were confirmed present in their package's tsconfig.test.json program via tsc --listFiles (1 hit each) — the type-check pass really does cover them.

Lint was narrowed, and here is the evidence it measured what it claims. eslint --no-inline-config on 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 sibling LocationField.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 in eslint.config.js (no projectService, no parserOptions.project), so this diff cannot move any untouched file's verdict. Repo-wide pnpm lint is CI's run.

Two gates read NOT MEASURED locally, and neither is a verdict on this diff. check:spec-floors and check:readme-exports both fail on an unbuilt workspace and say so in their own output (produced no build output to judge / the population COLLAPSED, both naming pnpm build as the remedy); their 12 and 7 findings name only packages and READMEs this diff never touches. check:spec-floors is 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.0 does carry LocationValueSchema (verified by unpacking 17.0.0 — 1 reference in dist/data/index.d.ts, 3 in dist/data/index.mjs), so @object-ui/fields' declared ^17.0.0 floor 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-form gains only a test file and rides the fixed group.

Out-of-scope findings, filed not fixed

The parseFloat leniency 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:

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

`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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 49 chunks) 3232.8 KB 3266.6 KB
Main entry chunk (gzip) 157.3 KB 350 KB
Entry file index-V6b0OF9m.js
Status PASS

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 11.89KB 4.50KB
app-shell (runtime-config.js) 20.61KB 7.35KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 510.58KB 116.01KB
core (index.js) 5.30KB 2.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 173.10KB 47.96KB
fields (index.js) 239.31KB 60.18KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.95KB 10.97KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.55KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.53KB 3.38KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.64KB 1.50KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 1.93KB 0.88KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.89KB 12.91KB
plugin-charts (index.js) 64.66KB 18.32KB
plugin-chatbot (index.js) 190.33KB 45.10KB
plugin-dashboard (index.js) 133.44KB 34.48KB
plugin-designer (index.js) 212.87KB 43.19KB
plugin-detail (index.js) 245.29KB 62.39KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 132.01KB 32.23KB
plugin-gantt (index.js) 165.20KB 40.37KB
plugin-grid (index.js) 201.51KB 54.54KB
plugin-kanban (index.js) 53.11KB 14.62KB
plugin-list (index.js) 113.01KB 27.57KB
plugin-map (index.js) 20.17KB 6.66KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.51KB 11.94KB
plugin-timeline (index.js) 26.44KB 7.59KB
plugin-tree (index.js) 9.00KB 3.08KB
plugin-view (index.js) 85.87KB 21.12KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 67.73KB 22.54KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 2.44KB 1.21KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.72KB 2.24KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-sales
os-sales marked this pull request as ready for review August 29, 2026 01:35
@os-sales
os-sales added this pull request to the merge queue Aug 29, 2026
Merged via the queue into main with commit f46bd39 Aug 29, 2026
30 checks passed
@os-sales
os-sales deleted the claude/issue-6714-location-range-refusal branch August 29, 2026 01:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(fields): LocationField emits out-of-range coordinates the spec rejects

2 participants