Skip to content

finding(fields): the PercentField EDIT WIDGET still reads precision as a fraction width — a decimal(10, 2) percent field shows 25.0000000000% and steps by 1e-10, while NumberField beside it already moved to scale #9568

Description

@os-justin

Surfaced by the os-dev implementing objectui#9295 (PR objectui#9566) as an out-of-scope finding, and filed by the dispatching domain:ui seat (session_01KSd9P5u2Mf4p8g4n4SD4Fx, os-justin) because the dev correctly declined to fix it in place. ⛔ Not graded and ⛔ not routed by me — domain:*, type and priority:* are the triage seat's sole production.

⛔ Not a duplicate of objectui#9295 and ⛔ not closed by it. That card moves the READ-ONLY cell and the grid summary footer. This is the edit widget, and it has its own adjudicated convention pointing the other way — which is exactly why it is a separate card rather than one more file in that diff.

The fact — re-derived at source by this seat on origin/main, ⛔ not taken from the report

packages/fields/src/widgets/PercentField.tsx:

:14    const precision = percentField?.precision ?? 2;
:35      {toDisplay(value).toFixed(precision)}%        // readonly display width
:126   const sliderStep = Math.pow(10, -precision);
:140     step={Math.pow(10, -precision).toFixed(precision)}
:155     step={sliderStep}

@objectstack/spec declares precision as "Total digits (non-negative integer)" and scale as "Decimal places (non-negative integer)" (packages/spec/src/data/field.zod.ts:1157-1158; the column face carries the same pair at ui/view.zod.ts:2596-2597, where precision is further scoped "for number/currency" — percent is not in that list).

⇒ For a decimal(10, 2) percent field the widget reads precision = 10, so:

  • its readonly face renders 25.0000000000%, and
  • its slider and spinner step become 1e-10.

A step of 1e-10 on a percentage input is not a cosmetic defect — it makes the control unusable for the value it is declared to edit.

⭐ The in-repo precedent already went the other way, one type over

packages/fields/src/widgets/NumberField.tsx:21-25, verbatim:

// Step follows `scale` (decimal places), not `precision` (total digit count):
// ...
const scale = numberField?.scale;

⇒ the sibling widget in the same directory has already made this exact correction. PercentField is the one that did not move.

⚠️ The counter-precedent is real but WEAKER than it first reads — this is the part worth checking before acting

The dev cited objectui#4361 as having ruled that an authored precision IS a display width for CurrencyField, which would cut against moving PercentField to scale. I read it at source and it does not say quite that. packages/fields/src/widgets/CurrencyField.tsx:14-16:

precision is a display width the caller resolved — either the field's authored precision or, when it declared none, the currency's own ISO 4217 minor-unit count (see the derivation at the call site, objectui#4361).

⇒ there, precision is a PARAMETER NAME for a width the CALLER already resolved, not a direct read of field.precision by the widget. PercentField does the opposite: it reads the member straight off the field. So the two are not the same shape, and 「CurrencyField reads precision」 is ⛔ not a precedent for 「a percent widget may read field.precision as a fraction width」.

⚠️ I did not read objectui#4361 itself — only the call-site comment citing it. Whoever takes this card reads the card.

⚠️ One thing this card must NOT be read as asking for

The cell and this widget already disagree today in the absent case (12% against 12.35%), so widget-versus-cell agreement is ⛔ not an invariant that objectui#9295 breaks, and ⛔ not one this card can assume. Whether the two faces should agree at all is part of the question, not a premise.

Dedup words

PercentField widget precision · percentField?.precision ?? 2 · sliderStep Math.pow(10, -precision) · readonly toFixed(precision) percent · percent input step precision

⛔ Not deduped by me (filer attaches the words, triage runs them). ⚠️ Search on this board has produced false zeros before — any zero needs a lit control, and dedup must include CLOSED cards.

Provenance

out_of_scope_findings of the objectui#9295 dev report, class (a) with a stated reason for not repairing in place: it fails the bounded-in-place test on mechanicality, because which precedent governs is a design call rather than a substitution. The seat verified every code reading above independently before filing.


Generated by Claude Code

Activity

  1. self-assigned this
    on Sep 18, 2026
  2. os-sales commented on Sep 18, 2026

    @os-sales
    Collaborator

    Claim: PM loop round 1
    Session: session_01Xm4WFhEe5mwcgyqHjxR2hn
    Branch: claude/issue-9568-percent-widget-scale
    Worktree: objectui-issue-9568
    Domain: domain:ui
    Seat: domain:ui#3 (seat post objectui#9800)
    File surface: packages/fields/src/widgets/PercentField.tsx plus its tests under packages/fields/src/__tests__/ and one .changeset/ entry (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default judgment tier
    Clause-②: no
    Thread-read: 5713392905
    Serial constraints cleared: PR objectui#9798 (seat 1's objectui#9485) is LIVE in the same directory but on packages/fields/src/widgets/FileField.tsx — file surfaces are disjoint, so ordinary concurrency, and the two .changeset/ entries have distinct names. PR objectui#9566 (objectui#9295) MERGED 2026-09-15T16:47Z and its changed-file list does NOT contain PercentField.tsx — that is what leaves this card live. No other in-flight domain:ui card declares packages/fields: seat 2 holds packages/components/src/renderers/overlay/ (#9710), packages/mobile (#9691), packages/plugin-charts (#9675), packages/app-shell/src/views/DeclaredActionsBar.tsx (#9572); seat 1's other three (#9484 #9464 #9462) declare no File surface: line and their PR/branch names place them in app-shell, metadata-admin and components/plugin-view. Queued same-convention siblings objectui#9574 (plugin-form) and objectui#9575 (plugin-list, p3) are deliberately NOT folded into this one — see the fold ruling below — and stay queued.

    Why this is serial-clear rather than a fold — the five gates, answered

    fold-or-serial is mandatory here because three queued cards share one convention (precision vs scale). Answer: NOT folded, dispatched alone. Gate ② fails outright — objectui#9568 is packages/fields, objectui#9574 is packages/plugin-form, objectui#9575 is packages/plugin-list: three packages, three changesets, three queue positions, so they cannot ride one worktree. Gate ① is also weaker than it looks: this card is the edit widget with its own counter-precedent to weigh, while #9574 is a step derivation and #9575 a missing copied key. Triage said so explicitly on this card: 「⛔ 派发时不要合并」.

    ⛔ Excluded from this dispatch and named so they are not mistaken for family members: objectui#9295 / PR objectui#9566 (the read-only cell, the grid summary footer and the detail summary chip — already landed, and its file list proves it did not reach the widget), objectui#9574, objectui#9575.

    This card is the producer-side member of the trio — packages/fields is where the widget convention lives — so it goes first by design, and the consumer-side cards get re-priced against what lands here rather than against their filing-time text.

    Tier, and why no --tier output is quoted

    ⚠️ scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectui REFUSES by design: gate families are a property of the tree the process runs in and that script exists only in objectstack-ai/objectstack, so any answer from there would describe the wrong repo. The tier is therefore this seat's explicit decision and ⛔ no --tier line is quoted, rather than a citation being invented for one. Default judgment tier, because this card carries a real design call (which precedent governs) and not a substitution — the objectui#9295 dev declined to fix it in place for exactly that reason. The contract-review tier is ⛔ not reachable here: it is reserved for the skills seat, the spec seat's clause-② review and the maintainer-summoned director (maintainer ruling 2026-09-10).

    Clause-② no, with the contract text the no rests on

    A no that rests on "the published contract already refuses this" must quote that text. @objectstack/spec declares the pair verbatim as precision: "Total digits (non-negative integer)" and scale: "Decimal places (non-negative integer)" (packages/spec/src/data/field.zod.ts:1157-1158, as quoted on this card). Reading precision as a fraction width is a misread the published text already refuses, so correcting it pulls behaviour back to the declared contract and ⛔ does not widen an accepted set or a public face. The exported widget's props do not change.

    Posted by the domain:ui seat 3 at 2026-09-18T06:03Z.


    Generated by Claude Code

  3. os-sales commented on Sep 18, 2026

    @os-sales
    Collaborator

    os-dev-report

    {
    "issue": 9568,
    "status": "done",
    "branch": "claude/issue-9568-percent-widget-scale",
    "pr": "#9804",
    "session": "session_01Xm4WFhEe5mwcgyqHjxR2hn",
    "premise_still_valid": true,
    "summary": "PercentField now takes its decimal-place width from scale instead of precision, read ONCE and shared by the readonly toFixed face, the number input's step attribute and the slider's step, guarded with typeof because scale: 0 is a valid declaration. A decimal(10, 2) percent field renders 25.00% and steps by 0.01 instead of rendering 25.0000000000% and stepping by 1e-10. An absent scale keeps this widget's own 2, so the repair is invisible to a percent field declaring neither member; the whole-percent convention detected from max > 1 is untouched. No exported type, prop or symbol changes (the widget reads its field bag through a cast), so Clause-2: no holds. Card assignee was os-sales (set by PM) and untouched; the newest Claim: comment 5725884752 names THIS branch and THIS session. The issue body shows NO sanitizer truncation - it ends with its attribution footer and all three fenced blocks plus the dedup-word line are intact.",
    "premise_checks": {
    "zone2_1_defect_sites": "CONFIRMED, line numbers included, on base 1b969ae (the brief's 0ee6e31 had moved; re-read as instructed). const precision = percentField?.precision ?? 2; at :14, the readonly toFixed(precision) at :35, const sliderStep = Math.pow(10, -precision) at :126, the input step={Math.pow(10, -precision).toFixed(precision)} at :140 and the slider step={sliderStep} at :155 - all five still present and still with the stated effect.",
    "zone2_2_counter_precedent_4361": "READ AT SOURCE (body + both comments) and it is NOT a fork - but the card's stated REASON is half wrong and must not be reused. objectui#4361's ruling point 2 says an explicitly authored precision WINS, and CurrencyField resolves it in the widget itself (currencyField?.precision ?? currencyFractionDigits(currency)), so it is a direct read of the field member, not merely 'a parameter name for a width the caller already resolved' as the card states. What still makes it no precedent here: its competing source is a currency's ISO 4217 minor-unit count and never scale; a currency field's face carries no decimal-place member precision could be confused with; and that card deliberately escalated the contract question upstream (objectstack#7918) rather than settling what an authored precision means. It rules nothing about percent and nothing about scale.",
    "zone2_3_scale_reaches_widget": "CONFIRMED at four independent points rather than assumed: (a) FieldSchema in @objectstack/spec declares scale on the flat field face for every field type, with the verbatim 'Decimal places (non-negative integer)'; (b) the metadata-admin designer offers the control under the predicate type in ['number','currency','percent'] (pinned in predicate.test.ts); (c) @objectstack/objectql's record validator enforces scale by REJECTION (max_scale) on the branch that includes t === 'percent'; (d) both detail hosts that build the inline-edit field bag - RecordDetailPanel and RelatedList - already forward scale beside precision, and InlineFieldInput hands that bag straight to PercentField. CAVEAT, filed as finding 1: @object-ui/types' PercentFieldMetadata declares precision and NOT scale, which is why those hosts cast.",
    "zone2_4_sibling_reach": "CONFIRMED. PR objectui#9566 merged 2026-09-15T16:47:37Z with 12 files: .changeset/9295-percent-surfaces-read-scale.md, three PercentCellRenderer* pins, packages/fields/src/index.tsx, packages/plugin-detail/src/DetailView.tsx, three summaryChip.* pins, two useColumnSummary.* pins and packages/plugin-grid/src/useColumnSummary.ts. No widget file, so PercentField.tsx is untouched by it - the premise of the whole card holds."
    },
    "decisions": {
    "readonly_face": "Takes scale; absent scale keeps this widget's own 2. Rule applied: move the MEMBER that is read and nothing else, so the repair is invisible to a field declaring neither member (pinned as a MUST-NOT-CHANGE control). Choosing 0 instead (agreeing with PercentCellRenderer) would change behaviour for every percent field in the tree that declares nothing - a bigger user-visible move than the defect, on no contract basis, and ruling 4 makes agreement neither goal nor premise.",
    "step": "Takes the SAME single scale read with the same absent default, so the input's step attribute and the slider step stay tied to the displayed width. Reason stated rather than inherited: a control whose step is finer than its displayed width offers a value it then rounds away, and the widget's own pre-existing comment already tied slider granularity to the input.",
    "guard": "typeof declaredScale === 'number', not ??. The dispatch's Zone 3 note said a ?? 2 default reintroduces the scale: 0 bug; MEASURED, it does not - 0 ?? 2 is 0, so only || drops a declared zero. typeof was chosen for a different reason: it also refuses a scale: \"2\" arriving from JSON metadata, and it is the guard NumberField and this widget's own max read already use.",
    "no_type_widening": "PercentFieldMetadata was NOT widened to declare scale, although it arguably should (finding 1). The widget reads field as any, so the fix compiles and works without it; widening an exported type would void this card's Clause-2: no per ruling 3, and packages/types is outside the declared file surface."
    },
    "gates": [
    {
    "command": "pnpm exec vitest run packages/fields/ (166 files, 2863 tests, under os-verify-lock.sh)",
    "exit": 0
    },
    {
    "command": "pnpm exec turbo run build --filter=@object-ui/fields --concurrency=2 (10 tasks, under the lock)",
    "exit": 0
    },
    {
    "command": "pnpm --filter @object-ui/fields run type-check (tsc --noEmit && tsc -p tsconfig.test.json, under the lock, AFTER the build)",
    "exit": 0
    },
    {
    "command": "pnpm --filter @object-ui/fields run type-check (BEFORE the build - unbuilt dependency closure, not a defect)",
    "exit": 2
    },
    {
    "command": "pnpm exec eslint . in packages/fields (249 files judged, 0 errors, 1029 pre-existing warnings)",
    "exit": 0
    },
    {
    "command": "pnpm check:control-bytes",
    "exit": 0
    },
    {
    "command": "pnpm check:new-line-citations (0 citations added; re-run on the merged head)",
    "exit": 0
    },
    {
    "command": "pnpm check:test-path-roots",
    "exit": 0
    },
    {
    "command": "node scripts/check-changeset-presence.mjs (re-run on the merged head)",
    "exit": 0
    },
    {
    "command": "pnpm changeset:check (check-changeset-fixed + check-changeset-no-major)",
    "exit": 0
    },
    {
    "command": "pnpm check:changeset-claims (report-only)",
    "exit": 0
    },
    {
    "command": "pnpm check:pending-changeset-literals",
    "exit": 0
    },
    {
    "command": "pnpm check:designer-field-key-parity",
    "exit": 0
    },
    {
    "command": "node scripts/check-component-surface-parity.mjs --type percent (report-only, exits 0 by design; no percent finding printed)",
    "exit": 0
    },
    {
    "command": "pnpm exec vitest run packages/fields/src/tests/PercentField.scale-9568.test.tsx (7 tests; re-run on the merged head 2e421f1)",
    "exit": 0
    },
    {
    "command": "grep -naP '[control-byte class]' over the three changed files (self-scan beyond the gate)",
    "exit": 1
    }
    ],
    "gate_derivation": "scripts/pm/dispatch-gates.mjs refuses to answer for objectui by design, so the list was derived from this repo's own package.json scripts and .github/workflows/ against what the diff really touches. Deliberately NOT run, with reasons: pnpm lint / pnpm test / pnpm type-check whole-farm turbo runs (CI owns them; the affected package's own script was run in full instead), pnpm check (builds the CLI, nothing in the diff reaches it), check:handler-key-reads (covers on* handler keys only - measured from its own header, scale is not in its population), check:vi-mock-* (the new test declares no vi.mock), check:i18n-* (no locale strings moved), test:dist (pins @object-ui/components artifacts). Exit codes were captured with cmd > log 2>&1; EXIT=$? in every case - never across a pipe.",
    "tests": "NEW: packages/fields/src/__tests__/PercentField.scale-9568.test.tsx, 7 cases, every one through the component and reading the DOM it produced - the readonly span's text and the step attribute the browser's spinner obeys - plus one driven keystroke for the slider, whose step is a prop rather than an attribute. REVERSE VERIFICATION (fix committed first, then the pre-fix file put back on disk): mutation proved by object hash (on-disk 8f3636857 == base blob, != HEAD blob 2706ad136) and by anchor counts (percentField?.precision ?? 2 back to 1, declaredScale 0, Math.pow(10, -scale) 0); vitest exit 1 with 6 failed / 1 passed. The single pass is the MUST-NOT-CHANGE control, which is the point of it. Measured failure texts: '25.0000000000%' where '25.00%' expected; step '0.0000000001' where '0.01' expected; slider ArrowRight emitting 0.250000000001 where 0.2501 expected; '25.00%' where '25%' expected for scale: 0. RESTORE proved by hash equality to the HEAD blob and an empty git diff HEAD, from a trap ... EXIT INT TERM with absolute paths. Typecheck coverage of the new test was verified rather than assumed: tsc -p tsconfig.test.json --listFiles lists both the test file and the widget.",
    "files_changed": [
    "packages/fields/src/widgets/PercentField.tsx",
    "packages/fields/src/tests/PercentField.scale-9568.test.tsx",
    ".changeset/9568-percent-widget-reads-scale.md"
    ],
    "deviations": [
    "Base commit: the brief's 0ee6e31 had moved; the worktree was cut from origin/main at 1b969aec6 and origin/main was merged once before the PR (bringing 6a0d1a435, a scripts-only change), giving head 2e421f19. The three differential gates and the new pin were re-run on that merged head.",
    "eslint narrowing, declared: pnpm lint is turbo run lint over every package and belongs to CI. What ran here is @object-ui/fields' own lint script IN FULL (249 files by eslint's own --format json count, both changed files in the population), so nothing inside the affected package was excluded. No config in this repo enables type-aware linting (no parserOptions.project / projectService), so this diff cannot move a verdict on a file it does not touch.",
    "Commit trailers use the repo's own model-free spelling (Co-authored-by: Claude with the noreply anthropic address - angle brackets deliberately not written here, because GitHub's body sanitizer eats tag-shaped fragments - plus Claude-Session:), which is what the last six commits on main carry and what the dispatch contract requires, rather than the model-named spelling the harness reminder offered.",
    "No label write: the dispatch's write budget named only the PR and this comment, and objectui applies path labels from its own labeler.yml workflow. needs:contract-review is a seat's to hang or not; it is neither hung nor removed here, and PR 9804 currently carries no labels.",
    "packages/fields type-check needs its dependency closure built first - the pre-build exit 2 is Cannot find module '@object-ui/components' across the package, not a defect. Reported as its own row rather than dropped."
    ],
    "mcp_calls": "0 - no MCP GitHub tool was called, read or write. Every GitHub touch went through the REST proxy with GITHUB_TOKEN.",
    "api_writes": "2 - POST /repos/objectstack-ai/objectui/pulls (draft PR 9804, HTTP 201, one retry after an HTTP 415 that carried no body and wrote nothing: the first attempt omitted Content-Type) and POST /repos//issues/9568/comments (this report). Branch pushes went over git, not REST: one empty-branch probe push and one content push. PR body read back in full after creation and byte-compared against what was sent - line-for-line identical, both footers intact, session-URL form preserved.",
    "open_questions": [],
    "out_of_scope_findings": [
    "to file (class c, dedupe words: PercentFieldMetadata scale missing / percent field metadata precision only / types field-types percent scale / PercentFieldMetadata precision declared / percent metadata scale undeclared): PercentFieldMetadata in @object-ui/types declares precision?: number and NOT scale, while NumberFieldMetadata beside it declares both plus step. An author - an AI author especially - reading the published interface is offered exactly the member that no longer does anything in this widget, and the member it now reads is undeclared; the two detail hosts that forward scale have to cast to reach it. Plausibly the root cause of this very card. NOT folded in: widening an exported type would void Clause-2: no (ruling 3), and packages/types is outside the declared file surface.",
    "to file (class a, dedupe words: scale above 100 RangeError / toFixed digits out of range percent / maximumFractionDigits out of range / spec scale no upper bound / percent render throws large scale): a spec-valid scale above 100 crashes the render on BOTH percent faces. FieldSchema accepts scale as any non-negative integer with no upper bound, while MEASURED on node 22 (25).toFixed(101) throws RangeError: toFixed() digits argument must be between 0 and 100 and new Intl.NumberFormat('en', { maximumFractionDigits: 101 }) throws RangeError: maximumFractionDigits value is out of range - so PercentField throws and PercentCellRenderer throws. Pre-existing and unchanged in exposure by this branch (the trigger moves from precision to scale); not repaired because one ruling should cover both faces and neither a clamp nor a refusal is this widget's to invent (the spec's own note refuses consumer-invented semantics for a malformed declaration).",
    "to file (class b, dedupe words: percent scale fraction storage magnitude / max_scale percent fraction 100x / percentScaleOf scale display / percent scale stored vs displayed / record validator max_scale percent widget step): scale means STORED decimals to the write path and DISPLAYED decimals to the display path, 100x apart for a fraction-stored percent. A percent field stores a 0-1 fraction unless it declares max > 1 (percentScaleOf in @objectstack/spec, whose numeric-column note states 33.333% reaches the column as 0.33333 - i.e. scale: 5), and the record validator judges scale against that fraction, while objectui#9295's landed and pinned convention applies scale to the percentage-point value. So a fraction-stored field declaring scale: 2 now displays and steps in hundredths of a percentage point while the platform refuses any write finer than one whole percent (max_scale). This branch deliberately follows the landed display convention rather than re-litigating objectui#9295 (triage wrote the fold refusal verbatim), so the widget agrees with the cell beside it; the divergence is real and spans both repositories, and the decision belongs to whoever rules on it.",
    "noted, not filed: the absent-scale disagreement between this widget (12.35%) and PercentCellRenderer (12%). A design question with a real cost either way rather than a defect, and the card names it as part of the question. Successor: the queued same-convention siblings objectui#9574 (plugin-form) and objectui#9575 (plugin-list) are the next seats in this convention family and would be re-priced against any ruling that settles it.",
    "noted, not filed: two prose repairs inside the file this branch already edits - the widget's top doc comment said 'configurable decimal precision' and now names scale, and the recorded Chromium measurement said it was driven with precision: 2 and now says 'a two-decimal width', because the field that produces that width is no longer precision. Neither restates a measurement that was not taken. Successor: this file's next editor."
    ]
    }


    Generated by Claude Code

  4. os-sales commented on Sep 18, 2026

    @os-sales
    Collaborator

    ACCEPT — PR objectui#9804, reviewed against GitHub at 2026-09-18T06:33Z

    domain:ui seat 3 (session session_01Xm4WFhEe5mwcgyqHjxR2hn, Seat: domain:ui#3). Reviewed against the PR's real diff, file list and check runs — ⛔ not against the report's own account of itself. Implemented-by: the dev on claim 5725884752; Reviewed-by: this seat, which is the same session, so the dependent readings below are spot-checks I took myself rather than restatements.

    Checklist conclusion

    item reading
    PR shape draft ✅, base main ✅, body's first line is exactly Fixes #9568 ✅
    Path surface vs the declared fence exact match, no breach — packages/fields/src/widgets/PercentField.tsx, packages/fields/src/__tests__/PercentField.scale-9568.test.tsx, .changeset/9568-percent-widget-reads-scale.md
    Governed face NONE — read off /pulls/9804/files, so this takes the ordinary merge-queue path, ⛔ not the maintainer-visible route
    Changeset present ✅ (so the skip-changeset branch is not in play)
    CI 27 success · 3 skipped · 0 red, 6 still in_progress at review time — an honest converging reading, ⛔ not read as green
    mcp_calls 0, as reported and consistent with api_writes: 2 (the PR + the report comment). No tool from the deny roster appears
    Labels on the PR tests, package: fields — applied by this repo's own labeler.yml, not by the dev

    Spot-checks I took rather than accepted

    • Clause-② no holds, verified on the diff and not on the declaration. The patch adds no export, changes no signature, and touches no published type: the widget still reads its field bag through field as any, so the scale read reaches it without PercentFieldMetadata moving. The entering-the-queue gate's path limb is also clean — no contract-schema file is in the diff.
    • All four precision reads are gone, replaced by ONE guarded read shared by the readonly toFixed face, the input's step attribute and the slider step. Math.pow(10, -scale).toFixed(scale) checks out at both ends: scale: 0 yields step="1" (whole percentage points), scale: 2 yields step="0.01".
    • The reverse verification is the row that earns the accept. The fix was committed, the pre-fix file restored on disk (mutation proved by object hash against the base blob, not by eye), vitest re-run: 6 failed / 1 passed, with measured failure texts — 25.0000000000% where 25.00% was expected, step 0.0000000001 where 0.01 was expected. The single pass is the MUST-NOT-CHANGE control for a field declaring neither member. ⇒ Green here is not "the suite is green", it is "these assertions go red without this change", which is the thing a passing test is usually asked to prove and usually does not.
    • Both gate rows that are not exit 0 are correctly presented and are not failures. type-check before the build exits 2 with Cannot find module '@object-ui/components' (unbuilt dependency closure; exits 0 after the build, and both rows were reported rather than the inconvenient one dropped). The control-byte self-scan exits 1, which for grep means no match — i.e. clean — and it was listed with its raw exit code instead of being silently interpreted.

    ⭐ Correction to MY dispatch brief — a Zone 3 assumption of mine, publicly falsified

    My Zone 3 note asserted that "a ?? 2 style default would reintroduce exactly that bug" for scale: 0. That is wrong, and the dev measured it. 0 ?? 2 evaluates to 0; only || drops a declared zero. The claim was mine, it was in the optional zone precisely so it could be tested, and it was. ⛔ It must not be reused from my brief by anyone reading this thread later.

    typeof is still the right guard, for the reason the dev gives rather than the reason I gave: it also refuses a scale: "2" arriving from JSON metadata, and it is the guard NumberField and this widget's own max read already use.

    Second correction, to this card's own text rather than to my brief: the card argued objectui#4361 is no precedent because there precision is "a PARAMETER NAME for a width the CALLER already resolved". Read at source, that reason is half wrong — CurrencyField resolves it in the widget (currencyField?.precision ?? currencyFractionDigits(currency)), so it IS a direct read of the field member. The card's conclusion survives on different grounds: #4361's competing source is a currency's ISO 4217 minor-unit count and never scale, and that card escalated the contract question upstream (objectstack#7918) rather than settling what an authored precision means. ⚠️ Whoever picks up objectui#9574 or objectui#9575 should take the conclusion and ⛔ not the stated reason.

    Deviations, all declared and all accepted

    1. Base moved under the dispatch. My brief named 0ee6e31; the worktree was cut at 1b969aec6 and origin/main merged once before the PR (head 2e421f19). The dev re-read the base as instructed and re-ran the differential gates on the merged head. This is the brief working as intended, not a deviation against it.
    2. eslint narrowed from the whole-farm turbo run lint to @object-ui/fields' own script in full (249 files, both changed files in the population). Accepted: this repo enables no type-aware linting, so this diff cannot move a verdict on a file it does not touch, and CI owns the farm run.
    3. No label write, correctly — the write budget named only the PR and the report comment. needs:contract-review is this seat's to hang and is deliberately not hung: clause-② is no and verified above on the diff.

    Landing

    Path surface is clean and carries no governed file, so this goes the ordinary route: ready → merge queue once every check is green or an expected skip. ⛔ Not flipped while 6 checks are still in_progress — in_progress is an honest reading and not a green one. ⛔ This seat does not merge it and does not approve it.

    Out-of-scope findings

    Three are class-(a)/(b)/(c) and are being filed as their own cards by this seat, with the dev's dedup words carried over; two are correctly noted, not filed. ⛔ None is folded into this PR. Links follow in the next comment once filed.

    Reviewed by the domain:ui seat 3.


    Generated by Claude Code

  5. os-sales commented on Sep 18, 2026

    @os-sales
    Collaborator

    Out-of-scope findings — filed, as promised in the ACCEPT above

    card class verified by this seat?
    objectui#9807 — PercentFieldMetadata declares precision and NOT scale, so after this PR the published face offers the one member that does nothing (c) AI-authored metadata trap ✅ yes — re-derived at packages/types/src/field-types.ts:427-432, with NumberFieldMetadata at :402-410 as the lit control (it declares both, and documents precision as "Not a display setting")
    objectui#9808 — a spec-valid scale above 100 crashes BOTH percent faces (a) reproducible defect ✅ yes — re-measured on node v22.22.2: toFixed(101) and maximumFractionDigits: 101 both throw RangeError, toFixed(100) does not, so the boundary is exactly 100. ⚠️ The spec's "no upper bound" leg is the dev's reading, not re-verified here
    objectui#9810 — scale means STORED decimals to the write path and DISPLAYED decimals to the display path, ~100x apart for a fraction-stored percent (b) violates a declared contract, cross-repo ⚠️ NO — filed with its readings attributed to the dev. This seat's attempt to re-derive the percentScaleOf leg hit a dead instrument: @objectstack/spec is not installed in this checkout (0 files) and the positive control returned zero too, so that zero is ⛔ not an absence and no conclusion was drawn from it. The card says so on its face

    The two noted, not filed items are correctly not filed: the absent-scale disagreement between this widget and PercentCellRenderer is the design question this card explicitly declines to make a premise (objectui#9810 is where a ruling on it would land), and the two prose repairs rode this diff because it already edits that file.

    ⭐ objectui#9807 is plausibly the root cause of this card — the widget read precision because precision is what the percent metadata face declares. Worth triage seeing it in that light rather than as a tidy-up.

    Landing status at 2026-09-18T06:36Z

    CI on head 2e421f19: 29 success · 3 skipped · 0 red, with the four Test shards still in_progress. Test is this repo's required aggregator context, so ⛔ not flipped to ready and ⛔ not queued yet — 入队资格 is every check green or an expected skip, and in_progress is neither. Path surface carries no governed file, so once the shards land this takes the ordinary ready → merge-queue route. ⛔ This seat does not merge and does not approve.

    Posted by the domain:ui seat 3.


    Generated by Claude Code

  6. os-sales commented on Sep 18, 2026

    @os-sales
    Collaborator

    os-closed-card-sweep — machine-findable marker for this generated comment.

    Removed the pm-loop state label(s) this closed card no longer claims: pm:dispatched.

    A state label claims work is in flight. This card is closed on a merged delivery, so the claim
    is stale; every other label is left exactly as it was found. Nothing here is a judgement about
    the card, and no verdict-bearing label is ever touched by this sweep.

    domain:ui#3 round 10 closed-card residue strip, 2026-09-18T17:05Z

    Generated by Claude Code


    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:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions