You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
finding(fields): LocationField emits out-of-range coordinates the spec rejects #6714
Filed unassigned by the dev seat working #6664, as an out-of-scope finding. Not fixed there — that card is fenced to carrying the two optional keys across a coordinate edit, and this is a different defect on the same write path. Recording, not choosing.
Dedup before filing: repo-scoped REST list of open issues updated since 2026-08-27 (74 issues), grepped for LocationField / coordinate / latitude / lat+lng — only #6664 itself and the seat card matched. #6664's own dedup run the same day scanned open issues for LocationField and found only #6272 and #6660.
What
packages/fields/src/widgets/LocationField.tsx accepts any pair of finite numbers as a coordinate, but the spec constrains their range:
The widget's guard tests only finiteness — isLocationValue checks typeof x === 'number' && Number.isFinite(x), and the write path adds only !isNaN(...). So typing an out-of-range pair emits a value the platform's own validator refuses. Measured against valueSchemaFor({ type: 'location' }):
{"lat":999,"lng":999} widget emits it | spec: REJECT too_big@[lat], too_big@[lng]
{"lat":91,"lng":0} widget emits it | spec: REJECT too_big@[lat]
{"lat":0,"lng":181} widget emits it | spec: REJECT too_big@[lng]
Why it may matter
This is the contract-first failure class in the producer direction: a renderer that writes a value the spec rejects. Unlike #6664 — which needed a customer to author altitude before anything was lost — the path here is open to any user editing any location field, since typing the coordinates is the field's only interaction.
The widget already exposes the published validation slot (error / aria-invalid, objectui#3222), so an in-range check has somewhere to surface without inventing UI.
What is NOT measured
Whether anything downstream rejects or repairs the value before it is stored — the seat that filed this did not trace the save path.
Whether the right disposition is to refuse the emission (as the widget already refuses text that is not a coordinate pair, leaving the value alone) or to emit and mark the field invalid. Those are different user-visible behaviours and the choice is triage's, not this seat's.
Not to be confused with
The pre-existing parseFloat looseness on the same lines — parseFloat('12abc') is 12 — is a separate question and is not what this card reports.
Related
#6664 (the optional-key carry on this same write path, where this was found) · #6272 (the A1 rename that made the widget's declared value type the spec's)
⚠️Read the triage ruling above in full first. It answers the card's open question, explains why this is queue rather than decision box, and — critically — adds a measurement you must take before choosing your approach.
⛔ Take the pre-measurement BEFORE you implement
Quoted verbatim from the ruling:
⚠️派发前请先补一条测量(卡面自己标为未测) 「是否有下游在存储前拒绝或修复该值」没有人追过 save path。 这条决定手臂的取舍力度:
Do not fix the parseFloat leniency here. File it if you think it warrants a card.
Why p1
The ruling's reasoning, which should shape your test choices: entering coordinates is this field's only interaction. Unlike #6664 (which needed a customer to author altitude first), this path is open to every user who edits a location field. A renderer that emits a value the platform's own validator rejects is the producer direction of the contract-first failure class.
Anti-vacuity
The pin must be RED on your merge-base. Report red-before and green-after. Use the spec's actual refusal as the oracle — valueSchemaFor({ type: 'location' }) refuses 999, 999 with too_big at both lat and lng (the card measured this); assert against that, not against a hand-copied range.
⭐ Context you should reuse:#6664 just landed on this same file (carryOptionalKeys, a key-by-key pick, PR #6713). Its isFiniteNumber predicate is the natural place for a range check to live beside — read what's there before adding a parallel validator.
Then: changeset (score it yourself and state your reasoning), push, open a draft PR, and report.
{
"issue": 6714,
"status": "done",
"branch": "claude/issue-6714-location-range-refusal",
"pr": "https://github.com/objectstack-ai/objectui/pull/6717",
"premise_still_valid": true,
"summary": "PRE-MEASUREMENT FIRST (the reading triage required): downstream does NEITHER — it does not reject and does not repair. Drove a real ObjectForm (create mode, type:'location' field, fake DataSource) and typed the card's `999, 999`: dataSource.create was called ONCE with place={\"lat\":999,\"lng\":999} verbatim, aria-invalid on the control read \"false\", and the field rendered only its label — no error anywhere. Supporting statics: sanitizeFormData filters KEYS and never inspects a value; buildValidationRules has no 'location' branch; and valueSchemaFor has NO runtime call site in the whole repo (tests and comments only). So out-of-range coordinates reach storage silently. Per the ruling's own branch that pushed me to REFUSE THE EMISSION — the only arm that prevents the dirty write — implemented by extending the widget's existing refusal for non-coordinate text from format to range (same branch, same comment, no new UI or mechanism). The bounds are NOT hand-copied into the widget: the emission is put to the spec's own LocationValueSchema, which also holds the #6664 altitude/accuracy carry to the contract and refuses Infinity (parseFloat('Infinity') passed the old !isNaN gate). The READ guard isLocationValue deliberately stays range-free so an already-dirty record still renders for the person who can fix it. The measurement itself is kept as a pin rather than discarded.",
"tests": "All on the shipping commit 232813b45 (union re-run AFTER the final commit). GREEN: `pnpm exec vitest run packages/fields/src/__tests__/LocationField.range.test.tsx packages/fields/src/__tests__/LocationField.optionalKeys.test.tsx packages/fields/src/__tests__/LocationField.specShape.test.tsx packages/plugin-form/src/ObjectForm.locationRange.test.tsx` -> `Test Files 4 passed (4)` / `Tests 42 passed (42)`. ABLATION (anti-vacuity, run from the COMMITTED state): restored the merge-base widget (98188c284) into the tree. No rebuild is involved and none was needed — the root vitest alias table maps @object-ui/fields to packages/fields/src, not dist, so the source mutation is what ran (verified in vitest.config.mts). Mutation confirmed ON DISK before any measurement: guard-marker `isSpecAcceptedLocation` count 2 -> 0, and `git hash-object` == merge-base blob aac3b3cf and != HEAD blob 28f88fa2 (empty/missing hash treated as failure, script exits non-zero). A `trap '<restore>' EXIT INT TERM` using absolute paths (REPO_ROOT from git rev-parse --show-toplevel) held the restore leg, restoring via `git checkout HEAD -- <abs path>`. RED under ablation: `Test Files 2 failed | 2 passed (4)` / `Tests 11 failed | 31 passed (42)` — the 11 failures are exactly the two new suites; optionalKeys and specShape stayed GREEN, so the pin is targeted. Restoration proven: `git diff HEAD` empty, on-disk hash back to 28f88fa2, marker count back to 2. Direction predicted before running was plain RED (not diagnostic-inversion) and that is what was observed. GATES (exit codes captured by redirect-then-capture, never through a pipe; verdict lines quoted from the gates themselves): type-check for @object-ui/fields and @object-ui/plugin-form both echoed `tsc --noEmit && tsc -p tsconfig.test.json` and printed `Done` (not a zero-match silent pass), and BOTH new test files were confirmed in their package's tsconfig.test.json program via `tsc --listFiles` (1 hit each). 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, check:phantom-deps, check:self-import, check:esm-specifiers, check:vi-mock-specifiers all 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`. Also self-scanned the diff with `grep -naP` for raw control bytes: no matches. LINT NARROWING, DECLARED: eslint --no-inline-config on the 3 changed files -> filesLinted=3 (count read from --format json, not by hand), errors=0, warnings=8, all no-explicit-any — same class and count-per-file convention as the pre-existing sibling LocationField.optionalKeys.test.tsx (0 errors, 4 warnings). The narrowing cannot hide 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. NOT MEASURED (prerequisite unmet, NOT red on this diff): check:spec-floors and check:readme-exports both require a fully built 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 findings name only packages/READMEs this diff never touches. Because spec-floors is the gate most implicated (this PR adds a runtime spec import), its substantive question was answered directly instead: unpacked @objectstack/spec@17.0.0 and confirmed it DOES carry LocationValueSchema (1 ref 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.",
"mcp_calls": "0 — every GitHub read and write went through repo-scoped REST (probe returned 200); no MCP GitHub tool was invoked",
"open_questions": [],
"out_of_scope_findings": [
"filed as #6715: `parseFloat('12abc')` is `12`, so `\"12abc, 34\"` emits a plausible, spec-VALID, WRONG location — the leniency both the card and the ruling fenced out of this card. Unassigned, unlabelled for triage.",
"filed as #6716: both of the widget's refusals (format, and now range) are silent — it wires the #3222 validation slot but `error` is host-produced and `buildValidationRules` has no `location` branch, so nothing ever produces one. Unassigned, `finding` label. Explicitly not a request to reverse this card's arm."
]
}
ACCEPT — PR #6717, head 232813b45, ready and armed (29/29 green).
The pre-measurement decided the arm, exactly as the ruling required
Triage left the disposition open and demanded one reading first: does anything downstream reject or repair the value before storage? The answer is neither — measured, not reasoned:
driving a real ObjectForm and typing 999, 999 called dataSource.createonce, with place: { lat: 999, lng: 999 } verbatim;
aria-invalid on the control read "false" and no error text rendered anywhere;
sanitizeFormData filters keys and never inspects a value; buildValidationRules has no location branch.
⭐ And the sharpest supporting static: valueSchemaFor has no runtime call site in the entire repository — tests and comments only. The platform ships a value validator that nothing calls. That is what makes the widget the only place a refusal can work, and it is a finding well past this card.
⇒ Refusing the emission is therefore the only arm that prevents the dirty write, and it extends the rule this widget already applies to text that isn't a coordinate pair from format to range — same branch, same comment, no new UI.
⭐ The measurement was kept as a pin (ObjectForm.locationRange.test.tsx) rather than discarded once it had served its purpose, with the pass-through assertion explicitly called load-bearing: if a later change ever does add repair or validation on that path, that assertion is what notices.
⭐⭐ The bounds are not restated — and that is the deeper fix
A hand-copied -90..90 would be a second contract free to drift from the spec — the exact shape AGENTS.md #0.1 bans, and the shape this very card is about. So the emission is put to the spec's own schema. The test uses the same oracle, and says why: a hand-written range there "would keep passing on the day the spec moved — which is the whole failure this card is about, reintroduced in the test."
Two consequences fall out, both deliberate and both named:
⭐ Infinity is now refused.parseFloat('Infinity') is Infinity and !isNaN(Infinity) is true, so the old finiteness gate let it through. Same defect class, no extra branch.
⭐ The read path was deliberately left alone
isLocationValue stays range-free, so a record that already holds an out-of-range pair still renders — "blanking it would hide the dirty data from its only fixer." And the distinction from #6272 is drawn precisely: that card's empty render was for a value whose shape the widget cannot read; this shape is readable, it is merely not writable. Easy to get wrong by reflex; got right here.
The tests also assert the inclusive bounds in the passing direction (90, 180, -90, -180), on the grounds that "the poles and the antimeridian are real places, and an off-by-one in the guard would silently make them untypable."
Verification
Ablation redded exactly the two new suites (11 of 42) while optionalKeys and specShape stayed green — targeted, not blanket. Mutation proven on disk by anchored marker counts plus git hash-object against both the merge-base and HEAD blobs; restore proven by observation under a trap.
⭐ check:spec-floors was marked NOT MEASURED — and then its substantive question was answered anyway. Because it is the gate most implicated (this PR adds a runtime spec import), the dev unpacked @objectstack/spec@17.0.0 and confirmed it carries LocationValueSchema, so @object-ui/fields' declared ^17.0.0 floor is honest and no bump is owed. Marking a gate unmeasured and then answering what it would have asked, rather than shrugging.
⚠️ The honest cost of this arm, and two follow-ups
#6716 — both refusals, format and now range, are silent. The widget wires #3222's validation slot, but error is host-produced and nothing produces one. So a user typing 999, 999 now gets no write and no feedback. That is strictly better than a silent dirty write, and the dev was right not to reverse the arm over it — but this card trades a silent bad write for a silent no-op, and #6716 is where that gets closed.
#6715 — parseFloat('12abc') is 12, so "12abc, 34" emits a plausible, spec-valid, wrong location. Fenced out of this card by both the card and the ruling, correctly.
Scoring:patch on @object-ui/fields. Correct — behaviour-correcting, no capability added, no published type moved.
Record only — this card stays closed and its ruling stands unchanged. Posted because one of its stated premises stops holding, and the next person to read this thread should not have to re-derive that from scratch.
What moved
This card weighed "refuse the emission" against "emit and mark invalid" and chose refusal, on the premise quoted from its own measurement:
nothing downstream rejects or repairs the value, so an emission is a write
That premise was true when it was measured, and ObjectForm.locationRange.test.tsx is the pin that recorded it. As of #6744 it is no longer true. buildValidationRules now compiles a location branch that adjudicates a stored value against valueSchemaFor(field, 'stored'), so something downstream now does reject: an out-of-range value marks the control invalid and dataSource.update is called zero times.
What did NOT move
The ruling here is unchanged, and #6744 does not reverse it. The format arm settles that independently: for text like not a coordinate there is no location value to emit at all, so "emit and mark invalid" would mean writing a raw string into a typed location value — a worse contract violation than the one being fixed.
The two changes also do not overlap, which is the part most likely to be mis-derived later:
This card is about a refusal at input time. A refusal means onChange never fires, so the typed text never becomes a form value.
That is measured, not argued: the sibling pins in ObjectForm.locationRefusal.test.tsx and ObjectForm.locationResidue.test.tsx used to assert that buildValidationRules compiled no location rule at all. #6744 rewrote them to assert the property that actually survives — the rule exists, and it answers true for the undefined a refusal hands it — so the input-time announcement provably stays the widget's.
Where the prose now disagrees with the code
Two docblocks still say buildValidationRules "still has no location branch":
Both are prose; no assertion depends on either. They were left alone deliberately because PR #6801 is in flight over those two directories. They are flagged on #6744 for a one-line follow-up.
Activity
huangyiirene commentedon Aug 28, 2026
分诊:
pm:queue·domain:ui·priority:p1· typeBug卡面把处置选择明确交给分诊(「refuse the emission 还是 emit + mark invalid,the choice is triage's」),这里回答,并说清为什么它不进决策箱。
为什么是队列而不是人工地板
逐条对本席位的人工地板自查:新功能?否 —— spec 早已声明该范围,本卡只是让 widget 兑现它。协议/已发布契约变更?否 ——
LocationValueSchema不动,改的是生产者。破坏性/难回滚?否 —— 单个 widget 内的局部行为。门禁削弱?相反,是收紧。⇒ 不命中任何一条,可直接入队。两条手臂都在范围内,由 dev 择一,理由是本仓已有两条现成先例
error/aria-invalid,字段 widget 的错误提示键:spec 声明error,objectui 渲染errorMessage(declared ≠ enforced) #3222),卡面自己指出「有地方可以 surface,不必发明 UI」。⇒ 两条都不需要新机制、不需要新 UI,属实现者裁量。⛔ 本席不替 dev 选。
「是否有下游在存储前拒绝或修复该值」没有人追过 save path。 这条决定手臂的取舍力度:
⇒ 请在动手前先量这一条,再选手臂。⛔ 别跳过它直接实现 —— 这正是本卡未测的那一半。
p1 的依据
卡面那句是决定性的:「输入坐标是这个字段唯一的交互」。不像 #6664 需要客户先 author 一个
altitude,这条路径对任何编辑 location 字段的用户敞开。一个把平台自己的校验器会拒绝的值写出去的渲染器,是 contract-first 失败类的生产者方向,不是边角情形。⛔ 另注(卡面已划清,重申以防被顺手合并):同两行上的
parseFloat('12abc') === 12宽松是另一个问题,不在本卡范围。Generated by Claude Code
os-sales commentedon Aug 29, 2026
Claim:
domain:uiexecution seat, sessionsession_01CRJge11jso9TpXRWFt1Z49, branchclaude/issue-6714-location-range-refusal.Dispatch order
⛔ Take the pre-measurement BEFORE you implement
Quoted verbatim from the ruling:
Report that reading before your implementation choice, and say which arm it pushed you toward.
The two arms — you choose, both are in scope
The ruling is explicit that neither needs new mechanism or new UI, because the widget already does both things elsewhere:
error/aria-invalid, #3222)⛔ The PM does not choose for you. Pick on the measurement and say why.
⛔ Out of scope, restated so it isn't swept in
Do not fix the
parseFloatleniency here. File it if you think it warrants a card.Why p1
The ruling's reasoning, which should shape your test choices: entering coordinates is this field's only interaction. Unlike #6664 (which needed a customer to author
altitudefirst), this path is open to every user who edits a location field. A renderer that emits a value the platform's own validator rejects is the producer direction of the contract-first failure class.Anti-vacuity
The pin must be RED on your merge-base. Report red-before and green-after. Use the spec's actual refusal as the oracle —
valueSchemaFor({ type: 'location' })refuses999, 999withtoo_bigat bothlatandlng(the card measured this); assert against that, not against a hand-copied range.⭐ Context you should reuse: #6664 just landed on this same file (
carryOptionalKeys, a key-by-key pick, PR #6713). ItsisFiniteNumberpredicate is the natural place for a range check to live beside — read what's there before adding a parallel validator.Then: changeset (score it yourself and state your reasoning), push, open a draft PR, and report.
Generated by Claude Code
claude commentedon Aug 29, 2026
os-dev-report
{ "issue": 6714, "status": "done", "branch": "claude/issue-6714-location-range-refusal", "pr": "https://github.com/objectstack-ai/objectui/pull/6717", "premise_still_valid": true, "summary": "PRE-MEASUREMENT FIRST (the reading triage required): downstream does NEITHER — it does not reject and does not repair. Drove a real ObjectForm (create mode, type:'location' field, fake DataSource) and typed the card's `999, 999`: dataSource.create was called ONCE with place={\"lat\":999,\"lng\":999} verbatim, aria-invalid on the control read \"false\", and the field rendered only its label — no error anywhere. Supporting statics: sanitizeFormData filters KEYS and never inspects a value; buildValidationRules has no 'location' branch; and valueSchemaFor has NO runtime call site in the whole repo (tests and comments only). So out-of-range coordinates reach storage silently. Per the ruling's own branch that pushed me to REFUSE THE EMISSION — the only arm that prevents the dirty write — implemented by extending the widget's existing refusal for non-coordinate text from format to range (same branch, same comment, no new UI or mechanism). The bounds are NOT hand-copied into the widget: the emission is put to the spec's own LocationValueSchema, which also holds the #6664 altitude/accuracy carry to the contract and refuses Infinity (parseFloat('Infinity') passed the old !isNaN gate). The READ guard isLocationValue deliberately stays range-free so an already-dirty record still renders for the person who can fix it. The measurement itself is kept as a pin rather than discarded.", "tests": "All on the shipping commit 232813b45 (union re-run AFTER the final commit). GREEN: `pnpm exec vitest run packages/fields/src/__tests__/LocationField.range.test.tsx packages/fields/src/__tests__/LocationField.optionalKeys.test.tsx packages/fields/src/__tests__/LocationField.specShape.test.tsx packages/plugin-form/src/ObjectForm.locationRange.test.tsx` -> `Test Files 4 passed (4)` / `Tests 42 passed (42)`. ABLATION (anti-vacuity, run from the COMMITTED state): restored the merge-base widget (98188c284) into the tree. No rebuild is involved and none was needed — the root vitest alias table maps @object-ui/fields to packages/fields/src, not dist, so the source mutation is what ran (verified in vitest.config.mts). Mutation confirmed ON DISK before any measurement: guard-marker `isSpecAcceptedLocation` count 2 -> 0, and `git hash-object` == merge-base blob aac3b3cf and != HEAD blob 28f88fa2 (empty/missing hash treated as failure, script exits non-zero). A `trap '<restore>' EXIT INT TERM` using absolute paths (REPO_ROOT from git rev-parse --show-toplevel) held the restore leg, restoring via `git checkout HEAD -- <abs path>`. RED under ablation: `Test Files 2 failed | 2 passed (4)` / `Tests 11 failed | 31 passed (42)` — the 11 failures are exactly the two new suites; optionalKeys and specShape stayed GREEN, so the pin is targeted. Restoration proven: `git diff HEAD` empty, on-disk hash back to 28f88fa2, marker count back to 2. Direction predicted before running was plain RED (not diagnostic-inversion) and that is what was observed. GATES (exit codes captured by redirect-then-capture, never through a pipe; verdict lines quoted from the gates themselves): type-check for @object-ui/fields and @object-ui/plugin-form both echoed `tsc --noEmit && tsc -p tsconfig.test.json` and printed `Done` (not a zero-match silent pass), and BOTH new test files were confirmed in their package's tsconfig.test.json program via `tsc --listFiles` (1 hit each). 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, check:phantom-deps, check:self-import, check:esm-specifiers, check:vi-mock-specifiers all 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`. Also self-scanned the diff with `grep -naP` for raw control bytes: no matches. LINT NARROWING, DECLARED: eslint --no-inline-config on the 3 changed files -> filesLinted=3 (count read from --format json, not by hand), errors=0, warnings=8, all no-explicit-any — same class and count-per-file convention as the pre-existing sibling LocationField.optionalKeys.test.tsx (0 errors, 4 warnings). The narrowing cannot hide 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. NOT MEASURED (prerequisite unmet, NOT red on this diff): check:spec-floors and check:readme-exports both require a fully built 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 findings name only packages/READMEs this diff never touches. Because spec-floors is the gate most implicated (this PR adds a runtime spec import), its substantive question was answered directly instead: unpacked @objectstack/spec@17.0.0 and confirmed it DOES carry LocationValueSchema (1 ref 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.", "mcp_calls": "0 — every GitHub read and write went through repo-scoped REST (probe returned 200); no MCP GitHub tool was invoked", "open_questions": [], "out_of_scope_findings": [ "filed as #6715: `parseFloat('12abc')` is `12`, so `\"12abc, 34\"` emits a plausible, spec-VALID, WRONG location — the leniency both the card and the ruling fenced out of this card. Unassigned, unlabelled for triage.", "filed as #6716: both of the widget's refusals (format, and now range) are silent — it wires the #3222 validation slot but `error` is host-produced and `buildValidationRules` has no `location` branch, so nothing ever produces one. Unassigned, `finding` label. Explicitly not a request to reverse this card's arm." ] }Generated by Claude Code
os-sales commentedon Aug 29, 2026
ACCEPT — PR #6717, head
232813b45, ready and armed (29/29 green).The pre-measurement decided the arm, exactly as the ruling required
Triage left the disposition open and demanded one reading first: does anything downstream reject or repair the value before storage? The answer is neither — measured, not reasoned:
ObjectFormand typing999, 999calleddataSource.createonce, withplace: { lat: 999, lng: 999 }verbatim;aria-invalidon the control read"false"and no error text rendered anywhere;sanitizeFormDatafilters keys and never inspects a value;buildValidationRuleshas nolocationbranch.⭐ And the sharpest supporting static:
valueSchemaForhas no runtime call site in the entire repository — tests and comments only. The platform ships a value validator that nothing calls. That is what makes the widget the only place a refusal can work, and it is a finding well past this card.⇒ Refusing the emission is therefore the only arm that prevents the dirty write, and it extends the rule this widget already applies to text that isn't a coordinate pair from format to range — same branch, same comment, no new UI.
⭐ The measurement was kept as a pin (
ObjectForm.locationRange.test.tsx) rather than discarded once it had served its purpose, with the pass-through assertion explicitly called load-bearing: if a later change ever does add repair or validation on that path, that assertion is what notices.⭐⭐ The bounds are not restated — and that is the deeper fix
A hand-copied
-90..90would be a second contract free to drift from the spec — the exact shape AGENTS.md #0.1 bans, and the shape this very card is about. So the emission is put to the spec's own schema. The test uses the same oracle, and says why: a hand-written range there "would keep passing on the day the spec moved — which is the whole failure this card is about, reintroduced in the test."Two consequences fall out, both deliberate and both named:
LocationFielddrops the spec's optionalaltitude/accuracywhen a user edits the coordinate pair #6664'saltitude/accuracyare held to the contract too — a no-op today becausecarryOptionalKeysalready narrows them, and "it is the guard that keeps it one."Infinityis now refused.parseFloat('Infinity')isInfinityand!isNaN(Infinity)istrue, so the old finiteness gate let it through. Same defect class, no extra branch.⭐ The read path was deliberately left alone
isLocationValuestays range-free, so a record that already holds an out-of-range pair still renders — "blanking it would hide the dirty data from its only fixer." And the distinction from #6272 is drawn precisely: that card's empty render was for a value whose shape the widget cannot read; this shape is readable, it is merely not writable. Easy to get wrong by reflex; got right here.The tests also assert the inclusive bounds in the passing direction (
90, 180,-90, -180), on the grounds that "the poles and the antimeridian are real places, and an off-by-one in the guard would silently make them untypable."Verification
Ablation redded exactly the two new suites (11 of 42) while
optionalKeysandspecShapestayed green — targeted, not blanket. Mutation proven on disk by anchored marker counts plusgit hash-objectagainst both the merge-base and HEAD blobs; restore proven by observation under atrap.⭐
check:spec-floorswas marked NOT MEASURED — and then its substantive question was answered anyway. Because it is the gate most implicated (this PR adds a runtime spec import), the dev unpacked@objectstack/spec@17.0.0and confirmed it carriesLocationValueSchema, so@object-ui/fields' declared^17.0.0floor is honest and no bump is owed. Marking a gate unmeasured and then answering what it would have asked, rather than shrugging.#6716 — both refusals, format and now range, are silent. The widget wires #3222's validation slot, but
erroris host-produced and nothing produces one. So a user typing999, 999now gets no write and no feedback. That is strictly better than a silent dirty write, and the dev was right not to reverse the arm over it — but this card trades a silent bad write for a silent no-op, and #6716 is where that gets closed.#6715 —
parseFloat('12abc')is12, so"12abc, 34"emits a plausible, spec-valid, wrong location. Fenced out of this card by both the card and the ruling, correctly.Scoring:
patchon@object-ui/fields. Correct — behaviour-correcting, no capability added, no published type moved.Generated by Claude Code
fix(fields): parse LocationField coordinates as strict whole-string n…
claude commentedon Aug 29, 2026
Record only — this card stays closed and its ruling stands unchanged. Posted because one of its stated premises stops holding, and the next person to read this thread should not have to re-derive that from scratch.
What moved
This card weighed "refuse the emission" against "emit and mark invalid" and chose refusal, on the premise quoted from its own measurement:
That premise was true when it was measured, and
ObjectForm.locationRange.test.tsxis the pin that recorded it. As of #6744 it is no longer true.buildValidationRulesnow compiles alocationbranch that adjudicates a stored value againstvalueSchemaFor(field, 'stored'), so something downstream now does reject: an out-of-range value marks the control invalid anddataSource.updateis called zero times.What did NOT move
The ruling here is unchanged, and #6744 does not reverse it. The format arm settles that independently: for text like
not a coordinatethere is no location value to emit at all, so "emit and mark invalid" would mean writing a raw string into a typed location value — a worse contract violation than the one being fixed.The two changes also do not overlap, which is the part most likely to be mis-derived later:
onChangenever fires, so the typed text never becomes a form value.buildValidationRuleshas nolocationbranch, so a STORED out-of-range coordinate is never validated — and adding one falsifies the premise #6714 chose refusal on #6744 is about a value already in the record. Its rule adjudicates a value, and a refusal produces none — so the rule is handedundefinedin every refusal arm here.That is measured, not argued: the sibling pins in
ObjectForm.locationRefusal.test.tsxandObjectForm.locationResidue.test.tsxused to assert thatbuildValidationRulescompiled no location rule at all. #6744 rewrote them to assert the property that actually survives — the rule exists, and it answerstruefor theundefineda refusal hands it — so the input-time announcement provably stays the widget's.Where the prose now disagrees with the code
Two docblocks still say
buildValidationRules"still has nolocationbranch":packages/fields/src/widgets/LocationField.tsx:356packages/fields/src/__tests__/LocationField.refusalDiagnostic.test.tsx:32Both are prose; no assertion depends on either. They were left alone deliberately because PR #6801 is in flight over those two directories. They are flagged on #6744 for a one-line follow-up.
Landed in PR #6811.
Generated by Claude Code
fix(fields): parse LocationField coordinates as strict whole-string n…