Skip to content

Commit b94a2a7

Browse files
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) (#21449)
Fixes #21183 Clause-②: no This PR writes the protocol-text half of the maintainer's ruling **A (deferred)** on objectstack-ai/objectui#11254 (ruling record 5933283974, batch 261 item 4, 「其他四张同意」). The A′ paragraph of `packages/spec/src/data/field-scale.ts` stated a rule as protocol: "a computed result over a no-fixed-width column rounds to the widest decimal count among the values that entered it". A server-side `sum` / `avg` does not deliver that. The diff changes one comment and nothing else: ⛔ no schema, key, export or runtime change. ## What changes One paragraph changes in place, in the `number` bullet of the module docblock and in the same voice. It now says four things. - **Which computed results the rule binds today.** Only those whose inputs the renderer sees: the grid summary footer, which computes over the rows it holds, and the `object-metric` tile's `min` / `max`, whose answer is itself one of the inputs. - **What a server `sum` / `avg` over a `number` with no declared `scale` shows instead.** It does not get that width yet. The tile receives one number, and `AnalyticsResultResponseSchema`'s column metadata carries `format` / `currency` / `percentScale` and no width. The paragraph also says what each face shows at the objectui pin. - **The ruled end-state and its trigger.** End-state: the analytics result reports the width, which is the widest decimal count among the values the server read, or the declared `scale` when there is one. Trigger: the first first-party `object-metric` tile or dataset measure doing `sum` / `avg` over such a `number` with no `format`. A is then built in one go. - **The two declared channels an author has today.** One is the field's `scale`, which the `object-metric` tile reads through `resolveFieldScale`. The other is a `format`: the tile's own, or the dataset measure's on a dataset-bound tile. ## Readings (each measured before writing) 1. **Spec, at base `d7d5b4f96a`.** The paragraph stated the rule with no server-aggregate exception. `AnalyticsResultResponseSchema`'s `data.fields[]` declares `name`, `type`, `label`, `format`, `currency`, `percentScale` and `builtinAggregate`, and no width member. The schema now sits at about `analytics.zod.ts:94-130`, not the card's `:94-125`. Its content is as the card says. 2. **objectui, read at this repo's `.objectui-sha` pin `89cad75d5570`.** - `ObjectMetricWidget.tsx`, `inferredFormat`: an authored `format` wins. Otherwise a declared `scale` goes through `widthPattern`. With no width, `min` / `max` take the answer's own decimal count (`ownFractionDigits`), and `sum` / `avg` fall through to `'0,0'`. That is line 354, the same line the card cites on `8001068b9c`. The whole-number reading is confirmed. - `useColumnSummary.ts`, `widestFractionDigits`: the footer rounds a no-fixed-width `number` result to the widest decimal count among the values it read. Confirmed. - objectstack-ai/objectui#11375 (merge `b149617e6f`) **is** an ancestor of the pin. The objectui sibling clone is shallow, and its first reading answered exit 1, with the merge commit below the shallow boundary. After the pin's history was deepened into a private ref, `git merge-base --is-ancestor` answered exit 0, which proves ancestry on its own. - **Where the pin differs from the card.** The card says a server `sum` / `avg` "still shows a whole number". That is true of the `object-metric` tile only. - The dataset-bound tile (`DatasetWidget`, then `formatMeasure` in `@object-ui/core` `dataset-format.ts`) behaves differently when the measure has no `format`. It prints an integer as it is and rounds any other value to at most two decimals (`maximumFractionDigits: 2`). - A field's `scale` never reaches that tile. `service-analytics` copies only the measure's `format` into the column metadata (`analytics-service.ts`: `if (f.format == null && m.format) f.format = m.format`). - The docblock says what the pin does on each face. 3. **No generated page lifts the docblock.** `check:docs` is green. The reference pages come from `FieldSchema.scale`'s `.describe()`, which is unchanged and makes no claim about computed results. 4. **objectui citations.** objectui is cited by file and symbol, with no line numbers, in the historical `.objectui-sha` pin spelling. `check:objectui-pin-citations` is green: 49 asserting citations match the pin, and 73 historical ones are recorded. ## Changeset: none, `skip-changeset` (a measured deviation from the dispatch) The card and the dispatch asked for one `@objectstack/spec` `patch` changeset, "since the docblock ships in the published types". Measured after `pnpm --filter @objectstack/spec build`, it does not ship. A module-level docblock is attached to no exported declaration, so it reaches none of the paths in spec's `files[]`: `dist`, `json-schema`, `liveness`, `prompts`, `llms.txt`, `README.md`, `src/**/*.zod.ts`, `api-surface` and `spec-changes.json`. (`field-scale.ts` is not a `*.zod.ts` file.) - The new text (`binds TODAY`) is in 0 files. - The module docblock that was already there (`What an ABSENT`, `Why a resolver and not a Zod default`) is in 0 files. - Positive control: `resolveFieldScale`'s own TSDoc (`The decimal places this field displays`) is in 2 files, `dist/data/index.d.ts` and `dist/data/index.d.mts`. A diff that publishes nothing takes `skip-changeset` (AGENTS.md, Post-Task Checklist step 3). The changeset was written first, in commit `80ae4f4cb3`, and commit `97a757c216` removed it, with this measurement in its message. To reverse: restore `.changeset/21183-field-scale-server-aggregate-width.md` from `80ae4f4cb3` and remove the label. ## Tests and gates (head `97a757c216`) - `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2`: 600 files, 17684 passed, 1 todo. `src/data/field-scale.test.ts` alone: 13 passed. - `pnpm --filter @objectstack/spec run typecheck`: exit 0 (tsc, the scripts typecheck and `check:test-typecheck`). - `pnpm --filter @objectstack/spec check:generated`: all 15 artifacts are up to date. This was taken on `80ae4f4cb3`; `97a757c216` changes only `.changeset/`, and `check:docs` / `check:api-surface` / `check:authorable-surface` were re-run green on `97a757c216`. - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`, with no paths, derived 73 commands. All 73 ran and 72 exited 0. `--ran` reconciliation: `73 derived, 72 run, 1 NOT-MEASURED, 0 UNRUN`. - `check:doc-formula-expressions` and `check:lean-entry-closure` first refused with exit 3, because their prerequisites were not built. After `@objectstack/formula`, `@objectstack/lint` and `@objectstack/objectql` were built, both re-ran green. - **Lint, narrowed.** The population is read from eslint itself: `isPathIgnored: false` for the one changed file. `eslint --no-inline-config --format json` on it: 1 file, 0 errors, 0 warnings. Invariance: the config resolved for this file sets `parserOptions` to `ecmaVersion` / `sourceType` only, with no `project`, so no type-aware linting runs and this diff cannot move a verdict on an untouched file. The repo-wide `pnpm lint` is CI's run. ## NOT MEASURED - `check:dual-build-cjs-loads`: exit 3, PREREQUISITE NOT MET. It needs every workspace package built (`pnpm build`), which does not fit this container's foreground budget. Invariance: the diff is a comment that reaches no emitted file (measured above), so no package's CJS output differs from base. CI runs it. ## Acceptance notes - **The dataset-bound tile's `min` / `max`.** Over a no-fixed-width `number` with no measure `format`, they also round to at most two decimals, through the same `formatMeasure` arm. So "a `min` / `max` keeps its own decimal count" holds on the `object-metric` tile only, and the docblock names only that tile's `min` / `max` as bound. Not filed: there is no producer. No example dataset or metric in this repo aggregates `min` / `max` (grep on `d7d5b4f96a`), and no carrier touches `formatMeasure`. When A is built, a width on the analytics column would cover this face too. - **Trigger reading, on `d7d5b4f96a`.** All 11 dataset `sum` / `avg` measures in `examples/` carry a `format`. The one `object-metric` `sum` KPI (`app-showcase` `command-center.page.ts`, `cc_k6`) carries `'0.0a'`. Within these spellings, no first-party producer has fired the trigger. - **objectui sibling checkout.** The pin's history was deepened into a private ref, `refs/os-dev/issue-21183/pin`, for the ancestry reading, and the ref was deleted afterwards. objectui's working tree and its branch refs were not touched. --- _Generated by [Claude Code](https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 086ad0a commit b94a2a7

1 file changed

Lines changed: 25 additions & 1 deletion

File tree

‎packages/spec/src/data/field-scale.ts‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,31 @@
6868
* private `?? 0` and read the width through {@link resolveFieldScale}, and a
6969
* computed result over a no-fixed-width column rounds to the widest decimal
7070
* count among the values that entered it — derived from the data, ⛔ never a
71-
* constant. Grouping is not this key's to decide:
71+
* constant. That rule binds TODAY only the computed results whose inputs the
72+
* renderer sees: the grid summary footer, which computes over the rows it
73+
* holds, and the `object-metric` tile's `min` / `max`, whose answer is itself
74+
* one of those inputs. A server-side `sum` / `avg` is not one of them: the
75+
* tile receives one number, and `AnalyticsResultResponseSchema`'s column
76+
* metadata (`@objectstack/spec/api`) carries `format` / `currency` /
77+
* `percentScale` and no width. So a `sum` / `avg` over a `number` with no
78+
* declared `scale` does NOT yet show the width this rule derives — measured
79+
* at `.objectui-sha` pin `89cad75d5570`, the `object-metric` tile still shows
80+
* it as a whole number, and the dataset-bound tile, given no measure
81+
* `format`, prints an integer answer as it is and rounds any other to at
82+
* most two decimals. The end-state is ruled (maintainer ruling 5933283974 on
83+
* objectui#11254, batch 261 item 4, letter A deferred, 「其他四张同意」): the
84+
* analytics result REPORTS the width — its column metadata carries the
85+
* widest decimal count among the values the server read, or the field's
86+
* declared `scale` when there is one — and the tile rounds to that. Nothing
87+
* is built before its trigger, the first first-party producer: an
88+
* `object-metric` tile or dataset measure doing `sum` / `avg` over a `number`
89+
* that declares no `scale` and whose author wrote no `format`. Then A is
90+
* built in one go, with no new decision. Until then an author who needs
91+
* decimals there has two declared channels: the field's `scale`, which the
92+
* `object-metric` tile reads through {@link resolveFieldScale}, or a
93+
* `format` — the `object-metric` tile's own, or on a dataset-bound tile the
94+
* measure's, the only channel that reaches that face while its column
95+
* metadata has no width member. Grouping is not this key's to decide:
7296
* `FieldSchema.useGrouping` decides, and the renderer's interim heuristic
7397
* for an absent `useGrouping` reads the DECLARED `scale` only — a declared
7498
* `scale: 0` marks a discrete integer (a year, a fiscal period, an ordinal)

0 commit comments

Comments
 (0)