Repository navigation
fix(plugin-dashboard,plugin-gantt,plugin-grid): the metric tile, the gantt percent row and the mobile card read the field width through resolveFieldScale (objectui#11254) - #11375
Conversation
…th on the metric tile, the gantt percent row and the mobile card Red on the base tree: a declared percent or number width never reaches these faces, and an undeclared number's min/max is rounded to a whole number on the metric tile. Claude-Session: https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ Co-authored-by: Claude <noreply@anthropic.com>
…gantt percent row and the mobile card read the field width through resolveFieldScale Ruling A' (objectstack#19628): a declared or protocol-resolved width is read through the spec's resolveFieldScale, with no private default. - ObjectMetricWidget: a percent or number aggregate's pattern is built from the resolved width instead of the type-inferred '0,0%' / '0,0'. A number declaring no width shows a min / max at the value's own decimals (the value is one of the inputs). A sum / avg over such a number keeps the whole-number pattern: the tile does not see the inputs' widths, and that question is returned on the card. - ObjectGantt tooltip: the percent row passes the resolved width. - ObjectGrid mobile card: a percent field's width is the resolved one; a non-percent column filed under percent by its name is unchanged. plugin-dashboard now depends on @objectstack/spec at runtime and plugin-gantt raises its floor, both to ^17.5.0. Claude-Session: https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ Co-authored-by: Claude <noreply@anthropic.com>
|
changeset-claim-re-read
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Inputs: card objectui#11254 (body, triage ① Derived judgments
② Semver levelThree
③ Boundary flags
Implemented-by: VERDICT: PASS |
…a no-fixed-width `number` does not yet carry its width, and the ruled end-state (objectstack-ai#21183) (objectstack-ai#21449) Fixes objectstack-ai#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>
Part of #11254
Clause-②: no
Why no, from the claim: display formatting reads the protocol's declared or resolved width through the existing published resolver; no prop, export, type or accept set changes. This diff keeps to that: no prop, export, type or accepted set moves. The one carrier that could not be done inside it,
GroupRow, is stopped and returned (see "Stopped" below), and that is also why this saysPart ofrather than closing the card.What this changes
Three faces now read a
percentornumberfield's width throughresolveFieldScalefrom@objectstack/spec/data, the metric half and the carriers of ruling A′ (objectstack-ai/objectstack#19628, ruling5791809146, directed by triage5917890527). The list cell, the detail chip, the grid footer and the percent edit widget already ask the same function since PR objectui#11246.object-metrictile,ObjectMetricWidget'0,0%'for percent,'0,0'for numberresolveFieldScale({ type, scale })percentrow inObjectGanttformatPercent(value, undefined, locale)formatPercent(value, resolveFieldScale({ type, scale }), locale)ObjectGridformatPercent(value, undefined, locale)percentfield's resolved width; any other column unchangedMeasured through the components on the base tree
d0fba91aa, then on this branch:numberwithscale: 2, avg 3.75 (the card's repro)43.75numberwithscale: 3, sum 1.521.500percentwithscale: 2, avg 0.123412%12.34%percentwithscale: 1, sum 0.550%50.0%numberwith noscale, min 0.99 / max 2.1251/20.99/2.125numberwith noscale, max 0.7 withinvert00.3percentwithscale: 2, 0.123412%12.34%percentwithscale: 2, 0.123412%12.34%Unchanged on purpose: an undeclared
percent(whole percents, the protocol's row), a declaredscale: 0, a malformedscale: "2"(no declaration), acount, and an authoredformaton the tile.The metric tile, and a
numberthat declares no widthWhat the tile knows: it already fetches the object schema (
getObjectSchema) and resolves the aggregated field's definition before it formats, so the field'stypeandscaleare in hand without a new query.numberhas no absent-width row, so with nothing declared the resolver answers "no fixed width", and A′ rounds a computed result to the widest decimal count among the values that entered it, never to a constant. The tile reads a server aggregate, not its inputs. Options that need no query or spec change, and what each gives:min/maxthis is exact: the result is one of the values that entered, so its own decimal count is the A′ reading, and the grid footer prints the same bytes over the same rows. Withinvert, the values that entered1 - vare1andv, so the width is read offvbefore the inversion (otherwise1 - 0.7prints its binary residue). Taken formin/max. Forsumit holds only when the server's arithmetic is exact; over a float store0.1 + 0.2comes back as0.30000000000000004. Foravgthe result's decimals are not bounded by the inputs at all (1, 2, 2averages to1.6666666666666667).sum/avg.So for
sum/avgover an undeclarednumber, every reading that honours A′ needs the query or the spec to report a width:AnalyticsResultResponseSchema's column metadata carriesformat,currencyandpercentScalebut no width. That is triage's stop line. Those two aggregates keep the whole-number pattern they already had, and the question goes back in the report on objectui#11254 with the four-axis analysis. It is not answered here.The carriers
ObjectGantt, thepercenttooltip row: on the resolver.ObjectGrid, the mobile card: on the resolver for apercentfield, read off the same field def the card's amount slot reads its currency from. The card fills its percent slot by column NAME (classify'spercentKeys). Anumbernamed like a rate counts itsscalein the stored value's decimals, not in the percentage points the card prints, so a non-percent column's scale is not read and that column is unchanged. The name classification itself is not this card's subject.formatPercent'sprecision = 0default in@object-ui/fields: not retired, and@object-ui/fieldsis not touched. Callers that still reach the default:PercentCellRenderer, forprogressand for textual fields promoted byformat: 'percent'. The resolver has no row for those types, andprogressstays as is: the claim says not to invent a row.Every other caller passes a number: the detail chip, the grid footer, the gantt row, the tile's
MetricWidget, and the dashboard's record-field renderer.Stopped:
GroupRow'stoFixed(2)Measured on the base tree by rendering
GroupRowwith one aggregate. It receives{ field, type, value }only, so its text depends on the value alone:avg: 0.12;scale: 3, avg 1.5, readsavg: 1.50;scale: 0, sum 2.75, readssum: 2.75;sum: 3.63;sum: 1234.50.Putting it on the resolver needs the field's
typeandscale. NeitherGroupRownor theAggregationResultit is handed carries them. The field metadata lives inObjectGrid, and the unit needsformatPercent/formatCurrencytoo. Every route therefore adds an input to an exported component or type:GroupRow/GroupRowPropsandAggregationResultare all exported from@object-ui/plugin-grid. That moves Clause-② to yes and is an API-shape decision this dispatch does not carry. It is returned as an open question with options, andGroupRow.tsxis not edited.Spec floors and the dependency edge
@object-ui/plugin-dashboardnow imports@objectstack/spec/dataat runtime.@objectstack/specmoves fromdevDependencies(^17.0.0) todependenciesat^17.5.0, the first release exportingresolveFieldScale. The package already reached it through@object-ui/fields, whose floor is^17.5.0.@object-ui/plugin-ganttraises its floor from^17.0.0to^17.5.0.@object-ui/plugin-gridwas already at^17.5.0.Tests and gates
All at head
9958d455edunless noted. Commands ran from the repo root, and heavy runs went through the shared verify lock.New pins, each red on the base tree first:
ObjectMetricWidget.resolvedWidth-11254(plugin-dashboard), 14 cases. Its agreement rows render the real list cell for the same value and field. Base: 8 failed, 6 passed, exactly the predicted rows. Now: 14 passed.ObjectGantt.percentResolvedWidth-11254(plugin-gantt), 3 cases. Base: the declared row failed (12%), the 2 controls passed. Now: 3 passed.mobileCardPercentWidth-11254(plugin-grid), 3 cases, one of them the negative row for anumberfiled under percent by name. Base: the declared row failed, 2 passed. Now: 3 passed.Suites:
pnpm exec vitest run packages/plugin-dashboard/ packages/plugin-gantt/ packages/plugin-grid/ --maxWorkers=2gave Test Files 430 passed, 1 skipped (431), and Tests 3784 passed, 120 skipped (3904). The lock verdict was command-exit 0.Build and type-check:
pnpm --workspace-concurrency=2over the three^...filters (15 projects, exit 0).type-checkfor the three packages exited 0.tsconfig.test.json --listFilesOnlylists each new pin in its package.Spec floors: objectui's own
check-spec-range-floors.mjsanalyze, scoped to the three packages over their freshdist, reports 0 findings. Its control re-judges the same artifacts with every floor at^17.0.0and reportsfloor-too-lownamingresolveFieldScalefor each of the three packages. plugin-grid also gets four older symbols there.eslint on the 6 changed lintable files, the way the package
lintscripts run it: 0 errors. Warnings on the edited sources equal BASE, linted through--stdinat the same paths: 11, 81 and 208. The new pins carry onlyno-explicit-anywarnings: 3, 6 and 4.eslint.config.jssets noparserOptions.projectorprojectService, so type-aware linting is off and this diff cannot move an untouched file's verdict.Exit 0:
check-changeset-presence(3 packages, 3 changesets)changeset:checkcheck:control-bytescheck:new-line-citations(0 new)check:phantom-depscheck:unused-depscheck:lockfile-integritycheck:lockfile-dedupecheck:installed-pin-claimscheck:pending-changeset-literalscheck:changeset-claims(report-only; it lists 10 pending changesets naming files this touches, and none is made false by it)check:esm-specifierscheck:spec-symbolscheck:test-path-rootscheck:vi-mock-*gatescheck:unreferenced-sourcesmarkdown-test-inputs --auditNOT MEASURED:
check:eager-closure: it needs the console build, which belongs to CI's performance budget. plugin-dashboard's new edge is@objectstack/spec/data, already in its closure through@object-ui/fields.check:readme-exports: it needs the whole workspace built. No README is edited.Acceptance notes
None of these is filed.
numberrow (formatNumber(value, undefined, locale)) renders every number atformatNumber's own two decimals. It ignores a declaredscaleand A′'s no-fixed-width answer for an undeclared one. Same class, outside this claim's surface (only the percent row was named), and no first-party producer setstooltipFields. Carrier: none.rating,slider,formula): an aggregate over them showsMetricWidget's default whole number. Not widened here. Carrier: none.NumberCellRendererreadsscaleoff the field directly rather than through the resolver. Its answer equals the resolver's for a well-formed declaration and differs only for a malformed one. Carrier: none.packages/plugin-dashboard/README.mdcitessrc/ObjectMetricWidget.tsxby a line number that was already wrong on the base tree. The README is not edited here (AGENTS.md commandment 11: no sweep). Carrier: none.Session:
https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZGenerated by Claude Code