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 accepts a partly-numeric coordinate, emitting a plausible wrong location #6715
Filed unassigned by the dev seat implementing #6714, as an out-of-scope finding. Not fixed there — that card is fenced to the coordinate RANGE, and both the card body and the triage ruling explicitly fence this one out ("parseFloat('12abc') === 12 宽松是另一个问题"). Recording, not choosing.
Dedup before filing: repo-scoped REST list of open issues updated since 2026-08-18 (239 issues), grepped for parseFloat / LocationField / coordinate / latitude — only #6714 itself matched, where this appears as the explicitly excluded half.
What
packages/fields/src/widgets/LocationField.tsx parses the typed pair with bare parseFloat:
parseFloat stops at the first character it cannot read and returns what it got, so a coordinate that is only PARTLY a number is accepted as if it were whole:
Each of those emissions is a value the spec ACCEPTS — valueSchemaFor({ type: 'location' }) sees a well-formed in-range pair — so unlike #6714 the platform validator cannot be the oracle here. The defect is that the widget silently invents a coordinate the user did not type, rather than treating the text as the non-coordinate it is.
Why it may matter
Same write path and same exposure as #6714: typing the coordinates is this field's only interaction. The failure is quieter, though — #6714 produced a value the contract refuses, which something downstream could in principle have caught; this produces a plausible, contract-valid, WRONG location. The nearest precedent for how this repo weighs that is the || 0 removal in #6272: "a field that renders a plausible wrong place is worse than one that renders nothing".
What is NOT measured
How a user actually reaches this. "12abc" is not obvious typing; paste of a formatted coordinate (12°N, 34°E, 12.5 N, 34 E) is the plausible route and has not been tested.
Whether the right disposition is to reject non-numeric residue outright, or to parse the degree/hemisphere notations that a paste would produce. Those are different features, and the choice is triage's.
Related
#6714 (the range refusal on these same two lines, where this was fenced out) · #6272 (the A1 rename, and the || 0 precedent on plausible-wrong values)
四棱
① 长远合理性:A 让 widget 的契约收敛成一条线 —— 要么是完整坐标,要么不是。B 把 widget 变成坐标格式解析器,是能力面扩张。
② 业务拉动:⚠️空的。"12abc" 不是自然键入;卡面自己说真实路径猜测是粘贴格式化坐标,而那没测过(见置信缺口)。
③ 防错 —— 分水岭,且本仓已有明确取向。⭐ #6272 移除 || 0 时记下的原则逐字适用:「a field that renders a plausible wrong place is worse than one that renders nothing」。本卡正是那个形状 —— 不是拒绝,是悄悄发明一个用户没输入的坐标。按这条原则,A 是本仓价值观的直接应用。
④ 不扩散:A 是收紧一个已有判断;B 是新增一整块解析逻辑及其测试面。创业阶段 A 明显更省。
推荐:A,依据是 ③ 的在仓先例(#6272)。⛔ B 属人工地板,本席不代裁;建议先测 ② 再考虑它。
⭐ 置信缺口
没有人测过用户实际怎么走到这里。 卡面明说 "12abc" 不是显然的键入,真实路径是猜测的粘贴场景(12°N, 34°E / 12.5 N, 34 E),一次都没测过。这决定 B 是否值得存在:
Dispatched as ruled work, executing refuse per the 2026-08-29 director ruling — verbatim 「同意」: strict whole-string numeric parse, text carrying non-numeric residue (12abc, 1.2.3, 12deg) is a non-coordinate and is refused, never silently truncated into a plausible wrong location. ⛔ Degree/hemisphere paste parsing is deliberately not ruled and is out of scope — it becomes its own feature card if real demand arrives.
The sequencing dependency triage set is now satisfied, and that is why this is being dispatched only now. Triage wrote 「若裁 A,请与 #6716 一同或在其之后落地」, because option A adds a third silent refusal and #6716 was the card fixing that silence. PR #6754 (#6716) merged at 06:50Z; origin/main is now b76ca6764 and I verified the refusal machinery is actually present on it (refusalError resolves in packages/fields/src/widgets/LocationField.tsx) rather than assuming it from the merge. ⇒ the new refusal arm inherits an announcement instead of adding to a silence.
⚠️ This card and #6716 change the same parse lines, which is why it was held rather than dispatched in parallel — a branch cut before b76ca6764 would have been written against a widget with no way to announce its refusals.
Declared file face for the cross-lane in-flight check: packages/fields/src/widgets/LocationField.tsx (the parseFloat pair) plus pins. packages/plugin-form/src/ObjectForm.locationRange.test.tsx is read-only — it is #6714's pin and its no-location-branch docblock must stay true.
{
"issue": 6715,
"status": "done",
"branch": "claude/issue-6715-location-strict-numeric-parse",
"pr": "https://github.com/objectstack-ai/objectui/pull/6766",
"premise_still_valid": true,
"summary": "Base was origin/main at b76ca67646373cb65938e64ce87f5fe745b21503 — exactly PR #6754 (card #6716), so the sequencing dependency triage set was satisfied and the refusalError machinery was present to announce through. Reproduced the defect first on that base, driving a real ObjectForm: typing '12abc, 34' called dataSource.create with place {lat:12,lng:34}, aria-invalid=false, no diagnostic. Implemented the ruled REFUSE: each half of the typed pair is now tested against parseFloat's OWN grammar, ANCHORED (/^[+-]?(?:Infinity|(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?)$/), so text with a number at the front and leftover text after it is a non-coordinate and is refused through the SAME setRefusalError path the format and range arms use, with a message naming the half it could not read. Number() was deliberately NOT used as the test — it reads '0x10' as 16, '0b11' as 3 and '' as 0. No degree/hemisphere parsing was added; 12°N stays refused and the message deliberately does not advertise that notation. Nothing in the refusal/draft machinery was restructured or renamed — one new ParsedDraft outcome and one new arm.",
"boundary_i_decided": "REFUSED as residue (new arm): 12abc, 1.2.3, 12deg, 0x10, 0b11, 0o17, 1_000, 1e, 1.2e, '12 34', 1-2, 12°, '12.5 N'. ACCEPTED as whole numbers (unchanged): 12, 12.5, .5, 30., -12, +12, 1e3, 1E3, 1.5e-3, 1.e3, -.5, +.5, and surrounding whitespace (halves were already trimmed). Two rows are deliberate rather than fallout. (1) Text with NO number at the front (abc, NaN, 'here, there', --1, (12)) keeps the pre-existing FORMAT arm and its exact sentence — 'no number at all' and 'a number with text after it' are different mistakes and get different advice, and keeping the split keeps every #6716 pin saying what it said. (2) Infinity carries no residue (parseFloat reads the whole word), so it stays with #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 every 'no emission' pin stayed green. Both are pinned.",
"tests": "All readings name their commit. REPRODUCE on base b76ca6764, real ObjectForm + fake DataSource: create({place:{lat:12,lng:34}}) for '12abc, 34'; {lat:1.2,lng:4} for '1.2.3, 4'; {lat:12,lng:34} for '12deg, 34'; {lat:0,lng:34} for '0x10, 34'; {lat:12.5,lng:34} for '12.5 N, 34 E' (hemisphere silently dropped); aria-invalid=false in every row. NEW PINS: packages/fields/src/__tests__/LocationField.strictNumeric.test.tsx (45 tests) and packages/plugin-form/src/ObjectForm.locationResidue.test.tsx (14 tests). ABLATION against the COMMITTED implementation ec938a22b: grammar weakened to /^[\\s\\S]*$/ (const stays referenced so the mutation reaches dist), restore trapped on EXIT INT TERM at an absolute path and pinned to HEAD. Mutation confirmed ON DISK by grep, never by the editor exit code — injected=1, removed-grammar=0 — and both legs REBUILT with confirmation in dist (removed-grammar=0 mutated, grammar-present=1 restored). Mutated run exit=1: 'Test Files 2 failed | 4 passed (6)', 'Tests 27 failed | 77 passed (104)', all 27 residue pins, e.g. 'AssertionError: expected { lat: 12, lng: 34 } to be undefined'. DISCRIMINATING: zero clean-pair pins moved and #6714's and #6716's files did not move at all. Restore PROVEN by observed state: git diff HEAD = 0 bytes, blob 3bf3a6172b4ab3aebe45a79f626668efa0735cd7 == HEAD blob 3bf3a6172b4ab3aebe45a79f626668efa0735cd7, both non-empty. Restored run exit=0: 'Test Files 6 passed (6)', 'Tests 104 passed (104)'. UNION on final commit ec938a22b: 'pnpm exec vitest run packages/fields/ packages/plugin-form/' -> 'Test Files 194 passed (194)', 'Tests 2727 passed (2727)'. TYPE-CHECK both packages chained with && so the verdict covers both: exit 0, both echoed 'tsc --noEmit && tsc -p tsconfig.test.json' so neither matched zero scripts; both new test files confirmed as real program inputs via --listFiles (1 hit each), which matters because the package tsconfig excludes **/*.test.tsx from the build program. LINT: a DECLARED NARROWING — eslint . for the two touched packages only, 304 files judged / 0 errors (fields 197, plugin-form 107, counted from --format json); population from lint:coverage's own census '46/46 packages linted, 0 with outstanding errors'; eslint.config.js untouched by this branch and no type-aware linting configured (no projectService, no parserOptions.project), so the diff cannot move an untouched package's verdict. GATES, each captured by redirect-then-capture and 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-major. Quoting their own verdict lines: '✅ spec symbol derivation: 1320 files scanned against 4959 spec export names' and '✅ Every consumer-facing @objectstack/spec floor carries the symbols its package artifact references'. NOT MEASURED then resolved: check:spec-floors and check:readme-exports first reported their OWN unmet precondition on a partly-built tree ('no-artifact ... produced no build output to judge'; 'the population COLLAPSED -- this run proves nothing'); recorded as NOT MEASURED, not red, then the full workspace was built (turbo run build, 43/43 successful) and both returned a real green with readme-exports reporting '0 unbuilt'. CI status at report time is in_progress — not green, and I did not wait on it.",
"mcp_calls": "5 — issue_read get, issue_read get_comments, issue_write create (#6765), create_pull_request (#6766), add_issue_comment (this one). Dedup search and the PR-body read-back went through repo-scoped REST (probe returned 200), not MCP.",
"open_questions": [
{
"question": "NON-BLOCKING, for the record: triage's one empty dimension was 'nobody has measured how a user actually reaches this', with paste of a formatted coordinate named as the guess. I measured it while implementing, and the reading is sharper than the card assumed — on b76ca6764, '12.5 N, 34 E' stored {lat:12.5,lng:34}, silently DROPPING the hemisphere letter, so a pasted southern or western coordinate was stored on the wrong side of the equator or the meridian. Under this PR that paste is now refused loudly instead. Does the ruling stand as-is, or does this reading change the case for the deliberately-unruled degree/hemisphere feature card?",
"options": [
"A — ruling stands, nothing further. Paste now fails loudly instead of silently wrong, which is strictly better and is what the ruling asked for. The paste route is still unmeasured in the sense that matters (is anyone actually pasting), and this measurement does not answer that.",
"B — file the degree/hemisphere feature card now, citing the hemisphere-drop reading as demand evidence.",
"C — measure real usage first (does anyone paste into a location field), then decide."
],
"recommendation": "A. The hemisphere-drop reading is evidence about the SEVERITY of the old behaviour, not about DEMAND for the new notation — it says the truncation was worse than the card thought, which the refusal already handles. Filing B on this reading would be inferring a user path from a bug's blast radius, exactly the leap the ruling declined ('the paste route is unmeasured; it becomes its own feature card if real demand arrives'). On the four axes: real business need is still unevidenced (nobody has been observed pasting); long-term soundness favours a widget whose contract is one line (whole coordinate or not) over one that grows into a coordinate-format parser; making AI-authored metadata hard to get wrong favours the loud refusal, since a partial notation parser would manufacture new plausible-wrong values (12°S read as +12 is exactly the defect this card removes); and startup scope discipline says do not add a parsing surface with no pull. I did NOT build for paste."
}
],
"out_of_scope_findings": [
"filed as #6765: CurrencyField (:89, :118) and PercentField (:45) use the same bare parseFloat with no whole-string guard of their own, relying entirely on browser type=number value sanitization — and happy-dom, this package's own test environment, does NOT implement it (measured: setting '12abc' on a type=number input leaves value '12abc'), so any test written for those widgets exercises a path no browser takes. Observation-class, labelled finding, not reproduced as a user-visible defect. Not fixed here.",
"not filed, already open as #6755: the new refusal sentence is hard-coded English, the same shape as the two #6716 added. #6755 is in the decision box and names LocationField's #6716 sentences explicitly, so this one falls under it — filing a second card would be a duplicate.",
"not filed, no defect: packages/fields/tsconfig.json excludes **/*.test.tsx from the build program, which would make 'type-check is clean' silent about test files — but the package's type-check script chains tsc -p tsconfig.test.json, and --listFiles confirms both new files are real inputs there. Checked, sound, recorded only so the next reader does not re-derive it."
]
}
Answering the implementer's open question: A — the ruling stands, and nothing new is filed on this reading
While implementing, the implementer measured triage's one empty dimension — 「没有人测过用户实际怎么走到这里」, with paste of a formatted coordinate named as the guess. The reading is sharper than the card assumed: on b76ca6764, '12.5 N, 34 E' stored {lat: 12.5, lng: 34}, silently dropping the hemisphere letter ⇒ a pasted southern or western coordinate was stored on the wrong side of the equator or the meridian. Under this PR that paste is refused loudly instead.
It asked whether that changes the case for the deliberately-unruled degree/hemisphere feature card. It does not, and its own reasoning is the right one, adopted here verbatim rather than re-derived:
The hemisphere-drop reading is evidence about the SEVERITY of the old behaviour, not about DEMAND for the new notation — it says the truncation was worse than the card thought, which the refusal already handles. Filing B on this reading would be inferring a user path from a bug's blast radius, exactly the leap the ruling declined.
⇒ The ruling's words were "the paste route is unmeasured; it becomes its own feature card if real demand arrives". What was measured is that the old behaviour on a pasted string was worse than recorded — not that anyone pastes. A card opened on this evidence would be a feature justified by the size of a defect rather than by a user, which is the shape the ruling refused.
⛔ Nothing filed. If real demand for degree/hemisphere notation arrives, it opens on that demand.
⭐ Recording the severity reading on this card anyway, because it belongs to the history of the fix: the pre-fix behaviour was not merely "accepts junk", it silently relocated a coordinate across a hemisphere. That is a stronger justification for the refusal than the card's own 12abc example, and a future reader weighing whether this fix was worth it should have it.
Two boundary rows I want on the record as deliberate
The order asked the implementer to state which spellings are whole numbers and which are residue rather than let a regex decide silently. It did, and two rows are choices rather than fallout:
Text with no number at the front (abc, NaN, here, there, --1, (12)) stays on the pre-existing format arm with its existing sentence. "No number at all" and "a number with text after it" are different mistakes deserving different advice — and keeping the split keeps every finding(fields): LocationField refuses input silently — the validation slot it wires has no producer #6716 pin asserting exactly what it asserted before.
⭐ Infinity stays on finding(fields): LocationField emits out-of-range coordinates the spec rejects #6714's range arm.parseFloat consumes the whole word, so it carries no residue. The implementer's reason for not sweeping it in is the sharpest thing in the report: a strictness that swallowed it here "would have silently invalidated a minutes-old explanation while every 'no emission' pin stayed green" — finding(fields): LocationField emits out-of-range coordinates the spec rejects #6714's docblock in this same file describes that value as the range arm's. ⇒ a change that is correct on behaviour and yet turns a neighbouring docblock into a false statement, with no pin going red to say so. Anticipated rather than committed and then found.
Checks: 29 rows read against total_count 29, zero in_progress, 26 success + 3 conditionally skipped, all four Test (shard n/4)success. Ready flipped before auto-merge was armed.
⚠️ Held one probe longer, then held again by a rate limit: an earlier read showed shard 2/4 still in_progress while the other three had finished (it ended at 12:30), and the flip-to-ready itself was refused twice by API rate limit already exceeded before it went through. The PR sat green-but-unreleased for ~35 minutes; ⛔ that is a delay, not a verdict, and nothing was released on an unread table.
The sequencing dependency was satisfied by measurement, not by assumption
Base was b76ca6764 — exactly PR #6754 (card #6716). Before dispatching I verified that origin/main actually carried the refusal machinery (refusalError resolving in LocationField.tsx) rather than inferring it from the merge event. ⇒ the new residue arm inherits an announcement instead of adding a third silence, which is what triage's 「与 #6716 一同或在其之后落地」 was protecting.
⭐ The boundary is parseFloat's own grammar, anchored — not a second opinion about numbers
This is the thing that could most easily have gone wrong, because inventing a regex here means inventing a second definition of "a number" inside the widget. It did not:
⚠️ This is parseFloat's OWN grammar, ANCHORED — not a second, stricter notion of a number invented here. parseFloat reads the longest PREFIX matching this grammar and returns what it got, discarding the rest; the anchors are what turn "there is a number at the front" into "the whole text IS that number".
⛔ Number() was explicitly not used as the test, with the reason recorded: it reads '0x10' as 16, '0b11' as 3, '' as 0. ⛔ No degree/hemisphere parsing was added; 12°N stays refused and the message deliberately does not advertise the notation.
The order asked for the boundary to be STATED, and it was
The implementer returned an explicit boundary_i_decided — refused (12abc, 1.2.3, 12deg, 0x10, 0b11, 0o17, 1_000, 1e, 12°, '12.5 N', …) against accepted (12, 12.5, .5, 30., -12, +12, 1e3, 1.5e-3, -.5, surrounding whitespace) — and flagged two rows as choices rather than fallout:
⭐ Infinity stays on finding(fields): LocationField emits out-of-range coordinates the spec rejects #6714's range arm, because parseFloat consumes the whole word and it carries no residue. The reason for not sweeping it in is the sharpest line in the report: doing so "would have silently invalidated a minutes-old explanation while every 'no emission' pin stayed green" — finding(fields): LocationField emits out-of-range coordinates the spec rejects #6714's docblock in this same file describes that value as the range arm's. A change correct on behaviour that turns a neighbouring docblock into a false statement, with no pin going red to say so. Anticipated rather than committed.
The severity was worse than the card recorded
Measured on b76ca6764 through a real ObjectForm: '12abc, 34' → create({place:{lat:12,lng:34}}); '0x10, 34' → {lat:0,…} (objectui#6272's || 0 arriving through a different door); and '12.5 N, 34 E' → {lat:12.5, lng:34} — the hemisphere silently dropped, so a pasted southern or western coordinate was stored on the wrong side of the equator or the meridian. aria-invalid="false" in every row. That is a stronger justification for the refusal than the card's own 12abc example, and it is why the ruling declining the notation is still right (answered in full in the preceding comment: severity is not demand).
Ablation discriminates
Grammar weakened to /^[\s\S]*$/, mutation confirmed on disk by grep (injected=1, removed=0) and both legs rebuilt with dist confirmation: mutated run Tests 27 failed | 77 passed (104), all 27 the residue pins; zero clean-pair pins moved, and #6714's and #6716's files did not move at all. Restore proven by an empty git diff HEADand blob equality, both non-empty. Restored: 104 passed.
Recorded
check:spec-floors and check:readme-exports first reported their own unmet precondition on a partly-built tree ("produced no build output to judge"; "the population COLLAPSED — this run proves nothing") ⇒ NOT MEASURED, not red; the full workspace was then built (43/43) and both returned real greens with readme-exports reporting 0 unbuilt.
Out-of-scope finding finding(fields): CurrencyField/PercentField lean entirely on browser number-input sanitization, which happy-dom does not implement #6765: CurrencyField (:89, :118) and PercentField (:45) carry the same bare parseFloat with no whole-string guard, relying entirely on browser type=number sanitization — and ⭐ happy-dom, this package's own test environment, does not implement it (measured: setting '12abc' on a type=number input leaves value'12abc'). ⇒ tests written for those widgets exercise a path no browser takes. That is an environment-fidelity finding, not merely two more parseFloats.
A checked non-defect worth not re-deriving: packages/fields/tsconfig.json excludes **/*.test.tsx from the build program, so tsc --noEmit alone would say nothing about the new pins — the package's script chains tsc -p tsconfig.test.json, and --listFiles confirms both new files are real inputs there.
Closure will be confirmed once the queue lands it.
Activity
huangyiirene commentedon Aug 29, 2026
分诊:入决策箱 + 四棱块
定级:
needs-user-decision·domain:ui·priority:p2· typeBug。为什么这张进决策箱而 #6714 没有:#6714 的两条手臂都是「让生产者兑现已有声明」,属实现裁量;本卡的手臂 B 是新增解析能力 ⇒ 命中人工地板(新功能)。
<!-- os-decision-facets -->
一句话问题:
parseFloat('12abc') === 12,所以"12abc, 34"发射{lat:12,lng:34}—— 一个规格接受的、看起来合理的、错的坐标。要拒绝非数字残留,还是去解析度/半球记法?选项 × 真实客户可感成本
12°N, 34°E)四棱⚠️ 空的。
① 长远合理性:A 让 widget 的契约收敛成一条线 —— 要么是完整坐标,要么不是。B 把 widget 变成坐标格式解析器,是能力面扩张。
② 业务拉动:
"12abc"不是自然键入;卡面自己说真实路径猜测是粘贴格式化坐标,而那没测过(见置信缺口)。③ 防错 —— 分水岭,且本仓已有明确取向。⭐ #6272 移除
|| 0时记下的原则逐字适用:「a field that renders a plausible wrong place is worse than one that renders nothing」。本卡正是那个形状 —— 不是拒绝,是悄悄发明一个用户没输入的坐标。按这条原则,A 是本仓价值观的直接应用。④ 不扩散:A 是收紧一个已有判断;B 是新增一整块解析逻辑及其测试面。创业阶段 A 明显更省。
推荐:A,依据是 ③ 的在仓先例(#6272)。⛔ B 属人工地板,本席不代裁;建议先测 ② 再考虑它。
⭐ 置信缺口
没有人测过用户实际怎么走到这里。 卡面明说
"12abc"不是显然的键入,真实路径是猜测的粘贴场景(12°N, 34°E/12.5 N, 34 E),一次都没测过。这决定 B 是否值得存在:⇒ 建议裁前先测一次粘贴路径。成本很低,且它是本卡唯一空着的那一维。
本卡与 #6716(
LocationField静默拒绝、aria-invalid恒为false)咬合:A 增加的是第三种静默拒绝。#6716 已定pm:queue· p1,正是修那个沉默的卡。⇒ 若裁 A,请与 #6716 一同或在其之后落地;否则用户拿到的是「输入消失、无任何解释」,那正是 #6716 立卡要治的缺陷,而 A 会让它更频繁。
低摩擦裁决格式:回「A」/「A + 立 B 卡」/「先测粘贴路径」(推荐)。
裁后执行段:裁 A ⇒ 转
pm:queue(domain:ui),与 #6716 合并排期。裁「A + 立 B 卡」⇒ A 同上,B 由domain:ui席新立卡并带上粘贴路径实测。裁「先测」⇒ 留决策箱,测量回帖后再裁。Generated by Claude Code
os-sales commentedon Aug 29, 2026
Claim:
domain:uiexecution seat, sessionsession_01CRJge11jso9TpXRWFt1Z49.Branch:
claude/issue-6715-location-strict-numeric-parse.Dispatched as ruled work, executing refuse per the 2026-08-29 director ruling — verbatim 「同意」: strict whole-string numeric parse, text carrying non-numeric residue (
12abc,1.2.3,12deg) is a non-coordinate and is refused, never silently truncated into a plausible wrong location. ⛔ Degree/hemisphere paste parsing is deliberately not ruled and is out of scope — it becomes its own feature card if real demand arrives.The sequencing dependency triage set is now satisfied, and that is why this is being dispatched only now. Triage wrote 「若裁 A,请与 #6716 一同或在其之后落地」, because option A adds a third silent refusal and #6716 was the card fixing that silence. PR #6754 (#6716) merged at 06:50Z;
origin/mainis nowb76ca6764and I verified the refusal machinery is actually present on it (refusalErrorresolves inpackages/fields/src/widgets/LocationField.tsx) rather than assuming it from the merge. ⇒ the new refusal arm inherits an announcement instead of adding to a silence.b76ca6764would have been written against a widget with no way to announce its refusals.Declared file face for the cross-lane in-flight check:
packages/fields/src/widgets/LocationField.tsx(theparseFloatpair) plus pins.packages/plugin-form/src/ObjectForm.locationRange.test.tsxis read-only — it is #6714's pin and its no-location-branch docblock must stay true.Generated by Claude Code
os-sales commentedon Aug 29, 2026
os-dev-report
{ "issue": 6715, "status": "done", "branch": "claude/issue-6715-location-strict-numeric-parse", "pr": "https://github.com/objectstack-ai/objectui/pull/6766", "premise_still_valid": true, "summary": "Base was origin/main at b76ca67646373cb65938e64ce87f5fe745b21503 — exactly PR #6754 (card #6716), so the sequencing dependency triage set was satisfied and the refusalError machinery was present to announce through. Reproduced the defect first on that base, driving a real ObjectForm: typing '12abc, 34' called dataSource.create with place {lat:12,lng:34}, aria-invalid=false, no diagnostic. Implemented the ruled REFUSE: each half of the typed pair is now tested against parseFloat's OWN grammar, ANCHORED (/^[+-]?(?:Infinity|(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?)$/), so text with a number at the front and leftover text after it is a non-coordinate and is refused through the SAME setRefusalError path the format and range arms use, with a message naming the half it could not read. Number() was deliberately NOT used as the test — it reads '0x10' as 16, '0b11' as 3 and '' as 0. No degree/hemisphere parsing was added; 12°N stays refused and the message deliberately does not advertise that notation. Nothing in the refusal/draft machinery was restructured or renamed — one new ParsedDraft outcome and one new arm.", "boundary_i_decided": "REFUSED as residue (new arm): 12abc, 1.2.3, 12deg, 0x10, 0b11, 0o17, 1_000, 1e, 1.2e, '12 34', 1-2, 12°, '12.5 N'. ACCEPTED as whole numbers (unchanged): 12, 12.5, .5, 30., -12, +12, 1e3, 1E3, 1.5e-3, 1.e3, -.5, +.5, and surrounding whitespace (halves were already trimmed). Two rows are deliberate rather than fallout. (1) Text with NO number at the front (abc, NaN, 'here, there', --1, (12)) keeps the pre-existing FORMAT arm and its exact sentence — 'no number at all' and 'a number with text after it' are different mistakes and get different advice, and keeping the split keeps every #6716 pin saying what it said. (2) Infinity carries no residue (parseFloat reads the whole word), so it stays with #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 every 'no emission' pin stayed green. Both are pinned.", "tests": "All readings name their commit. REPRODUCE on base b76ca6764, real ObjectForm + fake DataSource: create({place:{lat:12,lng:34}}) for '12abc, 34'; {lat:1.2,lng:4} for '1.2.3, 4'; {lat:12,lng:34} for '12deg, 34'; {lat:0,lng:34} for '0x10, 34'; {lat:12.5,lng:34} for '12.5 N, 34 E' (hemisphere silently dropped); aria-invalid=false in every row. NEW PINS: packages/fields/src/__tests__/LocationField.strictNumeric.test.tsx (45 tests) and packages/plugin-form/src/ObjectForm.locationResidue.test.tsx (14 tests). ABLATION against the COMMITTED implementation ec938a22b: grammar weakened to /^[\\s\\S]*$/ (const stays referenced so the mutation reaches dist), restore trapped on EXIT INT TERM at an absolute path and pinned to HEAD. Mutation confirmed ON DISK by grep, never by the editor exit code — injected=1, removed-grammar=0 — and both legs REBUILT with confirmation in dist (removed-grammar=0 mutated, grammar-present=1 restored). Mutated run exit=1: 'Test Files 2 failed | 4 passed (6)', 'Tests 27 failed | 77 passed (104)', all 27 residue pins, e.g. 'AssertionError: expected { lat: 12, lng: 34 } to be undefined'. DISCRIMINATING: zero clean-pair pins moved and #6714's and #6716's files did not move at all. Restore PROVEN by observed state: git diff HEAD = 0 bytes, blob 3bf3a6172b4ab3aebe45a79f626668efa0735cd7 == HEAD blob 3bf3a6172b4ab3aebe45a79f626668efa0735cd7, both non-empty. Restored run exit=0: 'Test Files 6 passed (6)', 'Tests 104 passed (104)'. UNION on final commit ec938a22b: 'pnpm exec vitest run packages/fields/ packages/plugin-form/' -> 'Test Files 194 passed (194)', 'Tests 2727 passed (2727)'. TYPE-CHECK both packages chained with && so the verdict covers both: exit 0, both echoed 'tsc --noEmit && tsc -p tsconfig.test.json' so neither matched zero scripts; both new test files confirmed as real program inputs via --listFiles (1 hit each), which matters because the package tsconfig excludes **/*.test.tsx from the build program. LINT: a DECLARED NARROWING — eslint . for the two touched packages only, 304 files judged / 0 errors (fields 197, plugin-form 107, counted from --format json); population from lint:coverage's own census '46/46 packages linted, 0 with outstanding errors'; eslint.config.js untouched by this branch and no type-aware linting configured (no projectService, no parserOptions.project), so the diff cannot move an untouched package's verdict. GATES, each captured by redirect-then-capture and 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-major. Quoting their own verdict lines: '✅ spec symbol derivation: 1320 files scanned against 4959 spec export names' and '✅ Every consumer-facing @objectstack/spec floor carries the symbols its package artifact references'. NOT MEASURED then resolved: check:spec-floors and check:readme-exports first reported their OWN unmet precondition on a partly-built tree ('no-artifact ... produced no build output to judge'; 'the population COLLAPSED -- this run proves nothing'); recorded as NOT MEASURED, not red, then the full workspace was built (turbo run build, 43/43 successful) and both returned a real green with readme-exports reporting '0 unbuilt'. CI status at report time is in_progress — not green, and I did not wait on it.", "mcp_calls": "5 — issue_read get, issue_read get_comments, issue_write create (#6765), create_pull_request (#6766), add_issue_comment (this one). Dedup search and the PR-body read-back went through repo-scoped REST (probe returned 200), not MCP.", "open_questions": [ { "question": "NON-BLOCKING, for the record: triage's one empty dimension was 'nobody has measured how a user actually reaches this', with paste of a formatted coordinate named as the guess. I measured it while implementing, and the reading is sharper than the card assumed — on b76ca6764, '12.5 N, 34 E' stored {lat:12.5,lng:34}, silently DROPPING the hemisphere letter, so a pasted southern or western coordinate was stored on the wrong side of the equator or the meridian. Under this PR that paste is now refused loudly instead. Does the ruling stand as-is, or does this reading change the case for the deliberately-unruled degree/hemisphere feature card?", "options": [ "A — ruling stands, nothing further. Paste now fails loudly instead of silently wrong, which is strictly better and is what the ruling asked for. The paste route is still unmeasured in the sense that matters (is anyone actually pasting), and this measurement does not answer that.", "B — file the degree/hemisphere feature card now, citing the hemisphere-drop reading as demand evidence.", "C — measure real usage first (does anyone paste into a location field), then decide." ], "recommendation": "A. The hemisphere-drop reading is evidence about the SEVERITY of the old behaviour, not about DEMAND for the new notation — it says the truncation was worse than the card thought, which the refusal already handles. Filing B on this reading would be inferring a user path from a bug's blast radius, exactly the leap the ruling declined ('the paste route is unmeasured; it becomes its own feature card if real demand arrives'). On the four axes: real business need is still unevidenced (nobody has been observed pasting); long-term soundness favours a widget whose contract is one line (whole coordinate or not) over one that grows into a coordinate-format parser; making AI-authored metadata hard to get wrong favours the loud refusal, since a partial notation parser would manufacture new plausible-wrong values (12°S read as +12 is exactly the defect this card removes); and startup scope discipline says do not add a parsing surface with no pull. I did NOT build for paste." } ], "out_of_scope_findings": [ "filed as #6765: CurrencyField (:89, :118) and PercentField (:45) use the same bare parseFloat with no whole-string guard of their own, relying entirely on browser type=number value sanitization — and happy-dom, this package's own test environment, does NOT implement it (measured: setting '12abc' on a type=number input leaves value '12abc'), so any test written for those widgets exercises a path no browser takes. Observation-class, labelled finding, not reproduced as a user-visible defect. Not fixed here.", "not filed, already open as #6755: the new refusal sentence is hard-coded English, the same shape as the two #6716 added. #6755 is in the decision box and names LocationField's #6716 sentences explicitly, so this one falls under it — filing a second card would be a duplicate.", "not filed, no defect: packages/fields/tsconfig.json excludes **/*.test.tsx from the build program, which would make 'type-check is clean' silent about test files — but the package's type-check script chains tsc -p tsconfig.test.json, and --listFiles confirms both new files are real inputs there. Checked, sound, recorded only so the next reader does not re-derive it." ] }Generated by Claude Code
os-sales commentedon Aug 29, 2026
Answering the implementer's open question: A — the ruling stands, and nothing new is filed on this reading
While implementing, the implementer measured triage's one empty dimension — 「没有人测过用户实际怎么走到这里」, with paste of a formatted coordinate named as the guess. The reading is sharper than the card assumed: on
b76ca6764,'12.5 N, 34 E'stored{lat: 12.5, lng: 34}, silently dropping the hemisphere letter ⇒ a pasted southern or western coordinate was stored on the wrong side of the equator or the meridian. Under this PR that paste is refused loudly instead.It asked whether that changes the case for the deliberately-unruled degree/hemisphere feature card. It does not, and its own reasoning is the right one, adopted here verbatim rather than re-derived:
⇒ The ruling's words were "the paste route is unmeasured; it becomes its own feature card if real demand arrives". What was measured is that the old behaviour on a pasted string was worse than recorded — not that anyone pastes. A card opened on this evidence would be a feature justified by the size of a defect rather than by a user, which is the shape the ruling refused.
⛔ Nothing filed. If real demand for degree/hemisphere notation arrives, it opens on that demand.
⭐ Recording the severity reading on this card anyway, because it belongs to the history of the fix: the pre-fix behaviour was not merely "accepts junk", it silently relocated a coordinate across a hemisphere. That is a stronger justification for the refusal than the card's own
12abcexample, and a future reader weighing whether this fix was worth it should have it.Two boundary rows I want on the record as deliberate
The order asked the implementer to state which spellings are whole numbers and which are residue rather than let a regex decide silently. It did, and two rows are choices rather than fallout:
abc,NaN,here, there,--1,(12)) stays on the pre-existing format arm with its existing sentence. "No number at all" and "a number with text after it" are different mistakes deserving different advice — and keeping the split keeps every finding(fields):LocationFieldrefuses input silently — the validation slot it wires has no producer #6716 pin asserting exactly what it asserted before.Infinitystays on finding(fields):LocationFieldemits out-of-range coordinates the spec rejects #6714's range arm.parseFloatconsumes the whole word, so it carries no residue. The implementer's reason for not sweeping it in is the sharpest thing in the report: a strictness that swallowed it here "would have silently invalidated a minutes-old explanation while every 'no emission' pin stayed green" — finding(fields):LocationFieldemits out-of-range coordinates the spec rejects #6714's docblock in this same file describes that value as the range arm's. ⇒ a change that is correct on behaviour and yet turns a neighbouring docblock into a false statement, with no pin going red to say so. Anticipated rather than committed and then found.Both rows are pinned.
Generated by Claude Code
os-sales commentedon Aug 29, 2026
✅ ACCEPT — PR #6766, head
ec938a22bChecks: 29 rows read against
total_count29, zeroin_progress, 26 success + 3 conditionally skipped, all fourTest (shard n/4)success. Ready flipped before auto-merge was armed.in_progresswhile the other three had finished (it ended at 12:30), and the flip-to-ready itself was refused twice byAPI rate limit already exceededbefore it went through. The PR sat green-but-unreleased for ~35 minutes; ⛔ that is a delay, not a verdict, and nothing was released on an unread table.The sequencing dependency was satisfied by measurement, not by assumption
Base was
b76ca6764— exactly PR #6754 (card #6716). Before dispatching I verified thatorigin/mainactually carried the refusal machinery (refusalErrorresolving inLocationField.tsx) rather than inferring it from the merge event. ⇒ the new residue arm inherits an announcement instead of adding a third silence, which is what triage's 「与 #6716 一同或在其之后落地」 was protecting.⭐ The boundary is
parseFloat's own grammar, anchored — not a second opinion about numbersThis is the thing that could most easily have gone wrong, because inventing a regex here means inventing a second definition of "a number" inside the widget. It did not:
⛔
Number()was explicitly not used as the test, with the reason recorded: it reads'0x10'as16,'0b11'as3,''as0. ⛔ No degree/hemisphere parsing was added;12°Nstays refused and the message deliberately does not advertise the notation.The order asked for the boundary to be STATED, and it was
The implementer returned an explicit
boundary_i_decided— refused (12abc,1.2.3,12deg,0x10,0b11,0o17,1_000,1e,12°,'12.5 N', …) against accepted (12,12.5,.5,30.,-12,+12,1e3,1.5e-3,-.5, surrounding whitespace) — and flagged two rows as choices rather than fallout:abc,NaN,here, there,--1,(12)) stays on the pre-existing format arm with its existing sentence: "no number at all" and "a number with text after it" are different mistakes and get different advice, and keeping the split leaves every finding(fields):LocationFieldrefuses input silently — the validation slot it wires has no producer #6716 pin asserting what it asserted.Infinitystays on finding(fields):LocationFieldemits out-of-range coordinates the spec rejects #6714's range arm, becauseparseFloatconsumes the whole word and it carries no residue. The reason for not sweeping it in is the sharpest line in the report: doing so "would have silently invalidated a minutes-old explanation while every 'no emission' pin stayed green" — finding(fields):LocationFieldemits out-of-range coordinates the spec rejects #6714's docblock in this same file describes that value as the range arm's. A change correct on behaviour that turns a neighbouring docblock into a false statement, with no pin going red to say so. Anticipated rather than committed.The severity was worse than the card recorded
Measured on
b76ca6764through a realObjectForm:'12abc, 34'→create({place:{lat:12,lng:34}});'0x10, 34'→{lat:0,…}(objectui#6272's|| 0arriving through a different door); and'12.5 N, 34 E'→{lat:12.5, lng:34}— the hemisphere silently dropped, so a pasted southern or western coordinate was stored on the wrong side of the equator or the meridian.aria-invalid="false"in every row. That is a stronger justification for the refusal than the card's own12abcexample, and it is why the ruling declining the notation is still right (answered in full in the preceding comment: severity is not demand).Ablation discriminates
Grammar weakened to
/^[\s\S]*$/, mutation confirmed on disk by grep (injected=1, removed=0) and both legs rebuilt with dist confirmation: mutated runTests 27 failed | 77 passed (104), all 27 the residue pins; zero clean-pair pins moved, and #6714's and #6716's files did not move at all. Restore proven by an emptygit diff HEADand blob equality, both non-empty. Restored:104 passed.Recorded
check:spec-floorsandcheck:readme-exportsfirst reported their own unmet precondition on a partly-built tree ("produced no build output to judge"; "the population COLLAPSED — this run proves nothing") ⇒ NOT MEASURED, not red; the full workspace was then built (43/43) and both returned real greens withreadme-exportsreporting0 unbuilt.CurrencyField(:89,:118) andPercentField(:45) carry the same bareparseFloatwith no whole-string guard, relying entirely on browsertype=numbersanitization — and ⭐ happy-dom, this package's own test environment, does not implement it (measured: setting'12abc'on atype=numberinput leavesvalue'12abc'). ⇒ tests written for those widgets exercise a path no browser takes. That is an environment-fidelity finding, not merely two moreparseFloats.LocationFieldrefuses input silently — the validation slot it wires has no producer #6716's two — ⛔ correctly not filed as a duplicate, since finding(fields): widget-local diagnostics are hard-coded English while the package already carries a locale channel #6755 is already in the decision box and names those sentences explicitly.packages/fields/tsconfig.jsonexcludes**/*.test.tsxfrom the build program, sotsc --noEmitalone would say nothing about the new pins — the package's script chainstsc -p tsconfig.test.json, and--listFilesconfirms both new files are real inputs there.Closure will be confirmed once the queue lands it.
Generated by Claude Code
docs(fields): record the measured browser reading behind CurrencyFiel…
docs(fields): record the measured browser reading behind CurrencyFiel…