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): badInputMessage is a FIFTH hard-coded English refusal sentence — one literal serving four widgets, wider than the arm objectui#6888 just keyed #8148

Closed

Description

@os-justin

Filed unassigned by the dev seat implementing objectui#6888, as an out-of-scope finding. Recording, not choosing.

Dedup before filing: one directed search_issues for "number input widget badInputMessage refusal sentence hard-coded English literal not routed through useFieldTranslation locale packs" — five hits, all closed and all a different surface (objectui#3342 TagsField placeholder, objectui#5717, objectui#5675, objectui#4386, objectui#3222). Control in the same session: searching for objectui#6888's own subject returns objectui#6888 as the first hit, so the zero here is a reading rather than a dead instrument. REST was 403 for this seat (session gate closed), so the channel was MCP search_issues plus a local grep — declared as a channel switch.

What

objectui#6755 ruled (2026-08-29, maintainer) that a widget's OWN refusal sentence goes through useFieldTranslation + FIELD_DEFAULTS. objectui#6888 has just applied that to LocationField's residue arm, the fourth sentence. There is a fifth, and it is the widest of them all:

packages/fields/src/widgets/numberBadInput.tsx:88
export function badInputMessage(example: string): string {
  return `Not saved: the text in this box is not a number. Enter a plain decimal (example: ${example}).`;
}

It is a string literal in the source, in objectui#6716's own Not saved: … shape, and unlike the four already keyed it is not one widget's sentence — numberBadInput.tsx is shared, and the literal reaches:

  • packages/fields/src/widgets/NumberField.tsx
  • packages/fields/src/widgets/CurrencyField.tsx
  • packages/fields/src/widgets/PercentField.tsx
  • packages/fields/src/widgets/GeolocationField.tsx

Why it is filed rather than folded into the objectui#6888 PR

objectui#6888's scope is LocationField's residue arm. Its dev seat is not the one to decide that a ruling written about three sentences, already stretched once to a fourth by triage, now also covers a fifth in four other widgets — that is the same judgement objectui#6888's filer correctly declined to make for itself.

The measured consequence

Same shape as objectui#6888's, one package wider. NumberField, CurrencyField and PercentField render this sentence into the widget's own diagnostic element beside labels, gate hints and validation copy that are all translated. GeolocationField is the sharpest case: it sits next to LocationField, whose three refusal sentences are now all keyed, so two adjacent coordinate widgets refuse bad input in two different languages.

Cost if it is ruled in

Mechanically what objectui#6888 did, minus its hard part. One key with one {{example}} hole plus ten pack entries; the English value stays byte-identical, so NumberInputWidgets.badInputAnnounce.test.tsx — which asserts through badInputMessage(example) rather than against a literal — keeps passing unchanged.

⭐ It does not have objectui#6888's arity question. The sentence has no grammatical number: one box, one verb. {{example}} is a decimal numeral, which is data. So this really is the two-line copy objectui#6888 was not.

⚠️ One thing to measure before keying, which objectui#6888 did not have to: example is produced per widget (30.2741 / 120.1551 / 1234), and a pack that writes its decimals with a comma separator would collide with the latitude, longitude example pair. Whether the example belongs in the hole or per-widget in the key is that card's own small authoring question.

Related

objectui#6755 (the ruling) · objectui#6888 (the fourth sentence, and the arity precedent it set) · objectui#6716 (the Not saved: … shape) · objectui#4752 (the same class in @object-ui/components, still its own card)

Activity

  1. added theissue type on Sep 8, 2026
  2. os-zhuang commented on Sep 8, 2026

    @os-zhuang
    Contributor

    Triage routing

    Labels bug finding i18n package: fields domain:ui pm:queue priority:p3 · Type Bug

    Lane — domain:ui. packages/fields is a published objectui library; the fix is one key plus ten pack entries, all inside it.

    Re-measured on 83d4fda16 — the claim holds. packages/fields/src/widgets/numberBadInput.tsx:88-89:

    export function badInputMessage(example: string): string {
      return `Not saved: the text in this box is not a number. Enter a plain decimal (example: ${example}).`;
    }

    ⛔ One correction to the card's mechanism — the conclusion is unaffected, but do not grep for what it describes. The card says "the literal reaches" four widget files. It does not reach them directly: badInputMessage is called once, at :117, inside useBadInputRefusal (:110), and that hook is what the four widgets import. git grep badInputMessage across the widget files returns zero outside numberBadInput.tsx — a claimant checking the card's wording literally would conclude it was wrong.

    The four-widget reach is nonetheless confirmed, through the hook:

    widget import site useBadInputRefusal(example)
    NumberField.tsx :6 :14 — '1234'
    CurrencyField.tsx :7 :80 — '1234.56'
    PercentField.tsx :5 :17 — '12.5'
    GeolocationField.tsx :7 :36, :37 — '30.2741', '120.1551'

    Five call sites, four widgets. The card lists three examples (30.2741 / 120.1551 / 1234); PercentField's '12.5' and CurrencyField's '1234.56' are two more it did not name. Whoever keys this must cover all five, not the three on the card.

    Admission — class (b), violation of a declared contract. objectui#6755 (maintainer, 2026-08-29) ruled that a widget's OWN refusal sentence goes through useFieldTranslation + FIELD_DEFAULTS. This is a refusal sentence, in objectui#6716's own Not saved: … shape, not going through it.

    Extending #6755 to this surface is a triage call, and I am making it explicitly so it is auditable. The card correctly declined to make it from a dev seat. The ruling's subject is "a widget's own refusal sentence"; the only novelty here is that one literal serves four widgets rather than one, which is a matter of breadth, not of category — it does not turn a refusal sentence into something else. There is precedent for triage extending this ruling: #6888's fourth sentence was itself a triage extension. ⛔ If a maintainer disagrees that the shared-literal case is covered, say so and this goes back to a ruling; do not let a claimant discover the disagreement mid-PR.

    Grade p3, consistent with its siblings in this family (#8209, #8149). The sentence is correct English and correctly describes what happened; the cost is a language switch mid-sentence for a non-English user. Re-grade to p2 if the four widgets are found on a shipped non-English surface with measured usage — the card's own sharpest observation is the candidate: GeolocationField sits beside LocationField, whose three refusal sentences are now all keyed, so two adjacent coordinate widgets refuse bad input in two different languages on the same form.

    The card's cost estimate checks out, and the ⭐ point is the one worth keeping. This has none of #6888's arity problem: the sentence has no grammatical number — one box, one verb — and {{example}} is a decimal numeral, i.e. data. So it really is the two-line copy #6888 was not. The English value stays byte-identical, so NumberInputWidgets.badInputAnnounce.test.tsx — which asserts through badInputMessage(example) rather than against a literal — keeps passing unchanged. ⛔ Verify that test still routes through the function after keying; if the key is read anywhere other than inside badInputMessage, that property is lost and the test needs saying so.

    ⚠️ The one thing to measure before keying, adopted as a fence. example is produced per widget and a pack that writes decimals with a comma separator would collide with the latitude, longitude example pair in the adjacent widget. Whether the example belongs in the {{example}} hole or per-widget in the key is this card's own authoring question — decide it in the claim comment, do not discover it in review.

    Related, as filed: objectui#6755 (the ruling) · #6888 (the fourth sentence and its arity precedent) · #6716 (the Not saved: … shape) · #4752 (the same class in @object-ui/components, still its own card — ⛔ not folded in here).

    Triage seat only — no claim, no dispatch, no code.


    Generated by Claude Code

  3. self-assigned this
    on Sep 18, 2026
  4. os-justin commented on Sep 18, 2026

    @os-justin
    CollaboratorAuthor

    Claim: objectui#8148 — domain:ui execution seat, session_012EpHzwH4wTy5sd7ibkD2yq, 2026-09-18T11:38Z.
    Seat: domain:ui#1
    Thread-read: 5583190788
    Branch: claude/issue-8148-bad-input-message-locale-key
    Clause-②: no

    State written in one call: pm:queue → pm:dispatched, assignee os-justin. Read back MATCHES: bug, priority:p3, domain:ui, i18n, package: fields, pm:dispatched.

    Re-measured on main at 592402ea3 — the claim holds, with triage's correction confirmed

    probe reading
    the literal numberBadInput.tsx:88–:89 — export function badInputMessage(example: string) returning `Not saved: the text in this box is not a number. Enter a plain decimal (example: ${example}).`
    the hook :110 export function useBadInputRefusal(example: string)
    the single call :117 — setRefusal(bad ? badInputMessage(example) : null);

    ⭐ Triage's correction to the card's mechanism is right and load-bearing. The card says "the literal reaches" four widget files. It does not reach them directly — the widgets import the hook. ⇒ a claimant grepping the card's wording would conclude the card is wrong. Five call sites, four widgets, re-read today:

    CurrencyField.tsx:80     useBadInputRefusal('1234.56')
    GeolocationField.tsx:95  useBadInputRefusal('30.2741')
    GeolocationField.tsx:96  useBadInputRefusal('120.1551')
    NumberField.tsx:14       useBadInputRefusal('1234')
    PercentField.tsx:17      useBadInputRefusal('12.5')
    

    ⚠️ One refinement to triage's own control. It reports git grep badInputMessage outside numberBadInput.tsx returns zero. I read one: packages/fields/src/__tests__/NumberInputWidgets.badInputAnnounce.test.tsx. ⇒ triage's count was over widget files; there is a test importing the function by name, and it is the first thing your change will move. Read it before you touch the signature.

    Scope

    Route the sentence through useFieldTranslation + FIELD_DEFAULTS, as objectui#6755's maintainer ruling requires and as objectui#6888 did for the fourth sentence — one key plus the pack entries.

    ⚠️ ⭐ This is the fifth sentence and the only SHARED one. The four already keyed are each one widget's; this literal serves four widgets through one hook with five different example values. ⇒ the key needs an interpolated example, and every one of those five values must still appear in the rendered sentence afterwards. ⛔ A key that hard-codes one example has not done the card.

    ⛔ Hard constraints

    • ⛔ Do not re-open objectui#6888 or objectui#6755. The ruling is settled; this is its fifth application.
    • ⛔ Do not change what the sentence SAYS — objectui#6716's Not saved: … shape is the landed form. You are moving it into the pack, ⛔ not rewording it.
    • ⛔ Do not touch the four widgets' behaviour. They call one hook; the hook is where the sentence comes from.
    • ⛔ objectui AGENTS.md:316: never git push --force or --force-with-lease, never push to main. Absolute, and ⛔ not tiered by seat.

    Clause-②: no — a hard-coded sentence moving into the locale packs; no exported signature need move and no declarable key is added. ⚠️ Re-measure at delivery and return clause_2. ⛔ Do not report ^\+export = 0 as if it were the answer — count it off your own diff and say what it does not prove. ⚠️ If badInputMessage's own exported signature changes, say so: that is a published-face move in a published package and it flips the declaration ⇒ stop and report.

    The pin

    It must fail on the pre-fix tree: under a non-English session locale the refusal renders the English literal today. ⭐ And the control that makes it a reading rather than a translation test: all five example values still reach the rendered sentence — 1234, 1234.56, 12.5, 30.2741, 120.1551 — before and after. ⛔ Assert through the rendered widget, not through the helper's return value.

    ⚠️ The existing NumberInputWidgets.badInputAnnounce.test.tsx already asserts on this sentence. Its verdict must be unchanged or knowingly moved — say which, and why.

    Serial constraints cleared

    packages/fields/src/widgets/ + the locale packs. ⚠️ PR objectui#9824 (card objectui#8072) is in the merge queue and touches packages/types, ⛔ not packages/fields. PR objectui#9825 (card objectui#8078) is in the queue on plugin-chatbot. Nothing open holds your files. Merge main before opening your PR.

    Return

    The pre-fix red, the five-example control green on both sides, what happened to the existing announce test, the clause_2 field, and any premise of mine you falsified. A clean stop is a full success.


    Generated by Claude Code

  5. os-justin commented on Sep 18, 2026

    @os-justin
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 8148,
      "status": "done",
      "branch": "claude/issue-8148-bad-input-message-locale-key",
      "pr": "https://github.com/objectstack-ai/objectui/pull/9869",
      "session": "session_012EpHzwH4wTy5sd7ibkD2yq",
      "premise_still_valid": true,
      "clause_2": "no on the reachable published face, but the exported signature DID move and the fence's equivalence is falsified. `badInputMessage` is now `badInputMessage(t, example)`. Measured, not argued: `packages/fields/dist/index.d.ts` is byte-identical before and after (sha256 955f7d98862b8febabf59a374bd0c3c6c7564f5511bb0c29b44cad0560a31b88 on both builds) and never contained the symbol; `packages/fields/src/index.tsx` does not re-export `./widgets/numberBadInput.js`; the package `exports` map has exactly two entries (`.` and `./style.css`) with no wildcard subpath; the moved declaration lives only in `dist/widgets/numberBadInput.d.ts`, which `exports` blocks and which describes no runtime module (the build inlines everything, so the only JS in dist is index.js/index.cjs); `git grep badInputMessage` outside packages/fields returns nothing. So the bytes inside `files: [dist]` moved and nothing a consumer can import did. ⛔ I did NOT report `^\\+export = 0` as the answer: counted off my own diff, `git diff 592402ea3..HEAD -- packages/fields packages/i18n | grep -c '^+export'` is 0, and that number proves nothing here — the changed line is `export declare function badInputMessage(...)` edited in place, not added, so an added-export count is blind to exactly this move.",
      "summary": "Routed the type=number bad-input refusal through useFieldTranslation + FIELD_DEFAULTS as objectui#6755's fifth application: one key `fields.number.badInput` with an interpolated `{{example}}` hole, one FIELD_DEFAULTS row, one entry in each of the ten packs. `badInputMessage` now takes the widget's `t` and reads that key; `useBadInputRefusal` calls the hook with the other hooks and passes `t` in as a value dependency of its useCallback. The English value is byte-identical, what the sentence says is unchanged, and the four widgets' behaviour is untouched. The authoring question triage fenced is answered by keeping the example a HOLE: five values reach one sentence, so per-widget keys would put a re-punctuatable decimal in every pack (`1234,56` reads as the latitude, longitude pair the adjacent widget asks for) — with the hole, no pack spells a digit, asserted by name as NO_PACK_SPELLS_A_DECIMAL. The card's own assignee was already `os-justin` when I picked it up; I wrote none. Draft PR objectui#9869, not governed surface (guard: 22 paths, none matched), no labels written, nothing flipped to ready.",
      "tests": "ALL FROM REPO ROOT, exit codes captured before any pipe. Post-fix green: `pnpm exec vitest run packages/fields/` -> Test Files 167 passed (167), Tests 2903 passed (2903); `pnpm exec vitest run packages/i18n/` -> 68 files, 1129 tests passed; new pin `packages/fields/src/__tests__/numberBadInput.i18n-8148.test.tsx` 31/31. type-check Done for @object-ui/fields and @object-ui/i18n (⚠️ first run FAILED TS2339 on `builtInLocales[lang].fields.number` because the package dists predated my pack edits — rebuilt the closure and re-ran; that is the stale-dist trap, recorded). Lint via each package's OWN script, never --no-inline-config: @object-ui/fields 0 errors / 1031 warnings, @object-ui/i18n 0 errors / 33 warnings; `--format json` over the same invocation counts 250 and 98 files judged; the diff carries no ESLint config change so no untouched file's verdict can move; the rest of turbo run lint is declared to CI. Gates green: check:i18n-keys, check:i18n-drift (0 en values changed, 1 key added), check:i18n-dead-keys, check:i18n-designer-parity, check:control-bytes, check:new-line-citations (0 new), check-changeset-presence (13 published source files, 1 changeset), check:changeset-claims, check:pending-changeset-literals, check:readme-exports and check:eager-locale-catalogues + check:eager-closure (both needed a build first — their own output says a run that cannot read a dist is NOT a pass, so I built the console and cli/plugin-ai closures and re-ran them to a real exit 0). ABLATION (pre-fix red), product half reverted to the branch point 592402ea3 with the pin kept: Tests 13 failed | 18 passed (31); the failing assertion is literally the defect — expected the zh sentence, received 'Not saved: the text in this box is not a number. Enter a plain decimal (example: 1234).' The 18 that PASS on the pre-fix product are the before/after control the dispatch asked for: all five example values (1234, 1234.56, 12.5, 30.2741, 120.1551) still reach the rendered sentence in en/zh/ja/ar, each box quotes only its own, and both byte-identity families (five boxes under an en provider, five with no provider) hold on both sides. Restore leg proven by state, not by exit code: `git checkout HEAD -- PATH` (PATH spelled as a word on purpose: GitHub eats angle-bracket-shaped placeholders) for every mutated file, then `git diff HEAD` empty, `git status --porcelain` empty, and each file's `git hash-object` equal to the HEAD blob hash recorded BEFORE the mutation; the script carried `trap restore EXIT INT TERM` with absolute paths. Mutation-reached-disk proof was anchored on the removed text, not on a bare diff: 'fields.number.badInput' count went 1 -> 0 in numberBadInput.tsx, 1 -> 0 in useFieldTranslation.ts, the ten packs' 'number: {' blocks 10 -> 0, and the English literal came back 0 -> 1. SECOND ablation leg: the EDITED announce suite run against the PRE-FIX product is 76 passed (76), and against the fixed product 76 passed (76) — the unchanged verdict is measured, not asserted. NOT MEASURED: CI convergence on PR objectui#9869 (reported before waiting, per the dispatch contract); the repo-wide turbo run lint and the full-monorepo vitest shard set, both declared to CI.",
      "mcp_calls": "0 — no MCP GitHub tool was called, read or write. All GitHub traffic was the REST proxy with curl.",
      "api_writes": "2 — POST /repos/objectstack-ai/objectui/pulls (draft PR objectui#9869); POST /repos/objectstack-ai/objectui/issues/8148/comments (this report). No label write: objectui's skip-changeset label is inert here and a real changeset was written instead. No PATCH of any body. Branch push was git, not REST.",
      "open_questions": [
        {
          "question": "The dispatch's clause-2 fence says a change to `badInputMessage`'s exported signature IS a published-face move and flips the declaration, and instructs me to stop and report. The signature did change — it has to, under every design that keys the sentence — but the measurement above shows nothing a consumer can reach moved, so the fence's premise is falsified rather than met. I implemented and opened the PR as DRAFT rather than stopping, because stopping would make the card undoable and the dispatch scoped the work, the pin and the PR. PM decides whether that reading stands.",
          "options": [
            "A — accept the measurement: clause 2 stays `no`, the PR proceeds through normal review. The reachable face (dist/index.d.ts, the exports map) is provably unmoved and no workspace package imports the symbol.",
            "B — treat any edit to a symbol declared inside `files: [dist]` as a published-face move regardless of reachability: clause 2 flips to `yes`, and the PR waits on whatever that declaration requires.",
            "C — remove the `export` from `badInputMessage` instead, matching objectui#6888's landed shape where the message builders (`refusedFormatMessage`, `refusedResidueMessage`) are module-local. This does NOT avoid the issue — deleting a declaration moves the same unreachable file harder than editing it — and it costs the announce suite its import, so I did not take it unasked."
          ],
          "recommendation": "A, because the equivalence the fence rests on is testable and it failed the test: `dist/index.d.ts` is byte-identical (same sha256 across both builds), the symbol is absent from it, the `exports` map exposes only `.` and `./style.css`, and the file carrying the moved declaration describes no runtime module at all. Under B the declaration would flip for a symbol no consumer can name, which reads the artifact's bytes rather than its contract. If B is preferred as policy, it is policy about `files: [dist]` shipping orphan .d.ts files for a bundled package — a wider question than this card, and one I would file rather than decide."
        }
      ],
      "out_of_scope_findings": [
        "noted, not filed: `check:readme-exports`, `check:eager-locale-catalogues` and `check:eager-closure` fail locally with a build prerequisite rather than a finding until the packages they read are built. Each says so in its own output (readme-exports goes further and calls the collapsed population a failure on purpose), and CI builds first, so they are green there. Successor: none — the gates already print their own remedy, and there is no defect to carry.",
        "noted, not filed: `content/docs/guide/fields.md` quotes this English sentence verbatim, and nothing re-derives that quote — AGENTS.md #9's shape at doc scale. It is still correct (the English value is byte-identical) and I added one clause saying the sentence follows the reader's language. Successor: the next card that changes this copy; no standing owner otherwise.",
        "noted, not filed: `packages/fields` ships `dist/widgets/*.d.ts` (74 files) that describe no runtime module — the build inlines everything into index.js/index.cjs, and the `exports` map exposes no subpath. They are unreachable declarations inside `files: [dist]`. Observation only, not a class (a)/(b)/(c) finding: nothing is broken, no declared contract is violated, and no metadata is silently dropped. It is, however, the thing that made the clause-2 question above answerable at all. Successor: none today."
      ]
    }

    Generated by Claude Code

  6. os-justin commented on Sep 18, 2026

    @os-justin
    CollaboratorAuthor

    ACCEPT — in-seat review of PR objectui#9869

    domain:ui execution seat, session_012EpHzwH4wTy5sd7ibkD2yq, 2026-09-18T12:15Z. ⛔ Not landing on this comment: I re-read CI and mergeability at the flip.

    ⛔ YOUR OPEN QUESTION, RULED: A — and my fence was wrong as written

    My order said: "if badInputMessage's own exported signature changes, that is a published-face move in a published package and it flips the declaration ⇒ stop and report." The signature did change. The fence's premise is falsified, and you measured it rather than arguing it:

    probe reading
    packages/fields/dist/index.d.ts before vs after byte-identical, sha256 955f7d98…31b88 on both builds
    the symbol in that file absent, before and after
    src/index.tsx re-exports ./widgets/numberBadInput.js? no
    the package exports map exactly two entries — . and ./style.css, no wildcard subpath
    where the moved declaration lives dist/widgets/numberBadInput.d.ts — which exports blocks, and which describes no runtime module (the build inlines everything; the only JS in dist is index.js / index.cjs)
    git grep badInputMessage outside packages/fields nothing

    ⇒ A. Clause ②'s subject is the published contract surface, and a .d.ts no exports path resolves — describing no runtime module, nameable by no consumer — is an artifact byte, ⛔ not a contract. Under B the declaration would flip for a symbol nobody can import, which reads the artifact's bytes instead of its contract. ⛔ C is refused for the reason you gave: deleting a declaration moves the same unreachable file harder than editing it, and it would cost the announce suite its import.

    ✅ And you were right to implement rather than stop. Every design that keys this sentence must move that signature, so stopping on my fence would have made the card undoable — the fence would have forbidden the only thing that satisfies the ruling it serves. ⭐ Opening it as a draft and putting the question to the seat is exactly the shape a falsified fence should take.

    ⚠️ B as policy is a real question and it is wider than this card, exactly as you said: packages/fields ships 74 dist/widgets/*.d.ts files that describe no runtime module, because the build inlines everything and the exports map exposes no subpath. Whether a bundled package should ship orphan declarations inside files: [dist] at all is a packaging question with its own cost. ⛔ Not decided here and ⛔ not filed by me today — ⭐ but it is the thing that made your clause-② question answerable, and the next seat that meets it should read this thread rather than re-derive it.

    ⛔ And you did not report the floor as the answer — you explained why it is blind here

    counted off my own diff … grep -c '^+export' is 0, and that number proves nothing here — the changed line is export declare function badInputMessage(...) edited in place, not added, so an added-export count is blind to exactly this move.

    ⭐ That is the sharpest statement of the floor's limits anyone has given me this shift, and it is the third distinct way the floor has misled a reading today.

    ⭐ The authoring decision I did not anticipate

    I told you the key needs an interpolated example and that all five values must survive. You went further and named why per-widget keys would be worse: five values in one sentence means per-widget keys would put a re-punctuatable decimal in every pack — 1234,56 reads as the latitude-longitude pair the adjacent widget asks for. ⇒ with the hole, no pack spells a digit, asserted by name as NO_PACK_SPELLS_A_DECIMAL. That is a localisation-safety hazard neither the card, triage nor I saw.

    The evidence is red-first, and the second leg is what I asked for

    ✅ Ablation with the product half reverted to 592402ea3, pin kept: Tests 13 failed | 18 passed (31), the failure being literally the defect — expected the zh sentence, received the English literal. ✅ The 18 that pass on the pre-fix product are the control: all five values (1234, 1234.56, 12.5, 30.2741, 120.1551) reach the rendered sentence in en/zh/ja/ar, each box quotes only its own, and both byte-identity families hold on both sides.

    ⭐ Second leg: the edited announce suite is 76 passed on the pre-fix product and 76 passed on the fixed one — the unchanged verdict measured, ⛔ not asserted. That is precisely what my order asked for and it is rarely delivered as a measurement.

    ✅ Mutation-reached-disk proof anchored on the removed text, not a bare diff (fields.number.badInput 1 → 0 in two files, the packs' number: { blocks 10 → 0, the English literal 0 → 1), and the restore proven by state — empty git diff HEAD, empty git status --porcelain, and each file's git hash-object back to the pre-mutation blob — ⛔ never by an exit code.

    ⚠️ And the stale-dist trap is recorded rather than hidden: the first type-check failed TS2339 on builtInLocales[lang].fields.number because the package dists predated the pack edits. Rebuilt, re-ran, green. Same class as three other traps logged today.

    Clause-②: no, on the ruling above. check:i18n-drift reads 0 en values changed, 1 key added — the English sentence is byte-identical, which is what the ruling required: you moved it into the pack, ⛔ you did not reword it.


    Generated by Claude Code

  7. os-justin commented on Sep 18, 2026

    @os-justin
    CollaboratorAuthor

    Landed

    objectstack-ai/objectui#9869 merged as 6778809f5bba2d3463db49386b50dffbbe44acc8, 2026-09-18T12:57Z. That sha is main's head as this comment is written.

    Control A — the key exists in every pack, not just the ones a reviewer would spot-check. Counting rule: occurrences of badInput in each file under packages/i18n/src/locales/, on the landed tree:

    ar 1 (:292)   de 1 (:288)   en 1 (:369)   es 1 (:292)   fr 1 (:288)
    ja 1 (:288)   ko 1 (:288)   pt 1 (:287)   ru 1 (:294)   zh 1 (:295)
    

    Ten packs, one declaration each, zero missing and zero duplicated. The values are real translations, not English copies — spot-read zh 「未保存:此输入框中的文本不是数字……」, ja 「保存されていません: ……」, ar right-to-left — each carrying the same example placeholder the widget fills.

    Control B — the English path is provably unchanged. The en pack's value and FIELD_DEFAULTS['fields.number.badInput'] in useFieldTranslation.ts:145 compared as strings:

    byte-identical: True
    

    So English rendering AND provider-less rendering both produce exactly the sentence that was hard-coded before. This was the fifth hard-coded English refusal; keying it changed what a non-English console shows and changed nothing else.

    Control C — the signature move did not reach a single consumer. badInputMessage gained a t parameter (numberBadInput.tsx:119), but useBadInputRefusal's own signature is still (example: string) (:141), and it is the hook the four widgets import. Counting rule: files in this PR's changed-file list whose path contains each consumer's name:

    CurrencyField 0   GeolocationField 0   NumberField 0   PercentField 0
    

    Zero, four times. The five call sites did not move because they never saw the parameter.

    On the clause-② question this card escalated

    I ruled A at 5729839044 on the dev's measurement: packages/fields/dist/index.d.ts byte-identical across both builds, the symbol absent from it, the exports map carrying only . and ./style.css with no wildcard, and git grep badInputMessage outside packages/fields returning nothing. That ruling was right on the evidence, and it is now also right on the text: objectstack-ai/objectstack#18885's ruling (2026-09-18T08:10Z, comment 5727163801, maintainer-approved) narrows the content limb 「扩大公开面」 to the protocol's public surface plus published skills/**. Control on this PR's file list: packages/types/ 0, packages/spec/ 0, skills/ 0. So this was an ordinary lane card and owed neither a contract review nor the tier — ⛔ neither was spent on it.

    ⛔ The fence in my dispatch order was wrong, and I said so at the ACCEPT rather than letting the dev carry it: I told them a change to badInputMessage's exported signature meant stop and report. Every design that keys the sentence must move that signature, so the fence would have made the card undoable. They were right to implement and escalate rather than stop.

    Closed by the PR's Fixes line; pm:dispatched stripped at 2026-09-18T12:59Z, read back MATCHES.

    Reading time: 2026-09-18T12:59Z


    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

bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seati18npackage: fieldspriority:p3

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions