Skip to content

docs(spec): field.useGrouping is live at the objectui pin, and its docblock describes the heuristic the renderer uses - #20908

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-20313-use-grouping-live
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-20313-use-grouping-live

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20313

Clause-②: no

field.useGrouping moves planned → live in the liveness ledger, citing the objectui reader at this repo's .objectui-sha pin, and the key's JSDoc stops describing a grouping heuristic the renderer does not use. Six files, the claim's surface as the seat amended it for the patch round (claim 5915464335; see Patch round below):

  • packages/spec/liveness/field.json: the useGrouping row only. status: live, verifiedAt: 2026-09-30, evidenceScope: cross-repo, evidence = the reader, producer = the seven relay sites, and a rewritten note that no longer says the consumer is in flight.
  • packages/spec/liveness/state-counts/field.md: regenerated by pnpm --filter @objectstack/spec gen:liveness-counts, live 91 → 92, planned 2 → 1.
  • packages/spec/src/data/field.zod.ts: the JSDoc above useGrouping only. The .describe() string was true and is unchanged, so no generated file moves (check:generated: all 15 artifacts up to date).
  • .changeset/20313-use-grouping-live.md: one @objectstack/spec patch. The ledger and src/**/*.zod.ts both ship in the tarball. The two test files below do not ship, so the changeset is unchanged by the patch round.
  • packages/spec/scripts/liveness/check-liveness.test.ts (patch round): the planned missing-file case makes its own planned row, and the typo fixture's comment says what holds.
  • packages/spec/src/data/field.test.ts (patch round): the comment at :843-846 stops repeating the false closing sentence.

Reader citations: objectui at the pin db11afd4967cd9d39381c5e21dc2deec9d706204

The pin carries the landing. REST compare ae582b70a...db11afd4967c on objectui answers ahead, ahead_by 21, behind_by 0 (read 2026-09-30). The local objectui clone is shallow and lacks ae582b70a, so no local ancestry reading is claimed.

ledger field objectui file#symbol line at the pin what it does
evidence packages/core/src/utils/number-display.ts#shouldGroupDisplayNumber :200 (authored boolean returns :209, currency :212, scale !== 0 :214) the one grouping decision; an authored boolean answers first
evidence packages/core/src/utils/number-display.ts#formatDisplayNumber :227 (:263-266) spells the decision for Intl.NumberFormat: useGrouping: false, or true when authored true
evidence packages/fields/src/index.tsx#NumberCellRenderer :770 (reads :810-812, passes :819) reads field.useGrouping, booleans only
producer packages/plugin-grid/src/ObjectGrid.tsx#generateColumns :3357, :3586, :3761 copies it beside scale on all three column paths
producer packages/plugin-detail/src/fieldEnrichment.ts#ENRICHED_FIELD_METADATA_KEYS :208 in the key list copied onto a detail-view field
producer packages/plugin-detail/src/RecordDetailPanel.tsx#buildRecordDetailFields :171 relays it beside scale
producer packages/plugin-detail/src/RelatedList.tsx#effectiveColumns :1562 relays it beside scale
producer packages/plugin-list/src/ObjectGallery.tsx#ObjectGallery :376 relays it beside scale

These are the landing note's (5890301145) readers and seven relay sites, each re-located at the pin. The relay sites go in producer per the README's table: the cell reads field metadata those sites build, never the object definition. The gate attributes every one of these paths to objectui, so it counts them and never resolves them. scanEvidence on the new row returns zero local paths: 2 foreign in evidence, 5 in producer.

The docblock: what was false at the pin, and what it says now

The dispatch named two false sentences. A third one was false too, and it is changed by tense only.

  1. The heuristic. It said scale: 0 with no upper bound keeps grouping and that a small bounded integer range drops it. The pin's heuristic never reads a bound: a declared scale: 0 drops grouping, and any other scale, or none, keeps it. It now says so, keeping objectui#4033, which answers 200 and is the issue that introduced the heuristic.
  2. The closing sentence. It said no default is declared "until the renderer half of this contract (objectui#4033) lands and retires the interim heuristic". The renderer half landed as objectui ae582b70a, and it keeps the heuristic for an absent key rather than retiring it. The sentence now says that and names objectui's shouldGroupDisplayNumber. No tracker number is added.
  3. The motivation sentence said console renderers "construct Intl.NumberFormat with grouping unconditionally ON", so a scale: 0 year "renders 2,026 everywhere it is shown". That is false at the pin, where the same field renders 2026. Only the three verbs change: constructed, rendered, was shown.

The rest of the docblock is untouched, its other tracker numbers included (#20234 owns those).

Controls

Every reading below is objectstack at this branch, base 33b6e8bece, and objectui at the pin.

  • Before the flip (33b6e8bece, untouched tree): pnpm --filter @objectstack/spec check:liveness exit 0, field 94 classified (live 91, planned 2, dead 1), total 967 live · 21 planned.
  • Flip, before the regeneration: exit 1, packages/spec/liveness/state-counts/field.md is STALE.
  • After (b842de02e0): exit 0, field 94 classified (live 92, planned 1, dead 1), total 968 live · 20 planned. The count moved by exactly one. The success line still claims every repo-local evidence path resolves (664/664). Attributed-to-another-repo count 228 → 230, rows citing a producer 118 → 119, cross-repo scope 142 → 143, verifiedAt 652 → 653.
  • Ablation (node scripts/ablation-replace.mjs, wrap mode, over the committed b842de02e0): the anchor ; objectui: packages/fields/src/index.tsx#NumberCellRenderer becomes ; packages/fields/src/index.tsx#NumberCellRenderer. That drops the realm marker, which is the realistic way this row rots. The anchor count went 1 → 0 and the blob went ec2da038770a → 40c4a5c2ec52. check:liveness exited 1 with ✗ 1 'live' / 'planned' / 'experimental' / 'live-elsewhere' entr(ies) cite a file that is missing from THIS repo: field/useGrouping → packages/fields/src/index.tsx. Restored: blob after = blob at HEAD = ec2da038770a, git diff HEAD empty, git status clean. A re-run is exit 0 with live 92, planned 1.

Tests and gates, first round (at b842de02e0)

  • pnpm --filter @objectstack/spec build: exit 0, and it moved no tracked file.
  • check:generated: exit 0, all 15 artifacts up to date.
  • spec vitest --project local: 582 files / 17161 tests passed (1 todo).
  • spec test:repo: 46 files / 813 tests passed.
  • spec typecheck (tsc, scripts, test layer): exit 0.
  • node scripts/pm/dispatch-gates.mjs derived 80 commands, all run, exit codes captured before any pipe. --ran reconciliation: 80 derived, 79 run, 1 NOT-MEASURED, 0 UNRUN. The exceptions:
    • check:dual-build-cjs-loads: exit 3, NOT MEASURED (PREREQUISITE NOT MET: it needs the built output of 86 packages, and this worktree built the spec, formula, lint and objectql closures only). CI builds the whole tree.
    • check:platform-checklist: exit 1 with 2 problems, coverage.json · picklist: UNCLASSIFIED and areas/identity-auth.json … auth-plugin.ts#twoFactor. Pre-existing: the same run on a detached checkout of the base 33b6e8bece exits 1 with the identical two problems. Neither is touched here.
    • check:doc-formula-expressions and check:lean-entry-closure first exited 3 (prerequisites not built). After turbo run build of the formula, lint and objectql closures, both exit 0.
  • Lint, a declared narrowing: eslint --no-inline-config --format json over the 4 changed files returns 4 results. field.zod.ts has 0 errors and 0 warnings. For the other three, eslint's own output is "File ignored because no matching configuration was supplied". eslint.config.mjs never enables type-aware linting (no parserOptions.project, no typed rules), so this diff cannot move the verdict on any untouched file.

Patch round (the seat's ruling on the report's two open questions)

Head f06c26f536. It holds three commits after b842de02e0:

  • 8c1fa09815, the patch itself.
  • 1608b8857f, a merge of origin/main at 9509ea106a. dispatch-gates flagged the tree as stale on a family file (scripts/pm/check-governed-queue-guard.mjs), so main was merged. The merge brings nothing into packages/spec: git diff 8c1fa09815 1608b8857f -- packages/spec is empty.
  • f06c26f536, a three-line correction to one comment.

check-liveness.test.ts, the planned missing-file case (:403-426). It no longer relies on the shipped ledger grading field.useGrouping planned. The case now does four things in the copy:

  1. sets the copied row to planned;
  2. moves the copy's count shard with it, using the file's existing moveCount;
  3. writes the rotted pointer;
  4. reads the row back as its control: expect(readRow(...).status).toBe('planned').

setStatus moves verbatim from the typo block to module scope, and readRow is new.

  • The copied row keeps its producer, evidenceScope and verifiedAt, because none of them trips anything on a planned row. Measured on a scratch copy:

    • status planned alone: exit 1, and the only finding is state-counts/field.md is STALE;
    • status planned plus the count move: exit 0.

    So the rotted pointer stays the run's only cause.

  • Control: an ablation through scripts/ablation-replace.mjs --delete, wrap mode, over the committed 8c1fa09815.

    • It deleted setStatus(root, 'field', 'useGrouping', 'planned'); (anchor 1 → 0, blob 06861b68122f → 27ad6cf2ec7b).
    • The case went red on its control, AssertionError: expected 'live' to be 'planned', in 19 ms and before the gate was spawned. The live and experimental siblings stayed green.
    • Restored: blob = HEAD 06861b68122f, and git diff HEAD is empty.
    • Unmutated, the three cases pass by name.
    • The mutation is in the test file itself, which vitest runs from source, so there is no dist/ leg.
    • f06c26f536 changes only a comment in another block of this file.

The typo fixture's comment (:934-939) now states what holds. This was read from the gate, not assumed:

  • check-liveness.mts:886 scans evidence only for a status in EVIDENCE_SCANNED_STATUSES, and planed is not one of them.
  • :880-885 resolves producer at any status, but every path the row's producer cites is objectui-attributed. scanEvidence returns zero local paths, so none is resolved against this checkout.
  • producer.mts fails producer or evidenceScope only when the value is malformed.

field.test.ts:843-846 now says the renderer half keeps the interim heuristic for an absent key, and names objectui's shouldGroupDisplayNumber. It is the same wording class as JSDoc sentence (b).

The changeset is unchanged. Neither test file ships: files[] carries src/**/*.zod.ts, never *.test.ts, and nothing under scripts/.

Readings. At f06c26f536 unless noted:

  • check:liveness: exit 0, field 94 classified (live 92, planned 1, dead 1).
  • The two changed test files: 2 files / 332 tests passed. The same count also at 8c1fa09815 and 1608b8857f.
  • spec typecheck: exit 0. The test-layer ledger is unchanged at 52 files / 249 errors / 137 signatures.
  • spec test:repo: 46 files / 813 tests passed at 1608b8857f and at 8c1fa09815. It was not re-run at f06c26f536: that commit changes only three comment lines in a local-project test file.
  • spec vitest --project local: 582 files / 17161 tests passed at 8c1fa09815. The spec tree is byte-identical at 1608b8857f.
  • Gates: dispatch-gates --commands derived 87 commands. The test-file kind adds check:engine-double-contract, check:objectql-double-limit, check:pm-governed-merges, check:query-options-erasure, check:type-check-coverage, check:type-check-debt and check:where-matcher. All 87 were run after a full turbo run build of ./packages/* and ./packages/*/*, 71/71 tasks.
    • --ran reconciliation: 87 derived, 87 run, 0 NOT-MEASURED, 0 UNRUN.
    • 86 exited 0, including check:dual-build-cjs-loads (not measured in the first round) and check:type-check-debt ("none above its recorded number").
    • check:platform-checklist exits 1 with the same two pre-existing problems measured on the base in the first round.
  • Lint: eslint --no-inline-config --format json over the 6 changed files. The 3 it lints (check-liveness.test.ts, field.test.ts, field.zod.ts) have 0 errors and 0 warnings. The other 3 report "no matching configuration". Type-aware linting is never enabled.

Acceptance notes

Findings outside the claim's file surface as first delivered. Two are folded into the patch round; the rest are untouched.

  1. Folded in the patch round: the check-liveness.test.ts fixtures, which this flip had left driving a live row. See Patch round.
  2. packages/spec/src/system/metadata-form-zod-reconciliation.test.ts:387-393, the field / useGrouping omit row, says "declared, not enforced yet — liveness verdict planned (the renderer read side … is not landed)". It is stale after this flip, by the file's own rule at :307-309: once the key is enforced, "delete it and decide the offer then — that decision belongs to the enforcement". The seat ruled the offer: offer it. Carrier: the seat's follow-up card, filed at ACCEPT. That card deletes the omit row, adds a useGrouping row on field.form.ts for the number type, and regenerates the catalogs the form rows feed. Neither file is touched here.
  3. Folded in the patch round: packages/spec/src/data/field.test.ts repeated the false closing sentence. See Patch round.
  4. objectui packages/plugin-form (deriveMasterDetail.ts:271, :323; sectionFields.ts:409) copies scale without useGrouping. This is a read-only inference, not reproduced, and it may be only an input widget. The ledger's CAVEAT limits the live claim to the surfaces producer names. Carrier: none.
  5. The one-word rider offered in 5915472266 ("supplies" at field.zod.ts 2649-2650) is not taken, because it is outside the claim's surface. Its pointer names A select / radio field with neither options nor picklist: refuse it at the FieldSchema door (an accept-set narrowing), or keep it at the ADR-0078 completeness gate? (split from #19518) #20827 as the carrier.

Generated by Claude Code

…jectui reader at the pin

The row flips planned to live with verifiedAt 2026-09-30, cross-repo
scope, the reader as evidence (shouldGroupDisplayNumber,
formatDisplayNumber, NumberCellRenderer) and the seven relay sites as
producer, all read at objectui db11afd4967c, this repo's pin. The note
no longer says the consumer is in flight. state-counts/field.md is
regenerated by gen:liveness-counts: live 91 to 92, planned 2 to 1.

Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6
Co-authored-by: Claude <noreply@anthropic.com>
… the renderer actually uses

Three sentences of the FieldSchema.useGrouping JSDoc were false at the
objectui pin db11afd4967c. The interim heuristic never reads bounds: a
declared scale 0 drops grouping and any other scale, or none, keeps it.
The renderer half has landed and keeps that heuristic for an absent key
rather than retiring it. And the motivating sentence about renderers
grouping unconditionally is past, so it now reads in the past tense.
The .describe() string was true and is unchanged. One spec patch
changeset for the ledger flip and this text.

Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tooling labels Sep 30, 2026
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 1 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/spec/liveness/field.json, packages/spec/liveness/state-counts/field.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-driven.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/data-modeling/external-datasources.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/data-modeling/field-types.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/data-modeling/validation-rules.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/deployment/troubleshooting.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/deployment/validating-metadata.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/getting-started/quick-reference.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/kernel/contracts/data-engine.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/protocol/backward-compatibility.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/protocol/objectql/types.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/protocol/objectui/concept.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/ui/forms.mdx (via FieldSchema (symbol, a top-level const))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-1.mdx (via FieldSchema (symbol, a top-level const))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/liveness/field.json, packages/spec/liveness/state-counts/field.md) — pages documenting those are invisible to this run
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json cb4c31dd5279c5c960d82526f7a3054d7de76ae9 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 081b10a820dc0ea2eeaac8d90047d3de085a8eed — the merge of head f06c26f536230b7326d7ddfd8620b2c39c0255b6 into base cb4c31dd5279c5c960d82526f7a3054d7de76ae9, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 081b10a820dc0ea2eeaac8d90047d3de085a8eed && git checkout 081b10a820dc0ea2eeaac8d90047d3de085a8eed
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cb4c31dd5279c5c960d82526f7a3054d7de76ae9 f06c26f536230b7326d7ddfd8620b2c39c0255b6 && git checkout -B drift-repro cb4c31dd5279c5c960d82526f7a3054d7de76ae9 && git merge --no-ff f06c26f536230b7326d7ddfd8620b2c39c0255b6

node scripts/docs-audit/affected-docs.mjs --json cb4c31dd5279c5c960d82526f7a3054d7de76ae9

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs cb4c31dd5279c5c960d82526f7a3054d7de76ae9 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…and two useGrouping comments say what holds

The liveness gate's planned-entry missing-file case relied on
field.useGrouping being planned in the shipped ledger. Once the row went
live the case kept passing while it drove a live row. It now sets the
copied row to planned, moves the copy's count shard with it so the rot
stays the run's only cause, and reads the row back as its control.
setStatus moves to module scope for that; readRow is new.

The typo fixture's comment no longer says the row carries no evidence.
The gate never scans evidence at an unrecognised status, and the row's
producer paths are all objectui-attributed. field.test.ts's comment on
the absent default stops saying the objectui half will retire the
interim heuristic: it landed and keeps it for an absent key.

Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the tests label Sep 30, 2026
…resolved, not that they are counted

The gate counts foreign paths for evidence only; for producer it reads
nothing but the missing list, so "counted" was wrong about producer.

Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f06c26f536230b7326d7ddfd8620b2c39c0255b6
Local-runs: none

Read: card #20313 (body and all 13 comments — triage 5860413673, the split 5880505400, the landing note 5890301145, the unlock scan 5891854242, the restart probe 5905170840, the two deferrals, the claim 5915464335, the rider pointer 5915472266, both os-dev-reports 5916149759 / 5917253464, the seat's patch-round ruling 5916202706 and its correction 5916211005); PR #20908 (body, 6-file list, net diff origin/main... head at merge-base 9509ea106a); the 42 check-runs on the head; objectstack origin/main (.objectui-sha, liveness/README.md, check-liveness.mts, the reconciliation test, packages/spec/package.json); objectui at the pin db11afd4967cd9d39381c5e21dc2deec9d706204. Nothing built, run or re-run. The objectui clone was shallow and lacked ae582b70a; its history was deepened by git fetch --deepen from the pin so the ancestry read could be made locally — a fetch, not a checkout. The driverless git merge-tree --write-tree origin/main refs/review/pr20908 (main at def279a39b) was the one allowed probe: clean, and the six files land byte-identical to the head.

Check-runs on the head, read at 2026-09-30T18:30Z: 42, all completed; 37 success, 5 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in), and the second Auto Label / Check PR Size runs — path-conditional skips; .objectui-sha is unchanged, so the pin gate has nothing to build); 0 failures, 0 in progress. Spec property liveness, Check Changeset, Governed Surface Queue Guard, Lint & Repo Gates, all four Type Check jobs, Test Core 6/6 and Build Core are success.

① Derived judgments

Accept-set: none moves. useGrouping stays z.boolean().optional(), its .describe() is byte-unchanged, and no export, schema, JSON schema or generated doc moves (the head's Lint & Repo Gates carries check:generated; a grep at the head finds no other copy of the three retired sentences outside packages/spec/CHANGELOG.md, which is release history). What DOES move is prose and ledger data in files @objectstack/spec ships (files[] on origin/main: liveness, src/**/*.zod.ts):

  1. liveness/field.json useGrouping: planned → live — RIGHT. Every claim in the row re-read at the pin, not on objectui main:
    • evidence — number-display.ts#shouldGroupDisplayNumber at :200-215: if (typeof useGrouping === 'boolean') return useGrouping (:209), then if (currency) return true (:212), then return scale !== 0 (:214). One decision, authored boolean first in either direction; only an absent key reaches the heuristic, which reads scale and currency and never min/max. #formatDisplayNumber at :227 spells it at :263-267: useGrouping: false when ungrouped, useGrouping: true only for an authored true — the 1:1 mapping. fields/src/index.tsx#NumberCellRenderer at :770 reads field.useGrouping booleans-only at :810-813 and passes it at :819. All three as the row says.
    • producer — the seven relay sites sit at the cited lines and their symbols exist as code: ObjectGrid.tsx#generateColumns (const generateColumns = useCallback, :3108) copies at :3357, :3586, :3761, each beside scale; fieldEnrichment.ts#ENRICHED_FIELD_METADATA_KEYS (:197) lists it at :208 and the loop at :270 copies it; RecordDetailPanel.tsx#buildRecordDetailFields (:111) at :171; RelatedList.tsx#effectiveColumns (:1363) at :1562; ObjectGallery.tsx#ObjectGallery (:241) at :376. A full git grep useGrouping of objectui source at the pin returns exactly these seven field-metadata relays plus the reader and the cell; the remaining hits (i18n/spec-formatters.ts, plugin-charts, PivotTable.tsx) are unrelated Intl uses. "Seven hand-copy sites" is the true count, and producer is owed under the README's table because the cell reads field metadata built elsewhere.
    • note — ae582b70a is feat(core,fields): a number field's authored useGrouping decides its thousands separator, relayed to every cell (objectui#11026) (#11089); git merge-base --is-ancestor ae582b70a db11afd4967c answers yes and rev-list --count is 21, so "ahead 21, behind 0" holds locally, not only by REST. "Never authorWarn'd": no authorWarn in packages/spec/src names useGrouping. The flip condition cited is the old note's own ("once Analytics: a timeDimensions-bucketed query drops the bucket label from rows and fields — trend charts get counts with no x-axis #4033 or its successor actually reads the key"). The objectui#4033 attribution of the heuristic matches number-display.ts:110. The CAVEAT bounds the claim to the seven surfaces — honest.
    • README rules for a live, cross-repo row: a realm marker on every foreign path (the dev's realm-marker ablation reddened the gate, so the paths are parsed), the commit pinned in the prose (objectui @db11afd4967c) in both evidence and producer, evidenceScope: cross-repo, verifiedAt: 2026-09-30 (today, not future), file#symbol anchors throughout. Spec property liveness on the head is success.
  2. liveness/state-counts/field.md 91/2 → 92/1 — RIGHT. Gate-verified: the shard is checked for staleness on every run, and the head's liveness check is green.
  3. field.zod.ts JSDoc, three sentences — RIGHT, and only the JSDoc. (a) The heuristic sentence now says a declared scale: 0 drops grouping, any other scale or none keeps it, no bound is read — true at :213-214. (b) The closing sentence now says the renderer half has landed, reads an authored value first, and keeps the heuristic for an absent key — true at :209 and :214. (c) The motivation sentence moved to past tense — right: a scale: 0 year renders 2026 at the pin. The unchanged .describe() ("Absent = renderer decides (interim heuristic today, locale default eventually); false = opts out; true = pins grouping on") is still true of :209-214 and :263-267. No tracker number was added to a runtime string.
  4. field.test.ts:843-846 comment — RIGHT, the same class as (b), and unpublished.
  5. check-liveness.test.ts — RIGHT. The planned missing-file case (:403-426) reads the shipped status, sets the copied row to planned unconditionally, moves the copy's count shard with the file's existing moveCount (module-scope STATUS_COLUMNS import) only when the shipped status differs, writes the rotted pointer, and asserts readRow(...).status === 'planned' before the gate is spawned. So the case drives a planned row whatever the shipped ledger grades, and the control is real: the gate's own output cannot tell the leg apart (the missing-file heading is the shared SCANNED_LABEL), so the read-back is the only thing that proves it, and the dev's ablation deleting the setStatus line failed on exactly that assertion while the live and experimental siblings stayed green. producer / evidenceScope / verifiedAt kept on a planned row trip nothing — measured by the dev and consistent with the gate. The typo fixture's new comment (:934-939) is true of check-liveness.mts on the head (unchanged from main): :880 resolves producer whatever the status, :886 scans evidence only for EVIDENCE_SCANNED_STATUSES (:534: live / planned / experimental / live-elsewhere), which planed is not, and every producer path is objectui-attributed. setStatus moved verbatim to module scope; readRow is new. Test Core 6/6 success on the head.
  6. Changeset — see ②.
  7. No governed surface among the six paths; Governed Surface Queue Guard success. The record is owed by the seat's claim (the packages/spec/src/** non-test review face), not by tier.

② Semver level

.changeset/20313-use-grouping-live.md: '@objectstack/spec': patch — RIGHT. The diff publishes three shipped files (liveness/field.json, liveness/state-counts/field.md, src/data/field.zod.ts), so skip-changeset would be wrong; nothing widens or narrows what an author can write or what the runtime accepts, so patch is the level, not minor. The two test files do not ship (files[] carries neither scripts/ nor *.test.ts), so the patch round rightly left the changeset alone. Its body is accurate: the heuristic reads only a declared scale: 0, the renderer half reads an authored value first, "text and ledger only".

Clause-②: no — RIGHT, in the PR body and in the changeset body, with no (widening) / (narrowing) arm, as the claim declared. No accept or refuse outcome moves. Check Changeset success.

③ Boundary flags

Dev flags, round 1 (5916149759):

  • Third JSDoc sentence changed, tense only — answered, right: false at the pin and inside the claimed docblock.
  • Branch base 33b6e8bece not b1aab1e309 — answered: after the merge commit the head's merge-base with main is 9509ea106a; PR docs(spec): say only what holds before the picklist reference is resolved (#19518) #20878's field.zod.ts hunks are disjoint; merge-tree against current main def279a39b is clean.
  • Attribution: AGENTS.md's model-free trailer pair over the harness reminder — answered, right: all five commits on the branch carry Claude-Session: and a model-free Co-authored-by:, no model id; AGENTS.md wins.
  • Lint narrowed to the changed files — answered: the head's Lint & Repo Gates success covers the tree.
  • check:dual-build-cjs-loads not measured in round 1 — answered: exit 0 in round 2 after a full build; Build Core / Test Core success on the head.

Open questions, round 1:

  • OQ1 (check-liveness.test.ts fixtures): the seat took A; delivered in 8c1fa09815 + f06c26f536 — judged right under ① item 5.
  • OQ2 (the field/useGrouping omit row, metadata-form-zod-reconciliation.test.ts:387-393): the seat ruled "offer it", carried by a follow-up card filed at ACCEPT, not in this PR. Ruling judged: leaving the row until the follow-up is acceptable, and it must NOT move in this PR. Why: (i) the reconciliation gate reads no liveness ledger — its imports are the form registry and the schemas — so the row is prose in a test-only file that does not ship, and Test Core is green with it; (ii) the file's own rule at :307-309 assigns the offer decision to the enforcement and says "delete it and decide the offer then": deleting the row alone makes the gate demand a field.form.ts control or another ruled reason, and the closed reason set has no "enforced, offer pending" category, so the deletion can only ride with the form row and the platform-objects catalog regeneration — a second package, outside this card's surface; (iii) Prime Directive chore: version packages #10: note it, never expand scope. The row's "not enforced yet" is false from the moment this lands, so the ACCEPT on fields: number rendering honours the authored useGrouping (1 key) #20313 must name the filed card's number. The seat's correction (5916211005: one app, hotcrm-heimao#35/#40/#59, three times — matches the docblock at :1284) leaves the ruling intact.

Dev flags, round 2 (5917253464):

  • PR body not written by the dev; the seat was asked to write the replacement byte-exact — verified: the stored body is 14531 bytes, sha256 a8f8c6ac220c8213731df3dc70c8ca19cd04a462189bb73fa79978f008d7a60d, equal to the report's digest, ending in the session-URL footer once.
  • check-liveness.test.ts is in the local vitest project, not test:repo — informational; both projects ran, and Test Core covers both on the head.
  • origin/main merged as 1608b8857f — verified: git diff 8c1fa09815 1608b8857f -- packages/spec is empty.
  • moveCount added to the planned case — verified in the diff; without it a STALE shard would be a second cause and mask the leg.
  • f06c26f536 corrects the dev's own comment ("counted" → "never resolved" for producer) — verified: :880-884 reads only pv.missing, out-of-range and anchors for producer; only evidence feeds the foreign count (:887). Two lines.

Round 2 open_questions: none. Out-of-scope findings: (1) the omit row — see OQ2; (2) objectui plugin-form deriveMasterDetail.ts / sectionFields.ts copy scale without useGrouping — acceptable as noted, carrier none: an unreproduced, read-only inference on what reads as an input surface, and the row's CAVEAT bounds the live claim to the seven display surfaces; a reproduced display surface showing 2,026 for an authored false would be an objectui defect to file then; (3) the "supplies" rider at field.zod.ts:2649-2650 — acceptable, outside the surface and already carried by #20827. The landing note's prose flag (5890301145, the bound-sensitive heuristic) is closed by JSDoc sentence (a); as 5913858829 corrected, it was the JSDoc, never the .describe().

Card acceptance: the row leaves planned for live citing file#symbol readers and a producer; check:liveness green on the head; the family's byStatus shard regenerated. Met.

Implemented-by: claude/issue-20313-use-grouping-live
Reviewed-by: session_018fxqvRJW12TaHC7DUQ89Y6

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 30, 2026 18:44
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit d78a0bd Sep 30, 2026
43 of 44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20313-use-grouping-live branch September 30, 2026 19:14
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
… contract, and an identity-only column is judged as the type it renders (objectstack-ai#20901) (objectstack-ai#20927)

Closes objectstack-ai#20901

Clause-②: yes (narrowing)

- `FormViewSchema.subforms[].columns` references
`InlineGridColumnSchema` (was `z.array(z.any())`): the card's typed
`currency` + `scale` column and its `zzz_not_a_key` column are refused
at the view parse.
- `defineStack`'s cross-reference check
(`packages/spec/src/stack.zod.ts#collectHydratedInlineColumnErrors`,
called from `validateCrossReferences`) re-parses a column that declares
no `type` as the type it renders (`currency` over a `currency` child
field) through `InlineGridColumnSchema`, on both carriers. The card's
identity-only column with `scale` is refused there with the column
schema's own message; there is no second `scale` rule.
- ADR-0087: D3 entries `form-view-subform-columns-closed` and
`inline-grid-column-identity-only-currency-scale-refused`; the registry
was regenerated after merging a `main` that carries objectstack-ai#20903 and objectstack-ai#20911.

Evidence at `feba1a99bd`: pins
`packages/spec/src/inline-grid-column-carriers.test.ts` 13/13;
`@objectstack/spec` suite 584 files / 17179 tests green;
`@objectstack/spec` typecheck green; 114/114 derived gates exit 0.
Ablations, each restored with `git diff HEAD` empty: reference removed,
5 red; cross-reference call removed, 3 red. `os validate` on a probe
stack: identity-only column, exit 1 `STACK_CROSS_REFERENCE_INVALID`;
typed and bogus columns, exit 1 `STACK_SCHEMA_INVALID`; valid columns,
exit 0.

## Acceptance notes

- No check read `subforms[].childObject` before this change
(`validateCrossReferences` read only `form.data.object`), so the
child-object lookup is new here.
- The identity-only check runs in `defineStack` only. A view saved
through the metadata door, or a subform whose child object lives in
another package, gets the schema half alone (NOT MEASURED at the save
door). objectui's `@object-ui/types` mirror is still `z.any()`, and the
render-time warning stays the backstop there.
- `field.zod.ts`'s `scale` describe still names only the declared-type
refusal. PR objectstack-ai#20908 holds that file.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…s stale not-enforced-yet omit row (objectstack-ai#20934)

Fixes objectstack-ai#20920

Clause-②: no

## What

`field.useGrouping` is `live` since `d78a0bda07`, the enforcement
carried by objectstack-ai#20313: objectui's `shouldGroupDisplayNumber` answers an
authored value first. The reconciliation ledger's own rule assigns the
offer decision to that enforcement ("Once a key is enforced its row is
stale: delete it and decide the offer then"), and its seat ruled **offer
it** (`5916202706`, corrected by `5916211005`). The at-tier review
`5917470646` on PR objectstack-ai#20908 routed the move to this card. This PR carries
it out:

- `metadata-form-zod-reconciliation.test.ts`: the `field/useGrouping`
"declared, not enforced yet" omit row is deleted, not re-reasoned.
- `field.form.ts`: one `useGrouping` row beside `scale`, with `scale`'s
gate `visibleWhen: "data.type == 'number'"`.
- The four `platform-objects` metadata-form catalogs, regenerated with
the generator. The two new leaves are authored in `zh-CN`, `ja-JP` and
`es-ES`.
- Two population pins that the row moves mechanically, and one
changeset.

| key | form, section | control | the row it copies | gate |
|:--|:--|:--|:--|:--|
| `field.useGrouping` | `field.form.ts`, Configuration | plain row, no
`widget`, no `type` | `field.form.ts` `allowCreate` (the same
`z.boolean().optional()` node, no default) | `number` (`scale`'s) |

The help text follows the key's `.describe()`: "Digit grouping
(thousands separators) in displayed values. Unset: the renderer decides;
an untouched switch writes nothing, so it reads off even where the
renderer groups. Off: never grouped, e.g. a year or an ID. On: always
grouped." The middle clause restates, for this switch, the sentence the
object form's `userActions` row carries for the same renderer behaviour
(see Acceptance notes).

No schema accept set moves and no export changes: `check:api-surface`
reports the surface unchanged, and `check:authorable-surface` exits 0.

## The control (dispatch assumption 1)

| commit | tree | `metadata-form-zod-reconciliation.test.ts` |
|:--|:--|:--|
| `699517cfb9` | omit row deleted, no form row | **red**: `field.(root):
accepted by the Zod but unauthorable in the form — offer it, or add a
root ledger entry that records why it is not offered: expected [
'useGrouping' ] to deeply equal []`. Tests 1 failed, 75 passed (76).
`VERDICT command-exit 1` |
| `75773930ed` | form row added | **green**: Tests 76 passed (76).
`VERDICT command-exit 0` |

At the final head the same file is green inside the full
`@objectstack/spec` run below.

## The console renders it (dispatch assumption 4)

Read in objectui at the pin `db11afd4967c`, source only, no browser:

- The served node is `{ "type": "boolean", "description": … }`
(`packages/spec/json-schema/data/Field.json`).
- `SchemaForm.tsx` `inferWidget`: the row has no `widget` and no `type`,
so the JSON Schema branch runs, and `type === 'boolean'` gives
`'switch'`. No name detector fires: `useGrouping` admits no string, so
the condition, secret, icon, colour and field-ref detectors all decline.
- `resolveFieldFace`: `'switch'` is not a `WIDGETS` key but is in
`KNOWN_PASSTHROUGH_WIDGETS`, so the scalar chain runs, and
`effectiveType === 'boolean'` gives `{ kind: 'scalar' }`.
- `FieldControl`'s boolean branch renders a `Switch` with
`checked={!!value}` and writes only from `onCheckedChange`. Nothing is
written on mount.
- objectui's own `SchemaForm.requiredMarker-10367.test.tsx` renders a
bare `{ type: 'boolean' }` property as `role="switch"` with no wiring.
The field form's `allowCreate` row takes the same path today.

So the row renders with no extra wiring, and the offer is not the
ADR-0078 shape in reverse.

## Catalogs (dispatch assumption 3)

1. `node scripts/check-i18n-bundles.mjs --write`, the regenerator
AGENTS.md names for all nine bundle sets. Only `platform-objects` moved:
the `en` pair, English fills in the three translated locales, and two
provenance rows per locale. That output is committed unedited as
`b9a8778e0b`.
2. The six translated leaves are written by hand in this catalog's
authored vocabulary: "Unset:" is 未设置 / 未設定の場合は / Sin definir, "renderer"
is 渲染端 / レンダラー / renderizador, and the untouched-switch clause follows
each locale's authored `object.fields.userActions.helpText`. Labels:
千位分隔符 / 桁区切り / Separador de miles.
3. `--write` again. Merge mode kept every value and dropped the
provenance rows, so the three `*.source-hashes.generated.ts` files are
byte-identical to the base (`7fea903c24`). This is the rule `5757463712`
and `f4ce10c89d` read: an extractor fill is a byte copy recorded in the
provenance table, and a translated leaf is authored.

## Pins this PR moves

Each went red at exactly the predicted number first, and then moved:

- `packages/lint/src/validate-predicate-path-refs.test.ts`: the
shipped-form predicate census 82 → 83, and its literal-comparison count
57 → 58. Measured against the merge base `d78a0bda07`: the shipped
`*.form.ts` corpus adds exactly `field :: useGrouping :: data.type ==
'number'` and removes none.
-
`packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts`:
the translated-label positive control 658 → 659, one row label authored
in all three locales.

`field-panel-echo-decisions.test.ts` did not move: its no-echo rule
reads the two new leaves as authored. Every changed file is inside the
claim's file surface.

## Changeset

`.changeset/20920-use-grouping-form-row.md`: `@objectstack/spec`
**minor** and `@objectstack/platform-objects` **patch**, `Clause-②: no`.
These are PR objectstack-ai#20428's levels for the same shape (form rows plus their
catalog leaves). What ships was measured after the build, with a
positive control each time:

- spec: the new row's text is in `dist/data/index.mjs`,
`dist/system/index.js` and their `dist/browser/` twins (`dist` is in
`files[]`), beside the control `Number of decimal digits`. The served
form payload gains an authoring control, which is a backwards-compatible
addition, so the level is minor.
- platform-objects: after a rebuild, the new leaves are in
`dist/plugin.mjs`, `dist/index.mjs` and
`dist/metadata-translations/index.mjs` (`Separador de miles` literally,
the zh-CN label as escapes), beside the control `Decimales`. This is
catalog content only, so the level is patch.
- No accept or refuse outcome moves, so the declaration is `no`.

## Verification, head `841a8734bd`

Every build, test and typecheck ran through
`scripts/pm/os-verify-lock.sh`. Each reported `VERDICT command-exit 0`,
except the three first-red measurements named above.

| run | result |
|:--|:--|
| `pnpm build` (whole workspace) | 72 of 72 tasks successful |
| `pnpm --filter @objectstack/spec test` | 583 files passed · 17166
tests passed, 1 todo |
| `pnpm --filter @objectstack/spec test:repo` | 47 files passed · 831
tests passed |
| `pnpm --filter @objectstack/platform-objects test` | 59 files passed ·
948 tests passed |
| `pnpm --filter @objectstack/lint test` | 117 files passed · 5438 tests
passed |
| spec / platform-objects / lint `typecheck` | exit 0 each.
`check:test-typecheck` OK at 52/248/136, 1/3/2 and 2/6/2
files/errors/signatures held |
| the form-registry readers in other packages: cli unit `i18n-coverage`
+ `i18n-duplicate-demand`; metadata-protocol `protocol.meta-types-*` (4
files); objectql `protocol-meta-types-rich` | 2 files / 27 tests, 4 /
58, 1 / 5, all passed |
| `pnpm --filter @objectstack/spec check:generated` | all 15 generated
artifacts up to date |
| `pnpm check:i18n` | OK: 9 packages, all bundles in sync, no undeclared
authoring keys |
| `pnpm check:i18n-stale-fill` | OK: 0 stale-fill leaves, 0 baselined |

**Gates.** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 86 commands at `841a8734bd`. All 86
ran, and each exit code was written to disk before any pipe. `--ran`
reports: 86 derived, 86 run, 0 NOT-MEASURED, 0 UNRUN, and all 86 exited
0.

**Lint, narrowed.** The 8 changed `.ts` files, each matched by eslint's
own config (`--print-config` exits 0 for each). The `--format json`
output reads 8 files, 0 errors, 0 warnings. Invariance:
`eslint.config.mjs` enables no type-aware linting (no
`parserOptions.project`), so this diff cannot move the verdict on a file
it does not touch. The repo-wide `pnpm lint` is CI's run.

No ablation was run: this PR adds a row and moves population pins, and
adds no guard. The reconciliation red/green pair above is the
measurement of the delta.

`origin/main` is three commits past this branch's base (`67c1b11a20`,
`aaad682dbc`, `87847a27ce`). None of them touches a file in this diff,
so it was not merged in.

## Acceptance notes

- **A two-state control on a three-state key.** The switch cannot show
"unset", and cannot put a touched field back to unset: once toggled, an
author reaches unset again only in the Source tab. The help text states
that the untouched state reads off. The same bound holds for every
switch over an optional boolean whose absence is not `false`, the object
form's `userActions` entries among them. Nothing is refused or dropped,
so no card is owed. Carrier: none.

---
_Generated by [Claude
Code](https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/s tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fields: number rendering honours the authored useGrouping (1 key)

2 participants