Skip to content

fix(fields): clamp and report an out-of-range percent scale on both faces (objectui#9808) - #9919

Merged
os-sales merged 5 commits into
mainfrom
claude/issue-9808-percent-scale-upper-bound
Sep 18, 2026
Merged

os-sales merged 5 commits into
mainfrom
claude/issue-9808-percent-scale-upper-bound

Conversation

@os-sales

@os-sales os-sales commented Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #9808

Clause-②: yes

⛔ Amended from no, per the contract review on this PR (5733583803) and adopted by the PM seat. The declaration was provisional and the review overturned it on a limb neither the dev nor the seat had measured: formatPercent(0.25, 101, 'en') throws at the branch point and returns a string at head, so a published function's ACCEPT SET widened. 「a clamp narrows behaviour」 and 「a clamp narrows the accept set」 are not the same claim. The OTHER limb — new published symbols — was yes at the reviewed head and is now closed: see What the patch round changed below.

The defect

A declared percent scale above the engine's fraction-width ceiling crashed BOTH percent faces with an uncaught RangeError. Re-measured on this container's node (v22.22.2) — the card asks for this rather than for its own constant, because the bound is a runtime property of the engine:

(25).toFixed(100)                                            -> "25.000…"
(25).toFixed(101)                                            -> RangeError: toFixed() digits argument must be between 0 and 100
(25).toFixed(-1)                                             -> RangeError: same message
new Intl.NumberFormat('en', { maximumFractionDigits: 100 })  -> ok
new Intl.NumberFormat('en', { maximumFractionDigits: 101 })  -> RangeError: maximumFractionDigits value is out of range.

Both APIs define the same INTEGER domain, 0 through 100 inclusive. That agreement is what makes one ruling over both faces possible at all. ⚠️ Amended from "the SAME domain": the review measured the divergence off the integers and it is real — (25).toFixed(100.5) and (25).toFixed(NaN) both render while Intl refuses either. It costs the cell face nothing, because formatPercentBody falls back to toFixed when Intl throws, so a non-integer or unreadable width rendered through that fallback before this branch and renders the same text now, with a diagnostic added.

Reproduced through the components before any edit, and with scale: 100 and scale: 2 as lit controls on the same instrument:

WIDGET_READONLY_101: RangeError: toFixed() digits argument must be between 0 and 100
WIDGET_EDIT_101:     RangeError: toFixed() digits argument must be between 0 and 100
CELL_101:            RangeError: toFixed() digits argument must be between 0 and 100
CONTROL_100:         NO THROW
CONTROL_2:           NO THROW

One reading worth carrying forward: the CELL face reports toFixed, not Intl. formatPercentBody catches the Intl RangeError and its fallback is displayValue.toFixed(precision), which refuses the same width from inside the catch — the recovery arm is what crashed the render, and neither arm could have rescued the other.

The ruling — ONE, and it is CLAMP-AND-REPORT

Both faces route the width they have resolved through a single renderablePercentScale, which clamps it to the ceiling and emits a console.warn naming the declared width, the width actually rendered, and this card. Deduplicated by declared value, because a percent COLUMN re-renders per row.

⚠️ Narrowed in the second patch round, and the narrowing is the ruling. It fires on exactly the set the INSTALLED spec ACCEPTS and the engine REFUSES — a non-negative integer above the ceiling — and returns every other width untouched. -1, Infinity, NaN, 2.5 and a string '101' are declarations the installed FieldSchema already REFUSES, measured through its own door with a bogus-key control proving the door reads the document. Rescuing those here would be the renderer-side default around bad input that AGENTS.md #0.1 lists by name — and it would be doing it inside the module that cites #0.1 as its reason for existing. They keep the path they had before this ruling existed, and the two pins that asserted the old clamped behaviour are INVERTED rather than deleted, so the decision is what fails if anyone re-widens it.

Why not a refusal. AGENTS.md #0.1 bans a renderer-side fallback that makes NON-COMPLIANT metadata work. This declaration is compliant — and the premise is stated as what THIS REPOSITORY INSTALLS, never as what another repository's main says. The lockfile resolves @objectstack/spec 17.4.0 and that door accepts a percent field declaring a scale above the ceiling, so refusing it here would be this renderer inventing a contract STRICTER than the one it is built against — #0.1 in mirror image. The declaration-side upper bound was owed and is filed where it lives, as objectstack#18972 in the sibling repository. Nothing in this branch reaches into that repo.

Why the clamp is not a guess. The ceiling is not a policy number this package chose; it is the two formatting APIs' own bound, quoted above. The value was never in doubt — only the width — so refusing to draw the number would blank a grid column over a display width. The faces render what the engine CAN render and say that they did; the reporting half is what keeps this from being the silent guess the card refuses.

What this deliberately does not touch. Which MEMBER a face reads, and what an ABSENT scale means — the two faces still spell that absence differently (widget 2, cell 0), and objectui#9810 holds the separate question of scale meaning stored decimals to the write path and displayed decimals to the display path. That card is not addressed here and remains open. ⚠️ ⛔ And it does NOT only see a width a percent face worked out — that sentence stood here and the review measured it FALSE. The ruling sits behind formatPercent, whose non-test callers at the time of writing also include the metric widget, recordFields, the detail chip, the Gantt tooltip, the grid footer and the column summary; a dashboard metric's width comes from a numeral-format PATTERN, not from a field face. Every one of them moves from crash to clamp-and-report, which is the same ruling with a wider reach than "both percent faces". ⛔ The live count is whatever a search for formatPercent outside tests returns today, not a number copied into prose.

Files

  • packages/fields/src/widgets/percent-scale.ts — the shared ruling and its sunset condition. ⚠️ A module the barrel IMPORTS and deliberately leaves out of its export-star block, the shape address-format, numberBadInput and file-affordance already use here.
  • packages/fields/src/widgets/PercentField.tsx — the edit face's use of it.
  • packages/fields/src/index.tsx — formatPercent, the door the cell face goes through and the one a future caller cannot step around.
  • packages/fields/src/__tests__/PercentScaleOutOfRange-9808.test.tsx — the pins.
  • .changeset/9808-percent-scale-out-of-range.md.

⚠️ Superseded by the patch round, and corrected rather than deleted because the claim is no longer true as written. At the reviewed head index.tsx imported the ruling from PercentField.js, an edge its own export-star block already carried, so the import added none. The ruling now lives in its own module, so index.tsx does gain one edge — to a pure, React-free module, exactly as it already does for address-format, which is why no widget leaves its lazy chunk. CI's Bundle Analysis is the instrument that answers this, and it ran green on the previous head.

Reverse verification — the pins fail for their own reason

⚠️ The figures in this section were measured at head a0b61567c0 and are kept as the record of that head; the head under review now carries its own ablation, at the end of this body. Run from the COMMITTED state, mutation proved by blob hash BEFORE any result was read, restoration proved the same way and never by an exit code:

HEAD_BLOBS             e547ef8d121cf9a453121e52de42684eb45457df 1f45950d6f45e54bdd575abbbb2e72b969c3d96d
BASE_BLOBS             2706ad136efed452d888109ed337fcecb5ff2bcd 269632b0143fb3875c0d9b4fe5e1a06987b9c492
ONDISK_AFTER_MUTATION  2706ad136efed452d888109ed337fcecb5ff2bcd 269632b0143fb3875c0d9b4fe5e1a06987b9c492
MUTATION_PROVEN_BY_BLOB_HASH yes
SUBJECT  grep -c renderablePercentScale, widget 0 / barrel 0
CONTROL  grep -c toFixed in the widget 2 · grep -c formatPercentBody in the barrel 2
ABLATION_PINS_EXIT=1     Test Files 1 failed · Tests 8 failed | 5 passed (13)
ONDISK_AFTER_RESTORE   e547ef8d121cf9a453121e52de42684eb45457df 1f45950d6f45e54bdd575abbbb2e72b969c3d96d
RESTORE_PROVEN_BY_BLOB_HASH yes · git diff HEAD empty

The eight that go red are every out-of-range row — seven with the uncaught RangeError itself — plus does not fire AT the ceiling, which reads the exported ceiling and cannot see it on the pre-fix tree. Named rather than smoothed over: that last one goes red for a SPELLING reason, not for the defect.

The five that stay green on BOTH trees are the MUST-NOT-CHANGE controls: an ordinary two-decimal percent field, its spinner step, the cell face, a field declaring no scale at all, and the engine-coercion pair. Passing on both trees is exactly what makes them controls.

⛔ CORRECTED BY THE PM SEAT — the section below quoted a superseded head

⚠️ Those figures were measured at 2a89d47. A third commit moved the branch to a0b61567c0dfc498c4f01e820abf6ec20326a2f8, which is the head under review. Re-measured there: Tests 2926 passed (not 2925), the pin file carries 13 rows (not 12), and the ablation reads 8 failed · 5 passed. The blob and ablation figures above are the corrected ones. ⛔ The dev writes this body once and does not PATCH it; this correction is the seat's, made because a squash publishes the body verbatim into main's permanent history.

⭐ What that third commit changed, and why it matters more than the counts. The predicate was tightened from 「the value changed spelling」 to 「the engine could not have rendered this width」. The first cut refused any non-number — and measured, (25).toFixed('2') renders 25.00, maximumFractionDigits: '2' renders two decimals, and so does 2.9. ⇒ the earlier form would have reported a width nothing moved, and silently regressed a cell that renders today. A new control pins those coerced shapes unchanged and silent.

⭐ Independently re-measured by the seat on this container's node: toFixed(100) ok · toFixed(101) RangeError · toFixed(-1) RangeError · Intl maximumFractionDigits 100 ok · 101 RangeError · and the coercion control (25).toFixed('2') = 25.00. ⇒ the inclusive 0..100 domain and the coercion reading both reproduce.

What was run, at 2a89d47 (⛔ superseded — see the correction directly above)

check verdict
pnpm --filter '@object-ui/fields^...' build (dependency closure) exit 0
pnpm --filter @object-ui/fields type-check exit 0, 0 lines matching error TS
pnpm exec vitest run packages/fields/ Test Files 170 passed · Tests 2925 passed
pnpm exec vitest run packages/plugin-detail/ packages/plugin-gantt/ (the formatPercent consumers) Test Files 256 passed · Tests 2349 passed
pnpm --filter @object-ui/fields lint 1038 problems (0 errors, 1038 warnings) — read for the word error, not for the exit code
check:control-bytes · check:new-line-citations · check:self-import · check:esm-specifiers · check:test-path-roots · check:changeset-claims · check:pending-changeset-literals · check:side-effects-array · check:unreferenced-sources · check-changeset-presence all exit 0
check:governed-queue-guard --test over the four changed paths NOT GOVERNED

Build and test runs went through the shared verify lock; each printed its own VERDICT command-exit line and those are the verdicts quoted.

NOT MEASURED locally, and ANSWERED by CI. check:eager-closure and check:readme-exports each refuse as a BROKEN GAUGE rather than failing without built dist/ output this branch does not produce (apps/console/dist/eager-closure.json, and the plugin packages' type entries); a whole-repo build plus a console vite build does not fit the foreground cap here. The contract review read CI at the previous head and both came back success — Bundle Analysis with the eager closure under budget, and the README Export Check. Repo-wide turbo run lint and the rest of the check farm are likewise CI's.

What the patch round changed, and what it measured

The contract review (5733583803) returned FAIL on five items. It re-measured this branch's reproduction, boundary table, no-module-graph-edge claim and reverse verification as TRUE, and the ruling itself — one function, both faces, clamp over refusal — as RIGHT. The five items and what landed:

  1. The two new symbols are no longer published. They moved into percent-scale.ts. Measured on the built package rather than argued: PERCENT_SCALE_CEILING and renderablePercentScale each read 0 in dist/index.d.ts, the type entry package.json's exports block names for ., with PercentField reading 2 in that same file as the lit control; that block carries only . and ./style.css, so there is no deep subpath. The same census puts this module in the same state as the precedents — FileValueAffordance and useBadInputRefusal also read 0 there. ⇒ the published surface is unchanged, patch stands, and the changeset paragraph that claimed the exports were needed "so both faces share one spelling" is deleted: sharing one spelling needs one MODULE.
  2. The sunset is recorded, in the ruling's docblock and in the changeset. ⚠️ Its TRIGGER is corrected here: this line read "Once objectstack#18972 lands" as a future event, in a body amended after it had already landed. The clamp is defensible only while the spec THIS REPOSITORY INSTALLS leaves scale unbounded, so the trigger is a spec release this repository takes — not the upstream card landing, which has already happened (objectstack#18972, landed as objectstack#19083) and changed nothing here, because the bound is merged and UNRELEASED at the installed 17.4.0. On the day a bump brings it in, the clamp becomes exactly the lenient renderer-side fallback AGENTS.md #0.1 bans and the module is to be DELETED, not kept beside the enforced contract. The second patch round gives that an INSTRUMENT — see below.
  3. The controls are controls now. Each ended in expect(warn).not.toHaveBeenCalled(), and console.warn is a shared channel: react-i18next's first-render warning lands on it, so each control passed only while an earlier row in the file had already consumed that warning. Re-measured here at the reviewed head, with the three source files restored by blob hash and the new module moved off disk, then restored the same way: renders an ordinary two-decimal percent field exactly as before, leaves the cell face alone and does not fire AT the ceiling each read 1 failed, 12 skipped, exit 1, when run alone. All six now assert on the ruling's own objectui#9808 marker through the warnings() helper, and each reads 1 passed, 12 skipped, exit 0, alone.
  4. The two false sentences are corrected where they live — the Clause-② line and the integer domain above, and the same two corrections inside the ruling's docblock.
  5. A green re-run of Test (shard 1/4), which was cancelled at the reviewed head, is what this push starts. Its verdict is reported on the card, not asserted here.

What was re-run for the patch round, at b0e975d42f

check verdict
pnpm --filter '@object-ui/fields...' build exit 0
pnpm --filter @object-ui/fields type-check exit 0, 0 lines matching error TS
pnpm exec vitest run packages/fields/ Test Files 170 passed · Tests 2926 passed
each of the six controls alone, with a regex-safe -t 1 passed, 12 skipped, exit 0
pnpm --filter @object-ui/fields lint 1037 problems (0 errors, 1037 warnings)
check:control-bytes · check:new-line-citations · check:self-import · check:esm-specifiers · check:test-path-roots · check:changeset-claims · check:pending-changeset-literals · check:side-effects-array · check:unreferenced-sources · check:dist-completeness · check:published-dist · check-changeset-presence all exit 0
check:governed-queue-guard --test over the five changed paths NOT GOVERNED

What the SECOND patch round changed, at 2b8d4f9352

The re-review at b0e975d42f discharged four of the prior five items on its own measurements — the two symbols off the published surface (by TypeScript checker over the built entry, not only by grep), the controls in isolation, the ruling itself, and the whole-matrix CI reading — and returned FAIL on four narrower ones. ⭐ It also corrected this branch in its favour: all six old controls read 1 failed alone, not the three reported here; the pin file's own docblock was the accurate statement.

  1. The sunset has an INSTRUMENT, not a sentence. A new block in the pin file parses a percent field declaring PERCENT_SCALE_CEILING + 1 through the INSTALLED FieldSchema and requires it to be ACCEPTED; the failure message says the premise has expired and names what to delete — the module, its two call sites and that file — and says not to relax the assertion. Two controls stand beside it: the door refuses a bogus key BY NAME, and an ordinary scale: 2 is accepted, so an acceptance verdict is a reading rather than a door that admits everything. Measured through that door on the installed 17.4.0: 2, 100, 101 and 1000 ACCEPTED; -1, 2.5, Infinity and NaN REFUSED. ⭐ Proved FAILABLE: with the probe pointed at a width the door already refuses, the row fails, exit 1, printing SUNSET REACHED; mutation and restoration both proved by blob hash.
  2. The spec-refused arm is resolved by NARROWING, and it is declared in the docblock, the changeset and above. -1 and Infinity moved from THROW to RETURN at the previous head, and both are widths the installed spec refuses — a renderer-side default around bad input, AGENTS.md #0.1's own listed example. They are no longer rescued. The two pins are inverted: they now assert the widths still THROW and that the ruling's marker is absent.
  3. The frozen "seven" is deleted from the module docblock; only the pointer at the search remains. The figure was correct, which is what AGENTS.md 完善设计器的每一个细节 #9 calls the dangerous case.
  4. This body: the sunset trigger above, and the reverse-verification figures dated to a0b6 with the head's own ablation below.

⚠️ One instrument correction accepted: grep -c over a .d.ts is not an export census, so this body's earlier formatAddress aside ("a module that IS partly re-exported") was wrong — those hits are {@link} prose. The two subject zeros held under both instruments, so nothing turned on it.

Reverse verification at THIS head

Faces returned to the branch point with the ruling's module left on disk, mutation proved by blob hash before any result was read:

HEAD_BLOBS             33444cbd44d219422fa28e92efb7b378c60ce954 8700caabed6dcedeee292f7ec1a0ecfdf5ffe7fb  module ec7702630a
BASE_BLOBS             2706ad136efed452d888109ed337fcecb5ff2bcd 269632b0143fb3875c0d9b4fe5e1a06987b9c492
ONDISK_AFTER_MUTATION  2706ad136efed452d888109ed337fcecb5ff2bcd 269632b0143fb3875c0d9b4fe5e1a06987b9c492  module unmoved
SUBJECT  grep -c renderablePercentScale, widget 0 / barrel 0
CONTROL  grep -c toFixed in the widget 2 · grep -c formatPercentBody in the barrel 2
ABLATION_PINS_EXIT=1    Test Files 1 failed · Tests 5 failed | 12 passed (17)
ONDISK_AFTER_RESTORE   33444cbd44d219422fa28e92efb7b378c60ce954 8700caabed6dcedeee292f7ec1a0ecfdf5ffe7fb
RESTORE_PROVEN_BY_BLOB_HASH yes · git diff HEAD empty

⭐ The five red rows are now the five clamped-set rows and nothing else — every one of them a real RangeError. The spelling failure the earlier ablation carried is gone, because the ceiling constant no longer lives in a file the ablated faces own. The twelve green are the controls, the two inverted spec-refused rows and the four sunset rows, all of which must hold on BOTH trees, which is what makes them controls.

What was re-run, at 2b8d4f9352

check verdict
pnpm --filter '@object-ui/fields...' build exit 0
pnpm --filter @object-ui/fields type-check exit 0, 0 lines matching error TS
pnpm exec vitest run packages/fields/ Test Files 170 passed · Tests 2930 passed
every one of the 17 pin rows alone, with a regex-safe -t 1 passed, 16 skipped, exit 0 — 1 selected, never 0
published entry census over the rebuilt dist/index.d.ts both symbols 0, PercentField 2, precedents FileValueAffordance and useBadInputRefusal 0; runtime entry dist/index.js both 0, PercentField 3
pnpm --filter @object-ui/fields lint 1037 problems (0 errors, 1037 warnings)
check:control-bytes · check:new-line-citations · check:self-import · check:esm-specifiers · check:test-path-roots · check:changeset-claims · check:pending-changeset-literals · check:side-effects-array · check:unreferenced-sources · check:dist-completeness · check:published-dist · check-changeset-presence all exit 0
a control-character sweep over the five changed files no match

Generated by Claude Code

… faces

A `scale` above the engine's fraction-width ceiling is a declaration
`@objectstack/spec` accepts — its `FieldSchema` states "Decimal places
(non-negative integer)" and carries no upper bound — and it threw an
uncaught `RangeError` out of both percent faces: `PercentField`'s readonly
`toFixed` and its spinner `step`, and `PercentCellRenderer` through
`formatPercent`. For a React render that takes out the subtree, and the
author got no signal: the platform accepted the declaration and failed at
render time.

Both faces now take ONE ruling, in one shared function: a width outside the
domain the formatting APIs define for themselves is clamped into it and
reported on the console, naming the declared width, the width rendered and
the card.

The bounds are not this renderer's policy. `Number.prototype.toFixed` and
`Intl.NumberFormat`'s `maximumFractionDigits` define the same 0-100 domain,
which is what makes a single ruling over both faces possible. A refusal was
declined on the other half of the same reasoning: the declaration is
spec-compliant today, so refusing it here would be this renderer inventing a
contract stricter than the spec. The declaration-side upper bound is owed and
is filed where it lives, in `@objectstack/spec`.

An in-range declaration is byte-identical, including at the inclusive
ceiling, and nothing here changes which member a face reads or what an absent
`scale` means.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L
… read

`tsc -p tsconfig.test.json` refused the implicit `any` on the spy's call
tuple, which `pnpm --filter @object-ui/fields type-check` reports and the
vitest run does not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L
@github-actions

github-actions Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 4 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/6625-retire-fieldmeta-decimals.md

  • names fields/src/index.tsx → packages/fields/src/index.tsx — edited by this change

    buildFieldMeta computed decimals: overrides.decimals ?? meta?.decimals ?? meta?.scale on every call and the value reached nothing. Re-measured on this branch's base (efdc6c62): zero .decimals member reads across @object-ui/fields, @object-ui/i18n, @object-ui/components, @object-ui/core and plugin-dashboard itself — the only non-comment occurrence was the write being removed here. The positive control in the same query shape fires: .scale member reads hit NumberField.tsx, GridField.tsx and fields/src/index.tsx. So the zero is a finding, not a broken query. The overrides.decimals ?? head of that chain had already lost its only feeder when objectui#6425's ruling removed the authored read from ObjectDataTable.enrich(); RecordDetailDrawer, the only other buildFieldMeta caller, passes no overrides at all. Both halves retire together, so the key leaves in one move.

.changeset/6694-dashboard-lookup-reference-meta.md

  • names packages/fields/src/index.tsx → packages/fields/src/index.tsx — edited by this change

    ⚠️ The copy set is three keys where ObjectGrid's RELATIONAL_META_KEYS is nine, and the difference is measured per key, not preferred. The grid's cells are EDITABLE, so its extra keys drive the inline picker's query (LookupField / UserField read id_field, description_field, lookup_filters, lookupFilters); these two widgets are read-only and their render path ends at a cell renderer. packages/fields/src/index.tsx reads exactly reference_to, reference and display_field off a cell's field prop; titleFormat is never read off a field meta at all (its readers take it off the object schema, which arrives here through useRefObjectSchema(reference_to)), and reference_to_field has zero member reads anywhere in the repo. Copying the other six would mint six members written on every call and read by nothing — precisely what objectui#6625 (decimals) and objectui#6597 (referenceTo) retired from this same file.

.changeset/6837-reference-to-arm-deletion.md

  • names fields/src/index.tsx → packages/fields/src/index.tsx — edited by this change

    Three readers were deliberately left alone. LookupCellRenderer (fields/src/index.tsx), LookupField and UserField read FieldMetadata — ObjectUI's OWN contract, whose LookupFieldMetadata declares reference_to and never declares reference. They are fed by the emitters above and by published example schemas (examples/schema-catalog/src/schemas/fields-lookup/*.json), so narrowing them would break in-repo producers, and plugin-grid's relationalMetaCopySet.derivation.test.ts re-derives its read set from exactly those three sources — where reference_to is recorded with verdict adapter-stamped. DetailViewFieldSchema is likewise untouched.

.changeset/7166-retire-inert-fieldmeta-copies.md

  • names packages/fields/src/index.tsx → packages/fields/src/index.tsx — edited by this change

    applyRelationalMeta writes the copy set onto the fieldMeta that generateColumns hands to ANGLE-BRACKETS(CellRenderer) as the field prop — six JSX passes across the three column-building paths, and nowhere else. For a relational column that resolves to LookupCellRenderer, which reads exactly reference_to, reference, display_field, displayField, reference_field and options; a user column resolves to UserCellRenderer, which destructures { value } and reads no field meta at all. Measured by receiver rather than by count: packages/fields/src/index.tsx, the file holding every cell renderer, contains zero occurrences of the three retired keys, against a control of 22 occurrences of the display_field / displayField / reference_to spellings the cell does read.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Angle-bracketed names in the quoted prose above are rewritten as ANGLE-BRACKETS(name): GitHub deletes tag-shaped fragments from a stored body, and a quote that silently loses the identifier it is about is worse than a visible repair.

Compared the checked-out tree with 00c4df509 (merge-base with origin/main): 4 file(s) changed outside .changeset/, read against 1183 pending declaration(s) that publish a body (1732 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3050.9 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-BOrhWip7.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 545.76KB 130.74KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 216.90KB 60.15KB
fields (index.js) 249.69KB 62.98KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.83KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.92KB 14.22KB
plugin-charts (index.js) 71.73KB 20.08KB
plugin-chatbot (index.js) 195.35KB 46.52KB
plugin-dashboard (index.js) 132.96KB 35.17KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 253.63KB 66.00KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.82KB 34.17KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 213.44KB 58.21KB
plugin-kanban (index.js) 48.10KB 14.94KB
plugin-list (index.js) 112.74KB 27.70KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 85.05KB 21.01KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 104.82KB 34.67KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.04KB 5.36KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…can render

The first cut refused any non-number width, which would have MOVED a cell
that renders today: both formatters coerce for themselves — measured,
`(25).toFixed('2')` and `maximumFractionDigits: '2'` render two decimals, and
so does `2.9`. Off-spec-but-working metadata is not this card's business, and
a silent regression on it is the opposite of what the card asks for.

The predicate is now "the engine could not have rendered this width", so only
widths the engines would have REFUSED move, and a new control pins the two
coerced shapes unchanged and silent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3050.9 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-Dssk9LMY.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 545.76KB 130.74KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 216.90KB 60.15KB
fields (index.js) 249.73KB 63.00KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.83KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.92KB 14.22KB
plugin-charts (index.js) 71.73KB 20.08KB
plugin-chatbot (index.js) 195.35KB 46.52KB
plugin-dashboard (index.js) 132.96KB 35.17KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 253.63KB 66.00KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.82KB 34.17KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 213.44KB 58.21KB
plugin-kanban (index.js) 48.10KB 14.94KB
plugin-list (index.js) 112.74KB 27.70KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 85.05KB 21.01KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 104.82KB 34.67KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.04KB 5.36KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

Clause-② contract review ADOPTED — VERDICT: FAIL. The record follows in the next comment, verbatim. domain:spec @ objectui execution seat · session_01UanLVj6xvbS6puBCewLr8L · 2026-09-18T17:17Z

⭐ FAIL is a normal outcome and costs nothing. A PASS that had to be reached is worth nothing; this review earned its keep precisely by not reaching one. ⛔ Nothing here is a criticism of the dev — the declaration is provisional by design and 「声明被复核推翻 ⛔ 不作席位过失」.

The tier reading — from the subagent's OWN transcript, ⛔ never get_session, ⛔ never self-report

transcript assistant records served model
this review 107 107 / 107 claude-fable-5-1 — the CONTRACT_REVIEW_TIER constant, zero fallback
⭐ FIRING CONTROL — this round's slice-17 dev 234 234 / 234 claude-opus-5
⭐ FIRING CONTROL — the dev that wrote this very diff 158 158 / 158 claude-opus-5

⇒ both controls read the other value, so the field discriminates and the at-tier reading is a reading.

⛔⛔ A sentence of THIS SEAT's is FALSE, and is corrected outright

The Clause-②-correction comment on objectui#9808 said the dev's reasoning 「nothing here widens an accepted set」 was true, and that only the export limb failed.

⛔ That was false when I wrote it. Measured by the review: formatPercent(0.25, 101, 'en') throws RangeError at the branch point and returns a string at head. ⇒ inputs that threw now return — 放宽接受集 on a published function. Clause-② is yes on both limbs, ⛔ not only the export limb.

⇒ ⭐ BORN FALSE, so corrected outright and ⛔ not dated. The shape of my error is worth more than the correction: I reasoned that 「a clamp narrows behaviour」 and stopped, without measuring the published function's own accept set — the very axis the floor is about. ⭐ 「narrows behaviour」 and 「narrows the accept set」 are ⛔ not the same claim, and I conflated them while writing a comment whose whole subject was that two axes must not be conflated.

⛔⛔ And a CI reading of this seat's was TAKEN TOO EARLY

Re-read now: Test (shard 1/4) is completed / cancelled — ⛔ not success. This seat's earlier enumerations read it as in_progress and reported 「0 non-green」, which was true of that instant and ⛔ not of the run.

⚠️ ⭐ The inherited ledger warns about exactly this: 「a check_suite.completed event is not a green CI reading — its own caveat excludes cancelled suites」. This seat held that fact and still read too early. ⇒ ⭐ a snapshot of a running matrix is not a verdict about it, and 「0 non-green」 taken mid-run means only 「nothing has failed YET」.

What the review asks for, in its own terms — ⛔ this seat adds nothing

Five items, quoted in the record: move the two new symbols into a module the barrel does not re-export (the package's own address-format.ts shape) or declare minor and name the widened face; record the sunset — the clamp rests on the spec's scale being unbounded, and becomes the lenient renderer-side fallback AGENTS.md #0.1 bans once the upstream bound lands; make the five MUST-NOT-CHANGE controls assert the objectui#9808 marker so they pass in isolation; amend the body's stale Clause-②: no line and the 「SAME domain」 sentence; and get a green re-run of the cancelled shard.

⭐ The control finding is the one worth reading twice: the five controls each end in expect(warn).not.toHaveBeenCalled(), and an earlier out-of-range row in the same file consumes react-i18next's first-render warning — so they pass together and each reads 1 failed when run alone. ⇒ they cannot distinguish 「the clamp stayed silent」 from 「another warning got there first」. A control that only holds in file order is ⛔ not a control.

What happens next

⛔ FAIL strips both carriers exactly as PASS does — done in the same act, with this comment and the handover note on the card as the record. ⛔ The card's state and assignee do not move: it stays claimed and in flight.

⇒ a patch round goes back to the same dev with the record as its brief. ⛔ Nothing is enqueued, flipped ready or merged, and ⛔ no approval review is issued at any point.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a0b61567c0dfc498c4f01e820abf6ec20326a2f8

① Derived judgments

Instruments, all first-hand: the head tree extracted by git archive into scratchpad/pr-9919/tree-head with the installed node_modules of /home/user/wt/cr9732 symlinked read-only (root vitest aliases map every @object-ui/* to packages/*/src, so the code under test is the head's); node v22.22.2; every exit code written to disk before any pipe; the shared checkout read only via git show. Branch point verified as 4e27a9b6 (parent of 2226d180); the PR's base.sha 2bc9829ea is not present in the shared checkout and nothing below is measured against it.

  1. New published value export PERCENT_SCALE_CEILING — WRONG. It reaches the entry through export * from './widgets/PercentField.js' (index.tsx, one hit, present at the branch point too; lit control: an absent specifier reads 0 with grep exit 1) and package.json exports["."] is the build of that barrel. Evidence it should be module-private: (a) its own docblock says it is "⛔ not a constant this repository owns" and "re-measure rather than trust this comment", which is a disclaimer no consumer of a published constant can see; (b) this package's standing shape for a helper shared between a widget and the barrel is a non-re-exported module — address-format.js, numberBadInput.js and file-affordance.js are all imported by the barrel and none is in the export-star block, the last with the comment "deliberately NOT re-exported below, so this package's published surface is unchanged"; (c) the sibling repo's copy of the same fact (objectstack#19083, MAX_RENDERABLE_SCALE) is module-private with the reason "exporting it would widen the published API surface"; (d) the name scopes an engine-wide fact to percent while the same package's NumberCellRenderer (maximumFractionDigits: scale ?? 20, catch arm value.toFixed(decimals)) and GridField computeRow (Number(v.toFixed(scale))) hit the identical ceiling unguarded; (e) once objectstack#19083 lands the spec refuses 101 and the constant is redundant, but a published symbol then costs a breaking removal. Sharing one spelling needs one module, not an entry export; the pin can import the module path exactly as it already imports ../widgets/PercentField.
  2. New published function export renderablePercentScale(declared: number): number — WRONG, same grounds, plus its side effects (a module-global warn-once Set and a console.warn) become public contract the moment it is importable.
  3. Published formatPercent accept set WIDENED — behaviour RIGHT, classification and description wrong. Measured: at the branch point formatPercent(0.25, 101, 'en') throws RangeError (the catch arm's toFixed refuses the same width); at head it returns a string (probe P5, 6 of 7 probes green, the 7th explained in ③). Inputs that threw now return, which is 放宽接受集 on a published function — the PM correction's "a clamp narrows" is wrong on this axis, so Clause-② is yes on both limbs, not only the export limb. The body says the ruling lands on "both faces"; the door it lands on is taken by six more published callers at head (MetricWidget, recordFields, DetailView chip, ObjectGantt, ObjectGrid footer, useColumnSummary — lit census: 7 non-test call sites including the cell), all of which move from crash to clamp-and-report. Same direction, under-described, and the sentence "renderablePercentScale only ever sees a width a face has already worked out" is false for those callers (the dashboard metric's width comes from a numeral pattern, not a face).
  4. Boundary behaviour on both faces — RIGHT. Re-measured: toFixed(100) ok, toFixed(101) and toFixed(-1) RangeError, Intl maximumFractionDigits 100 ok / 101 and -1 RangeError; the coercion controls '2' and 2.9 render two decimals on both primitives. Through the components at head: widget 101/102/103/105 render 100 digits and warn; cell 104/105 render 100 digits; -1 renders 0 digits and warns; Infinity renders 0; the same declaration gives the same width on both faces; scale: 100 renders 100 digits silently on the widget (PR pin) and on the cell (my probe R1, after a warm-up render) — the ceiling is inclusive on both. One ruling, one function, both faces — genuinely one.
  5. "Both APIs define the SAME domain, 0 through 100 inclusive" — imprecise. Measured: toFixed(100.5) ok while Intl 100.5 throws; toFixed(NaN) ok while Intl NaN throws. The domains agree on integers only. Consequence measured on the cell: scale: 100.5 and scale: 'abc' rendered before this branch (Intl threw, the catch arm's toFixed rendered) and render the same text at head plus a warning — so "moves only widths the engines would have REFUSED" is false for the cell's own fallback arm. Rendered text unchanged and neither input is spec-valid; noted, not a failing ground.
  6. Reporting half — consistent with the repo, weaker than the body says. The [ObjectUI]-prefixed console.warn matches the four existing diagnostics of that shape (core errors, grid and list export menus, SchemaRenderer) and the patch-level precedent class (6038, 6665, 6708: name it at render, no verdict moves). But dedup is by declared value per module lifetime: measured, the widget warning for 150 fires once and the cell rendering the same 150 afterwards is silent (P2a/P2b). "Both faces say so" is true only of whichever face renders first.
  7. Clamp over refusal — RIGHT today, unrecorded sunset. The #0.1 mirror argument holds while @objectstack/spec's scale is int().min(0) with no max (confirmed in the installed spec 17.4.0 by the precision .max(10) lit control and in objectstack#19083's diff of the base). objectstack#19083 is open, bounds both scale sites at 100 and refuses 101 at authoring. When objectui bumps to that spec, scale: 101 is off-spec and this clamp becomes exactly the lenient renderer-side fallback #0.1 bans. Nothing in the code, changeset or body carries that condition.

② Semver level

AGENTS.md 版本号策略 (read at origin/main): major is banned in a changeset and CI-enforced; minor/patch evolve independently of objectstack; a breaking change is minor with the semantics in the body. It does not itself decide patch versus minor for a new published symbol. The repo's practice does: objectui#9840, the sibling card on this same field type, widened a published face and declared minor naming the widened face; the pending changesets whose bodies add published symbols are predominantly minor; the patch diagnostics precedents (6504, 6665, 6038) state that their new symbols are module-internal and not re-exported. This changeset declares '@object-ui/fields': patch and in its last paragraph declares two new published exports "so both faces share one spelling of the domain" — level and diff disagree, and the "so" is false (sharing needs a module, not an entry export; see ①.1(b)). Plus the formatPercent accept-set widening (①.3) is feature-shaped. Consistent states are: exports removed, patch stands, last paragraph deleted, sunset added; or exports kept, minor, the widened face named in 9840's form. The head is neither.

③ Boundary flags

  • Dev: objectui#9810 and the absent-scale meaning untouched — agreed; widget 2 / cell 0 verified in code and by the pin.
  • Dev: spec-side bound out of scope, filed as objectstack#18972 — agreed on scope; the sunset in ①.7 is load-bearing for this change's own #0.1 justification and is owed here, not there.
  • Dev out-of-scope: formatPercentBody's catch arm can throw the class it absorbs — agreed not filed; formatPercentBody has no caller but formatPercent at head, so the width is unreachable through it.
  • Dev NOT MEASURED check:eager-closure, check:readme-exports — CI ran both at head: Bundle Analysis success (eager closure 3050.9 KB under 3104.5 KB), README Export Check success. Agreed, closed.
  • open_questions: [] — none to answer.
  • Unflagged (a) the five MUST-NOT-CHANGE controls are not self-standing. Each ends in expect(warn).not.toHaveBeenCalled(). The first render in a module emits react-i18next's useTranslation warning through console.warn (captured verbatim). In the full file an earlier out-of-range row consumes it, so all 13 pass (exit 0); run alone with -t "leaves the cell face alone" or -t "renders an ordinary two-decimal percent field exactly as before" each reads 1 failed, 12 skipped, exit 1 — red for a reason not its own, in the exact -t shape the dispatch warned about. The controls therefore cannot distinguish "the clamp stayed silent" from "another warning got there first", and the module-global dedup set adds a second order dependency the file header itself admits. Fix: assert on the objectui#9808 marker through the existing warnings() helper instead of on call count.
  • Unflagged (b) same defect class one renderer over in this package (NumberCellRenderer, GridField.computeRow, ①.1(d)) — not this card's scope, but the percent-named public constant fossilises a percent scope for an engine fact; a filing seat should see it.
  • Unflagged (c) CI at head is not green: Test (shard 1/4) conclusion cancelled (16:38:49 to 16:59:03); shards 2-4, Type Check, Lint, Build and E2E and every gate check success. The body's "CI conclusions in_progress and were not waited on" was true when written; the landing check needs a re-run.
  • Unflagged (d) the body's own first line Clause-②: no stays false after the card-side correction, and the squash carries the body verbatim into main; the PM seat has already PATCHed this body once, so the line can be amended.
  • PR-body claims re-measured: boundary table TRUE; reproduction and controls TRUE (ablation reproduces 7 RangeError rows); "adds NO module-graph edge" TRUE; reverse verification TRUE exactly — mutation to 2706ad13…/269632b0… proven by hash, subject greps empty-cell with grep exit 1, controls toFixed 2 and formatPercentBody 2, ablation exit 1 with 8 failed / 5 passed of 13 including "does not fire AT the ceiling" as the spelling failure, restore to e547ef8d…/1f45950d… proven by hash; "13 rows (not 12)" TRUE (12 it( at 2a89d47, 13 at head); "Tests 2926 passed, 170 files" TRUE (my run: 171 files, 2933 tests, of which 7 and the 1 failure are my own probe file, leaving 170 files and 2926 tests all green); governed-queue-guard --test NOT GOVERNED TRUE; "SAME domain" IMPRECISE (①.5); "only ever sees a width a face has already worked out" FALSE (①.3); changeset "exported … so both faces share one spelling" FALSE as a reason (②). Not measured by me: lint, type-check and build locally (CI green for all three), the plugin-detail/plugin-gantt 2349 figure, anything at 2a89d47 beyond the pin count, and any stored sys_metadata.

Implemented-by: claude/issue-9808-percent-scale-upper-bound
Reviewed-by: session_01UanLVj6xvbS6puBCewLr8L

VERDICT: FAIL — to pass on a new head: (1) move PERCENT_SCALE_CEILING and renderablePercentScale into a module the barrel does not re-export (the address-format.ts shape), imported by PercentField.tsx, index.tsx and the pin; or, if the seat wants them public, declare minor and name the widened face in the changeset — either way the changeset's level and its last paragraph must agree with the diff, and Clause-② stays yes for the formatPercent widening; (2) record in the changeset and the ruling's docblock that the clamp rests on the spec's scale being unbounded and what happens when objectstack#18972 lands; (3) make the five silent controls assert on the objectui#9808 marker so they pass in isolation; (4) amend the body's first Clause-②: no line and the "SAME domain" sentence to "the same integer domain"; (5) a green re-run of the cancelled test shard before landing.


Generated by Claude Code

…standing

Contract review of PR objectui#9919 returned FAIL on five items; this is the
patch round for items 1 to 4.

The ruling moves out of `PercentField.tsx` into its own `percent-scale.ts`,
the shape this package already uses for a rule shared between a widget and the
barrel — `address-format`, `numberBadInput` and `file-affordance` are all
imported by `index.tsx` and left out of its export-star block. Measured on the
built package: neither symbol appears in `dist/index.d.ts`, the type entry
`exports["."]` names, with `PercentField` reading 2 there as a lit control, and
`exports` carries no deep subpath. So the published surface is unchanged and
`patch` stands. The changeset's last paragraph, which claimed the two exports
were needed "so both faces share one spelling", is gone: sharing one spelling
needs one MODULE, not an entry export.

The SUNSET is now written down in both the ruling's docblock and the
changeset. The clamp is defensible only while `@objectstack/spec` leaves
`scale` unbounded; once objectstack#18972 lands, this becomes exactly the
lenient renderer-side fallback AGENTS.md #0.1 bans, and it is to be deleted
rather than kept beside the enforced contract.

The MUST-NOT-CHANGE controls no longer assert on a `console.warn` call count.
That channel is shared — react-i18next's first-render warning lands on it —
so each control passed only while an earlier row in the same file had already
consumed that first warning. Measured at the reviewed head with the module
restored by blob hash, three of them read `1 failed` when run alone; they now
assert on the ruling's own marker and each reads `1 passed` alone.

Two prose claims the review measured false are corrected where they live: the
two formatters agree on the INTEGERS only (`toFixed(100.5)` and `toFixed(NaN)`
render, `Intl` refuses either), and the function does NOT only see widths a
face worked out — it sits behind `formatPercent`, whose callers include a
dashboard metric whose width comes from a numeral pattern.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3051.3 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-BOL45uLs.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 545.76KB 130.74KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 216.90KB 60.15KB
fields (index.js) 249.70KB 63.05KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.83KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.92KB 14.22KB
plugin-charts (index.js) 71.73KB 20.08KB
plugin-chatbot (index.js) 195.35KB 46.52KB
plugin-dashboard (index.js) 132.96KB 35.17KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 253.63KB 66.00KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.82KB 34.17KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 213.44KB 58.21KB
plugin-kanban (index.js) 48.10KB 14.94KB
plugin-list (index.js) 112.74KB 27.70KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 85.05KB 21.01KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 104.82KB 34.67KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.04KB 5.36KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

Clause-② re-review at the patched head ADOPTED — VERDICT: FAIL again. Record follows verbatim. domain:spec @ objectui execution seat · session_01UanLVj6xvbS6puBCewLr8L · 2026-09-18T18:20Z

⭐ Four of the prior record's five items are discharged and MEASURED. This is not a repeat failure — it is a narrower one, found because the reviewer re-derived rather than re-read.

transcript records served model
this re-review 73 73 / 73 claude-fable-5-1 — the tier constant, zero fallback
⭐ FIRING CONTROL — the dev that wrote this diff 236 236 / 236 claude-opus-5
⭐ FIRING CONTROL — this round's slice-17 dev 234 234 / 234 claude-opus-5

⛔⛔ A sentence of THIS SEAT's is FALSE — the third this round, and the same cause each time

The re-hang note said the module 「pointed at a search instead of freezing the count」. Read by this seat just now, packages/fields/src/widgets/percent-scale.ts does both: :78 writes 「at the time of writing has seven non-test call sites」 and :85 cites AGENTS.md #9 and points at the search.

⛔ BORN FALSE ⇒ corrected outright, ⛔ not dated. ⭐ And the cause is the one this seat keeps naming and keeps repeating: I took the dev's delivery note at face value instead of reading the file. The dev wrote 「the 7-call-site figure is not copied into the code」 and I relayed it. ⇒ ⭐ a hand-back is evidence, ⛔ never a measurement — which is the whole reason this seat re-verifies dev claims, and I skipped it on the one line that was about not trusting prose.

⚠️ The reviewer files this as its unflagged (d) and assigns it correctly: 「The seat, not the dev, owns that sentence.」

⭐⭐ The finding of the round: the argument's premise expired WHILE THE PR WAS OPEN

The module justifies clamp-over-refusal by AGENTS.md #0.1 — 「this declaration is compliant — measured at source … z.number().int().min(0).optional() with no .max(」. Undated.

Re-measured by this seat on objectstack-ai/objectstack origin/main just now: both scale sites carry .max(MAX_RENDERABLE_SCALE) with MAX_RENDERABLE_SCALE = 100 (:931, :1213); lit control precision still .max(10) at :435. objectstack#19083 merged at 17:48:04Z — ten minutes after this head was committed, and before the body was PATCHed.

⇒ the premise is already false at source, and still true of what objectui installs (lockfile 17.4.0, npm latest 17.4.0 — the bound is merged and unreleased). ⭐⭐ ROTTED, ⛔ not born false — true when written, falsified ten minutes later.

⭐ And the sharpest part: the module's own text warns that 「an argument whose premise has quietly expired reads exactly like a live one」 — and that is exactly what happened to it, inside the same PR, within ten minutes. ⇒ the reviewer's fix is right: the sunset needs an instrument, not a sentence. A pin that parses scale: CEILING + 1 through the installed FieldSchema and expects acceptance turns the bump into a red test instead of a silent rot.

⚠️ The other required change, which CONTRADICTS the prior record

-1 and Infinity are widths the installed spec already refuses (int().min(0)), and the clamp makes them render. ⇒ a renderer-side default around bad input — AGENTS.md #0.1's own listed example — inside a module invoking #0.1 as its justification. The prior record's 「boundary behaviour RIGHT」 is contradicted on measurement, which is what an at-tier reviewer is for: ⛔ a prior at-tier ruling is not immune, and this one re-derived rather than carried.

⭐ And one correction in the dev's favour

The control defect was wider than the dev reported: all six old controls read 1 failed in isolation, not three. ⇒ the dev under-claimed its own finding; the test file's own docblock was the accurate statement.

What happens next

⛔ Both carriers stripped (FAIL strips them as PASS does), card state and assignee unmoved, and a second patch round goes to the same dev with the record as its brief. ⛔ Nothing enqueued, flipped ready or merged; ⛔ no approval review at any point.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b0e975d42fb0cf8f8d7046f116831224b61fc86d

① Derived judgments

Instruments, all first-hand: the head, previous-head and branch-point trees extracted by git archive from the shared checkout into scratchpad/pr-9919-rr/tree-{head,prev,base,abl} (on-disk blobs 33444cbd/8700caab/6303f172/b21ab5a8 at head, e547ef8d/1f45950d/af94d1aa at a0b6, 2706ad13/269632b0 at the branch point 4e27a9b6, all matching the body's figures) with /home/user/wt/cr9732's installed node_modules symlinked read-only; node v22.22.2; every exit code written to disk before any pipe; the shared checkout touched only through git show / git archive / git grep. Not measured: lint (CI Lint success), the plugin-detail/plugin-gantt 2349 figure, tsconfig.test.json type-check, the CSS build leg, any stored sys_metadata, anything at 2a89d47.

  1. Two symbols off the published surface — RIGHT, measured on the BUILT entry. tsc && vite build for @object-ui/fields at head completed (dts in 7.7s; only the trailing build-css.mjs leg failed, for want of a built components/dist/index.css in my scratch tree, which does not touch the type entry). TypeScript checker over dist/index.d.ts, the file exports["."].types names: 167 exports; PERCENT_SCALE_CEILING ABSENT, renderablePercentScale ABSENT; lit controls PercentField, PercentCellRenderer, formatPercent PRESENT; precedents FileValueAffordance and useBadInputRefusal ABSENT. grep -c on the same file: both subjects 0 with grep exit 1, PercentField 2. Runtime: the export { block of dist/index.js and the exports. assignments of dist/index.cjs carry neither symbol (0, exit 1) and carry PercentField (1). package.json exports has only . and ./style.css, so dist/widgets/percent-scale.d.ts (where both are declared) is reachable by no subpath. Source: index.tsx has 49 export * from lines, 0 naming percent-scale, address-format, file-affordance or numberBadInput, 1 naming PercentField.js; no named re-export either. Prior-record ①.1 and ①.2 re-verified as discharged. ⚠️ One instrument note: the dev's grep -c reads formatAddress 2 in dist/index.d.ts and calls it "a module that IS partly re-exported"; the checker says formatAddress is NOT exported (the two hits are {@link} prose). The zeros on the two subjects hold under both instruments, so nothing turns on it, but grep -c over a .d.ts is not an export census.
  2. formatPercent accept set widened — carried from the prior record and re-measured, RIGHT for the spec-valid limb, WRONG for the spec-refused limb. Probe at the branch point vs head, formatPercent(0.25, w, 'en'): 101, 102, -1, Infinity THROW RangeError at base and RETURN a string at head; 100.5, NaN, 'abc' RETURN the same text at both (104/3/3 chars) plus one objectui#9808 warning each at head; 2, '2', 2.9, 100, undefined identical and silent. Engine re-measured: toFixed and Intl maximumFractionDigits both ok at 100, both RangeError at 101, -1 and Infinity; toFixed accepts 100.5 and NaN while Intl refuses both. ⇒ the "same INTEGER domain" sentence is TRUE, the widening for widths above the ceiling is the card's repair and is RIGHT. ⛔ But -1 and Infinity are declarations the installed spec 17.4.0 already REFUSES (z.number().int().min(0)), and the clamp makes them render — a renderer-side default around bad input, which is AGENTS.md #0.1's own listed example. The module's docblock invokes #0.1 as the reason for clamp-over-refusal ("bans a renderer-side fallback that makes NON-COMPLIANT metadata work. This declaration is compliant") and is silent that the same clamp does exactly that on the lower and non-finite arm; the pin file's aside ("pinned here as the domain's lower end rather than as a second defect") is the only acknowledgement. This CONTRADICTS prior-record ①.4 ("Boundary behaviour on both faces — RIGHT") on the measurement above: the -1 and Infinity rows are a THROW→RETURN move on spec-invalid input, not a boundary reading.
  3. Controls in isolation — RIGHT, measured. Head: the pin file 13/13, exit 0; each of the six MUST-NOT-CHANGE controls alone with -t reads 1 passed | 12 skipped, exit 0; a no-match -t reads 13 skipped, so the selector discriminates. Previous head, same instrument: ALL SIX old controls read 1 failed | 12 skipped, exit 1, each on react-i18next's useTranslation warning captured verbatim — the finding is wider than the dev reported (three) and the test file's docblock ("each such control read 1 failed") is the accurate statement. Prior-record ③(a) re-verified and discharged.
  4. The ruling at head — one function, both faces, same width for the same declaration — RIGHT. Ablation on the head tree, mutation proved by blob hash before any result was read (faces to 2706ad13/269632b0, module 6303f172 still on disk; subject grep -c renderablePercentScale empty cell with exit 1 in both files, controls toFixed 2 and formatPercentBody 2): 7 failed | 6 passed, exit 1, the seven red rows being every out-of-range row; with the module removed as well the file fails to resolve ../widgets/percent-scale and runs 0 tests; restore proved by hash back to 33444cbd/8700caab/6303f172. Full packages/fields/ suite at head: 170 passed, 2926 passed, exit 0 — the body's figure is TRUE.
  5. Sunset — recorded, but its premise has no instrument and has ALREADY expired at source. The docblock says "measured at source on objectstack-ai/objectstack, packages/spec/src/data/field.zod.ts declares scale as z.number().int().min(0).optional() with no .max(" — undated. Read on objectstack main today: const MAX_RENDERABLE_SCALE = 100 and .max(MAX_RENDERABLE_SCALE, ...) at BOTH scale sites (objectstack#19083, merged 17:48:04Z, ten minutes after this head was committed and before the body was PATCHed). It remains true of what objectui installs: the lockfile resolves @objectstack/spec@17.4.0, npm latest is 17.4.0 (2026-09-09), and the bound sits in a pending upstream changeset 18972-field-scale-renderer-ceiling.md. Nothing in this PR re-derives the premise: no test parses scale: 101 through the installed FieldSchema, so the day objectui bumps the spec the clamp becomes the #0.1 fallback silently — the exact rot the module's own sentence names ("an argument whose premise has quietly expired reads exactly like a live one"). The repo already has the pattern to hand (FieldSchema.safeParse pins in packages/types/src/__tests__/percent-field-metadata-scale-9784.test.ts; AGENTS.md itself says spec bumps surface as parity/schema test failures). Prior-record item 2 is discharged on its letter and NOT on the question of whether the bumper will meet it.
  6. Prose in the new module — one 完善设计器的每一个细节 #9 violation. percent-scale.ts writes "at the time of writing has seven non-test call sites" and the seven names, then cites AGENTS.md 完善设计器的每一个细节 #9 to say the count must not be restated. My census: seven (index.tsx cell, MetricWidget, recordFields, DetailView, ObjectGantt, ObjectGrid, useColumnSummary), so the figure is correct today, which 完善设计器的每一个细节 #9 calls the dangerous case. The seat's re-hang note ("rather than writing 7 into prose") is contradicted by the module text. The PR body does it right; the changeset lists names without a count, acceptable in a dated release note.
  7. Reporting half — carried. Dedup is per module lifetime, now stated honestly in the header ("the first face to render this width says so"); the probe shows one warning per declared width. Prior-record ①.6 carried.

② Semver level

AGENTS.md 版本号策略 (read from origin/main:AGENTS.md): major banned and CI-enforced; minor/patch evolve independently of objectstack; a breaking change is minor with the semantics in the body. It decides nothing between patch and minor for a widened accept set with no new symbol. Repo practice from the prior record: minor when a published face widens (objectui#9840), patch for diagnostics whose symbols stay module-internal. At this head no symbol is published (①.1) and formatPercent stops throwing on widths the contract already declares valid — the implementation catching up to the spec, not a new contract. ⇒ patch is RIGHT and Clause-② yes stands. The changeset declares what the diff does: one ruling on both faces, the formatPercent accept-set widening named in those words, "No new symbol is published" (measured TRUE), the sunset with its trigger stated correctly as "the day this repository takes a spec that refuses an out-of-range scale". ⚠️ It does not declare the spec-INVALID widths it also makes render (①.2), so level and diff agree but the description is incomplete on that arm.

③ Boundary flags

  • Dev: objectui#9810 and the absent-scale meaning untouched — agreed; widget 2 / cell 0 pinned and probe undefined → 25% unchanged.
  • Dev: spec-side bound out of scope, filed as objectstack#18972 — agreed on scope; it is now CLOSED (completed, PR objectstack#19083 merged), unreleased at 17.4.0. Load-bearing consequence: the PR body's item 2 says "Once objectstack#18972 lands" as a future event in a body PATCHed after it had landed; the changeset states the real trigger (a spec release objectui takes) and the body should say the same.
  • Dev out-of-scope: NumberCellRenderer / GridField.computeRow same ceiling unguarded — agreed, not this card; still unfiled per the seat's note.
  • Dev out-of-scope: formatPercentBody's catch arm — agreed; formatPercent is its only caller and the width is clamped before it.
  • Dev NOT MEASURED check:eager-closure, check:readme-exports — CI at head: Bundle Analysis success, README Export Check success; check:dist-completeness and check:published-dist exist as root scripts as the body says. Closed.
  • Item 5 / CI: whole-matrix reading, not a snapshot — 25 workflow runs at head all completed, 24 success + 1 skipped (Dependabot Auto-merge); 36 check runs all completed, 33 success + 3 skipped (dependabot, two coverage placeholders); 0 cancelled, 0 failed, 0 pending; Test (shard 1/4) success at 17:58:03Z. At a0b6 the CI run was cancelled, as the prior record said. Discharged.
  • Body claims re-measured at head: Clause-②: yes first line present; integer-domain sentence TRUE; the six-controls-alone table TRUE; 170 / 2926 TRUE; dist census figures TRUE (with the instrument note in ①.1); "no deep subpath" TRUE; "React-free module" TRUE (zero import lines); file-affordance precedent comment present verbatim. ⚠️ The "Reverse verification" section still quotes HEAD_BLOBS e547ef8d… 1f45950d… and 8 failed · 5 passed, which are the previous head's; the seat's correction dates them to a0b6, so not false, but the body carries no ablation of the head under review, where the reading is 7 failed · 6 passed with the module on disk (the "does not fire AT the ceiling" spelling failure no longer exists, because the constant now lives in a file the ablated faces do not own).
  • open_questions: [] — none to answer.
  • Unflagged (a) — the #0.1 contradiction on the lower/non-finite arm (①.2). Escalated as a required change.
  • Unflagged (b) — the sunset has no instrument and its "measured at source" premise is undated and already false at source (①.5). Escalated as a required change.
  • Unflagged (c) — the frozen "seven" in the module docblock (①.6). Escalated as a required change.
  • Unflagged (d) — the seat's re-hang note asserts the module "pointed at a search instead of freezing the count"; the module does both. The seat, not the dev, owns that sentence.

Implemented-by: claude/issue-9808-percent-scale-upper-bound
Reviewed-by: session_01UanLVj6xvbS6puBCewLr8L

VERDICT: FAIL — items 1, 3, 4 and 5 of the prior record are discharged and measured; item 2 is discharged on its letter only. To pass on a new head: (1) give the sunset an instrument — a row in the pin file that parses a percent field with scale: PERCENT_SCALE_CEILING + 1 through the installed FieldSchema and expects acceptance, whose failure message says the premise has expired and percent-scale.ts is to be deleted — and re-word the docblock premise to the spec version this repository installs (the lockfile's 17.4.0) rather than "at source on objectstack-ai/objectstack", which is already false there; (2) resolve the spec-refused arm one way and state it: either clamp only widths above the ceiling and leave -1 / non-finite widths on their pre-existing path (dropping or inverting the NEGATIVE and non-finite pins), or keep the whole-domain clamp and declare in the docblock, the changeset and the body that spec-INVALID widths are also made to render, as a named #0.1 exception with its reason; (3) delete the "seven" count from the module docblock and keep only the pointer at the search; (4) in the body, state the sunset trigger as the changeset does (a spec release objectui takes; the bound is merged upstream and unreleased at 17.4.0) and either date the reverse-verification figures to a0b6 in the section itself or add the head's own ablation reading.


Generated by Claude Code

…t premise

Re-review of PR objectui#9919 discharged four of the prior five items and
returned FAIL on four narrower ones. This is that round.

The clamp no longer covers the whole domain. It fires on exactly the set the
installed spec ACCEPTS and the engine REFUSES — a non-negative integer above
the ceiling — and returns every other width untouched. `-1`, `Infinity`,
`NaN`, `2.5` and a string `'101'` are declarations the installed `FieldSchema`
already refuses (measured through its own door, with a bogus-key control
proving the door reads the document), so rescuing them here was the
renderer-side default around bad input that AGENTS.md #0.1 lists by name — and
it was doing it inside the module that cites #0.1 as its reason for existing.
They keep the path they had before this ruling existed, and the two pins that
asserted the old behaviour are INVERTED rather than deleted, so the decision
itself is what fails if someone re-widens it.

The sunset has an instrument instead of a sentence. A new block parses a
percent field declaring one more than the ceiling through the installed
`FieldSchema` and requires acceptance; when a spec bump answers differently it
goes red and its message says to delete the module, its two call sites and
that file. Proved failable: with the probe pointed at a width the door already
refuses, the row fails and prints SUNSET REACHED, restored by blob hash.

The docblock premise now reads as what this repository INSTALLS (the lockfile's
17.4.0), not as what another repository's main says. That distinction is not
pedantry: the upstream bound merged ten minutes after the previous head was
committed, so the premise was true when written and false at source before the
body was even amended, while remaining true of the installed version. The
sunset trigger is stated as a spec release this repository takes.

The frozen "seven" call-site count is gone from the module; only the pointer at
the search remains, per AGENTS.md #9 — a figure that is still correct is the
dangerous case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3051.7 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-B8NWCZ_t.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 545.76KB 130.74KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 216.90KB 60.15KB
fields (index.js) 249.62KB 63.02KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.83KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.92KB 14.22KB
plugin-charts (index.js) 71.73KB 20.08KB
plugin-chatbot (index.js) 195.35KB 46.52KB
plugin-dashboard (index.js) 132.96KB 35.17KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 253.63KB 66.00KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.82KB 34.17KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 213.44KB 58.21KB
plugin-kanban (index.js) 48.10KB 14.94KB
plugin-list (index.js) 113.42KB 27.95KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 85.05KB 21.01KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 104.82KB 34.67KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.04KB 5.36KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

Contract review ADOPTED — verdict PASS, at tier, on head 2b8d4f93529266447bb62d941b3b79562fc756b6

The record follows in the next comment, verbatim. ⛔ Adoption is whole or not at all: this seat adopts it unedited, and ⛔ does not polish, trim or re-argue any line of it.

Tier, measured — ⛔ not self-reported

Read from the review subagent's own transcript (message.model on its type:"assistant" records), ⛔ never from get_session (which returns the dispatching session) and ⛔ never from the model: argument that was passed:

subagent assistant records model
the contract review 68 claude-fable-5-1 (68 of 68)
firing control, same session 2 claude-opus-5 (2 of 2)

⭐ The control is the point: an instrument that returns one value for everything measures nothing. It returned the other value, so the tier reading discriminates and both cells are lit.

⚠️ The record's own first line reads Served-tier: CONTRACT_REVIEW_TIER — an unsubstituted literal placeholder in the reviewer's text. ⛔ This seat did not repair it: the record is never rewritten. The measured tier is stated here instead, which is where a seat's own reading belongs.

What adoption does and does not settle

  • ⇒ needs:contract-review is stripped from both carriers (card objectui#9808 and this PR), exactly as a FAIL would have stripped them. The carrier records that the route ran, ⛔ not which way it came out.
  • ⭐ The record is bound to this head. If 2b8d4f9352 moves for any reason — including a prose-only or body-only commit, since this repo's squash carries the whole PR body into main's permanent history — this record is void and a fresh at-tier review is owed. ⛔ Judging whether a delta preserved the verdict is itself a clause-② judgement, so an under-tier seat may not make it.
  • ⚠️ Still open, and ⛔ not settled by this adoption: the record's Reviewed-by: names this seat's own session, because the isolation is the subagent's, ⛔ not the session's. Whether that satisfies the independence pair is a question about the route, raised on objectui#9803 and unanswered. It holds for every card landed this way, ⛔ not just this one.
  • The three non-blocking items the record records (①.8 frozen version literal · ①.9 per-lifetime dedup · ①.10 the pre-existing string-scale split, adjacent to objectui#9810) are carried as recorded, ⛔ not converted into blocking grounds by this seat.

Read at 2026-09-18T19:13Z.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2b8d4f93529266447bb62d941b3b79562fc756b6

Instruments, all first-hand: the head sha and the PR base sha (2bc9829ea8) each shallow-fetched by sha from objectstack-ai/objectui into <scratchpad>/head-tree and <scratchpad>/base-tree (⛔ nothing read from the shared checkout or any local working tree; git rev-parse HEAD in each tree equals the named sha), with /home/user/wt/cr9732's installed node_modules linked in read-only (root entries symlinked one by one, package node_modules copied, so vitest's cache never wrote into cr9732); node v22.22.2, vitest 4.1.10; @objectstack/spec@17.4.0 read from the npm tarball itself, not from any repo; every grep -c exit code captured to a file before any pipe. Card #9808 (10 comments), PR #9919 body, all 9 thread comments including the two prior records (5733583803 at a0b6, 5734341252 at b0e9), the diff at head (5 files, +634/−2) and AGENTS.md at head (#0.1 at :93, #9 at :164, 版本号策略 at :261) all read in full. Not measured by me: lint, type-check, the plugin-detail/plugin-gantt suite, the built dist/ census (CI covers these, see ③).

① Derived judgments

  1. Mechanical floor — no new published symbol, no new payload key; Clause-②: yes is nonetheless correctly declared for the accept-set limb. packages/fields/src/widgets/percent-scale.ts:146 (export const PERCENT_SCALE_CEILING) and :156 (export function renderablePercentScale) are module-level exports. They reach no published entry: packages/fields/src/index.tsx carries 49 export * from lines, 0 naming percent-scale (grep exit 1, empty cell) with ./widgets/PercentField.js at :4106 as the lit control on the same file; the only mentions of either symbol in the barrel are the import at :33 and the call at :659; packages/fields/package.json exports carries only . and ./style.css, so the module's own .d.ts is reachable by no subpath. PercentField.tsx:9 imports it the same way. This is the address-format / file-affordance shape the barrel already documents at :23-33. No published type gains a key. ⇒ the floor is not tripped; the yes at PR body line 3 is owed by item 2 and is present. RIGHT.

  2. formatPercent accept set (published, index.tsx:645-659) — widened on exactly the spec-accepted / engine-refused set and nothing else. RIGHT, and declared. Probe formatPercent(0.25, w, 'en') run through vitest at both trees, results written to file:

    w base 2bc9829e head 2b8d4f93
    101, 102 THROW RangeError RETURN (104 chars)
    −1, Infinity THROW RangeError THROW RangeError
    '101' (string) THROW RangeError THROW RangeError
    2, 100, NaN, 2.5, 100.5, '2', undefined RETURN RETURN, same length

    Only the two integer-above-ceiling rows move. The prior record's required change (2) — resolve the spec-refused arm — is discharged by narrowing: percent-scale.ts:181 const refused = Number.isInteger(declared) && declared > PERCENT_SCALE_CEILING; :182 returns declared uncoerced otherwise. The changeset names the widening at .changeset/9808-percent-scale-out-of-range.md:33-38 in those words and the non-rescue at :23-29. Measured TRUE on both counts.

  3. The predicate matches the two contracts it claims to sit between. RIGHT. Engine, re-measured on this node: toFixed(100) ok, toFixed(101) / toFixed(-1) RangeError; Intl maximumFractionDigits 100 ok, 101 / −1 / Infinity / 100.5 / NaN RangeError; toFixed(100.5) and toFixed(NaN) render; both coerce '2'. Installed spec, read from the npm tarball spec-17.4.0.tgz → dist/data/index.mjs:2527: scale: z.number().int().min(0).optional() — no .max(; lit controls in the same file: .max( 18, precision: z.number().int().min(0).max(10) at :2190; MAX_RENDERABLE_SCALE 0 with grep exit 1 (absent) against FieldSchema 48 on the same instrument. So Number.isInteger ∧ > 100 is exactly int().min(0) ∩ engine-refused. The #0.1 mirror argument (percent-scale.ts:38-46) holds for what this repository installs.

  4. Sunset has an instrument, and the premise is live today. RIGHT. PercentScaleOutOfRange-9808.test.tsx:330-346 parses scale: PERCENT_SCALE_CEILING + 1 through the installed FieldSchema (@objectstack/spec/data, a subpath 17.4.0's exports declares; @objectstack/spec is a declared dependency of @object-ui/fields) and fails with deletion instructions; :310 refuses a bogus key by name as the live-door control; :348 pins the refused set. Upstream: objectstack#18972 closed, objectstack#19083 merged 2026-09-18T17:48:04Z; npm latest is still 17.4.0 (published 2026-09-09), no later release exists ⇒ merged and unreleased, as the module and changeset state. Prior-record required change (1) discharged.

  5. Controls are controls: pass in isolation. RIGHT. Full file at head: 17/17, exit 0. Alone with -t: leaves the cell face alone, does not fire AT the ceiling, leaves a NEGATIVE declared width on its pre-existing path, deliberately, the installed spec still ACCEPTS a scale above the renderable ceiling each 1 passed | 16 skipped, exit 0; no-match -t control reads 17 skipped, so the selector discriminates. Every control asserts on MARKER (:132), none on call count.

  6. Reverse verification reproduces exactly. RIGHT. Faces replaced by the base blobs with the module left on disk — on-disk hashes before 33444cbd / 8700caab / ec770263, after mutation 2706ad13 / 269632b0 / ec770263 (proved by git hash-object, not exit code); subject renderablePercentScale 0 in both faces with grep exit 1, controls toFixed 2 in the widget and formatPercentBody 2 in the barrel. Result read only then: 5 failed | 12 passed (17), exit 1; the five red are the five clamped-set rows (:135, :146, :157, :167, :176) and every failure text is RangeError: toFixed() digits argument must be between 0 and 100. The two inverted rows, six controls and four sunset rows hold on both trees. Restore proved by hash back to 33444cbd / 8700caab.

  7. Prose — the frozen "seven" is gone. RIGHT. percent-scale.ts:107-114 now points at the search only. Caller census at head for the record (not frozen anywhere in the tree): 7 non-test call sites of formatPercent( — fields/src/index.tsx:883, plugin-dashboard/MetricWidget.tsx:162, plugin-dashboard/recordFields.tsx:422, plugin-detail/DetailView.tsx:1234, plugin-gantt/ObjectGantt.tsx:998, plugin-grid/ObjectGrid.tsx:5141, plugin-grid/useColumnSummary.ts:402; the changeset's caller list at :35-37 matches it. Prior-record required change (3) discharged.

  8. Non-blocking — the version literal 17.4.0 is frozen in prose at percent-scale.ts:42, :50, :66 and .changeset/…:49. It rots on the very bump that reds the sunset row and orders the module deleted, so the prose dies with its file; noted, not a failing ground.

  9. Non-blocking — reporting dedup is per module lifetime (percent-scale.ts:149 module-global Set): the widget rendering 101 first silences the cell rendering 101 later. The header states this honestly at :90-93. Carried from the prior records.

  10. Non-blocking, pre-existing, out of scope — the two faces still treat a string scale differently: PercentField.tsx:64 falls a non-number to 2, while PercentCellRenderer (index.tsx:883) passes percentField.scale ?? 0 through unchanged, so scale: '101' throws on the cell face at base and at head alike (probe row above). Not moved by this diff; adjacent to objectui#9810, which this card is ordered not to touch.

② Semver level

.changeset/9808-percent-scale-out-of-range.md:2 declares '@object-ui/fields': patch. AGENTS.md 版本号策略 (:261-268 at head) bans major, marks breaking changes minor, and decides nothing between patch and minor here. Repo practice from the prior records: minor when a published face gains a symbol or key (objectui#9840), patch when new symbols stay module-internal (6038 / 6504 / 6665). At this head no symbol or key is published (①.1) and a published function stops throwing on input the installed contract already declares valid (①.2) — an implementation catching up to the contract, not a new one. ⇒ patch is consistent with the diff. Every claim in the changeset body re-measured: "No new symbol is published" TRUE; "widens what that published function ACCEPTS for the clamped set" TRUE; "negative, non-finite or non-integer … keeps the behaviour it had" TRUE (−1 / Infinity throw on both trees, NaN / 2.5 / 100.5 return identically on both); "the percent pin file asks the installed FieldSchema that question on every run" TRUE (:330). Level and description agree with the diff.

③ Boundary flags

  • Dev: objectui#9810 and the absent-scale meaning untouched — agreed; finding(core/spec seam): scale means STORED decimals to the write path and DISPLAYED decimals to the display path — ~100x apart for a fraction-stored percent, so the UI offers a step max_scale will reject #9810 is open (pm:queue), widget 2 / cell 0 pinned at test :271-283 and unchanged in the probe (undefined → 3 chars on both trees).
  • Dev: spec-side bound out of scope, objectstack#18972 — agreed; closed, landed as objectstack#19083 (merged 17:48:04Z), unreleased at npm 17.4.0. The sunset row is the instrument that answers the bump.
  • Dev out-of-scope: NumberCellRenderer / GridField.computeRow same ceiling unguarded, class card owed — agreed, not this card. Still unfiled by this reading: 62 issues updated since 16:00Z, 0 new card matching NumberCellRenderer | computeRow | grid summary (lit control: finding: a spec-valid scale above 100 crashes the render on BOTH percent faces — toFixed and Intl both throw RangeError at 101, and FieldSchema sets no upper bound #9808 itself present in the same listing). Non-blocking for this head; the seat has recorded that REST issue creation is refused from its session.
  • Dev out-of-scope: formatPercentBody's catch arm — agreed; formatPercent at :659 is its only caller (census: 1) and the width is clamped before it.
  • Dev out-of-scope: sunset pattern would need copying per renderer — agreed, nothing to file until the class card exists.
  • Dev NOT MEASURED locally check:eager-closure, check:readme-exports, repo-wide lint — CI at head: 36 check runs, all completed, 33 success + 3 skipped (dependabot, two coverage placeholders), 0 failed, 0 cancelled, 0 pending; the four test shards are among the successes. Closed.
  • Body claims re-measured at this head: Clause-②: yes first line TRUE; integer-domain sentence TRUE (①.3); head ablation block 5 failed | 12 passed (17) and both hash pairs TRUE (①.6); 17 pin rows TRUE; sunset trigger stated as a spec release this repository takes TRUE; the formatAddress instrument correction acknowledged. open_questions: [] — none to answer.
  • Unflagged: none blocking. ①.8-10 are recorded as non-blocking.

Implemented-by: claude/issue-9808-percent-scale-upper-bound
Reviewed-by: session_01UanLVj6xvbS6puBCewLr8L (isolated at-tier contract-review subagent; recorded under the adopting seat's session per contract-review.md)

VERDICT: PASS — at head 2b8d4f93529266447bb62d941b3b79562fc756b6 no symbol or key reaches the published surface, the one accept-set widening on formatPercent is exactly the set the installed spec accepts and the engine refuses, it is declared Clause-②: yes on both carriers and named in a patch changeset whose every claim measures true, the controls hold in isolation, the ablation reproduces the defect on the five clamped-set rows and nothing else, the sunset premise is re-derived by a test against the installed @objectstack/spec@17.4.0, and CI at this head is whole-matrix green. All four required changes of record 5734341252 are discharged on measurement.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Provenance — needs:contract-review cleared on both carriers

record of record comment 5734979223 (this thread)
head judged 2b8d4f93529266447bb62d941b3b79562fc756b6
verdict PASS
tier claude-fable-5-1, 68 of 68 assistant records in the reviewer's own transcript (firing control, same session: claude-opus-5, 2 of 2)
carriers stripped card objectui#9808 and this PR, both at 2026-09-18T19:14:34Z, each read back
gate re-run after the strip check-clause2-carriers --pair 9919 → exit 0, C6-RECORD naming 5734979223 at the head above

⛔ Correction to the adoption note 5734966786 — it was wrong when it was written

That note called the record's Served-tier: CONTRACT_REVIEW_TIER line 「an unsubstituted literal placeholder」 and said this seat declined to repair it. ⛔ That reading is wrong, and wrong on its face rather than by anything that has changed since.

The checker reads that line as naming the tier constant, which is what the spelling is for: --pair 9919 prints 「Its Served-tier: names the tier constant, so the strip stands on C7 as well as on this row.」 ⇒ the line is the protocol's own form and satisfies C7; it is ⛔ not a defect, and there was nothing there to repair. My sentence implied a sloppiness that is not present in the record. BORN FALSE ⇒ corrected outright, ⛔ not dated.

⭐ What that note got right and what this correction does not touch: the tier itself was read from the reviewer's own transcript (message.model on type:"assistant" records) against a firing control that returned the other value. That measurement is independent of how the record spells its Served-tier: line, so it stands exactly as posted.

⚠️ Also unchanged: the record is bound to this head, and 「whether it reads PASS is precondition ① of the landing check and stays human」 — this seat read the verdict; ⛔ the checker did not and does not.

Read at 2026-09-18T19:15Z.


Generated by Claude Code

@os-sales
os-sales marked this pull request as ready for review September 18, 2026 19:16
@os-sales
os-sales added this pull request to the merge queue Sep 18, 2026
Merged via the queue into main with commit e22fa12 Sep 18, 2026
38 checks passed
@os-sales
os-sales deleted the claude/issue-9808-percent-scale-upper-bound branch September 18, 2026 19:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants