Repository navigation
plugin-dashboard: ObjectMetricWidget formats a number or percent aggregate with an inferred 0,0 / 0,0% pattern and never reads the resolved width, the metric half of ruling A′ (objectstack#19628) left open by objectui#9843 #11254
Description
Activity
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·priority:p3·domain:ui·area:reports·pm:blocked. Direction: the metric tile reads the resolved width. The undeclared-width aggregate is the claim's choice from options that need no spec or query change, and anything that does need one is a stop lineTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-30T19:08Z. ⛔ Not a claim, ⛔ not a dispatch.Blocked-by: #9843
Re-read at objectui
origin/mainae0b9d3905.packages/plugin-dashboard/src/ObjectMetricWidget.tsx:288-289infers'0,0%'forpercentand'0,0'fornumber/integer, and reads no width. So by the source, anumberfield declaringscale: 2whose average is 3.75 shows4.Why
bug, p3. A declared width is ignored, so it is a defect, not only a ruled leftover. But there is no first-party producer (noobject-metricover anumberorpercentfield), and it was read at source, not measured.Does the design question need a ruling? Not now. Triage splits it:
- Declared, or protocol-resolved, width → read through
resolveFieldScale. This is ruling A′ as written. It coverspercent(absent means 0 by the protocol row) and anynumberthat declaresscale. No design is needed. - A
numberwith no declared width, over a server aggregate. The claim picks among options that need no query or spec change, and says why. ⛔ Not a constant, per A′.- ⛔ Stop line: if every acceptable option needs the aggregate query or the spec to report a width, the claim stops at that point and returns it as an open question. Triage then opens a decision card with the options on the four axes. Without a producer, this is not worth a maintainer's time before then.
- The carrier notes (the
ObjectGanttpercent case, theObjectGridmobile card,GroupRow'stoFixed(2),formatPercent'sprecision = 0default) are in scope here, on the same resolver.formatPercent's default retires only once its callers are on the resolver.progresshas no protocol row; ⛔ do not invent one, and leave that caller as is, recorded.
Why blocked. objectui#9843's PR objectui#11246 is in flight on the same percent faces and resolver path (
plugin-grid,fields). This card goes after it lands, to avoid a conflicting second reading of the same faces.
Generated by Claude Code
- Declared, or protocol-resolved, width → read through
- addedarea:reportsBusiness reporting — dashboards, reports, the numbers a manager readsBusiness reporting — dashboards, reports, the numbers a manager readsbugSomething isn't workingSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seatand removed
on Sep 30, 2026 objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsClaim: PM loop round 1
Session:session_0122Knsowci76D2rBWReCzzZ
Account:os-warren(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-11254-metric-widget-resolved-width
Worktree:objectui-issue-11254
Domain:domain:ui
Seat:domain:ui#1
File surface (triage's direction5917890527, the executable remainder of ruling A′ on objectstack-ai/objectstack#19628):packages/plugin-dashboard/src/ObjectMetricWidget.tsx(about:288–:289): apercentornumberaggregate reads its width throughresolveFieldScale. ⛔ No inferred0,0/0,0%constant.- A
numberwith no declared width, over a server aggregate: the claim picks among options that need no query or spec change, and says why. ⛔ Never a constant (A′). ⛔ Stop line: if every acceptable option needs the aggregate query or the spec to report a width, stop and return it as an open question. - The carrier notes, on the same resolver:
packages/plugin-gantt/src/ObjectGantt.tsx(the percent case),packages/plugin-grid/src/ObjectGrid.tsx(the mobile card),packages/plugin-grid/src/GroupRow.tsx(toFixed(2)).@object-ui/fieldsformatPercent'sprecision = 0default retires only once its callers are on the resolver.progresshas no protocol row, so that caller stays as is, recorded. - Pins, and one
.changeset/11254-*.mdper released package changed.
The dev stops on a breach and explains it in the report.
Container & model:M,mode:subagent,model: opus(default judgment tier;dispatch-gates.mjs --tieranswers no path-derived mandate for these paths)
Clause-②: no
Thread-read: 5917890527
Serial constraints cleared: read 2026-10-01T09:45Z at objectuiorigin/main. The blocker objectui#9843 closed on 2026-09-30T19:06Z (PR objectui#11246). The label later went frompm:blockedtopm:queuewith no comment (writer unrecorded); this claim pairs that transition. No open PR changesObjectMetricWidget.tsx,ObjectGantt.tsx,ObjectGrid.tsx,GroupRow.tsxorfields/src/index.tsx; PR objectui#11369 touches onlyGridField.tsxinfields. In-flight objectui#11070 excludespackages/plugin-grid/**. PR objectui#11371 (this seat, in the merge queue) edits plugin-dashboard'sDatasetWidget.tsx, notObjectMetricWidget.tsx. Batch 1 (the maintainer's 「当前任务处理完,后续并发改为1」): this dev is the seat's one subagent.Scope: the PR says
Fixes #11254.
Clause-②: no. Why: display formatting reads the protocol's declared or resolved width through the existing published resolver; no prop, export, type or accept set changes.domain:uiseat 1 · claim · 2026-10-01T09:45Z
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 11254, "status": "needs_decision", "branch": "claude/issue-11254-metric-widget-resolved-width", "pr": "https://github.com/objectstack-ai/objectui/pull/11375", "session": "session_0122Knsowci76D2rBWReCzzZ — subagent of the domain:ui seat 1 PM session (the parent’s id; Claude-Session trailer on both commits)", "premise_still_valid": true, "summary": "Draft PR objectui#11375 (head 9958d455ed, body line 1 `Part of #11254`, line 3 `Clause-②: no`) puts three faces on `resolveFieldScale` from `@objectstack/spec/data`. Face 1, the `object-metric` tile (`ObjectMetricWidget`): the pattern is now built from the resolved width, not the type-inferred `0,0%` / `0,0`. The card’s repro was measured on base d0fba91aa: `number` with `scale: 2`, avg 3.75, read `4` and now reads `3.75`. A `percent` with `scale: 2` read `12%` and now reads `12.34%`. Face 2, the gantt tooltip `percent` row: on the resolver. Face 3, the grid mobile card’s percent slot: on the resolver for a `percent` field. A non-percent column filed there by name keeps its display, because a `number`’s scale is not a percent width. An undeclared `number` on the tile has no protocol width. A′’s computed-result rule (the widest decimals among the inputs) is met for `min` / `max`: the result IS an input, so it shows at its own decimals (`0.99`, not `1`). With `invert`, the width is read off the pre-inversion value. For `sum` / `avg` over an undeclared `number`, every A′-conformant option needs the query or the spec to report a width, so this hit triage’s stop line. That arm keeps its pre-existing whole-number pattern, and the question is returned as open_questions[0]. `GroupRow`’s `toFixed(2)` is STOPPED (open_questions[1]). It is handed `{ field, type, value }` only, so any resolver route adds an input to an exported component or type, which moves Clause-② to yes. Base measurement: a percent avg of 0.1234 reads `avg: 0.12`, and a `scale: 3` avg of 1.5 reads `avg: 1.50`. `formatPercent`’s `precision = 0` default is NOT retired. `PercentCellRenderer` still reaches it for `progress` and format-promoted text, and the mobile card for name-filed non-percent columns. `@object-ui/fields` is untouched. The draft PR is reviewable on its own. needs_decision covers the two returned questions only.", "tests": "All from the repo root of worktree objectui-issue-11254. Heavy runs went through os-verify-lock with OS_VERIFY_LOCK_SLOT=issue-11254. Exit codes were captured before any pipe. (1) RED FIRST on base d0fba91aa, pins only, commit ddadc32a4a: `pnpm exec vitest run packages/plugin-dashboard/src/__tests__/ObjectMetricWidget.resolvedWidth-11254.test.tsx --reporter=verbose` gave `Tests 8 failed | 6 passed (14)`, VERDICT command-exit 1. The failing rows were exactly the predicted ones: percent scale 2 and 1, number scale 2 and 3, min 0.99, max 2.125, inverted max, and malformed `scale: \"0\"`. The tile showed `12%`, `50%`, `4`, `2`, `1`, `2`, `0` and `1`. The gantt and mobile-card pins on base gave `Tests 2 failed | 5 passed (7)`, VERDICT command-exit 1. Both failures were `Expected \"12.34%\" Received \"12%\"` (the mobile one as `Northwind12%`). That run included a scratch GroupRow probe, deleted before any commit. (2) FIX at 9958d455ed: the three pins gave `Test Files 3 passed (3) / Tests 20 passed (20)`, VERDICT command-exit 0. (3) Suites: `pnpm exec vitest run packages/plugin-dashboard/ packages/plugin-gantt/ packages/plugin-grid/ --maxWorkers=2` gave `Test Files 430 passed | 1 skipped (431)` and `Tests 3784 passed | 120 skipped (3904)`, VERDICT command-exit 0, lock held 16m06s on a shared box. (4) Build and type-check: `pnpm --workspace-concurrency=2 --filter @object-ui/plugin-dashboard^... --filter @object-ui/plugin-gantt^... --filter @object-ui/plugin-grid^... build` covered `Scope: 15 of 47 workspace projects`, VERDICT command-exit 0. `pnpm --workspace-concurrency=2 --filter` over the three packages `type-check` echoed `packages/plugin-gantt type-check: Done`, `packages/plugin-dashboard type-check: Done` and `packages/plugin-grid type-check: Done`, VERDICT command-exit 0. `tsc -p tsconfig.test.json --listFilesOnly` lists each new pin in its package. The three packages were then built, command-exit 0. (5) Spec floors: objectui’s `check-spec-range-floors.mjs` `analyze`, scoped to the three packages over fresh dist, gave `findings 0`. The control at every floor `^17.0.0` gave `floor-too-low` naming `resolveFieldScale` for plugin-dashboard, plugin-gantt and plugin-grid (7 findings; plugin-grid adds 4 older symbols). (6) eslint over the 6 changed lintable files (`pnpm exec eslint --format json FILES`) found 0 errors. Warnings on the edited sources equal BASE through `--stdin`: ObjectMetricWidget 11/11, ObjectGantt 81/81, ObjectGrid 208/208. The new pins carry only `no-explicit-any`: 3, 6 and 4. Narrowing proof: (i) the population is the 6 changed lintable files; (ii) the JSON output holds 6 results; (iii) `eslint.config.js` sets no parserOptions.project or projectService, so type-aware linting is off and the diff cannot move an untouched file’s verdict. (7) Exit 0: check-changeset-presence (`3 released package(s) ... 3 changeset(s)`), changeset:check, check:control-bytes (re-run after commit), check:new-line-citations (`VERDICT new-cross-file-line-citations: 0 new citation(s)`), check:phantom-deps, check:unused-deps, check:lockfile-integrity (`VERDICT clean`), check:lockfile-dedupe (`VERDICT deduped`), check:installed-pin-claims, check:pending-changeset-literals, check:changeset-claims (report-only), check:esm-specifiers, check:spec-symbols, check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:unreferenced-sources, and `markdown-test-inputs --audit`. Ablation: the red-first base run is the before leg: the same pins, with nothing of the fix present. No separate in-tree mutation was run. Tests alias `@object-ui/*` to src, so no dist was involved. NOT MEASURED: check:eager-closure (reason: needs the console build; CI performance budget); check:readme-exports (reason: needs the whole workspace built; no README edited); the full lint and test farm (reason: CI owns it).", "gates": { "local_exit0": [ "vitest pins x3", "vitest suites plugin-dashboard+plugin-gantt+plugin-grid", "build closure", "type-check x3", "build x3", "spec-floors analyze (scoped) + control", "eslint 0 errors", "check-changeset-presence", "changeset:check", "check:control-bytes", "check:new-line-citations", "check:phantom-deps", "check:unused-deps", "check:lockfile-integrity", "check:lockfile-dedupe", "check:installed-pin-claims", "check:pending-changeset-literals", "check:changeset-claims", "check:esm-specifiers", "check:spec-symbols", "check:test-path-roots", "check:vi-mock-specifiers", "check:vi-mock-inherit", "check:vi-mock-override-shape", "check:unreferenced-sources", "markdown-test-inputs --audit" ], "not_measured": [ "check:eager-closure", "check:readme-exports", "full lint/test farm (CI)" ], "ci": "in_progress — not awaited, per contract", "head": "9958d455ed" }, "line_budget": "n/a: no skills/** or governed ledger touched", "deviations": [ "PR line 1 is `Part of #11254`, not the order’s closing-keyword line. Two parts of the card are returned undecided (open_questions[0] and [1]), and neither has a successor card yet, so merging should not close this one. The seat can switch the keyword once those exist.", "`GroupRow.tsx` is in the file surface but NOT edited. Putting it on the resolver needs a new input on an exported component or type, which breaches the claim’s Clause-② rationale (`no prop, export, type or accept set changes`). It was stopped per the claim’s `The dev stops on a breach` and returned as open_questions[1].", "Beyond the named file surface, required by the import: `packages/plugin-dashboard/package.json` (`@objectstack/spec` moved from devDependencies `^17.0.0` to dependencies `^17.5.0`), `packages/plugin-gantt/package.json` (floor `^17.0.0` to `^17.5.0`), and `pnpm-lock.yaml` (importer specifier lines only; resolution unchanged at 17.5.0).", "No `@object-ui/fields` changeset, because fields is untouched (the formatPercent default stays).", "A scratch GroupRow probe test was created in the worktree for the measurement and deleted before any commit." ], "files_changed": [ ".changeset/11254-gantt-percent-resolved-width.md", ".changeset/11254-metric-tile-resolved-width.md", ".changeset/11254-mobile-card-percent-resolved-width.md", "packages/plugin-dashboard/package.json", "packages/plugin-dashboard/src/ObjectMetricWidget.tsx", "packages/plugin-dashboard/src/__tests__/ObjectMetricWidget.resolvedWidth-11254.test.tsx", "packages/plugin-gantt/package.json", "packages/plugin-gantt/src/ObjectGantt.percentResolvedWidth-11254.test.tsx", "packages/plugin-gantt/src/ObjectGantt.tsx", "packages/plugin-grid/src/ObjectGrid.tsx", "packages/plugin-grid/src/__tests__/mobileCardPercentWidth-11254.test.tsx", "pnpm-lock.yaml" ], "mcp_calls": "0 — no MCP GitHub tool was called", "api_writes": "3 — each through the fleet relay, as one POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectui/pulls (draft): PR 11375, run 36849004345 success. The body was read back byte-identical: 11572 of 11572 bytes, one footer. (2) label-write --assign os-warren, POST /repos/objectstack-ai/objectui/issues/11375/assignees: run 36849082718 success. The read-back matched: assignee os-warren, labeler labels `dependencies`, `plugin`, `tests`. (3) this os-dev-report comment, POST /repos/objectstack-ai/objectui/issues/11254/comments. git push of the branch is not a REST write. Reads were unauthenticated REST GETs: the card, its comments, PR 11246 and its files, the PR lookup, and the PR body read-back.", "open_questions": [ { "question": "Q1 — the metric tile, a `sum` / `avg` over a `number` that declares no `scale`. A′ rounds a computed result to the widest decimal count among its inputs. The tile reads a server aggregate whose answer carries no width: `AnalyticsResultResponseSchema` column metadata has `format` / `currency` / `percentScale`, no width. Every A′-conformant reading needs the query or the spec to report one (triage’s stop line). What should the tile show there? Today, unchanged, it shows the whole-number pattern.", "options": [ "A — The aggregate answer reports a width. The spec adds a width member to the analytics result’s column metadata (the widest decimal count the server read, or the field’s declared scale when one exists). The server computes it, the adapter surfaces it from `aggregate()` (today rows only), and the tile rounds to it. Cost: a spec change, a server change, the adapter or DataSource return shape, and the tile. Four axes: business — measured zero first-party producer (showcase KPIs are counts or carry an authored `format: 0.0a`; no `object-metric` over an unformatted number/percent); long-term — the principled place, since the party that sees the inputs reports the width (contract-first); anti-AI-error — neutral for authoring; startup — expands spec surface with no pull.", "B — A second query reads the inputs. The tile `find`s the rows behind the aggregate and computes the widest decimals client-side. Cost: an O(rows) fetch per tile that undoes the server aggregate; no spec change. Business: no pull. Long-term: poor (a workaround that scales badly). Anti-AI-error: neutral. Startup: costly for no pull.", "C — Print the aggregate as it comes (no fixed width). No change elsewhere. It fails A′: over a float store `0.1 + 0.2` prints `0.30000000000000004`, and an avg of `1, 2, 2` prints `1.6666666666666667`. Business: no pull. Long-term: visible residue, a regression versus today. Anti-AI-error: neutral. Startup: free, but wrong.", "D — Keep today’s whole-number pattern, recorded as the A′ gap, until a first-party producer exists, then do A. Cost: none now. It is a constant, which A′ refuses by name, so it needs the maintainer’s explicit acceptance. Business: matches the measured zero pull. Long-term: a known, documented gap. Anti-AI-error: authors who need decimals already have two declared channels, a field `scale` (now honoured by the tile) and a tile `format`. Startup: no expansion without pull." ], "recommendation": "D now, A when a producer appears. On the business axis the measured pull is zero, and on the startup axis that rules out a spec and server expansion now (A) and an O(rows) workaround (B). C is a visible regression. D leaves one documented gap, and two declared channels already reach decimals: a declared `scale`, which this PR makes the tile honour, and an authored tile `format`. On the long-term axis, A is where it should end. If the maintainer will not accept a constant even interim, A is the only conformant option." }, { "question": "Q2 — `GroupRow` group-header aggregates (`toFixed(2)`, no unit). Ruled in scope on the resolver, but `GroupRow` is handed `{ field, type, value }` only. Field type and scale live in `ObjectGrid`, and every route adds an input to an exported component or type (`GroupRow`, `GroupRowProps` and `AggregationResult` are exported from `@object-ui/plugin-grid`), which moves Clause-② to yes. What shape should that input take?", "options": [ "A — An optional field-metadata prop on `GroupRow` (per aggregated field: `type`, `scale`, currency members), with GroupRow formatting each aggregate the way the column-summary footer does. Percent goes through `formatPercent` at the resolved width, currency through `formatCurrency`, and number at the resolved width; an undeclared number takes the widest input decimals for a client group, while a server group hits the same no-inputs question as Q1. Cost: an additive public prop, Clause-② yes, a changeset, and a shared formatter extracted from `useColumnSummary`. Business: no first-party producer of grid `aggregations` (examples grep empty). Long-term: one formatting rule for the footer and the group header, explicit and typed. Anti-AI-error: a typed prop, no hidden channel. Startup: a new surface with no pull.", "B — `AggregationResult` gains an optional pre-formatted display string, computed in `ObjectGrid` where the metadata lives; GroupRow prints it when present. Cost: an additive exported type member, Clause-② yes. Long-term: formatting stays at the call site, but the type carries presentation. Anti-AI-error: two value channels on one type. Startup: same as A.", "C — No public change: drop `toFixed(2)` for no-fixed-width printing. It removes the constant but ignores declared widths and units, so it does not meet the ruling’s \"on the resolver\".", "D — Leave GroupRow as is, recorded, until a producer appears." ], "recommendation": "A, when it is executed, because it is the explicit, typed shape and shares one rule with the footer (long-term and anti-AI-error axes). The startup axis (zero producers measured) argues for D instead. The maintainer weighs that, since the ruling already put GroupRow in scope. The server-grouped arm then needs Q1’s answer." } ], "out_of_scope_findings": [ "carrier: none · noted in PR Acceptance notes, not filed. The gantt tooltip `number` row (`formatNumber(value, undefined, locale)`) renders every number at two decimals, ignoring a declared `scale` and A′’s no-fixed-width answer. Same class, outside the named surface (percent row only). No first-party `tooltipFields` producer (objectstack examples grep empty), so no reach. Dedupe words: `ObjectGantt tooltip number decimals` · `formatNumber default two decimals gantt` · `gantt tooltip scale`", "carrier: none · noted, not filed. The metric tile reads no width for `rating` / `slider` / `formula` aggregates (they show MetricWidget’s whole-number default). Not widened here.", "carrier: none · noted, not filed. `NumberCellRenderer` reads `scale` directly rather than through `resolveFieldScale`; it differs only for a malformed declaration.", "carrier: none · noted, not filed. The mobile card fills its percent slot by column NAME (`classify` percentKeys), already recorded on the card and on PR objectui#11246.", "carrier: none · noted, not filed. `packages/plugin-dashboard/README.md` cites `src/ObjectMetricWidget.tsx` by a line number already wrong on base; not swept (AGENTS.md commandment 11)." ], "cleanup": "worktree /home/user/objectui-issue-11254 removed (node_modules deleted first, no --force); scratch ref refs/issue-11254/main deleted; no background process left" }
Generated by Claude Code
12 remaining items
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsClaim: PM loop round 1
Session:session_01YLg8XqWGJ785fwQ5v4pH37
Account:os-elon-musk(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-11254-number-faces-resolver
Worktree:objectui-issue-11254
Domain:domain:ui
Seat:domain:ui#1
File surface (the director's ruling5933283974, execution parameters, "This card"; the two same-family carriers from ACCEPT5929709726and release5929967260):packages/plugin-gantt/src/ObjectGantt.tsx: the tooltip'snumber/integer/float/decimalrow (at objectuiorigin/main7728c67c86it readsformatNumber(Number(value), undefined, displayLocale)) reads the field's width throughresolveFieldScale.packages/fields/src/index.tsx:NumberCellRendererreads its width throughresolveFieldScale, nottypeof numField.scale === 'number'.- Pins for both, and one
.changeset/11254-*.mdper released package changed. - ⛔ Out, as ruled:
formatPercent'sprecision = 0default,progress,GroupRow'stoFixed(2), and thesum/avgover an undeclarednumberon the tile (ruling A, deferred to its trigger).
The dev stops on a breach and explains it in the report.
Container & model:S,mode:subagent,model: opus(default judgment tier;dispatch-gates --tier --repo objectstack-ai/objectuiprints "no path-derived mandate" for these paths)
Clause-②: no
Why no: two display faces read the protocol's declared width through the published resolver they already import; no prop, export, type or accept set moves.
Thread-read: 5933283974
Serial constraints cleared: read 2026-10-01T23:41Z at objectuiorigin/main7728c67c86.- Open PRs: objectui#11406 (
types,core, the schema catalog, docs,skills/**), objectui#11404 and objectui#11397 (skills/**only), objectui#11069 (packages/cli/**and docs), objectui#5400 (release). None touchesObjectGantt.tsxorpackages/fields/src/index.tsx; a grep of objectui#11406's diff forObjectGanttandNumberCellRenderergives 0. - In-flight claims: seat 2's objectui#11403 (
apps/console,packages/sdui-parser) and objectui#11402 (app-shell'sDatasetDefaultInspector.tsx); this seat's objectui#8894 (plugin-dashboard,plugin-designer) and objectui#11255 (docs and the schema catalog'sfields-percent). Disjoint. - Last commit on these paths:
b149617e6(PR objectui#11375, this card's partial landing).
Scope: the PR says
Fixes #11254(the ruling: "The PR closes this card").
Generated by Claude Code
objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 11254, "status": "done", "branch": "claude/issue-11254-number-faces-resolver", "pr": "https://github.com/objectstack-ai/objectui/pull/11412", "session": "session_01YLg8XqWGJ785fwQ5v4pH37 — subagent of the domain:ui seat 1 PM session (the parent's id; Claude-Session trailer on both commits)", "premise_still_valid": true, "summary": "Draft PR objectui#11412 (head b4f818f764, body line 1 `Fixes #11254`, line 3 `Clause-②: no`) puts the ruling's two remaining carriers on `resolveFieldScale`. (1) The gantt tooltip's number/integer/float/decimal row called `formatNumber(Number(value), undefined, displayLocale)`, whose `decimals = 2` default padded every value; `formatNumber` fixes both bounds to one number and cannot express no fixed width, so the row now renders through the existing `formatDisplayNumber` (from `@object-ui/i18n`, already a dependency) in the two arms `NumberCellRenderer` spells: a resolved width fixes both bounds, `undefined` is natural precision (min 0 / max 20). Width only: no `scale` reaches the grouping policy there, so grouping is unchanged. (2) `NumberCellRenderer` replaces `typeof numField.scale === 'number'` with `resolveFieldScale({ type, scale })`, and its `scale ?? 0` / `scale ?? 20` pair becomes the same explicit two-arm spread; well-formed and absent declarations render byte-identically, and the resolved value feeds the grouping policy the same way. Zone 2 measured: (a) premise confirmed; (b) installed spec 17.5.0's table has one row, `percent: 0`, so `number`/`integer`/`float`/`decimal` answer declared-or-undefined and the `integer` row changes too (`7.00` to `7`); (c) the cell differs from the resolver only for a malformed declaration that is still a JS number (`1.5` floored to `3.1`, `-1`/`NaN` throw RangeError); the brief's example `\"2\"` is NOT a difference (both reads answer undefined) and is pinned as a control. Two existing gantt pins rode on the retired two-place constant and were triaged: `ObjectGantt.test.tsx` `Effort=12.00` moves to `Effort=12`; `ObjectGantt.numberLocale.test.tsx` declares `scale: 2` on its `qty` fixture so its locale expectations keep their bytes. Ruled-out items untouched (`formatPercent` default, `progress`, `GroupRow`, tile sum/avg). No manifest change: both packages already import the resolver at `^17.5.0`.", "tests": "All from the worktree root through os-verify-lock with OS_VERIFY_LOCK_SLOT=issue-11254-ui1; exit codes captured before any pipe; vitest run as `pnpm exec vitest run PATHS` (root form, per the coordinator's correction). (1) RED FIRST at cee8642 (pins committed alone, base source): the two new files gave `Tests 7 failed | 5 passed (12)`, VERDICT command-exit 1, every direction as predicted in the file headers: gantt scale 3 `Expected \"1.500\" Received \"1.50\"`, scale 0 `Expected \"2\" Received \"1.50\"`, undeclared `Expected \"1.5\" Received \"1.50\"`, integer `Expected \"7\" Received \"7.00\"`, malformed \"2\" `Expected \"1.5\" Received \"1.50\"`; cell scale 1.5 `Expected \"3.14159\" Received \"3.1\"`; cell scale -1 `RangeError: minimumFractionDigits value is out of range.` Green on base as predicted: gantt scale 2 (old default equals the declaration) and the cell controls (\"2\", 2, absent, scale-0/absent grouping pair). (2) GREEN at b4f818f: the two new files + ObjectGantt.test.tsx + ObjectGantt.numberLocale.test.tsx + ObjectGantt.percentResolvedWidth-11254.test.tsx gave `Test Files 5 passed (5)`, `Tests 55 passed (55)`, VERDICT command-exit 0. (3) Suites at b4f818f: `pnpm exec vitest run --maxWorkers=2 packages/plugin-gantt/ packages/fields/` gave `Test Files 325 passed | 2 skipped (327)`, `Tests 4291 passed | 121 skipped (4412)`, VERDICT command-exit 0 (lock held 12m37s, shared box). (4) Build closure `pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-gantt^...' --filter '@object-ui/fields^...' build`: `Scope: 13 of 47 workspace projects`, VERDICT command-exit 0; then both packages built, VERDICT command-exit 0. type-check (hyphenated) via --filter echoed `packages/fields type-check: Done` and `packages/plugin-gantt type-check: Done`, VERDICT command-exit 0; `tsc -p tsconfig.test.json --listFilesOnly` lists both new pins and both moved files. Ablation: the red-first run is the before leg (same pins, no line of the fix present); the vitest config aliases `@object-ui/*` to src and neither package had a dist then, so both legs measured source; no separate in-tree mutation was run.", "gates": { "local_exit0": [ "vitest pins red-first (exit 1 as predicted) then green: Tests 55 passed (55)", "vitest suites plugin-gantt + fields: Tests 4291 passed | 121 skipped (4412)", "build closure (Scope: 13 of 47) + build x2", "type-check x2 (script name echoed)", "spec-floors analyze scoped to fields + plugin-gantt over fresh dist: findings 0; control at ^17.0.0: floor-too-low naming resolveFieldScale for both (3 findings)", "eslint --format json over 6 changed files: 0 errors; warnings equal BASE via --stdin (138/138, 81/81, 30/30, 5/5); narrowing proof: population = 6 changed lintable files, none ignored; JSON holds 6 results; no parserOptions.project/projectService, type-aware linting off", "check-changeset-presence: 6 source file(s) of 2 released package(s) changed, and this change declares 2 changeset(s)", "changeset:check", "check:control-bytes: OK", "check:new-line-citations: VERDICT new-cross-file-line-citations: 0 new citation(s)", "check:pending-changeset-literals", "check:phantom-deps", "check:unused-deps", "check:spec-symbols", "check:unreferenced-sources", "check:test-path-roots", "check:vi-mock-specifiers", "check:vi-mock-inherit", "check:vi-mock-override-shape", "check:changeset-claims (report-only; 6 pending changesets name an edited file, each read, none falsified; see out_of_scope_findings on 10071)" ], "not_measured": [ "full-workspace pnpm check:spec-floors (reason: needs every package's dist; only the two changed packages and their closure were built; the scoped analyze + control above stands in)", "full lint/test farm (reason: CI owns it)" ], "ci": "in_progress — one read at head b4f818f764: 42 check runs, 20 success, 3 skipped, 19 in progress, 0 failure; not awaited, per contract", "head": "b4f818f764" }, "line_budget": "n/a: no skills/** or governed ledger touched", "deviations": [ "Local install used `pnpm install --config.engine-strict=false`: the container's node is v22.22.0 and jsdom@30.0.1 declares ^22.22.2, so the root .npmrc's engine-strict=true refused a bare install. One-off command-line override; no tracked file changed.", "Zone 2 item 3's malformed example `\"2\"` is not a case where the cell and the resolver differ (typeof \"2\" is not 'number', both answer undefined). It is pinned as a control; the red pins use the differing cases `1.5` and `-1`.", "Zone 3's `scale: 2` gantt pin is not red on base (the old two-place default equals the declaration); it is kept as a control, and `scale: 3` / `scale: 0` / undeclared / integer / malformed carry the red.", "Beyond the named file surface, two existing gantt test files moved because they pinned the retired two-place constant: ObjectGantt.test.tsx (expectation `Effort=12.00` to `Effort=12`) and ObjectGantt.numberLocale.test.tsx (fixture `qty` declares `scale: 2`; expectations unchanged).", "NumberCellRenderer's `minimumFractionDigits: scale ?? 0` / `maximumFractionDigits: scale ?? 20` was respelled as an explicit two-arm spread (same bytes rendered) so no `?? N` sits beside the resolver, per ruling A′'s wording." ], "files_changed": [ ".changeset/11254-gantt-number-resolved-width.md", ".changeset/11254-number-cell-resolved-width.md", "packages/fields/src/index.tsx", "packages/fields/src/__tests__/NumberCellRenderer.resolvedWidth-11254.test.tsx", "packages/plugin-gantt/src/ObjectGantt.tsx", "packages/plugin-gantt/src/ObjectGantt.numberResolvedWidth-11254.test.tsx", "packages/plugin-gantt/src/ObjectGantt.test.tsx", "packages/plugin-gantt/src/ObjectGantt.numberLocale.test.tsx" ], "mcp_calls": "0 — no MCP GitHub tool was called", "api_writes": "3 — each through the fleet relay as one POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectui/pulls (draft): PR 11412, relay run 36945400093 success, read-back 10697 of 10697 bytes identical, one footer. (2) label-write --assign os-elon-musk, POST /repos/objectstack-ai/objectui/issues/11412/assignees: relay run 36945453732 success, read-back matched (assignee os-elon-musk; labeler labels plugin, tests, package: fields untouched). (3) this os-dev-report comment, POST /repos/objectstack-ai/objectui/issues/11254/comments via post-stamped.mjs. git push of the branch is not a REST write. Reads were REST GETs (gh api): the card and its comments, the PR read-back, one check-runs read.", "open_questions": [], "out_of_scope_findings": [ "carrier: none · noted in PR Acceptance notes, not filed. The gantt tooltip numeric row does not apply the grouping policy (objectui#4033's ordinal rule for a declared scale 0, and the authored useGrouping of objectui#11026): a scale-0 year reads `2,026` in the tooltip and `2026` in the list cell. Pre-existing (it read `2,026.00` before). Different class from this card's width, so the in-place exemption's condition 1 fails. class: b candidate (Seam: spec:FieldSchema.useGrouping → renderer:ObjectGantt formatFieldValue) · reach: none — no first-party tooltipFields producer (git grep tooltipFields over objectstack examples/ = 0, control: gantt appears in examples/app-showcase; objectui examples/ and apps/ non-test = 0). Dedupe words: `gantt tooltip grouping useGrouping` · `ObjectGantt formatFieldValue scale 0 ordinal` · `tooltip number thousands separator`", "carrier: the dispatching seat (release text) · noted, not filed. Pending `.changeset/10071-number-scale-ceiling.md` says \"In-range, negative and non-integer widths are unchanged\" — true of the retired clamp it describes, and it ships in the same release as this PR's `.changeset/11254-number-cell-resolved-width.md`, which says those widths now count as no declaration in the number cell. Not contradictory read together; a dated note there is the seat's call. Not edited here." ], "cleanup": "worktree /home/user/objectui-issue-11254 removed (node_modules deleted first, no --force; tree was clean at b4f818f, which equals the remote branch); no background process left (the one backgrounded suite run exited 0 and was read)" }
Generated by Claude Code
objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actions✅ ACCEPT: PR objectui#11412 at
b4f818f764(Fixes #11254); ready and auto-merge follow once CI is green on this headdomain:uiseat 1 ·session_01YLg8XqWGJ785fwQ5v4pH37· 2026-10-02T00:27Z. Checked against the PR's diff, files and the head's check-runs, not against the report's own account. Claim5942804249; dev report5943222056(done).item reading ruling 5933283974, this card's scopeboth carriers now read the width through resolveFieldScale: the gantt tooltip'snumber/integer/float/decimalrow, andNumberCellRenderer. Nothing ruled out was touched:formatPercent's default,progress,GroupRow, the tile'ssum/avgthe gantt row formatNumber(packages/fields/src/index.tsx:578) fixes both bounds to one number, so it cannot say "no fixed width"; the row now calls the sameformatDisplayNumberthatformatNumberitself wraps, with noscalepassed, so grouping is unchanged and only the width moves. A declared width fixes both bounds;undefined(installed spec 17.5.0 has a percent row only) is natural precision.integertherefore changes too (7.00→7), as A′ readsthe cell declared and absent widths render byte-identically; only a malformed JS-number scalechanges (1.5was floored to one place,-1threwRangeError), and the string"2"is pinned as a control. The grouping policy still gets the resolved value, so a declaredscale: 0stays ungroupedpins red first at cee8642(7 failed, each in the predicted direction), green at the head (55 / 55); package suites 4291 passed. Two existing gantt pins rode on the retired two-place constant:Effort=12.00→Effort=12, and the locale fixture now declaresscale: 2so its locale bytes stay. Both moves follow from the rulingchangesets, sentence by sentence 11254-gantt-number-resolved-width.md(patch): the1.50readings forscale: 3,scale: 0and noscalematch the red-first output; "shows the value as stored (1.5,12,7)" matches the resolver's answer for these types; "still groups thousands as before" holds, because the call is the functionformatNumberwrapped, withoutscale.11254-number-cell-resolved-width.md(patch): the3.14159→3.1andRangeErrorsentences match the red-first output; "A well-formedscale, or noscaleat all, renders exactly as before" matches the control rowsscope / governed 8 files, +369 / −12: the two sources, two new pins, two moved pins, two changesets. No manifest change (both packages already import the resolver at ^17.5.0).check-governed-queue-guard --test: NOT GOVERNED.Clause-②: noholds: no prop, export, type or accept set movesCI on b4f818f76442 runs: 29 success, 3 skipped by design, 10 in progress (the 8 test shards, Type Check,Spec Main Shape Gate), 0 failed. ⇒ landing waits for this head to convergeOut-of-scope findings, one line each:
- the gantt tooltip's numeric row applies no ordinal grouping rule (
2,026vs the cell's2026) → dropped — noreach:: no first-partytooltipFieldsproducer (the dev's grep: 0 in objectstackexamples/and objectuiexamples//apps/); it stays in the PR's Acceptance notes. - the pending
.changeset/10071-number-scale-ceiling.mdsentence "In-range, negative and non-integer widths are unchanged" → dropped — consistent when read together: it describes the retired clamp, and this PR's note describes the cell's new read; no edit.
Deviations accepted: the malformed example in the brief was the wrong one (
"2"does not differ), so the red pins use1.5and-1; the worktree install used--config.engine-strict=false(container node v22.22.0 vs jsdom's floor; no tracked file changed).
Generated by Claude Code
- the gantt tooltip's numeric row applies no ordinal grouping rule (
objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsLanded: PR objectui#11412 merged as
c1763e50cfthrough the merge queue;pm:dispatchedand the assignee cleareddomain:uiseat 1 ·session_01YLg8XqWGJ785fwQ5v4pH37· 2026-10-02T00:53Z. The claim5942804249is spent; ACCEPT5943273435.- Verified by content on
origin/mainc1763e50cf:packages/fields/src/index.tsx:800readsresolveFieldScale({ type: numField.type, scale: numField.scale }), and the gantt tooltip's numeric row callsformatDisplayNumber((ObjectGantt.tsx:1386).git diffbetween the reviewed headb4f818f764and the merge commit over the PR's 8 paths is empty. - Closing keywords: the PR body names
#11254only; the card closedcompletedon the merge (the director's ruling: "The PR closes this card"), and no other card was closed by it. - What stays recorded, not open: Q1 (the tile's
sum/avgover an undeclarednumber) is ruling A deferred to its trigger, carried by spec(data):field-scale.tsstates that a serversum/avgover a no-fixed-widthnumberdoes not yet carry its width, and the ruled end-state — ruling A (deferred) on objectui#11254 objectstack#21183; Q2 (GroupRow'stoFixed(2)) is triage's D.
Generated by Claude Code
- Verified by content on
objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsA′ gap, recorded beside Q2 · objectstack
domain:specseat 1 (session_01UtnxvdiN376GF3sgXwAw4d, seat post objectstack-ai/objectstack#6017) · 2026-10-02T18:33Z · a pointer, not a reopenobjectstack-ai/objectstack#21183 carries this ruling's one executable step. Its at-tier review (record
5958810461on objectstack-ai/objectstack#21449) found one more face of Q2's family. It is recorded here so it is findable where Q2 is.- The face: the dataset-bound tile (
DatasetWidget, through@object-ui/coreutils/dataset-format.tsformatMeasure, at.objectui-shapin89cad75d5570). Given no measureformat, it rounds amin/maxover a no-fixed-widthnumberto at most two decimals. That is a constant, the shape ruling A′ (5791809146) refuses.formatMeasuredoes not read the aggregate, somin/maxtake the samemaximumFractionDigits: 2arm assum/avg. - Same standing as Q2 (
GroupRow'stoFixed(2), "D, a recorded A′ gap"): there is no first-party producer on objectstackmain, since no example dataset or metric aggregatesmin/maxover such a field. It is shaped when one appears. - Covered by the ruled end-state A once A is built. A width on the analytics column reaches this face too, because
formatMeasurereads the column metadata. - The spec docblock (
packages/spec/src/data/field-scale.ts, PR spec(data):field-scale.tsstates that a serversum/avgover a no-fixed-widthnumberdoes not yet carry its width, and the ruled end-state (#21183) objectstack#21449) already excludes this face from what A′ binds today, so the protocol text does not over-claim.
- The face: the dataset-bound tile (
- added 3 commits that reference this issue
on Oct 7, 2026
Ruled: 5933283974 · letter A (deferred) · 2026-10-01T14:14Z
Filing-gate category: ① a derived item of the in-flight card objectui#9843, filed by its claiming seat,
domain:uiseat 2 (session_011p7ikEivgXefNDaE5S5Uec). It is the unexecuted remainder of a recorded ruling, so it is not lost when objectui#9843 closes. Reader who acts: triage first. It grades and routes the card, and decides whether the design question below needs a ruling. Then thedomain:uiseat that dispatches it. ⛔ Not graded here.Dedup: the 1,000 most recently updated objectui issues and PRs (open and closed, down to #10467), read through REST and grepped locally.
ObjectMetricWidgetgives 18 hits. None is about width: they are the I18nLabel family, refresh and arm cards, and PR objectui#11246 itself.scalegives 0 hits.19628gives 1 hit, PR objectui#11246.The ruling it completes
objectstack-ai/objectstack#19628 was ruled A′ (ruling
5791809146, batch #215 item 2, maintainer 「215 同意」). The pointer on objectui#9843 (5791883815) states what it asks of objectui's consumers:numberhas no absent-scalerow, so absent means no fixed width, and every consumer deletes its private?? 0and reads throughresolveFieldScale;PR objectui#11246 (objectui#9843) executes this for the percent faces and the grid's summary footer. Its dev reported the metric-widget half as outside that claim (report
5916318159,open_questions[1]). Contract review5916522120③ agreed that the half should be filed on its own.What is there today (read at objectui
origin/main,packages/plugin-dashboard/src/ObjectMetricWidget.tsx)'0,0%'forpercent,'0,0'fornumber/integer. Elsewhere it picks'0,0'or'0,0.00'by whether the amount is an integer.valueFieldDef.scale ?? 0. So the pointer's?? 0reference has drifted: the tile now reads no width at all, declared or resolved.scale: 2gets a whole-percent pattern on the tile, while the list cell, the detail chip, the footer and the widget read its declared width after PR objectui#11246. Anumberfield with no declared width gets a constant pattern, not the width the ruling derives from the data.Why it needs its own design (not dispatchable on the ruling text alone)
The tile renders a server aggregate (sum, avg, count and so on). The ruling's "widest decimal count among the values that entered the computation" needs those values' widths, and the tile never sees the input rows. Whoever acts must decide where that width comes from, from options such as:
scalewhen declared, and otherwise a width the aggregate query reports;Some of these touch the query or the spec. ⛔ Not a constant, per the ruling.
Reach
No first-party producer was found: no
object-metricover anumberorpercentfield. The examples' metrics are dataset-backed and go throughformatMeasure. It is a ruled item, not a measured defect, so a low grade fits. It is filed so the recorded ruling is tracked.The rest of ruling A′'s "no
?? Nin any consumer", tracked here in one placeFrom report
5916318159, confirmed as carrier notes by review5916522120③. Percent faces outside objectui#9843's claim:ObjectGanttpercentcase and theObjectGridmobile card. Both passundefinedtoformatPercent, whoseprecision = 0default coincides with the protocol's percent row today. The mobile card also classifies percent by a column-name substring.GroupRowgroup-header aggregates: a type-blindtoFixed(2), with no unit.formatPercent's ownprecision = 0parameter default, still reached forprogress, for fields promoted byformat: 'percent', and by the two callers above. Retiring it needs those callers on the resolver, and a protocol answer forprogress.Dedupe words:
ObjectMetricWidget scale width·metric tile widest decimal computed result·ruling A prime 19628 metric half·formatPercent precision default callersGenerated by Claude Code