Repository navigation
fix(fields): the percent edit widget reads scale for its fraction width (objectui#9568) - #9804
Conversation
…idth `PercentField` took `precision` as its decimal-place count for the readonly face's `toFixed` width, the number input's `step` attribute and the slider's step. `@objectstack/spec` declares `precision` as "Total digits" and `scale` as "Decimal places" on the field face, so a `decimal(10, 2)` percent field rendered `25.0000000000%` and offered a `1e-10` step — a granularity nobody can operate, on the one face the user types into. The width now comes from `scale`, guarded with `typeof` rather than truthiness because `scale: 0` is a valid declaration. An absent `scale` keeps this widget's own two decimals, so the repair is invisible to a percent field that declares neither member; the whole-percent convention detected from a declared `max` above 1 is untouched. `NumberField` in the same directory already read `scale` for its step, and objectui#9295 made the same correction on the read-only cell, the grid summary footer and the record summary chip. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xm4WFhEe5mwcgyqHjxR2hn
…cent-widget-scale
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
|
| item | reading |
|---|---|
| Review verdict | ACCEPT, recorded on objectui#9568 (comment 5726162951) |
CI on head 2e421f19 |
33 success · 3 skipped · 0 pending · 0 non-success |
| The 3 skips | Test (coverage), dependabot, and Test (coverage shard ${{ matrix.shard }}/4) — an uninterpolated matrix placeholder. All expected skips, so the entry qualification 「每 check 绿或预期 skip」 is met |
| Draft flag | flipped to ready, confirmed by GET /pulls/9804 (draft: false) rather than by a status code |
| Mergeable | mergeable_state: clean |
| Path surface | no governed file ⇒ ordinary merge-queue route, not the maintainer-visible one |
What is blocking, exactly
PUT /repos/objectstack-ai/objectui/pulls/9804/ccr/auto_merge was refused by this environment's auto-mode classifier, reason [Self-Approval].
The cause is the shared protocol identity, not the PR: the dev pushed this branch under os-sales, which is the same GitHub account this seat runs on, so the author of the PR and the hand enabling auto-merge are one account. The classifier cannot tell a seat from its own dev under one token — and on that reading its refusal is correct, which is why this seat is ⛔ not arguing with it.
What this seat will ⛔ NOT do about it
- ⛔ No MCP
update_pull_requestand ⛔ no MCP auto-merge — both are denied by lock 1 and forbidden by the dispatch protocol independently of this refusal. - ⛔ No
PUT .../merge. This seat never merges a PR, and never one it dispatched. - ⛔ No second attempt through another channel. Retrying a refused write through a different door is the same act, and the protocol treats it as such.
What would unblock it — either is fine, and it is not this seat's call
- A maintainer enables auto-merge or takes it through the queue. Nothing else is outstanding: the review is done, CI is fully green, the branch is clean and the path surface carries no governed file.
- A Bash permission rule allowing this seat the
ccr/auto_mergeendpoint, if the intent is for seats to queue their own dispatched PRs. That is a permissions decision for the maintainer, ⛔ not something this seat edits for itself.
check-half-states, the reviewed-and-ready-but-unlanded class) — so it is recorded here rather than left to be rediscovered. This seat keeps it watched until it is merged or closed.
Posted by the domain:ui seat 3. The same refusal will apply to PR objectui#9812 when its checks go green; it is reported once, here, rather than twice.
Generated by Claude Code
⭐ Correction to this seat's own blocker note above — the identity explanation is REFUTED by measurementPosted by the My note above said the
| 2026-09-18T05:50:17Z | One second between ready and queued, by the same account that authored it — and that happened ten minutes before this seat was opened. Wider reading, same direction: of the 30 most recently merged ⇒ The account is not the obstacle, the repository permits it, and queueing its own dispatched PR is what peer seats actually do. The refusal is a property of THIS session's auto-mode configuration, not of the identity, the repo, or the dispatch protocol. What changes, and what does not
Corrected by its author rather than left standing. The original note is above and unedited. Generated by Claude Code |
Why this PR is reviewed, green and still not queued — the record, for all SEVEN of them
The R7 half-state sweep files this PR and objectui#9812 under H12 — "ready (= reviewed, in this protocol) with auto-merge unarmed and no activity for ~7h … an orphan landing: the PR left the merge queue (or never entered it) and no one is handling it." The row is right that nothing is queueing them. It is wrong that no one is handling it, and here is what is. The blocker, measured rather than assertedThe landing action available to this seat is ⭐ The wall is LOCAL to this session, ⛔ not GitHub's, and the measurement says so: objectui#9848 — same repository, same The seven, and their state as of this commentobjectui#9804 · #9812 · #9826 · #9845 · #9854 · #9872 · #9873 — every one open, non-draft, reviewed with an
What unblocks itEither a permission rule for that one endpoint in this session, or the maintainer queues the seven directly. Raised with the maintainer at each round report since 2026-09-18T10:59Z. ⛔ Nothing here is a request to any reviewer of this PR, and ⛔ nothing about this PR's own diff is in question. Generated by Claude Code |
Fixes #9568
Clause-②: noWhat moved
PercentFieldtookprecisionas its decimal-place count in three places atonce — the readonly face's
toFixedwidth, the number input'sstepattribute, and the slider's step.
@objectstack/specdeclares the pair on thefield face in its own words:
precisionis "Total digits (non-negativeinteger)" and
scaleis "Decimal places (non-negative integer)". So a percentfield carrying an accurate
decimal(p, s)pair was padded out to the column'sTOTAL width: a
decimal(10, 2)percent field rendered25.0000000000%readonly, offered
step="0.0000000001"while editing, and moved its slider in1e-10increments. The step half is not cosmetic — it is a control nobody canoperate for the value it is declared to edit.
The width now comes from
scale, read once and shared by all three faces,guarded with
typeofrather than truthiness becausescale: 0is a validdeclaration (a percent field that edits whole percents). This is the correction
NumberFieldin the same directory already carried for its own step, and theone objectui#9295 made on the read-only cell, the grid summary footer and the
record summary chip. The edit widget is the face that did not move.
The two decisions, taken explicitly
The dispatch asked for the readonly face and the step to be decided separately
rather than one falling out of the other.
scale, and an absentscalekeeps thiswidget's own two decimals. The rule applied is move the MEMBER that is
read, and nothing else: the repair is therefore invisible to every percent
field that declares neither member, which is pinned below as a
MUST-NOT-CHANGE control.
scaleread, with thesame absent default. Tied on purpose: a control whose step is finer than its
displayed width offers the user a value it then rounds away, and the widget's
own pre-existing comment already tied slider granularity to the input's.
PercentCellRendererspells theabsent-
scalecase as zero fraction digits and pins it, so the two faces stilldisagree for a field that declares nothing —
12%in a grid cell against12.35%in the editor — exactly as they did before this branch. objectui#9568is explicit that widget-versus-cell agreement is neither a goal nor a premise;
closing that gap changes behaviour for every percent field in the tree that
declares nothing, which needs its own ruling rather than arriving as a side
effect of moving the member that is read.
typeofand not??, since the dispatch's suggested route named?? 2asreintroducing the truthiness bug: measured,
??is not that bug —0 ?? 2is0, so a declaredscale: 0survives it and only||would drop it. Thetypeofguard was chosen for a different reason: it also refuses ascale: "2"arriving from JSON metadata, and it is the guardNumberFieldandthis widget's own
maxread already use.The card's premises, re-measured on the base commit
All four held, one with a correction worth reading.
precisionread, thereadonly
toFixed, thesliderStepderivation, the inputstepand theslider
stepprop, all five inPercentField.tsxon the base commit.files (
packages/fields/src/index.tsx,plugin-grid/src/useColumnSummary.ts,plugin-detail/src/DetailView.tsx, their pins and one changeset) and nowidget file among them. That absence is what leaves this card live.
scalereaches this widget. Confirmed at four independent points, notassumed:
FieldSchemain@objectstack/specdeclaresscaleon the fieldface for every field type; the metadata-admin designer offers the control for
type in ['number','currency','percent'];@objectstack/objectql's recordvalidator enforces
scaleby REJECTION on thepercentbranch of itsnumeric arm; and both detail hosts that build the field bag for inline edit
(
RecordDetailPanelandRelatedList) forwardscaleexplicitly next toprecision. The widget reads its field bag through a cast, so no publishedtype changes — see the acceptance notes for the type-surface gap that leaves.
does NOT rule that an authored
precisionis a fraction width forpercent-like widgets, so this card stands — but the card's stated reason is
half wrong and should not be reused. The card says
precisioninCurrencyFieldis only "a parameter name for a width the caller alreadyresolved"; in fact the widget itself resolves it, reading the field's own
precisionfirst and falling back to the currency's ISO 4217 minor-unitcount, and objectui#4361's ruling is precisely that the authored
precisionwins over that derived count. What makes it not a precedent here is narrower
and still decisive: its competing source is a currency's minor-unit count,
never
scale; a currency field's face carries no decimal-place member forprecisionto be confused with; and that card deliberately pushed thecontract question upstream (objectstack#7918) instead of settling what an
authored
precisionmeans. No fork, so no stop.Evidence
packages/fields/src/__tests__/PercentField.scale-9568.test.tsx— seven cases,all through the component and reading the DOM it produced (the readonly span's
text, the
stepattribute the browser's spinner obeys) plus one drivenkeystroke for the slider, whose step is a prop rather than an attribute.
Reverse verification — the fix was committed first, then the pre-fix file
was put back on disk, with the mutation proved by object hash (on-disk hash
equal to the base blob, unequal to the HEAD blob) and by counting the injected
and deleted anchors (
percentField?.precision ?? 2back to 1 occurrence,declaredScaleandMath.pow(10, -scale)down to 0). The restore was provedthe same way, by hash and by an empty
git diff HEAD, from a trap onEXITINTTERMwith absolute paths.The one case that passes on the defect is the MUST-NOT-CHANGE control, which is
the point of it. The six failures read as the card describes:
25.0000000000%where
25.00%is expected,step0.0000000001where0.01is expected, andone ArrowRight on the slider emitting
0.250000000001instead of0.2501.Gates, with each exit code captured BEFORE any pipe
scripts/pm/dispatch-gates.mjsrefuses to answer for this repository bydesign, so this list was derived from this repo's own
package.jsonscripts and.github/workflows/, against what the diff actually touches.pnpm exec vitest run packages/fields/(166 files, 2863 tests)pnpm exec turbo run build --filter=@object-ui/fields --concurrency=2(10 tasks)pnpm --filter @object-ui/fields run type-checkpnpm exec eslint .inpackages/fields(249 files judged, 0 errors)pnpm check:control-bytespnpm check:new-line-citations(0 added, re-run after the merge)pnpm check:test-path-rootsnode scripts/check-changeset-presence.mjspnpm changeset:checkpnpm check:changeset-claimspnpm check:pending-changeset-literalspnpm check:designer-field-key-paritynode scripts/check-component-surface-parity.mjs --type percent(report-only)Two readings behind that table rather than only the codes:
type-checkrun exited 2 and was not a defect — it wasCannot find module '@object-ui/components'and its siblings across the wholepackage, i.e. an unbuilt dependency closure. The build above is what makes the
reading valid, and the 0 is the post-build run.
eslintnarrowing is declared.pnpm lintisturbo run lintoverevery package and belongs to CI; what ran here is this package's own
lintscript in full, so nothing inside
@object-ui/fieldswas excluded — the filecount comes from eslint's own
--format jsonoutput and both changed filesare in it. No config in this repo enables type-aware linting (no
parserOptions.project/projectService), so this diff cannot move a verdicton a file it does not touch.
origin/main; the table'ssuite figures are from the pre-merge run of the same package, and the pin plus
the three differential gates were re-run on the merged head.
Acceptance notes
Out-of-scope findings, with dedup words attached for the filing seat — none of
them repaired here.
PercentFieldMetadatain@object-ui/typesdeclaresprecisionand notscale.NumberFieldMetadatabeside it declares both (andstep), andboth detail hosts that forward
scalehave to cast to reach it. An author —and 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. This is why the fix needed no type change
(the widget reads its field bag through a cast) and why it was NOT folded in:
widening an exported type would void this card's
Clause-②: no. Dedup words:PercentFieldMetadata scale missing·percent field metadata precision only·
types field-types percent scale·PercentFieldMetadata precision declared·percent metadata scale undeclared.scaleabove 100 crashes the render, on both faces.FieldSchemaacceptsscaleas any non-negative integer with no upperbound, while
Number.prototype.toFixedthrowsRangeErrorabove 100 digits(measured) and
Intl.NumberFormatrefuses the same range formaximumFractionDigits— so the widget andPercentCellRendererboth throwrather than render. Pre-existing and unchanged by this branch (the trigger
moves from
precisiontoscale); not repaired here because one rulingshould cover both faces and neither the clamp nor the refusal is this
widget's call to invent. Dedup 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.scalemeans stored decimals to the write path and displayed decimals tothe display path, 100x apart for a fraction-stored percent. A percent field
stores a 0-1 fraction unless it declares
max > 1, so the record validatorjudges
scaleagainst the fraction (33.333%needsscale: 5, as thespec's own numeric-column note states), while objectui#9295's landed and
pinned convention applies
scaleto the percentage-point value. Afraction-stored field declaring
scale: 2therefore renders and steps inhundredths of a percentage point while the platform will refuse a write
finer than one whole percent. This branch deliberately follows the landed
display convention rather than re-litigating it — the two faces agreeing with
each other is worth more than this widget alone being right — but the
divergence is real and spans both repositories. Dedup 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.Noted, not filed:
scaledisagreement between the widget and the cell (12.35%against
12%). A design question with a real cost either way, not a defect,and the card names it as part of the question rather than the answer.
Successor: the queued same-convention siblings objectui#9574 (
plugin-form)and objectui#9575 (
plugin-list) are the next seats in this file family andwould be re-priced against whatever ruling settles it.
names
scale; the recorded Chromium measurement in the same file said it wasdriven with
precision: 2and now says "a two-decimal width", because thefield that produces that width is no longer
precision. Neither restates ameasurement that was not taken.
Session that produced this branch:
session_01Xm4WFhEe5mwcgyqHjxR2hn, seatdomain:ui#3.🤖 Generated with Claude Code
https://claude.ai/code/session_01Xm4WFhEe5mwcgyqHjxR2hn
Generated by Claude Code