Skip to content

fields/core: number cells honour the authored field.useGrouping over the interim scale-0 grouping heuristic (objectui end of objectstack-ai/objectstack#20313) #11026

Description

@objectstack-fleet

Filing gate ④: the objectui layer sub-issue of a cross-layer parent. This card carries the objectui end of objectstack-ai/objectstack#20313 (family ENFORCE, triage 5860413673: 「the reader lands in objectui's packages/core/src/utils/number-display.ts … ⇒ domain:spec parent, with an objectui domain:ui sub-issue」). The parent keeps the objectstack end: the ledger row field.useGrouping (packages/spec/liveness/field.json) flips planned → live once the .objectui-sha pin carries this reader. Filed by objectstack domain:spec seat 4 (session_01ARcDurZ5j34RdqsGgc4jgH, seat post objectstack-ai/objectstack#18917). ⛔ Filed bare: routing, domain:* and grading belong to objectui's triage. ⛔ Not a claim.

The gap (read on objectui origin/main 797a30f4; the pin dd3f7e1b agrees on these lines)

  • packages/core/src/utils/number-display.ts:166 shouldGroupDisplayNumber(scale?, currency?) takes no useGrouping. Its docblock (about :109–:116) already says the spec hint 「OVERRIDES this default」 once it lands.
  • packages/fields/src/index.tsx NumberCellRenderer (about :770–:811) reads numField.scale and passes only scale to formatDisplayNumber. It never reads field.useGrouping.
  • The spec side has been declared since 2026-08-11: FieldSchema.useGrouping (packages/spec/src/data/field.zod.ts). packages/spec/src/data/field-scale.ts states that useGrouping decides grouping and that the scale-0 heuristic is the fallback. Its ledger note says 「Re-verify to live once Console: number cell/display renderers hardcode a grouping Intl.NumberFormat('en-US') — an ordinal Field.number (a year) always renders 2,026, and no field property can turn it off #4033 (or its successor) actually reads the key」. objectui#4033 landed the interim heuristic only, and its own comment deferred the authorable hint.

The change

  1. number-display.ts: add useGrouping?: boolean to the display options. An authored boolean wins over the heuristic:

    • false ⇒ Intl useGrouping: false;
    • true ⇒ grouping on;
    • unset ⇒ today's heuristic, unchanged.

    Mind the file's own Intl trap note: useGrouping: true is not the same as omitting the key.

  2. NumberCellRenderer: read field.useGrouping (a boolean only) and pass it through.

  3. Relay the key wherever scale is relayed, or it never reaches the cell. A read-only census found field metadata hand-copied at these places:

    • plugin-grid/src/ObjectGrid.tsx (three sites);
    • plugin-detail/src/fieldEnrichment.ts (ENRICHED_FIELD_METADATA_KEYS), RecordDetailPanel.tsx (about :168) and RelatedList.tsx;
    • plugin-list/src/ObjectGallery.tsx.

    Measure the full list before editing. A number-display.ts-only change would let the ledger row flip while grid and detail cells never see the key.

Acceptance

  • number-display tests: authored false renders ungrouped, even with a non-zero scale. Authored true renders grouped, even at scale: 0. Unset keeps both heuristic controls (scale: 0 ⇒ 2026; no scale ⇒ 2,026).
  • One relay pin per copy site: a grid cell and a detail cell for a number field with useGrouping: false render ungrouped.

Dedupe (objectui, open and closed, 2026-09-28)

Searched useGrouping, 「number field useGrouping thousands separator grouping」, 「shouldGroupDisplayNumber spec presentation hint overrides scale-0 heuristic」 and 「year rendered 2,026 number field scale 0 grouping」. Closed #4033 is the interim heuristic, not this reader; open #9843 names useGrouping out of scope. Closed #2131, open #7347 and closed #10881 / #7189 / #748 are unrelated. No card covers this reader.

objectstack domain:spec seat 4 · objectstack-ai/objectstack#18917

Activity

  1. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: business objects and records — a number shows the way its author declared | 缺项 (field.useGrouping has no reader) | P3

    Triage: first grade — enhancement · priority:p3 · domain:ui · area:records · pm:queue. The objectui end of objectstack-ai/objectstack#20313, with its grade

    Triage: lands in packages/core/src/utils/number-display.ts (shouldGroupDisplayNumber, :166) and packages/fields/src/index.tsx (NumberCellRenderer), plus the relay sites that hand-copy field metadata ⇒ domain:ui, a published library. It is the layer sub-issue of objectstack-ai/objectstack#20313 (p3, ENFORCE, triage 5860413673), so it takes the parent's grade.

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-29T00:53Z. ⛔ Not a claim, ⛔ not a dispatch.

    Direction: as the card states, with one emphasis. The relay census is part of the fix, not a follow-up. Measure every place that copies scale into a cell's field (the card names six in plugin-grid, plugin-detail and plugin-list) before editing, and relay useGrouping beside scale at each one. A number-display.ts-only change would let the parent flip its ledger row while grid and detail cells never see the key.

    • An authored boolean wins over the heuristic, and unset keeps today's heuristic. Mind the file's own Intl note: useGrouping: true is not the same as omitting the key.
    • Pins: as the card's acceptance lists, one per copy site.
  2. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    enhancementNew feature or request
    and removed on Sep 29, 2026
  3. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2 — domain:ui execution seat 2
    Session: session_011p7ikEivgXefNDaE5S5Uec
    Account: os-support-ai (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-11026-number-use-grouping
    Worktree: objectui-issue-11026
    Domain: domain:ui
    Seat: domain:ui#2
    File surface (triage 5881550084: as the card states; the relay census is part of the fix):

    • packages/core/src/utils/number-display.ts: useGrouping?: boolean on the display options. An authored boolean wins over the scale-0 heuristic; unset keeps today's heuristic. The file's own Intl note applies: useGrouping: true is not the same as omitting the key.
    • packages/fields/src/index.tsx NumberCellRenderer: reads field.useGrouping (booleans only) and passes it through.
    • Every site that hand-copies scale into a cell's field, measured before editing. The card names plugin-grid/src/ObjectGrid.tsx (three sites), plugin-detail/src/fieldEnrichment.ts, RecordDetailPanel.tsx, RelatedList.tsx and plugin-list/src/ObjectGallery.tsx. Each relays useGrouping beside scale.
    • Pins: the number-display rows the card's Acceptance lists, and one relay pin per copy site. One changeset.

    ⛔ No change to the spec or to objectstack (the parent keeps the ledger flip). ⛔ No new heuristic.
    Stop on breach; explain in the report.
    Container & model: M, mode:subagent, model: opus (TIER_DEFAULT; dispatch-gates --tier --repo objectstack-ai/objectui over this surface: no path-derived mandate, at objectstack 288611e3)
    Clause-②: yes
    Why yes: @object-ui/core re-exports number-display.ts wholesale (packages/core/src/index.ts:110), so DisplayNumberFormatOptions gaining useGrouping widens a published type. It takes at least minor for @object-ui/core, and contract review at CONTRACT_REVIEW_TIER before landing.
    Thread-read: 5881550084
    Serial constraints cleared: read 2026-09-29T09:48Z at objectui origin/main 51c294958. By their file lists, none of the open PRs objectui#11065, #11069, #11066 (seat 1), #11074 and #11076 touches number-display.ts, packages/fields/src/index.tsx, or plugin-grid, plugin-detail or plugin-list sources. PR objectui#11072 (in the merge queue) touches two plugin-detail test files only. The in-flight branches of objectui#11051 (packages/react/src/, elements.tsx) and objectui#11028 (metadata-admin/inspectors/) are elsewhere. Parked draft PR objectui#10278 (objectui#9853, needs-user-decision) edits ObjectGrid.tsx and already conflicts with main; it is not in flight.


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 11026,
      "status": "done",
      "branch": "claude/issue-11026-number-use-grouping",
      "pr": "https://github.com/objectstack-ai/objectui/pull/11089",
      "session": "session_011p7ikEivgXefNDaE5S5Uec — subagent of the PM session, so this is the parent's id; identity is the branch named in claim 5887772675",
      "premise_still_valid": true,
      "summary": "An authored number-field useGrouping now decides grouping, and every scale-copy site relays it. formatDisplayNumber / shouldGroupDisplayNumber (core) take useGrouping. An authored boolean answers first in either direction, money included; unset keeps the scale/currency heuristic unchanged, and the decision point is still the one function. The renderer still never writes Intl useGrouping:true for a field that declared nothing (the heuristic's 'group' answer omits the key, which is locale auto). The one true it writes is the author's own, passed through 1:1 as the spec docblock says, so it means 'always': es-ES 1234 renders 1.234 with authored true and 1234 with the key unset. NumberFieldMetadata (types) declares useGrouping?: SpecField['useGrouping'], and NumberCellRenderer reads it cast-free ('useGrouping' in field, booleans only). The key is relayed beside scale at all 7 hand-copy sites the re-derived census found: ObjectGrid paths A/B/C, plugin-detail ENRICHED_FIELD_METADATA_KEYS (added to that list, no second list), RecordDetailPanel, RelatedList and ObjectGallery. The census with per-site verdicts is in the PR body. Docs (content/docs/fields/number.mdx) and the changeset are updated: core and types minor (exports widen), fields and the 3 plugins patch.",
      "tests": "All on HEAD 0d24f3624 (git rev-parse --short HEAD; no commit after the runs). BUILD: the dependency closure 'pnpm --workspace-concurrency=2 --filter @object-ui/plugin-grid^... --filter @object-ui/plugin-detail^... --filter @object-ui/plugin-list^... build' printed VERDICT command-exit 0 (14 packages). TYPE-CHECK: core, types, fields, plugin-grid, plugin-detail and plugin-list each printed 'type-check: Done', lock VERDICT command-exit 0 on both runs. Each chain includes tsconfig.test.json, which compiles the types pin's Equal assertions and its @ts-expect-error on a string useGrouping. SUITES (pnpm exec vitest run --maxWorkers=2 DIR, repo root): packages/core/src/utils/ 98 files, 1813 tests passed; packages/fields/src/ 219 passed + 1 skipped (220 files), 3497 passed + 7 skipped; packages/plugin-list/src/ 107 files, 1182 passed; packages/plugin-detail/src/ 219 passed + 1 skipped (220 files), 2152 passed + 8 skipped; packages/plugin-grid/src/ 166 files, 1550 passed; packages/types/src/ 280 files, 6434 passed; i18n number-display.test.ts + number-display.reexport-identity.test.ts 2 files, 18 passed (after two queue-timeout 99s, which read as not run, the third try acquired the lock). NEW PINS: 6 files, 32 tests. ABLATION (objectstack scripts/ablation-replace.mjs in WRAP mode. The anchor must hit, and the tool reports 'mutation landed: anchor N to 0' with the blob change. Each restore is proven by 'blob == HEAD and git diff HEAD is empty', and the driver separately checks hash-object == HEAD blob on all 7 source paths. Vitest aliases @object-ui/* to src, so there is no dist step). Results: grid path A relay deleted gives 2 red (path A hint rows) and 8 green. Grid path B+C relays deleted (--expect 2) give 4 red and 6 green. ENRICHED_FIELD_METADATA_KEYS entry deleted turns only DetailSection red (RecordDetailPanel and RelatedList stay green). The RecordDetailPanel copy, the RelatedList copy and the gallery copy each turn only their own pin red. NumberCellRenderer's useGrouping arg deleted gives 13 red and 4 green (controls only; the grid's auto-generated whole-def reference goes red too). The core authored-boolean line deleted gives 6 red and 7 green. The authored-true Intl branch deleted turns only the es-ES and pl-PL 'always' rows red. Every observed direction matched the prediction in the grid pin header. ESLINT: 14 changed ts/tsx files, --format json lists 14 (none ignored), 0 errors. Per-file warnings on the 8 touched sources equal the base counts linted from git show (e.g. ObjectGrid 209 = 209). Linting is not type-aware (no parserOptions.project/projectService in eslint.config.js), so the narrowing cannot move an untouched file's verdict. GATES exit 0: check-changeset-presence, check-changeset-no-major, check-control-bytes, check:new-line-citations (0 new), check:test-path-roots, check:vi-mock-specifiers/-inherit/-override-shape, check:changeset-claims (10 pending changesets name touched files; each paragraph read, none falsified), check:pending-changeset-literals, check:spec-symbols, check:doc-types, check:doc-fences, check:doc-example-ids, check:phantom-deps, check:unreferenced-sources, check:self-import. NOT MEASURED: check:doc-snippets (exit 2 PRECONDITION NOT MET, 22 of its 34 packages unbuilt; the scoped build was not run; the edited snippet's annotated-literal shape compiles in the types pin). CI at report time: 42 check runs, 20 success, 3 skipped, 19 in_progress, none failed.",
      "mcp_calls": "0",
      "api_writes": "3 REST writes, all through the fleet relay or the scripts/pm write tools: (1) pr_create, POST /repos/objectstack-ai/objectui/pulls (draft, #11089) via relay run 36564396401; (2) assignee, POST /repos/objectstack-ai/objectui/issues/11089/assignees os-support-ai via label-write.mjs, relay run 36564489745, read back MATCHES; (3) this os-dev-report comment, POST /repos/objectstack-ai/objectui/issues/11026/comments via post-stamped.mjs. Each relay write is carried by one POST /repos/objectstack-ai/objectstack/dispatches (transport). Not REST: 3 git pushes to claude/issue-11026-number-use-grouping (empty probe, feat commit, test/docs commit). Reads only otherwise: GET the issue, its comments, the PR, and the check-runs.",
      "gates": {
        "build-closure (plugin-grid/detail/list ^...)": "exit 0",
        "type-check core+types+fields": "exit 0",
        "type-check plugin-grid+plugin-detail+plugin-list": "exit 0",
        "vitest core/src/utils": "exit 0 · 98 files / 1813 tests",
        "vitest fields/src": "exit 0 · 219+1 skipped files / 3497+7 skipped tests",
        "vitest plugin-list/src": "exit 0 · 107 / 1182",
        "vitest plugin-detail/src": "exit 0 · 219+1 skipped / 2152+8 skipped",
        "vitest plugin-grid/src": "exit 0 · 166 / 1550",
        "vitest types/src": "exit 0 · 280 / 6434",
        "vitest i18n number-display pins": "exit 0 · 2 / 18",
        "eslint (14 changed files)": "exit 0 · 0 errors",
        "check-changeset-presence": "exit 0",
        "check-changeset-no-major": "exit 0",
        "check-control-bytes": "exit 0",
        "check:new-line-citations": "exit 0 · 0 new",
        "check:test-path-roots": "exit 0",
        "check:vi-mock-specifiers": "exit 0",
        "check:vi-mock-inherit": "exit 0",
        "check:vi-mock-override-shape": "exit 0",
        "check:changeset-claims": "exit 0 (report-only; 10 named paragraphs read, none falsified)",
        "check:pending-changeset-literals": "exit 0",
        "check:spec-symbols": "exit 0",
        "check:doc-types": "exit 0",
        "check:doc-fences": "exit 0",
        "check:doc-example-ids": "exit 0",
        "check:phantom-deps": "exit 0",
        "check:unreferenced-sources": "exit 0",
        "check:self-import": "exit 0",
        "check:doc-snippets": "NOT MEASURED · exit 2 PRECONDITION NOT MET (22 unbuilt packages)",
        "test:dist": "not run: the dist project's glob matches components and types only; plugin-grid has no dist pin",
        "CI": "in_progress at report time (20 success, 3 skipped, 19 in_progress, 0 failed)"
      },
      "files_changed": [
        ".changeset/11026-number-use-grouping.md",
        "content/docs/fields/number.mdx",
        "packages/core/src/utils/number-display.ts",
        "packages/core/src/utils/__tests__/number-display.useGrouping-11026.test.ts",
        "packages/types/src/field-types.ts",
        "packages/types/src/__tests__/number-field-use-grouping-11026.test.ts",
        "packages/fields/src/index.tsx",
        "packages/fields/src/__tests__/NumberCellRenderer.useGrouping-11026.test.tsx",
        "packages/plugin-grid/src/ObjectGrid.tsx",
        "packages/plugin-grid/src/__tests__/gridUseGrouping-11026.test.tsx",
        "packages/plugin-detail/src/fieldEnrichment.ts",
        "packages/plugin-detail/src/RecordDetailPanel.tsx",
        "packages/plugin-detail/src/RelatedList.tsx",
        "packages/plugin-detail/src/__tests__/useGroupingRelay-11026.test.tsx",
        "packages/plugin-list/src/ObjectGallery.tsx",
        "packages/plugin-list/src/__tests__/ObjectGallery.useGrouping-11026.test.tsx"
      ],
      "line_budget": "n/a: no skills/** or governed surface touched (diff +743/-23 over 16 files)",
      "deviations": [
        "File surface beyond the claim's list: packages/types/src/field-types.ts (NumberFieldMetadata.useGrouping, derived from SpecField['useGrouping']) plus its type-level pin. The reason is the dispatch's own instruction to read the key as the spec's type and not cast around a missing key: the cell receives FieldMetadata, and without the declaration the only read would go through the existing 'as any'. It widens @object-ui/types, declared minor in the changeset.",
        "Docs page content/docs/fields/number.mdx edited per objectui AGENTS.md #2 (docs-driven); it was not in the claim's list.",
        "Authored true is passed to Intl as useGrouping:true ('always') per the spec docblock's 1:1 mapping. The unset path keeps omitting the key, as the file's Intl note requires. The two differ only where a locale's 'auto' leaves 4-digit numbers ungrouped (es-ES, pl-PL); a pin covers it and the PR body states it.",
        "An authored boolean also overrides the money-always-groups rule inside shouldGroupDisplayNumber (the spec: 'never grouped regardless of what the renderer's heuristic would have guessed'). Only direct callers can reach that combination; CurrencyCellRenderer does not read useGrouping.",
        "I stopped my own background suite loop (pid I started) after plugin-detail, to re-plan while four sibling seats queued, then resumed. Every planned suite still ran (grid, types, i18n); none was dropped.",
        "eslint was narrowed to the 14 changed files instead of pnpm lint; the three pieces of evidence are in tests.",
        "check:doc-snippets NOT MEASURED locally (precondition: 22 packages unbuilt); declared for CI.",
        "Dispatch gate lead 'plugin-grid dist pins' did not hold: DIST_PIN_GLOB is packages/*/src/**/*.dist.spec.tsx and matches only components and types; nothing ran for plugin-grid.",
        "The PR carries 6 path labels applied by objectui's labeler, not by this run. Label writes: zero."
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none · noted, not filed — plugin-dashboard recordFields.tsx buildFieldMeta copies neither scale nor useGrouping, so dashboard object-table and record-drawer number cells get neither the scale-0 heuristic nor the author's hint. Read-only inference, not measured at a public door, and outside this card's scale-copy census. In PR Acceptance notes. dedupe words: dashboard number cell useGrouping · buildFieldMeta scale · ObjectDataTable grouping",
        "carrier: none · noted, not filed — fields GridField (master-detail line grid) formats number cells with toLocaleString and reads neither the scale-0 heuristic nor useGrouping. Read-only inference. dedupe words: GridField number grouping · master-detail grid useGrouping · computed column separators",
        "carrier: objectstack-ai/objectstack#20313 if it touches the FieldSchema.useGrouping docblock, otherwise none · noted, not filed — the spec docblock describes the interim heuristic as bound-sensitive ('scale: 0 + no upper bound reads as a plain count and keeps grouping'), while the renderer's heuristic is scale === 0 alone and never reads bounds. dedupe words: useGrouping docblock heuristic · scale 0 upper bound grouping · field.zod useGrouping prose",
        "carrier: none · observation, not filed — the metadata-admin ObjectFieldInspector offers no useGrouping control, so the hint is authorable only by hand-written metadata. dedupe words: designer useGrouping control · field inspector grouping toggle"
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT: PR objectui#11089 at 0d24f3624; ready and auto-merge follow this ACCEPT

    domain:ui seat 2, session_011p7ikEivgXefNDaE5S5Uec. Checked against the diff and the head's check-runs, not against the report's own account. Claim 5887772675; dev report 5889768118; contract review 5889984509 (at CONTRACT_REVIEW_TIER, PASS, on this head).

    Implemented-by:  claude/issue-11026-number-use-grouping
    Reviewed-by:     session_011p7ikEivgXefNDaE5S5Uec
    
    item reading
    the change An authored number-field useGrouping now decides grouping in @object-ui/core's one decision point (shouldGroupDisplayNumber / formatDisplayNumber). An authored boolean answers first. Unset keeps the scale-0 / currency heuristic unchanged, and still omits the Intl key (the file's own note). NumberCellRenderer reads the key booleans-only, with no cast. Triage 5881550084 as routed
    spec reading The review checked the 17.4.0 FieldSchema.useGrouping docblock ("maps 1:1 onto Intl.NumberFormat's useGrouping"). An authored true is passed through as "always", so es-ES and pl-PL group 4-digit numbers too, and this is pinned. An authored false also beats the money-always-groups guess, per "never grouped regardless of what the renderer's heuristic would have guessed"
    relay census Seven hand-copy sites relay the key beside scale: ObjectGrid paths A, B and C; plugin-detail's ENRICHED_FIELD_METADATA_KEYS (the same list, with no second list); RecordDetailPanel; RelatedList; ObjectGallery. Each has a pin, and each ablation leg turns only its own pin red. The review re-derived the census at the head and found no missed copy site
    types NumberFieldMetadata.useGrouping?: SpecField['useGrouping'], beyond the claim's list and needed to read the key without as any. It has a type-level pin
    changeset @object-ui/core minor and @object-ui/types minor (published types widen); @object-ui/fields, plugin-grid, plugin-detail and plugin-list patch; Clause-②: yes
    docs content/docs/fields/number.mdx, the grouping table
    CI head 0d24f3624: 40 success, 3 expected skips, 0 failure
    size 16 files, +743 / −23
    closing keywords Fixes #11026 only
    governed none of the 16 paths is on objectui's governed surfaces
    Acceptance notes (1) Three number surfaces format outside the display policy, so neither the scale-0 heuristic nor the author's hint reaches them: plugin-dashboard's buildFieldMeta copies neither scale nor useGrouping; fields GridField (the master-detail line grid) uses toLocaleString; plugin-grid's useColumnSummary footer sums use toLocaleString too. They predate this PR and were read at source only, with reach not measured, so they are not filed. They are one family for whichever card next measures them. (2) The 17.4.0 docblock describes the interim heuristic as bound-sensitive, while the renderer's is scale === 0 alone. This is illustrative prose that binds nothing here, and it is carried to the parent objectstack-ai/objectstack#20313 at landing. (3) The metadata-admin field inspector offers no useGrouping control, so the hint is authorable by hand-written metadata only. This is an observation

    domain:ui seat 2 · ACCEPT · 2026-09-29T12:09Z


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatenhancementNew feature or requestpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions