Repository navigation
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
Activity
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.tsxplus its tests underpackages/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 onpackages/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 containPercentField.tsx— that is what leaves this card live. No other in-flightdomain:uicard declarespackages/fields: seat 2 holdspackages/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 noFile 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-serialis mandatory here because three queued cards share one convention (precisionvsscale). Answer: NOT folded, dispatched alone. Gate ② fails outright — objectui#9568 ispackages/fields, objectui#9574 ispackages/plugin-form, objectui#9575 ispackages/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 astepderivation 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/fieldsis 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
--tieroutput is quoted⚠️ scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectuiREFUSES by design: gate families are a property of the tree the process runs in and that script exists only inobjectstack-ai/objectstack, so any answer from there would describe the wrong repo. The tier is therefore this seat's explicit decision and ⛔ no--tierline 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 thenorests onA
nothat rests on "the published contract already refuses this" must quote that text.@objectstack/specdeclares the pair verbatim asprecision: "Total digits (non-negative integer)" andscale: "Decimal places (non-negative integer)" (packages/spec/src/data/field.zod.ts:1157-1158, as quoted on this card). Readingprecisionas 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:uiseat 3 at 2026-09-18T06:03Z.
Generated by Claude Code
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 fromscaleinstead ofprecision, read ONCE and shared by the readonlytoFixedface, the number input'sstepattribute and the slider's step, guarded withtypeofbecausescale: 0is a valid declaration. Adecimal(10, 2)percent field renders25.00%and steps by0.01instead of rendering25.0000000000%and stepping by1e-10. An absentscalekeeps this widget's own 2, so the repair is invisible to a percent field declaring neither member; the whole-percent convention detected frommax > 1is untouched. No exported type, prop or symbol changes (the widget reads its field bag through a cast), soClause-2: noholds. Card assignee wasos-sales(set by PM) and untouched; the newestClaim: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 readonlytoFixed(precision)at :35,const sliderStep = Math.pow(10, -precision)at :126, the inputstep={Math.pow(10, -precision).toFixed(precision)}at :140 and the sliderstep={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 authoredprecisionWINS, andCurrencyFieldresolves 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 neverscale; a currency field's face carries no decimal-place memberprecisioncould be confused with; and that card deliberately escalated the contract question upstream (objectstack#7918) rather than settling what an authoredprecisionmeans. It rules nothing about percent and nothing aboutscale.",
"zone2_3_scale_reaches_widget": "CONFIRMED at four independent points rather than assumed: (a)FieldSchemain@objectstack/specdeclaresscaleon 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 predicatetype in ['number','currency','percent'](pinned inpredicate.test.ts); (c)@objectstack/objectql's record validator enforcesscaleby REJECTION (max_scale) on the branch that includest === 'percent'; (d) both detail hosts that build the inline-edit field bag -RecordDetailPanelandRelatedList- already forwardscalebesideprecision, andInlineFieldInputhands that bag straight toPercentField. CAVEAT, filed as finding 1:@object-ui/types'PercentFieldMetadatadeclaresprecisionand NOTscale, 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, threePercentCellRenderer*pins,packages/fields/src/index.tsx,packages/plugin-detail/src/DetailView.tsx, threesummaryChip.*pins, twouseColumnSummary.*pins andpackages/plugin-grid/src/useColumnSummary.ts. No widget file, soPercentField.tsxis untouched by it - the premise of the whole card holds."
},
"decisions": {
"readonly_face": "Takesscale; absentscalekeeps 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 withPercentCellRenderer) 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 singlescaleread with the same absent default, so the input'sstepattribute 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?? 2default reintroduces thescale: 0bug; MEASURED, it does not -0 ?? 2is0, so only||drops a declared zero.typeofwas chosen for a different reason: it also refuses ascale: \"2\"arriving from JSON metadata, and it is the guardNumberFieldand this widget's ownmaxread already use.",
"no_type_widening": "PercentFieldMetadatawas NOT widened to declarescale, although it arguably should (finding 1). The widget readsfield as any, so the fix compiles and works without it; widening an exported type would void this card'sClause-2: noper ruling 3, andpackages/typesis 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.mjsrefuses to answer for objectui by design, so the list was derived from this repo's ownpackage.jsonscripts and.github/workflows/against what the diff really touches. Deliberately NOT run, with reasons:pnpm lint/pnpm test/pnpm type-checkwhole-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(coverson*handler keys only - measured from its own header,scaleis not in its population),check:vi-mock-*(the new test declares novi.mock),check:i18n-*(no locale strings moved),test:dist(pins@object-ui/componentsartifacts). Exit codes were captured withcmd > 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 thestepattribute 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 ?? 2back to 1,declaredScale0,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 forscale: 0. RESTORE proved by hash equality to the HEAD blob and an emptygit diff HEAD, from atrap ... EXIT INT TERMwith absolute paths. Typecheck coverage of the new test was verified rather than assumed:tsc -p tsconfig.test.json --listFileslists 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's0ee6e31had moved; the worktree was cut fromorigin/mainat1b969aec6andorigin/mainwas merged once before the PR (bringing6a0d1a435, a scripts-only change), giving head2e421f19. The three differential gates and the new pin were re-run on that merged head.",
"eslint narrowing, declared:pnpm lintisturbo run lintover every package and belongs to CI. What ran here is@object-ui/fields' ownlintscript IN FULL (249 files by eslint's own--format jsoncount, both changed files in the population), so nothing inside the affected package was excluded. No config in this repo enables type-aware linting (noparserOptions.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: Claudewith the noreply anthropic address - angle brackets deliberately not written here, because GitHub's body sanitizer eats tag-shaped fragments - plusClaude-Session:), which is what the last six commits onmaincarry 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 ownlabeler.ymlworkflow.needs:contract-reviewis a seat's to hang or not; it is neither hung nor removed here, and PR 9804 currently carries no labels.",
"packages/fieldstype-check needs its dependency closure built first - the pre-build exit 2 isCannot 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):PercentFieldMetadatain@object-ui/typesdeclaresprecision?: numberand NOTscale, whileNumberFieldMetadatabeside it declares both plusstep. 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 forwardscalehave to cast to reach it. Plausibly the root cause of this very card. NOT folded in: widening an exported type would voidClause-2: no(ruling 3), andpackages/typesis 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-validscaleabove 100 crashes the render on BOTH percent faces.FieldSchemaacceptsscaleas any non-negative integer with no upper bound, while MEASURED on node 22(25).toFixed(101)throwsRangeError: toFixed() digits argument must be between 0 and 100andnew Intl.NumberFormat('en', { maximumFractionDigits: 101 })throwsRangeError: maximumFractionDigits value is out of range- soPercentFieldthrows andPercentCellRendererthrows. Pre-existing and unchanged in exposure by this branch (the trigger moves fromprecisiontoscale); 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):scalemeans 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 declaresmax > 1(percentScaleOfin@objectstack/spec, whose numeric-column note states 33.333% reaches the column as 0.33333 - i.e.scale: 5), and the record validator judgesscaleagainst that fraction, while objectui#9295's landed and pinned convention appliesscaleto the percentage-point value. So a fraction-stored field declaringscale: 2now 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-scaledisagreement between this widget (12.35%) andPercentCellRenderer(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 namesscale, and the recorded Chromium measurement said it was driven withprecision: 2and now says 'a two-decimal width', because the field that produces that width is no longerprecision. Neither restates a measurement that was not taken. Successor: this file's next editor."
]
}
Generated by Claude Code
ACCEPT — PR objectui#9804, reviewed against GitHub at 2026-09-18T06:33Z
domain:uiseat 3 (sessionsession_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 claim5725884752;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 exactlyFixes #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.mdGoverned face NONE — read off /pulls/9804/files, so this takes the ordinary merge-queue path, ⛔ not the maintainer-visible routeChangeset present ✅ (so the skip-changesetbranch is not in play)CI 27 success · 3 skipped · 0 red, 6 still in_progressat review time — an honest converging reading, ⛔ not read as greenmcp_calls0, as reported and consistent with api_writes: 2(the PR + the report comment). No tool from the deny roster appearsLabels on the PR tests,package: fields— applied by this repo's ownlabeler.yml, not by the devSpot-checks I took rather than accepted
- Clause-②
noholds, 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 throughfield as any, so thescaleread reaches it withoutPercentFieldMetadatamoving. The entering-the-queue gate's path limb is also clean — no contract-schema file is in the diff. - All four
precisionreads are gone, replaced by ONE guarded read shared by the readonlytoFixedface, the input'sstepattribute and the slider step.Math.pow(10, -scale).toFixed(scale)checks out at both ends:scale: 0yieldsstep="1"(whole percentage points),scale: 2yieldsstep="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%where25.00%was expected,step0.0000000001where0.01was 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 0are correctly presented and are not failures.type-checkbefore the build exits 2 withCannot 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 forgrepmeans 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
?? 2style default would reintroduce exactly that bug" forscale: 0. That is wrong, and the dev measured it.0 ?? 2evaluates to0; 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.typeofis still the right guard, for the reason the dev gives rather than the reason I gave: it also refuses ascale: "2"arriving from JSON metadata, and it is the guardNumberFieldand this widget's ownmaxread 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
precisionis "a PARAMETER NAME for a width the CALLER already resolved". Read at source, that reason is half wrong —CurrencyFieldresolves 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 neverscale, and that card escalated the contract question upstream (objectstack#7918) rather than settling what an authoredprecisionmeans.⚠️ Whoever picks up objectui#9574 or objectui#9575 should take the conclusion and ⛔ not the stated reason.Deviations, all declared and all accepted
- Base moved under the dispatch. My brief named
0ee6e31; the worktree was cut at1b969aec6andorigin/mainmerged once before the PR (head2e421f19). 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. - eslint narrowed from the whole-farm
turbo run lintto@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. - No label write, correctly — the write budget named only the PR and the report comment.
needs:contract-reviewis this seat's to hang and is deliberately not hung: clause-② isnoand 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_progressis 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:uiseat 3.
Generated by Claude Code
- Clause-②
Out-of-scope findings — filed, as promised in the ACCEPT above
card class verified by this seat? objectui#9807 — PercentFieldMetadatadeclaresprecisionand NOTscale, 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, withNumberFieldMetadataat:402-410as the lit control (it declares both, and documentsprecisionas "Not a display setting")objectui#9808 — a spec-valid scaleabove 100 crashes BOTH percent faces(a) reproducible defect ✅ yes — re-measured on node v22.22.2: toFixed(101)andmaximumFractionDigits: 101both throwRangeError,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 hereobjectui#9810 — scalemeans 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 thepercentScaleOfleg hit a dead instrument:@objectstack/specis 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 faceThe two
noted, not fileditems are correctly not filed: the absent-scaledisagreement between this widget andPercentCellRendereris 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
precisionbecauseprecisionis 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 fourTestshards stillin_progress.Testis this repo's required aggregator context, so ⛔ not flipped to ready and ⛔ not queued yet — 入队资格 is every check green or an expected skip, andin_progressis 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:uiseat 3.
Generated by Claude Code
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.- Closing pull request: fix(fields): the percent edit widget reads
scalefor its fraction width (objectui#9568) #9804, merged. - Closing commit
9aa2a573bd, merged intomain. - Left untouched:
priority:p2,domain:ui— ownership, priority and outcome are not state claims. - The label set was read back after the write and matched.
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
- Closing pull request: fix(fields): the percent edit widget reads
Surfaced by the
os-devimplementing objectui#9295 (PR objectui#9566) as an out-of-scope finding, and filed by the dispatchingdomain:uiseat (session_01KSd9P5u2Mf4p8g4n4SD4Fx,os-justin) because the dev correctly declined to fix it in place. ⛔ Not graded and ⛔ not routed by me —domain:*,typeandpriority:*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 reportpackages/fields/src/widgets/PercentField.tsx:@objectstack/specdeclaresprecisionas "Total digits (non-negative integer)" andscaleas "Decimal places (non-negative integer)" (packages/spec/src/data/field.zod.ts:1157-1158; the column face carries the same pair atui/view.zod.ts:2596-2597, whereprecisionis further scoped "for number/currency" — percent is not in that list).⇒ For a
decimal(10, 2)percent field the widget readsprecision = 10, so:25.0000000000%, and1e-10.A step of
1e-10on 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:⇒ the sibling widget in the same directory has already made this exact correction.
PercentFieldis the one that did not move.The dev cited objectui#4361 as having ruled that an authored
precisionIS a display width forCurrencyField, which would cut against movingPercentFieldtoscale. I read it at source and it does not say quite that.packages/fields/src/widgets/CurrencyField.tsx:14-16:⇒ there,
precisionis a PARAMETER NAME for a width the CALLER already resolved, not a direct read offield.precisionby the widget.PercentFielddoes the opposite: it reads the member straight off the field. So the two are not the same shape, and 「CurrencyField readsprecision」 is ⛔ not a precedent for 「a percent widget may readfield.precisionas a fraction width」.The cell and this widget already disagree today in the absent case (
12%against12.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_findingsof 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