Repository navigation
feat(types): declare scale on PercentFieldMetadata (objectui#9784) - #9840
Conversation
The percent face declared `precision` / `min` / `max` and not `scale`, while `scale` is the member the percent renderers actually read for their decimal width. An author following the published type wrote `precision` and silently got zero decimal places. The shape is copied verbatim from `NumberFieldMetadata` in the same module, which already declares both members and spells the distinction out. `precision` is untouched: the installed `@objectstack/spec` declares the pair on its field face and its door accepts both on a percent field document. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L
✅ 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
|
Contract reviewServed-tier: ① Derived judgmentsDiff read from GET /pulls/9840 (application/vnd.github.diff) and cross-checked against
Nothing in the diff is unclaimed by the body: the three files, the two source lines, the docblock copy and the pin are all described, and the body's "I did not touch precision" is what the diff shows. Net: this PR widens what an author may write by one optional key, narrows nothing, changes no behaviour; that is precisely what the card and its two rulings authorise. Premise still valid at review time: origin/main (177d379) is 3 commits past the merge-base 3ae740c; ② Semver levelDeclared: Match with ①: an additive optional member on a published interface is a widening, which is minor, not patch. In this repo the fixed group forbids major (AGENTS.md 版本号策略, mechanised by the no-major gate), so minor is the highest level a widening can carry here. The changeset says which face moved, in those words: "the published authoring face of the percent field type", and states the excess-property fact that ① reproduced, plus "Nothing about how percent values render moves here", which the file list confirms. Its "copied verbatim from NumberFieldMetadata" claim is the 2-then-1 grep reading above. Its "I did not touch precision" clause matches the zero-deletion diff. No contradiction between level, prose and diff. ② passes. Nit, not a contract defect: the changeset body is written in the first person ("I added", "I did not touch"). It is the only one of the 1684 pending changesets at head that is (git grep, 1 file), and changeset bodies publish verbatim into the CHANGELOG. A rewrite into the repo's impersonal voice would be welcome but is not required for this verdict. ③ Boundary flagsMeasurements the body leans on, each reproduced from a
The implementer's open question — commit trailer names a model ("Co-Authored-By: Claude Opus 5") against a dispatch-contract rule the implementer says wants a model-free pair. Answer: the repository's AGENTS.md at origin/main sets no commit-trailer rule at all (grep for Co-Authored / trailer finds nothing but the force-push ban) and bans force-push absolutely (line 316, under 多 agent 协作纪律, not "section 3" as the report says). Option A stands: nothing repo-side is violated, and option B is unreachable without a banned force-push. The conflict is between the dispatch contract and the harness reminder, neither of which is among this review's inputs, so it is ESCALATED to the commissioning seat to reconcile those two documents; no action on this branch. Out-of-scope note 1 — CurrencyFieldMetadata. Confirmed at head: it declares precision and no scale (the arms census above). Read sites at origin/main: GridField.tsx line 321 Out-of-scope note 2 — no docblocks on the percent arm's precision, min, max. Re-documenting Out-of-scope note 3 — Reviewer-raised flags, none blocking:
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
Provenance —
⭐ Served tier — read from the reviewer's own transcript, ⛔ not from
|
| probe | reading |
|---|---|
SUBJECT — message.model over the review subagent's type:"assistant" records |
55 / 55 claude-fable-5-1 = CONTRACT_REVIEW_TIER, zero records reading anything else |
⭐ FIRING CONTROL — the same extractor over this round's three os-dev transcripts |
claude-opus-5 × 146, × 129, × 60 |
⇒ The field discriminates, so the tier reading is a reading. ⛔ Self-declared tier and a passed model: argument are both excluded by 「⛔ 自述档位与传参皆非读数」; this seat serves below CONTRACT_REVIEW_TIER, so it ⛔ did not self-review — it commissioned an isolated at-tier reviewer, fed it only the card, the rulings and the PR, and ⛔ neither the dispatch order nor this seat's own conclusions.
Independence pair: Implemented-by: claude/issue-9784-percent-field-metadata-scale · Reviewed-by: session_01UanLVj6xvbS6puBCewLr8L — ⛔ not the same identity, so this is not a self-review.
⭐ The record was adopted verbatim. ⛔ Nothing in it was rewritten, trimmed or polished — including the two flags that are mildly inconvenient (the first-person changeset prose, and the pin's regex coupling this types-package test to a file objectui#9808 holds). The route is adopt-entire or void-entire; there is no edit in between.
Two items the record escalates, and where they go
CurrencyFieldMetadatamay carry the same defect class — ⇒ a filing candidate for triage, ⛔ not a defect of this PR and ⛔ not folded in. The reviewer confirms every current read reaches the member through an untyped or inline carrier, ⛔ never through the interface, and that no runtime reading was taken.- The commit-trailer conflict — between this seat's dispatch contract and the harness attribution reminder, ⛔ neither of which was among the review's inputs. The reviewer is right that it is not the branch's problem: AGENTS.md sets no trailer rule and bans force-push outright, so the alternative is unreachable. ⇒ It is this seat's to reconcile, and it goes to the skills lane as a 平台事实/原则 item, ⛔ not onto this PR.
Read at 2026-09-18T09:22Z.
Generated by Claude Code
Fixes #9784
What I did
I added
scaletoPercentFieldMetadatain@object-ui/types, copied verbatim from theNumberFieldMetadataarm in the same module. I did not touchprecision— see Whyprecisionstays below, which is a reading, not an omission.Three files: the declaration (two lines), a pin under
packages/types/src/__tests__/, anda changeset.
path:linecitations, and the dispatch asked for line numbers. Every address below istherefore written as a dated reading taken at commit
6b63264b11, ⛔ not as a livepointer — the thing that re-derives each of these facts is the pin this PR adds, which
cites by content. Do not repair an address here; re-run the pin.
The four legs, read at source on this branch
Base:
git merge-base HEAD origin/mainon my branch printed3ae740c4a8da5d8778e3290febb1fd2b1333fcce— identical to the tip the dispatch quoted, andcomputed rather than taken from it. The checkout is not shallow
(
git rev-parse --is-shallow-repositoryprintsfalse), so the history readings belowcarry no shallow-window caveat.
Leg 1 — the percent face declared
precision/min/maxand noscaleAt the base commit,
PercentFieldMetadatainpackages/types/src/field-types.tsread:⭐ Lit control, same instrument and same corpus.
grep -n "scale" packages/types/src/field-types.tson the base commit returns exactly one hit —
scale?: number;, the member ofNumberFieldMetadata(dated address: line 409). So the instrument reads this file and thezero on the percent arm above is a reading of absence, not a dead probe. That same control
leg is carried into the pin as a test case, so it keeps having to answer.
Leg 2 —
PercentFieldMetadatais publishedpackages/types/src/index.tsnames it in the type-export block, verbatim on this branch:The package's
filesarray shipsdist, and afterpnpm --filter @object-ui/types buildthe emitted
packages/types/dist/field-types.d.tscarries the new member with itsdocblock. So this is a widening of a face consumers actually receive, not an internal note.
Leg 3 — the percent CELL renderer reads
scalepackages/fields/src/index.tsx, insidePercentCellRenderer(dated address: line 826):Its own comment names the correction and the authority: "Decimal places come from
scale,NOT
precision— the same correction objectui#2131 made on the currency arm andobjectui#2134 on the number arm, arriving one type later (objectui#9295)", and it quotes
@objectstack/specdeclaringprecisionas "Total digits (non-negative integer)" andscaleas "Decimal places (non-negative integer)". ⇒ An author who followed the publishedtype and wrote
precisionhit the?? 0branch and silently got zero decimals.I read this file and did not write to it.
Leg 4 — the second reader is an OPEN pull request, and I state it as such
main. I verified its statemyself rather than taking the dispatch's word:
GET /repos/objectstack-ai/objectui/pulls/9804answered
state: open,draft: false,merged: false, head2e421f196ecb, basemain,titled "fix(fields): the percent edit widget reads
scalefor its fraction width". Soan open pull request has the percent EDIT widget reading
scaletoo; onmaintodaythat second read does not yet exist. My pin deliberately does not assert anything about
that file, so nothing here races that branch.
⭐ A fifth reading the card did not carry — there is a THIRD reader, already on
mainpackages/plugin-detail/src/DetailView.tsx(dated address: line 1233) reads the samemember for the detail summary chip:
Its comment routes the chip onto the list cell as the authority and records that the member
moved with objectui#9295. So the undeclared key had two readers on
main(list cell +detail chip) before the open pull request adds a third. That strengthens the card's premise;
it does not change the fix.
Why
precisionstays — and why this is not a forkThe dispatch gave me an exit if I concluded
precisioncould not stay alongsidescale. Idid not reach that conclusion, and here is what I read instead.
The installed
@objectstack/specdeclares the two as a pair on one field face —FieldSchemain@objectstack/spec/data, under aNumber Constraintsheading:And its door accepts both on a percent field document. Measured against the installed
spec on this branch, and pinned in the test:
FieldSchema.safeParsescale: 2precision: 10scale: 2.5unrecognized_keysThat last row is the control: the door is strict, so the two acceptances above are readings
rather than a passthrough. ⇒
precisionis a spec-legal declaration on this face, andkeeping it is the spec-conformant state under commandment #0 (spec declaration outranks
implementation). Narrowing or retiring it would move a published accept set against the
spec's own answer. ⛔ I did not narrow it, deprecate it, retire it or re-document it, and
this PR takes no position on whether it should ever move.
Clause-②: yes, and the changeset says which face widened
The dispatching seat graded this
Clause-②: yeson the carrier rule — a new key on apublished payload — and I re-declare it on the same ground, independently of the key
already being read at runtime. The authoring face genuinely widens: a
PercentFieldMetadataobject literal carryingscalefailed excess-property checking at thebase commit and type-checks after this lands.
BaseFieldMetadatahas no index signature, sothat check is real. The changeset names the widened face in those words and declares
minor(this repo's version policy forbids
majorin a changeset;check:changeset-no-majorisgreen).
needs:contract-reviewrides on this PR from the moment it exists. ⛔ I do not clear it —that is the reviewing seat's act.
The pin, and its ablation
packages/types/src/__tests__/percent-field-metadata-scale-9784.test.ts, in this package'shouse form (the neighbour it copies is
field-metadata-depends-on-declared-6153.test.ts).It pins substance on two instruments, because membership pins here are type-level and do
not bite under the vitest runtime:
tsc -p tsconfig.test.json, this package'stype-checkchain): the invariant-equality helper asserts the percent
scaleis the same shape as thenumber arm's, an annotated literal carrying
scalemust compile, one carrying both digitcounts must compile, and a near-miss spelling must still be refused.
scaledocblock between the twoarms, with the number arm as the control; that
precisionis still declared; the readsite in the cell renderer; and the spec-door probe above with its bogus-key control.
with absolute paths, with the mutation proved on disk before either instrument ran:
Restoration proved by state, not by an exit code:
git diff HEADempty, working tree clean,and
git hash-object packages/types/src/field-types.tsback to0c8342c50bdb3fbe7db517b333e9d9880dadb7cb.⛔ Note the
AFTERcount is 1, not 0 — the docblock line I deleted from the percent armis byte-identical to the number arm's, which is the whole point of the parity pin, so the
surviving occurrence is the control. A
0there would have meant I had ablated the controltoo.
Verification run on this branch
Repo-root invocation throughout, per AGENTS.md (⛔ never
cdinto the package for vitest).Heavy runs went through the shared lock in the sibling checkout.
pnpm exec vitest run packages/types/src/__tests__/percent-field-metadata-scale-9784.test.tsTest Files 1 passed (1)·Tests 10 passed (10)pnpm exec vitest run packages/types/Test Files 205 passed (205)·Tests 4787 passed (4787)pnpm --filter @object-ui/types type-checkpnpm --filter @object-ui/types builddist/field-types.d.tscarriesscalewith its docblockpnpm --filter '@object-ui/types^...' buildbuildscript ⇒ nothing to build, a genuine no-op, ⛔ not a passpnpm --filter '@object-ui/fields^...' build && pnpm --filter @object-ui/fields type-checkTS2307 Cannot find moduleerrors — a prerequisite miss, ⛔ not a red gate; the reading above is the one after the closure was builtpnpm exec eslinton the two touched source files,--format jsonno-explicit-anywarnings, all on lines my diff does not touchcheck:control-bytes,check:new-line-citations,check:test-path-roots,check:changeset-claims,check:pending-changeset-literals,check:designer-field-key-parity,check:unreferenced-sourcesnode scripts/check-changeset-presence.mjs,node scripts/check-changeset-no-major.mjsnode scripts/check-governed-queue-guard.mjs --teston all three pathsgrep -naPfor the control-byte class over the three touched files⛔ Not measured locally, declared to CI: the repo-wide
pnpm lintandpnpm testruns. Idid not narrow them — I did not run them at all, and CI owns that farm.
Fences I was given, and what I did with them
packages/fields/src/widgets/PercentField.tsx— held by an open pull request. Notread into the diff, not written.
packages/fields/src/index.tsx— the subject of a separate open card. Read only;the pin on it asserts the read by content and deliberately tolerates the expression being
reshaped around it, so a clamp landing there does not collide with this pin.
precision— untouched, with the spec reading above as the reason.Acceptance notes
To file (⛔ not filed by me — for the triage seat, with dedupe words), one candidate:
CurrencyFieldMetadatamay carry the same defect class as this card. It declarescurrency/precision/min/maxand noscale, while the spec's field face declaresthe pair, and
scaleis read off field/column definitions on currency-typed columns —packages/fields/src/widgets/GridField.tsxresolvesscalewith a currency-specificdefault, and
packages/plugin-grid/src/useColumnSummary.tsreads it for the footer.of the evidence, honestly: every one of those reads reaches the member through an untyped
or cast carrier, ⛔ not through
CurrencyFieldMetadata, and I took no runtime reading — sothis is a candidate for the grading seat, ⛔ not a graded finding, and AGENTS.md is explicit
that a source grep alone cannot settle whether a declared key is read. Dedupe words:
CurrencyFieldMetadata,scale,precision,currency field metadata,decimal places.Noted, ⛔ not filed:
PercentFieldMetadatacarries no docblocks onprecision/min/max, where thenumber arm documents its pair. A documentation nit, ⛔ not a defect — and re-documenting
precisionis fenced by this card's ruling anyway. Who will meet this file: whoever takesthe
precisionquestion if it is ever opened; otherwise: no one.const percentField = field as any;.Declaring
scaledoes not by itself retire that cast, so the cast survives this PR. Whowill meet it: the open card against that same renderer. ⛔ Not mine to change under the
fence.
Generated by Claude Code