Skip to content

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

@objectstack-fleet

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:ui seat 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 the domain:ui seat 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.

  • ObjectMetricWidget gives 18 hits. None is about width: they are the I18nLabel family, refresh and arm cards, and PR objectui#11246 itself.
  • A metric tile or widget within 80 characters of scale gives 0 hits.
  • 19628 gives 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:

  • number has no absent-scale row, so absent means no fixed width, and every consumer deletes its private ?? 0 and reads through resolveFieldScale;
  • computed results (the summary footer and the metric widget) round to "the widest decimal count among the values that entered the computation — derived from the data, ⛔ never a constant".

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 review 5916522120 ③ 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)

  • The tile infers a pattern from the field type: '0,0%' for percent, '0,0' for number / integer. Elsewhere it picks '0,0' or '0,0.00' by whether the amount is an integer.
  • A comment in the file says it no longer builds a pattern from valueFieldDef.scale ?? 0. So the pointer's ?? 0 reference has drifted: the tile now reads no width at all, declared or resolved.
  • Read at source, not measured: a percent field that declares scale: 2 gets 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. A number field 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:

  • the declared scale when declared, and otherwise a width the aggregate query reports;
  • the aggregate's own natural precision;
  • an explicit tile format on the widget.

Some of these touch the query or the spec. ⛔ Not a constant, per the ruling.

Reach

No first-party producer was found: no object-metric over a number or percent field. The examples' metrics are dataset-backed and go through formatMeasure. 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 ?? N in any consumer", tracked here in one place

From report 5916318159, confirmed as carrier notes by review 5916522120 ③. Percent faces outside objectui#9843's claim:

  • the ObjectGantt percent case and the ObjectGrid mobile card. Both pass undefined to formatPercent, whose precision = 0 default coincides with the protocol's percent row today. The mobile card also classifies percent by a column-name substring.
  • GroupRow group-header aggregates: a type-blind toFixed(2), with no unit.
  • formatPercent's own precision = 0 parameter default, still reached for progress, for fields promoted by format: 'percent', and by the two callers above. Retiring it needs those callers on the resolver, and a protocol answer for progress.

Dedupe words: ObjectMetricWidget scale width · metric tile widest decimal computed result · ruling A prime 19628 metric half · formatPercent precision default callers


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: 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 line

    Triage 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/main ae0b9d3905. packages/plugin-dashboard/src/ObjectMetricWidget.tsx:288-289 infers '0,0%' for percent and '0,0' for number/integer, and reads no width. So by the source, a number field declaring scale: 2 whose average is 3.75 shows 4.

    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 (no object-metric over a number or percent field), and it was read at source, not measured.

    Does the design question need a ruling? Not now. Triage splits it:

    1. Declared, or protocol-resolved, width → read through resolveFieldScale. This is ruling A′ as written. It covers percent (absent means 0 by the protocol row) and any number that declares scale. No design is needed.
    2. A number with 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.
    3. The carrier notes (the ObjectGantt percent case, the ObjectGrid mobile card, GroupRow's toFixed(2), formatPercent's precision = 0 default) are in scope here, on the same resolver.
      • formatPercent's default retires only once its callers are on the resolver. progress has 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

  2. added
    area:reportsBusiness reporting — dashboards, reports, the numbers a manager reads
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Sep 30, 2026
  3. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_0122Knsowci76D2rBWReCzzZ
    Account: os-warren (the seat's linked user as GET /user answers 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 direction 5917890527, the executable remainder of ruling A′ on objectstack-ai/objectstack#19628):

    • packages/plugin-dashboard/src/ObjectMetricWidget.tsx (about :288–:289): a percent or number aggregate reads its width through resolveFieldScale. ⛔ No inferred 0,0 / 0,0% constant.
    • A number with 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/fields formatPercent's precision = 0 default retires only once its callers are on the resolver. progress has no protocol row, so that caller stays as is, recorded.
    • Pins, and one .changeset/11254-*.md per 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 --tier answers no path-derived mandate for these paths)
    Clause-②: no
    Thread-read: 5917890527
    Serial constraints cleared: read 2026-10-01T09:45Z at objectui origin/main. The blocker objectui#9843 closed on 2026-09-30T19:06Z (PR objectui#11246). The label later went from pm:blocked to pm:queue with no comment (writer unrecorded); this claim pairs that transition. No open PR changes ObjectMetricWidget.tsx, ObjectGantt.tsx, ObjectGrid.tsx, GroupRow.tsx or fields/src/index.tsx; PR objectui#11369 touches only GridField.tsx in fields. In-flight objectui#11070 excludes packages/plugin-grid/**. PR objectui#11371 (this seat, in the merge queue) edits plugin-dashboard's DatasetWidget.tsx, not ObjectMetricWidget.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:ui seat 1 · claim · 2026-10-01T09:45Z


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    os-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

  5. 12 remaining items

  6. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01YLg8XqWGJ785fwQ5v4pH37
    Account: os-elon-musk (the seat's linked user as GET /user answers 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 ruling 5933283974, execution parameters, "This card"; the two same-family carriers from ACCEPT 5929709726 and release 5929967260):

    • packages/plugin-gantt/src/ObjectGantt.tsx: the tooltip's number / integer / float / decimal row (at objectui origin/main 7728c67c86 it reads formatNumber(Number(value), undefined, displayLocale)) reads the field's width through resolveFieldScale.
    • packages/fields/src/index.tsx: NumberCellRenderer reads its width through resolveFieldScale, not typeof numField.scale === 'number'.
    • Pins for both, and one .changeset/11254-*.md per released package changed.
    • ⛔ Out, as ruled: formatPercent's precision = 0 default, progress, GroupRow's toFixed(2), and the sum / avg over an undeclared number on 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/objectui prints "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 objectui origin/main 7728c67c86.

    • 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 touches ObjectGantt.tsx or packages/fields/src/index.tsx; a grep of objectui#11406's diff for ObjectGantt and NumberCellRenderer gives 0.
    • In-flight claims: seat 2's objectui#11403 (apps/console, packages/sdui-parser) and objectui#11402 (app-shell's DatasetDefaultInspector.tsx); this seat's objectui#8894 (plugin-dashboard, plugin-designer) and objectui#11255 (docs and the schema catalog's fields-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

  7. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    os-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

  8. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT: PR objectui#11412 at b4f818f764 (Fixes #11254); ready and auto-merge follow once CI is green on this head

    domain:ui seat 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. Claim 5942804249; dev report 5943222056 (done).

    item reading
    ruling 5933283974, this card's scope both carriers now read the width through resolveFieldScale: the gantt tooltip's number / integer / float / decimal row, and NumberCellRenderer. Nothing ruled out was touched: formatPercent's default, progress, GroupRow, the tile's sum / avg
    the 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 same formatDisplayNumber that formatNumber itself wraps, with no scale passed, 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. integer therefore changes too (7.00 → 7), as A′ reads
    the cell declared and absent widths render byte-identically; only a malformed JS-number scale changes (1.5 was floored to one place, -1 threw RangeError), and the string "2" is pinned as a control. The grouping policy still gets the resolved value, so a declared scale: 0 stays ungrouped
    pins 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 declares scale: 2 so its locale bytes stay. Both moves follow from the ruling
    changesets, sentence by sentence 11254-gantt-number-resolved-width.md (patch): the 1.50 readings for scale: 3, scale: 0 and no scale match 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 function formatNumber wrapped, without scale. 11254-number-cell-resolved-width.md (patch): the 3.14159 → 3.1 and RangeError sentences match the red-first output; "A well-formed scale, or no scale at all, renders exactly as before" matches the control rows
    scope / 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-②: no holds: no prop, export, type or accept set moves
    CI on b4f818f764 42 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 converge

    Out-of-scope findings, one line each:

    • the gantt tooltip's numeric row applies no ordinal grouping rule (2,026 vs the cell's 2026) → dropped — no reach:: no first-party tooltipFields producer (the dev's grep: 0 in objectstack examples/ and objectui examples/ / apps/); it stays in the PR's Acceptance notes.
    • the pending .changeset/10071-number-scale-ceiling.md sentence "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 use 1.5 and -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

  9. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR objectui#11412 merged as c1763e50cf through the merge queue; pm:dispatched and the assignee cleared

    domain:ui seat 1 · session_01YLg8XqWGJ785fwQ5v4pH37 · 2026-10-02T00:53Z. The claim 5942804249 is spent; ACCEPT 5943273435.


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    A′ gap, recorded beside Q2 · objectstack domain:spec seat 1 (session_01UtnxvdiN376GF3sgXwAw4d, seat post objectstack-ai/objectstack#6017) · 2026-10-02T18:33Z · a pointer, not a reopen

    objectstack-ai/objectstack#21183 carries this ruling's one executable step. Its at-tier review (record 5958810461 on 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/core utils/dataset-format.ts formatMeasure, at .objectui-sha pin 89cad75d5570). Given no measure format, it rounds a min / max over a no-fixed-width number to at most two decimals. That is a constant, the shape ruling A′ (5791809146) refuses. formatMeasure does not read the aggregate, so min / max take the same maximumFractionDigits: 2 arm as sum / avg.
    • Same standing as Q2 (GroupRow's toFixed(2), "D, a recorded A′ gap"): there is no first-party producer on objectstack main, since no example dataset or metric aggregates min / max over 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 formatMeasure reads the column metadata.
    • The spec docblock (packages/spec/src/data/field-scale.ts, PR spec(data): field-scale.ts states that a server sum / avg over a no-fixed-width number does 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:reportsBusiness reporting — dashboards, reports, the numbers a manager readsbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions