Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
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

Closed

Description

@claude

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:

const lat = parseFloat(parts[0]);
const lng = parseFloat(parts[1]);
if (!isNaN(lat) && !isNaN(lng)) { ... }

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:

"12abc, 34"   emits {"lat":12,"lng":34}
"1.2.3, 4"    emits {"lat":1.2,"lng":4}
"12deg, 34"   emits {"lat":12,"lng":34}

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)


Generated by Claude Code

Activity

  1. added theissue type on Aug 29, 2026
  2. huangyiirene commented on Aug 29, 2026

    @huangyiirene
    Collaborator

    分诊:入决策箱 + 四棱块

    定级:needs-user-decision · domain:ui · priority:p2 · type Bug。

    为什么这张进决策箱而 #6714 没有:#6714 的两条手臂都是「让生产者兑现已有声明」,属实现裁量;本卡的手臂 B 是新增解析能力 ⇒ 命中人工地板(新功能)。

    <!-- os-decision-facets -->

    一句话问题:parseFloat('12abc') === 12,所以 "12abc, 34" 发射 {lat:12,lng:34} —— 一个规格接受的、看起来合理的、错的坐标。要拒绝非数字残留,还是去解析度/半球记法?

    选项 × 真实客户可感成本

    做什么 代价
    A 拒绝非数字残留:不是完整数字就不发射 零新能力,与 #6714 同向。⚠️ 用户会拿到「什么也没发生」—— 而 #6716 正在修那个沉默,见下
    B 解析度/半球记法(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 是否值得存在:

    • 若粘贴确实是主要输入方式 ⇒ A 单独做会让粘贴静默失败,B 才是用户真正要的;
    • 若没人粘贴 ⇒ B 是纯扩散,A 足够。

    ⇒ 建议裁前先测一次粘贴路径。成本很低,且它是本卡唯一空着的那一维。

    ⚠️ 排序依赖:A 不该在 #6716 之前单独落地

    本卡与 #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

  3. self-assigned this
    on Aug 29, 2026
  4. os-sales commented on Aug 29, 2026

    @os-sales
    Collaborator

    Claim: domain:ui execution seat, session session_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/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.


    Generated by Claude Code

  5. os-sales commented on Aug 29, 2026

    @os-sales
    Collaborator

    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

  6. os-sales commented on Aug 29, 2026

    @os-sales
    Collaborator

    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:

    1. 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.
    2. ⭐ 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.

    Both rows are pinned.


    Generated by Claude Code

  7. os-sales commented on Aug 29, 2026

    @os-sales
    Collaborator

    ✅ ACCEPT — PR #6766, head ec938a22b

    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:

    1. 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 and get different advice, and keeping the split leaves every finding(fields): LocationField refuses input silently — the validation slot it wires has no producer #6716 pin asserting what it asserted.
    2. ⭐ 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 HEAD and blob equality, both non-empty. Restored: 104 passed.

    Recorded

    Closure will be confirmed once the queue lands it.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatchedpriority:p2

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions