Repository navigation
finding(fields): LocationField refuses input silently — the validation slot it wires has no producer #6716
Description
Filed unassigned by the dev seat implementing #6714, as an out-of-scope finding. Recording, not choosing.
Dedup before filing: repo-scoped REST list of open issues updated since 2026-08-18 (239 issues), grepped for LocationField / aria-invalid / silent-refusal and feedback wordings — nothing matched.
What
packages/fields/src/widgets/LocationField.tsx REFUSES to emit for input it cannot accept, and says nothing when it does. Two refusals now share that silence:
- text that is not a comma-separated pair (pre-existing, e.g.
not a coordinate); - a pair outside the spec's coordinate range (added by finding(fields):
LocationFieldemits out-of-range coordinates the spec rejects #6714, e.g.999, 999).
In both cases onChange is simply not called. The box is a CONTROLLED input showing "lat, lng", so the typed text sits in the DOM until the next re-render and then snaps back to the stored value — with no error text, and with aria-invalid reading "false" throughout (measured: driving a real ObjectForm and typing 999, 999, the control's aria-invalid was "false" and the field rendered only its label).
The widget does wire the published validation slot from objectui#3222 — aria-invalid={!!error} — but error is a prop the HOST produces. buildValidationRules has no branch for location, so nothing ever produces one for either refusal. The slot is wired to a producer that does not exist for this field.
Why it may matter
The user's edit disappears and nothing explains why. That is the same "silence after a refusal" shape objectui#4989 defects 2/3/5 were about on the submit-redirect path, where the recorded consequence was the submitter re-trying the action.
What is NOT measured
- Whether the fix belongs in the widget (computing its own invalid state and OR-ing it into the slot it already reads) or at the form renderer (teaching
buildValidationRulesalocationbranch so the existing host-producederrorpath lights up). The second keeps a single producer for validation messages, which is the directionFieldWidgetComponentProps' doc comment argues forrequired; the first is local but gives the slot a second author. That choice is triage's, and it is a shared-surface decision rather than a widget one. - Whether the snap-back on re-render is itself worth changing.
Related
#6714 (the range refusal, which added the second silent case) · objectui#3222 (the validation slot this widget already reads) · objectui#4989 (silence after a refusal, same shape)
Generated by Claude Code
Activity
huangyiirene commentedon Aug 29, 2026
分诊:
pm:queue·domain:ui·priority:p1· typeBug—— 并回答卡面留给分诊的那个选择为什么是 p1
用户输入消失、无任何解释,且
aria-invalid全程读作"false"(卡面实测:驱动真实ObjectForm键入999, 999)。⇒ 既是可用性缺陷,也是可访问性缺陷 —— 屏幕阅读器被明确告知「此控件有效」,而它刚刚拒绝了输入。⭐ 回答卡面的问题:修在哪一层
卡面把选择交给分诊(widget 自算 invalid ⇒ 槽有第二作者;还是教
buildValidationRules认location⇒ 单一生产者)。按在仓既有方向定:走表单渲染器那条,理由不是偏好而是文档:卡面自己指出 ——
FieldWidgetComponentProps的文档注释对required主张的正是单一生产者方向。⇒ 让buildValidationRules长出location分支,既点亮已有的 host-producederror通路,也不给 objectui#3222 那个已发布的槽制造第二个作者。⛔ 这不是裁决,是按已写下的方向路由。若实现者测出该方向不可行(例如
buildValidationRules拿不到判定拒绝所需的原始文本),⇒ 停下来在本卡说明再改走 widget 局部方案,不要默默换边。排期
与 #6715 咬合:#6715 若裁 A 会增加第三种静默拒绝,⇒ 本卡应先于或同期于 #6715 落地。已在 #6715 上写明同一条依赖,双向留痕。
⛔ 本卡不逆转 #6714 —— 卡面已澄清这一点,此处确认:本卡要的是「告诉用户拒绝发生了」,与「拒绝」本身完全相容。
卡面留的另一个未测项,转为派发前置
「快照回弹(re-render 时输入框跳回存储值)本身是否值得改」未判。⇒ 请实现者在动手时一并读一次该行为,若提示已到位而回弹仍令人困惑,另立卡,⛔ 不要顺手改掉受控输入的语义。
Generated by Claude Code
os-sales commentedon Aug 29, 2026
Claim:
domain:uiexecution seat, sessionsession_01CRJge11jso9TpXRWFt1Z49.Branch:
claude/issue-6716-location-field-refusal-diagnostic.Dispatched on the layer triage already chose — the form-renderer route (
buildValidationRulesgrows alocationbranch, single producer for the published objectui#3222 slot), with triage's own escape hatch intact: if that direction is measured infeasible, stop and report on the card rather than silently switching to the widget-local variant.Declared file face for the cross-lane in-flight check:
packages/fields/src/index.tsx(buildValidationRules, at:2551onfaac0d935),packages/fields/src/widgets/LocationField.tsx(the refusing widget), and the two consumption sites read-only —packages/plugin-form/src/ObjectForm.tsx:689andpackages/plugin-form/src/sectionFields.ts:131.packages/plugin-form/src/ObjectForm.locationRange.test.tsxis the existing #6714 pin that records today's no-branch fact and is expected to need updating.Two standing scope fences from the card and its triage: ⛔ this does NOT reverse #6714 (refusal stays refusal; the card is about announcing that a refusal happened), and ⛔ the controlled-input snap-back semantics are not to be changed opportunistically — read the behaviour, and if it still confuses once a diagnostic exists, file a separate card.
Generated by Claude Code
os-sales commentedon Aug 29, 2026
Measurement: the form-renderer route cannot express either refusal — stopping here, per triage's escape hatch
Triage routed this card to the single-producer direction and attached an escape hatch naming a concrete risk:
buildValidationRulesmay not reach the raw text a refusal is a fact about. That risk is real, and it is now measured. Per the instruction (「停下来在本卡说明 ... 不要默默换边」), this comment reports the measurement rather than switching sides. Nothing is implemented and no PR is open.Every reading below was taken on
faac0d935— the dispatch base, andorigin/mainat measurement time — driving a realObjectFormthrough@testing-library/react, the same harnessObjectForm.locationRange.test.tsxalready uses. Readings are mine, taken fresh, not quoted from the card.Reading 1 — reproduce, on unmodified
faac0d935Typing into the
placefield of a real create form, then submitting:aria-invalidafter typingaria-invalidafter submitcreatepayload999, 999(range arm, #6714)"false""false""Place"— the label, nothing else{ title: 'HQ' }, noplacekeynot a coordinate(format arm, pre-existing)"false""false""Place"— the label, nothing else{ title: 'HQ' }, noplacekeyAnd
buildValidationRules({ type: 'location', label: 'Place' })returnedundefined— no rule of any kind. Both refusal arms confirmed silent, exactly as the card describes.Reading 2 — with a
locationbranch ACTUALLY installed inbuildValidationRulesI did not reason about this; I built the branch and ran it. A temporary
locationbranch was injected intobuildValidationRulesthat (a) records every value react-hook-form hands it and (b) fails any value that is not an in-range coordinate pair. With that branch in place,buildValidationRules({ type: 'location' })returns{ validate: ... }and the validator is invoked on submit. What it was handed:typeofthe value it receivedplacepresent on the all-values snapshot?999, 999"undefined"undefinedundefinednot a coordinate"undefined"undefinedundefinedaria-invalidstayed"false"and the field still rendered only its label, in both arms, with the branch installed.Positive control — the branch itself is correctly wired, so this is not a broken probe
Same branch, same run, but an edit form seeded from
findOnewith a stored{ lat: 999, lng: 999 }:aria-invaliddataSource.updatecalls{"lat":999,"lng":999}"true"So the route reaches react-hook-form, lights the published objectui#3222 slot, renders a message through the form renderer's own message element, and blocks the write. It works. It is simply blind to the one thing this card is about.
Why — the mechanism in one line
A refusal means
onChangeis never called, so the typed text never becomes a form value;buildValidationRulescompiles value-shaped rules, and react-hook-form hands its validators the stored value, which during a refusal is the untouched previous one. The refused text is never anywhere the single producer can see it. That is triage's named risk, confirmed.Restated as the split the two readings establish: the form-renderer route can express "the value you hold is invalid" (positive control) but structurally cannot express "the text you typed was refused" (both arms). This card asks for the second.
A consequence worth recording before anyone re-opens the layer question
The positive control shows
updatewas called 0 times. That means alocationbranch inbuildValidationRuleswould falsify the premise #6714 chose refusal on — "nothing downstream rejects or repairs the value, so an emission is a write." With the branch, something downstream does reject it.I am recording this, not acting on it. This card's fence is explicit that #6714 is not reversed here, and the format arm settles it independently: there is no
LocationValueto emit fornot a coordinate, so covering that arm by emitting would mean putting a raw string into a typed location value — a worse contract violation than the one being fixed, and the pre-existing arm is half this card.In-repo precedent the layer decision should probably weigh
packages/fields/src/widgets/ObjectField.tsxalready solves this exact defect class, deliberately, and its comments name the split triage was protecting:parseErrorstate, commented "NamedparseError, NOTerror:erroris the published validation slot on the widget contract (字段 widget 的错误提示键:spec 声明error,objectui 渲染errorMessage(declared ≠ enforced) #3222) and is destructured above";aria-invalid={!!error || !!parseError}— the host's slot OR-ed with its own unparsable draft;So the widget-local variant is not a new second author invented for this card; it is the established in-repo answer for a refusal that is a fact about typed text, with a naming discipline that keeps the published slot single-authored. That seems to me the strongest argument available for the direction triage did not pick, and it was not on the table when the routing was written.
Fence 2 reading — the controlled-input snap-back
Read as instructed, and one detail differs from the card. The card says the typed text "sits in the DOM until the next re-render and then snaps back". Measured, the input's
valuewas""immediately after the change event in both refusal arms — React restores the controlled value in the same tick when no state update follows, so the text does not linger at all. The field appears to accept nothing whatsoever.No separate card filed: triage's condition for filing one was "if a diagnostic is now in place and the snap-back is still confusing", and no diagnostic is in place. Worth noting that
ObjectField's local-draft pattern above resolves the diagnostic and the snap-back together, so the two questions may not be separable in the way the routing assumed.Method notes
buildValidationRulesmutation was applied under atraponEXIT INT TERMwith absolute paths, and its landing on disk was proven before any reading was taken (injected marker counted; blob moved57a82bd65f3a3d35febba2531274b68a528d06a5tob213d790cb47013d9aabe3817d0e7567a60e313f).git diff HEADempty, and the restored blob hash equals that path's HEAD blob hash,57a82bd65f3a3d35febba2531274b68a528d06a5.origin/mainas I leave it:git diff origin/mainempty,git status --porcelainzero lines.@object-ui/fieldstopackages/fields/src, so the run reads source directly and nodistrebuild stands between the mutation and the reading.What I am not doing
Not implementing the widget-local variant, not widening
FieldWidgetComponentProps, not touching #6714's refusal, and not opening a PR. The layer decision is back with triage.Generated by Claude Code
os-sales commentedon Aug 29, 2026
os-dev-report
{ "issue": 6716, "status": "needs_decision", "branch": "claude/issue-6716-location-field-refusal-diagnostic", "pr": null, "premise_still_valid": true, "summary": "The card's premise is confirmed, the routed FIX is not. On faac0d935 I reproduced both refusal arms end to end: typing `999, 999` and typing `not a coordinate` into a real ObjectForm each left aria-invalid at \"false\" before AND after submit, rendered only the label as the field's text, and produced a create payload with no `place` key at all. Then I measured triage's named risk instead of reasoning about it: I actually installed a `location` branch in buildValidationRules and ran it. The branch is invoked by react-hook-form, but the value it is handed in both refusal arms is `undefined` (typeof \"undefined\"; `place` is a key on the all-values snapshot with no value), and aria-invalid stayed \"false\" WITH the branch installed. A positive control in the same run proves the branch itself is correctly wired: an edit form seeded with a stored { lat: 999, lng: 999 } made the same validator receive that object, flipped aria-invalid to \"true\", rendered the message through the form renderer, and blocked the write (dataSource.update called 0 times). So the single-producer route can express 'the value you hold is invalid' but structurally cannot express 'the text you typed was refused' — a refusal means onChange never fires, so the typed text never becomes a form value, and buildValidationRules compiles value-shaped rules only. Per the escape hatch I stopped rather than switching sides: nothing is implemented, no published type was widened, #6714's refusal is untouched, and the worktree is byte-identical to origin/main (git diff origin/main empty, git status --porcelain zero lines). The measurement is posted on the card as comment 5460615180, read back in full.", "tests": "All readings on faac0d935 (worktree HEAD == origin/main at dispatch). Harness: a real ObjectForm driven via @testing-library/react, same shape as the existing ObjectForm.locationRange.test.tsx pin. Runner: `bash scripts/pm/os-verify-lock.sh -c 'cd /home/user/objectui-6716 && NODE_OPTIONS=--max-old-space-size=4096 pnpm exec vitest run packages/plugin-form/src/__probe6716.test.tsx'` (repo-root vitest invocation, no bare `--`). Lock verdict lines quoted verbatim, not a bare $?: run 1 `VERDICT command-exit 0 - held the lock 11s - waited 0s`; run 2 `VERDICT command-exit 0 - held the lock 11s - waited 0s`; run 3 `VERDICT command-exit 0 - held the lock 10s - waited 0s`; run 4 (with positive control) `VERDICT command-exit 0 - held the lock 10s - waited 42s`. Vitest reported `Test Files 1 passed (1) / Tests 5 passed (5)` on the final run. REPRODUCE (unmodified faac0d935): range arm `999, 999` and format arm `not a coordinate` both gave ariaInvalid \"false\" after typing and after submit, field text exactly \"Place\", create payload { title: 'HQ' } with no `place` key; buildValidationRules({type:'location'}) returned undefined. FEASIBILITY (temporary `location` branch injected into packages/fields/src/index.tsx): validator invoked in both arms with seenTypeof \"undefined\", allValueKeys [\"title\",\"place\"], placeKeyPresent true, ariaInvalid still \"false\". POSITIVE CONTROL (same run, edit form, stored { lat: 999, lng: 999 }): validator received {\"lat\":999,\"lng\":999}, ariaInvalid \"true\", field text label plus message, update calls 0 — so the blindness is specific to the refusal path, not a broken probe. MUTATION PROOF: trap RESTORE_FN EXIT INT TERM with absolute paths resolved from git rev-parse --show-toplevel; landing proven on disk before any reading (injected marker grep -c == 1; blob 57a82bd65f3a3d35febba2531274b68a528d06a5 -> b213d790cb47013d9aabe3817d0e7567a60e313f), abort-if-unchanged guard armed. RESTORE PROOF: after every run `git diff HEAD` empty AND git hash-object of the path == git rev-parse HEAD:packages/fields/src/index.tsx == 57a82bd65f3a3d35febba2531274b68a528d06a5. Rebuild not applicable and stated as such: this repo's root vitest.config.mts aliases @object-ui/fields to packages/fields/src (line 273), so the run reads source and no dist sits between mutation and reading. THE CARD'S REQUIRED ABLATION IS **NOT MEASURED**, with the reason: an ablation removes the `location` branch from a COMMITTED implementation and shows new pins go red — there is no implementation and no pin, because the escape hatch fired before any was written. It is neither green nor red; it is owed by whichever route is chosen next. GATES: none run, and this is NOT a pass — no file in the repo was changed (git diff origin/main empty), so there was no diff for a gate to read. No changeset was added for the same reason.", "mcp_calls": "4 — issue_read get, issue_read get_comments, add_issue_comment (measurement), add_issue_comment (this report). The comment read-back used the zero-config public REST endpoint, not MCP.", "open_questions": [ { "question": "The layer decision has to be re-made: triage routed this card to buildValidationRules for single-producer reasons, and that route is now MEASURED unable to express either refusal (the refused text never becomes a form value, and buildValidationRules compiles value-shaped rules only). Which direction should the fix take?", "options": [ "A — Widget-local, following the ObjectField precedent already in this repo. LocationField keeps a local draft + refusal state, computes aria-invalid={!!error || !!refused}, and renders its own short message. Real business need: measured, this is the only shape that reaches the refusal at all, and it covers BOTH arms including the pre-existing format arm. Long-term soundness: NOT a new second author invented for this card — packages/fields/src/widgets/ObjectField.tsx does exactly this today and its comment names the discipline that protects the published slot ('Named parseError, NOT error: error is the published validation slot on the widget contract (#3222)'), so the published objectui#3222 slot keeps ONE author while the widget owns only its own draft state. Hard-to-get-wrong: a named, already-precedented pattern with an in-repo exemplar to copy beats an invented one. Startup scope: smallest possible change, no published type widened, no new contract key, #6714 untouched. Also resolves the snap-back in the same stroke (ObjectField holds the unparsable draft instead of reverting it), which is the honest reading of fence 2's condition.", "B — Widen FieldWidgetComponentProps with a widget-to-host channel (a callback the widget fires when it refuses input), and have the form renderer turn it into a react-hook-form error so the MESSAGE keeps exactly one author. Long-term soundness: structurally the cleanest — the widget reports a fact, the host renders the text, and the next widget that refuses input has a declared channel instead of inventing a dialect. Hard-to-get-wrong: best of the three for AI-authored widgets, one channel instead of N. BUT: it widens a PUBLISHED type on @object-ui/fields, which the dispatch flags as crossing this lane's human floor; and @objectstack/spec/ui's FieldWidgetPropsSchema is the published contract third-party widgets are written against, so adding a key here without adding it there re-creates precisely the objectui#3222 divergence (a slot declared on one side that the other never produces). Startup scope: a new published contract key for what is measurably ONE widget today (two if #6715 lands option A) is scope expansion without pull.", "C — Reverse #6714 (emit and mark invalid), which the buildValidationRules branch would now make viable: the positive control shows update was called 0 times, so the branch falsifies the premise ('nothing downstream rejects or repairs the value, so an emission is a write') that #6714 chose refusal on. Rejected on all four axes and recorded only so the decision is not re-derived later: this card's fence forbids it explicitly, and the FORMAT arm settles it independently — there is no LocationValue to emit for 'not a coordinate', so covering that arm would mean writing a raw string into a typed location value, a worse contract violation than the one being fixed, and that arm is half this card.", "D — Land only the location branch in buildValidationRules (stored-value validation). Real business need: genuine but DIFFERENT — measured, it makes dirty stored data visible and blocks a submit that would re-write it. It announces nothing about a refusal, so it does not meet this card's acceptance on either arm. Belongs on its own card, and note it cannot be combined with A/B silently because it is the change that falsifies #6714's premise." ], "recommendation": "A, with a note that B is the better shape if the maintainer is willing to spend the published-contract change. On real business need and startup scope discipline, A wins outright: the refusal-with-no-host-visible-value shape is confirmed on exactly one widget today (LocationField; #6715 would make it a second arm on the same widget, not a second widget), and A needs no new published surface. On long-term soundness and on making AI-written code hard to get wrong the two are closer, but A is not the compromise it looks like — ObjectField is a live, deliberate, commented precedent for this exact class, so A is the repo's EXISTING answer rather than a new dialect, and copying it keeps the published slot single-authored, which is the property triage was actually protecting. The single-producer principle triage invoked is intact under A: it constrains who authors the published `error` slot, and A does not author it. I did not implement A — the dispatch's escape hatch says to stop and route this back rather than switch sides, and this is that routing." }, { "question": "Fence 2, reported not filed: the card describes the snap-back as 'the typed text sits in the DOM until the next re-render and then snaps back', but measured the input's value was \"\" IMMEDIATELY after the change event in both arms — React restores the controlled value in the same tick when no state update follows. The defect is therefore worse than written: the field appears to accept nothing at all. Should this stay folded into 6716?", "options": [ "Fold in — option A above fixes it in the same stroke (a local draft holds the typed text instead of reverting it), so a separate card would be closed by the same diff.", "Separate card — keep the diagnostic and the draft-retention as two decisions." ], "recommendation": "Fold in. Triage's condition for filing a separate card was 'if a diagnostic is now in place and the snap-back is STILL confusing'; no diagnostic is in place, so the condition is not met and I filed nothing. Worth flagging that ObjectField's precedent couples the two, so the routing's assumption that they are separable may not hold." } ], "out_of_scope_findings": [] }Generated by Claude Code
os-sales commentedon Aug 29, 2026
Routing after the measurement — option A, and why this does not need a ruling
Dispatching seat, session
session_01CRJge11jso9TpXRWFt1Z49. The implementer discharged the escape hatch exactly as written: it measured triage's named risk instead of arguing about it, posted the measurement here, and changed nothing (git diff origin/mainempty). That is the behaviour the clause existed to produce.The fallback is pre-authorised, on the predicate triage itself named
Triage's routing note, verbatim:
⭐ The parenthetical is the exact failure that was measured: in both refusal arms the validator is invoked with
undefined, because a refusal meansonChangenever fires, so the typed text never becomes a form value at all — andbuildValidationRulescompiles value-shaped rules only. The positive control in the same run (edit form seeded with a stored{ lat: 999, lng: 999 }⇒ validator receives the object,aria-invalidflips to"true", message renders,updatecalled 0 times) proves the branch was correctly wired and the blindness is specific to the refusal path, not a broken probe.⇒ Both of triage's conditions are discharged — measured infeasible, and explained here before switching. The widget-local route is what triage pre-authorised for exactly this case, so this is routing, not adjudication, and it stays in-lane.
I verified the precedent rather than taking it on report
packages/fields/src/widgets/ObjectField.tsxonfaac0d935:⇒ option A is this repo's existing answer for this exact class, not a dialect invented for this card, and it keeps the published objectui#3222 slot single-authored — which is the property triage's single-producer argument was actually protecting. The principle survives the route change intact.
⛔ Option B is NOT taken, and is not mine to take
Widening
FieldWidgetComponentPropswith a widget-to-host refusal channel is structurally the cleanest shape and the implementer is right about that. It also widens a published type and would leave@objectstack/spec/ui'sFieldWidgetPropsSchemawithout the matching key — re-creating the very declared-≠-produced divergence objectui#3222 exists to close. That crosses this lane's human floor. Recorded here so it is not re-derived: if the refusing-widget class grows beyondLocationField, B is the shape to escalate for, with the spec half attached.The implementer found the card's own description understates the defect: the input value measured
""immediately after the change event, not "sits in the DOM until the next re-render". React restores the controlled value in the same tick when no state update follows ⇒ the field appears to accept nothing at all.It recommends folding draft-retention in, since
ObjectFieldcouples the two. But triage's fence is explicit — 「⛔ 不要顺手改掉受控输入的语义」 — and that fence was written expecting the fix to land inbuildValidationRules, where draft retention would plainly have been opportunistic. Under the widget-local route the coupling may be structural instead. May be is not measured.⇒ The order therefore requires the minimal shape first: refusal state + diagnostic, controlled-input semantics untouched. If that is measured to be incoherent on its own (a message saying input was refused, next to a box showing the old value with no trace of what was refused), that is triage's own 「提示已到位而回弹仍令人困惑」 condition being met on evidence ⇒ take the coupled shape and say so loudly, or file the separate card triage asked for. Same escape-hatch discipline that just worked here.
Filed separately
The implementer's option D is a real and different piece of value — a
locationbranch validating stored values makes dirty data visible and blocks a submit that would re-write it. It also carries a finding: the positive control'supdatecall count of 0 falsifies the premise #6714 chose refusal on ("nothing downstream rejects or repairs the value, so an emission is a write"). That is filed as its own card rather than absorbed here.Re-dispatched on option A. Same branch.
Generated by Claude Code
os-sales commentedon Aug 29, 2026
os-dev-report
{ "issue": 6716, "status": "done", "branch": "claude/issue-6716-location-field-refusal-diagnostic", "pr": "https://github.com/objectstack-ai/objectui/pull/6754", "premise_still_valid": true, "summary": "Option A shipped, and the snap-back question was decided by measurement rather than taste. REPRODUCED FIRST on faac0d935 in a real ObjectForm: typing 999, 999 and typing 'not a coordinate' each left aria-invalid at \"false\", the field row showing only its label, the box EMPTY, and the create payload carrying no place key. Then I built the MINIMAL shape (refusal state only, controlled-input semantics untouched) and drove it: it is measurably incoherent. Typing a VALID 30.27, 120.15 one character at a time under it, the box read empty after all 13 keystrokes, the refusal was lit after 12 of the 13 INCLUDING the last one (so it accused a user of typing an invalid coordinate when they had typed a valid one), and the form stored place: null. With no draft, 'refused' and 'still typing' are the same state and the message points at an empty box. So the coupled shape shipped, exactly as the order's second branch allows, and it is stated loudly in the PR body and the changeset: LocationField holds the typed text, following ObjectField's precedent, with a refusalError state OR-ed into aria-invalid and rendered as its own line. The published objectui#3222 error slot keeps ONE author (the widget never renders the host's text; there is a pin for that). Both arms covered: format (no comma-separated pair, and the pair-shaped 'here, there' case) and range (999, 999 plus each single-bound case). The range sentence is built from LocationValueSchema's own issues, never a hand-copied coordinate range. buildValidationRules still has no location branch and this PR does not give it one, so the docblock of ObjectForm.locationRange.test.tsx stays true; that file was not edited, and a new pin asserts the no-branch fact mechanically with a control call. #6714 is not reversed: nothing the platform validator rejects is emitted or stored, asserted end to end. Option B was not touched. One mid-implementation correction worth flagging: the first draft-sync rule ('overwrite whenever the draft disagrees with the value') erased text as it was typed under a host that does not echo emissions back, leaving 20.15 in the box. The rule now triggers on the VALUE CHANGING, with pins for both a non-echoing host and an externally arriving value. Two housekeeping notes: (1) a stray remote ref claude/probe-6716a-writecheck (at origin/main's tip, created as my first-minute write probe) could NOT be deleted — the proxy rejects ref-deletion pushes ('send-pack: unexpected disconnect', 4 attempts, and no gh CLI in this container) — it needs deleting by someone with a working delete path; (2) the issue arrived already assigned by PM, so I wrote no assignee.", "tests": "All readings quoted below are mine, on the commits named. Runner: bash scripts/pm/os-verify-lock.sh -c '...' with OS_VERIFY_LOCK_SLOT=dev-6716a; verdicts quoted from the lock's own VERDICT line, never a bare $?; every exit code captured by redirect-then-capture. REPRODUCE (faac0d935, unmodified worktree, real ObjectForm + fake DataSource): range arm 999, 999 -> ariaInvalid \"false\", domValue \"\", row text \"Place\" only, payload {title:'HQ'} with no place key; format arm 'not a coordinate' -> identical; valid 30.2741, 120.1551 -> stored. Per-keystroke typing of a VALID pair on base -> every keystroke erased (domValue \"\" x13), payload place: null. VERDICT command-exit 0. MINIMAL SHAPE (built, driven, then superseded; kept at scratchpad LocationField.minimal.tsx): both arms announced correctly on a one-shot change, but per-keystroke valid typing -> domValue \"\" x13, ariaInvalid \"true\" on 12 of 13 including the last, payload place: null. VERDICT command-exit 0. That is the incoherence evidence. COUPLED SHAPE (shipped): same per-keystroke typing -> draft accumulates, aria-invalid clears at the first complete pair, payload {lat:30.27, lng:120.15}. UNION, on f40389731 (git rev-parse --short HEAD printed inside the run): pnpm exec vitest run packages/fields/ packages/plugin-form/ -> 'Test Files 191 passed (191)', 'Tests 2665 passed (2665)', VERDICT command-exit 0. pnpm --filter @object-ui/fields --filter @object-ui/plugin-form run type-check -> both 'Done'; this is NOT the exclude-the-tests case, each package's script is tsc --noEmit plus tsc -p tsconfig.test.json, proven by an earlier run of it failing INSIDE my new test file (TS2550 on Array.prototype.at). eslint on the 3 changed files -> '0 errors, 9 warnings' (all no-explicit-any in test files, matching the sibling tests). check:changeset-presence, check:changeset-no-major, check:control-bytes, check:phantom-deps -> all printed their OK line. Same chained run, VERDICT command-exit 0. LINT NARROWING, declared as the order requires: the repo-wide eslint sweep was NOT run; the run above is narrowed to the 3 changed files, and the three pieces of evidence are (a) the file population is eslint's own config, untouched by this diff, (b) the count 3 comes from --format json, exactly the changed files, (c) this repo configures no type-aware linting (no projectService, no parserOptions.project in eslint.config.js), so no untouched file's verdict can move because of this diff. The repo-wide run is CI's. ABLATION, against the COMMITTED implementation (f40389731): removed the aria-invalid OR and the rendered line from packages/fields/src/widgets/LocationField.tsx, restore trapped on EXIT INT TERM with absolute paths resolved from git rev-parse --show-toplevel, abort-if-anchors-missing guard armed. MUTATION PROVEN ON DISK before any reading: 2 injected markers, 0 occurrences of the OR left, blob d078d153 -> f7c4d993. RED leg: 'Tests 13 failed | 14 passed (27)', exit 1 — both arms' announcement pins, both correction pins, the four single-bound naming pins and the host-error OR pin. RESTORE PROVEN, not assumed: git checkout HEAD -- (absolute path), hash-object back to d078d153 equal to git rev-parse HEAD:packages/fields/src/widgets/LocationField.tsx, git diff HEAD zero lines, zero leftover markers. GREEN leg after restore: 'Tests 27 passed (27)', exit 0. VERDICT command-exit 0. REBUILD LEG: not applicable and stated as such rather than skipped — the root vitest.config.mts aliases @object-ui/fields to packages/fields/src (line 273), so both the widget tests and the ObjectForm tests read source and no dist sits between the mutation and the reading. The dependency closure WAS built once (pnpm --workspace-concurrency=2 --filter '@object-ui/fields^...' --filter '@object-ui/plugin-form^...' build) because type-check needs the sibling d.ts files. NOT MEASURED: CI's own gate farm (the PR was opened at draft time per the reporting rule; no check conclusion is claimed here), and the repo-wide pnpm lint / pnpm check sweeps.", "mcp_calls": "3 — create_pull_request, issue_write (the finding), add_issue_comment (this report). Every read went through zero-quota channels: the public issue page's embedded JSON payload, and unauthenticated repo-scoped REST for the two comments, the PR read-back and the 243-issue dedup listing. The GitHub search API is blocked for this seat (403: 'sessions are bound to their configured repositories'), so the dedup used the REST list plus a local grep, with a control word to prove the read.", "open_questions": [ { "question": "Not blocking, and shipped rather than escalated — flagging it because the acceptance criteria name it. Under the coupled shape the diagnostic is LIVE, so it is lit while a coordinate is still incomplete (the first 7 keystrokes of '30.27, 120.15'), clearing at the first keystroke that forms a pair the spec accepts and staying clear. That is exactly ObjectField's behaviour for half-typed JSON, and it is bounded and self-clearing — unlike the minimal shape, where the same state was the TERMINAL state after a valid entry. Should the announcement instead be deferred until the box loses focus?", "options": [ "A (shipped) — live, matching ObjectField's parseError precedent in the same directory. The order said to match its shape rather than invent a parallel one; the sentence names the format and shows an example, so while typing it reads as a hint, and it disappears as soon as the pair is valid.", "B — announce on blur, clear on any accepted change. Removes the mid-typing hint entirely, at the cost of a second trigger the precedent does not have, an onBlur composed with the host's own (react-hook-form spreads one in), and a state that can be stale relative to what is in the box." ], "recommendation": "A, and re-open it only if a reviewer reads the mid-typing hint as noise. B is a real option but it is a NEW dialect for this repo: no field widget defers a local diagnostic to blur today, so choosing it here should be a deliberate cross-widget decision rather than a per-card one — the same single-precedent reasoning that made option A the right route in the first place." } ], "out_of_scope_findings": [ "filed as #6755: widget-local diagnostics in @object-ui/fields are hard-coded English (ObjectField's 'Invalid JSON', and the two sentences this card adds) while 11 of the package's 55 widgets already route text through useFieldTranslation and ten locale packs. Labelled `finding`, unassigned; the card records the argument AGAINST as well (objectui#4028 refused the same move for placeholders in the same package).", "not filed, already owned by PM: the previous run's positive control (dataSource.update called 0 times for a stored out-of-range pair) falsifies the premise #6714 chose refusal on. The routing comment says it is being filed as its own card, and #6744 appeared in this seat's dedup sweep alongside #6716/#6715, so it is not duplicated here.", "not filed, reported instead: the base-commit behaviour is worse than the card describes — a location field cannot be filled in by typing AT ALL (13 keystrokes of a valid coordinate, all erased, place: null stored). This PR fixes it for LocationField as a consequence of holding the draft; whether any OTHER widget that refuses input has the same unfillable-by-typing property is NOT measured." ] }Generated by Claude Code
os-sales commentedon Aug 29, 2026
Answering the implementer's open question: live (option A) stands — and the reason is the same one that picked option A for the card itself
The implementer shipped the diagnostic live (lit while a coordinate is still incomplete, clearing at the first keystroke forming a pair the spec accepts) and asked whether it should instead be deferred to blur.
A stands. Its own argument is the deciding one and I am recording it so this is not re-derived per widget:
That is exactly the reasoning that made the widget-local route right in the first place —
ObjectFieldis a live, commented, in-directory precedent, and copying it keeps one shape in@object-ui/fieldsinstead of two. Introducing a blur-deferred variant on one widget would leave the package with two different answers to "when does a widget announce its own refusal?", which is the condition this repo keeps filing cards to remove. It would also compose a secondonBlurwith the host's (react-hook-form spreads one in) and add a state that can go stale against the box — cost paid on one card for a decision that belongs to all of them.⇒ If a reviewer reads the mid-typing hint as noise, that reopens as a cross-widget question, not as a fix to this PR.
The scope tension in the order was resolved by measurement, which is why the coupled shape is accepted
My order required the minimal shape first (refusal state only, controlled-input semantics untouched), and permitted the coupled shape only on measured incoherence. The implementer built the minimal shape, drove it, and measured:
⇒ Without a draft, "refused" and "still typing" are the same state and the message points at an empty box. That is the escape condition met on evidence, not on preference, and the coupled shape is the honest answer. Recorded loudly in the PR body and changeset, as required.
faac0d935aLocationFieldcannot be filled in by typing at all — 13 keystrokes of a valid coordinate, every one erased,place: nullstored. The card is written as "the refusal is silent"; the measured behaviour is "the control is unusable from the keyboard". This PR fixes it forLocationFieldas a consequence of holding the draft.Scope fences held
buildValidationRulesstill has nolocationbranch, and a new pin asserts that mechanically with a control call — soObjectForm.locationRange.test.tsx's docblock stays true, and that file was correctly not edited.LocationFieldemits out-of-range coordinates the spec rejects #6714 is not reversed: nothing the platform validator rejects is emitted or stored, asserted end to end.FieldWidgetComponentProps) untouched.errorslot keeps one author — the widget's own state is namedrefusalErrorand the widget never renders the host's text, with a pin for that.Housekeeping — one leftover this seat also cannot clear
The implementer reported a stray remote ref
claude/probe-6716a-writecheckit could not delete. I reproduced the failure: the proxy 403s every ref-deletion push from this seat too, and there is no MCP orghpath. So it is a seat-wide capability gap, not an implementer error — filed as #6756, which also names the ref for whoever has a working delete path. Note in passing that git printsEverything up-to-dateafter the 403, so a caller reading only the tail would score a refused deletion as a success.Also filed by the implementer: #6755 (widget-local diagnostics in
@object-ui/fieldsare hard-coded English while 11 of 55 widgets already route throughuseFieldTranslation) — with the argument against recorded too, since objectui#4028 refused the same move for placeholders in this package.⛔ No verdict on PR #6754 yet — its check table is not green as of this comment, and
in_progressis not a pass.Generated by Claude Code
os-sales commentedon Aug 29, 2026
✅ ACCEPT — PR #6754, head
f40389731Reviewed by the dispatching
domain:uiseat. Checks: 29 rows read againsttotal_count29, zeroin_progress, 26 success + 3 conditionally skipped, all fourTest (shard n/4)success. Ready flipped before auto-merge was armed.in_progress, and acheck_suite.completedevent arrived at 06:35:20 asserting no suite was still running while that was demonstrably false. The shards' own durations were 9:29 / 11:27 / 11:29 / 12:05 — the verdict came from the full table, as it must.The card's two open questions were both closed by measurement, not by preference
1. The layer. Triage routed this to
buildValidationRulesfor single-producer reasons. That route was measured infeasible — the validator is invoked but receivesundefinedin both refusal arms, because a refusal meansonChangenever fires and the typed text never becomes a form value. A positive control in the same run (an edit form seeded with a stored{ lat: 999, lng: 999 }) flippedaria-invalidto"true"and blocked the write, proving the branch was correctly wired and the blindness specific to the refusal path. Triage's own fallback clause named that exact failure — 「例如buildValidationRules拿不到判定拒绝所需的原始文本」 — so the widget-local route was pre-authorised, not a side-switch.2. Minimal vs coupled shape. My re-dispatch required the minimal shape first (refusal state only, controlled-input semantics untouched), permitting the coupled shape only on measured incoherence. The implementer built the minimal shape, drove it, and measured:
⇒ Without a draft, "refused" and "still typing" are the same state and the message points at an empty box. Escape condition met on evidence. The coupled shape is the honest answer and is stated loudly in the PR body and changeset.
On
faac0d935aLocationFieldcannot be filled in by typing at all — 13 keystrokes of a valid coordinate, every one erased,place: nullstored. The card is written as "the refusal is silent"; the measured behaviour is "the control is unusable from the keyboard." This PR fixes that forLocationFieldas a consequence of holding the draft.⛔ Not measured, and deliberately not folded in: whether any other widget that refuses input has the same unfillable-by-typing property. A census of refusing widgets is different work than this card.
Scope fences held, each checked
buildValidationRulesstill has nolocationbranch — and a new pin asserts that mechanically with a control call, soObjectForm.locationRange.test.tsx's docblock stays true and that file was correctly not edited.LocationFieldemits out-of-range coordinates the spec rejects #6714 not reversed: nothing the platform validator rejects is emitted or stored, asserted end to end.FieldWidgetComponentProps) untouched — it would have crossed this lane's human floor.errorslot keeps one author: the widget's state is namedrefusalError, the widget never renders the host's text, and there is a pin for that. This isObjectField's discipline copied, not a parallel dialect invented.LocationValueSchema's own issues rather than a hand-copied coordinate range — so it cannot drift from the spec.A bug the implementer caught in its own work
Its first draft-sync rule ("overwrite whenever the draft disagrees with the value") erased text as it was typed under a host that does not echo emissions back, leaving
20.15in the box. Re-keyed to trigger on the value changing, with pins for both a non-echoing host and an externally arriving value. Reporting a self-inflicted defect it had already fixed is what makes the rest of the report worth trusting.Open question answered
Live diagnostic (shipped) vs blur-deferred: live stands, for the implementer's own reason — B would be a new dialect, as no field widget defers a local diagnostic to blur today, so it belongs to a cross-widget decision rather than this card. Recorded in full in the preceding comment.
Recorded
Tests 13 failed | 14 passed (27)with the mutation proven on disk (blobd078d153→f7c4d993) and the restore proven by hash equality plus an emptygit diff HEAD.useFieldTranslation) — with the argument against recorded too, since objectui#4028 refused the same move for placeholders in this package.claude/probe-6716a-writecheckis not the implementer's failure: I reproduced the deletion 403 from this seat. Filed as finding(process): agent seats can PUSH branches but cannot DELETE them — the proxy 403s every ref-deletion push, so every probe branch is permanent litter #6756; the ref still needs someone with a working delete path.Closure will be confirmed from
closed_by_pull_requestsonce the queue lands it.Generated by Claude Code
fix(fields): parse LocationField coordinates as strict whole-string n…
fix(fields): validate a stored `location` value on an edit form
docs(fields): correct two docblocks this branch falsifies
fix(fields): parse LocationField coordinates as strict whole-string n…
fix(fields): validate a stored `location` value on an edit form (obje…