Skip to content

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

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21183-field-scale-server-aggregate
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21183-field-scale-server-aggregate

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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.
    • 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) 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

claude added 2 commits October 2, 2026 17:28
…and the ruled end-state

The A-prime paragraph promised that a computed result over a no-fixed-width
`number` column rounds to the widest decimal count among its inputs. That
holds only where the renderer sees the inputs (the grid summary footer, the
object-metric tile's min/max). A server-side sum/avg carries no width: the
analytics column metadata has format/currency/percentScale and no width
member. The docblock now says so, records the ruled end-state (the analytics
result reports the width) and its trigger, and names the two declared
channels an author has today (a field scale, or a format).

No schema, key, export or runtime change.

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
…ships in no published file

Measured after `pnpm --filter @objectstack/spec build`: the module docblock of
`src/data/field-scale.ts` reaches none of the paths in spec's `files[]`
(`dist`, `json-schema`, `liveness`, `prompts`, `llms.txt`, `README.md`,
`src/**/*.zod.ts`, `api-surface`, `spec-changes.json`). The new text, and the
pre-existing module docblock text, both hit 0 files; the positive control,
the TSDoc on `resolveFieldScale` itself, hits `dist/data/index.d.ts` and
`index.d.mts`. A diff that publishes nothing takes `skip-changeset`.

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/s label Oct 2, 2026
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 2, 2026
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/data/field-scale.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/src/data/field-scale.ts) — pages documenting those are invisible to this run
  • 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 — 138 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 53fd35e3e3a0b18b790ab79bd2c65f353e11ab65 → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 97a757c2162d44ea4b86c4d340b50d8ab2bc4acb
Local-runs: none

PR #21449 (card #21183). Net diff origin/main...97a757c216: one file, packages/spec/src/data/field-scale.ts, +25 / -1, every changed line a * line inside the module docblock's number bullet. Inputs read: the card body and both of its comments (claim 5957645758, dev report 5958445882), ruling 5933283974 on objectui#11254, the PR body and file list, the check-runs on this head, and read-only context on origin/main 53fd35e3e3, on objectui at the .objectui-sha pin 89cad75d5570, and in the published @objectstack/spec@17.6.0 tarball. Isolated reviewer; the dispatch order and the seat's own conclusions were not read.

① Derived judgments

  • Accept set and public surface: no change. The 26 changed lines are all comment lines in the detached module-level docblock (head lines 71 to 95). No schema, key, export, type or runtime moves; AnalyticsResultResponseSchema, resolveFieldScale and FieldScaleMeta are untouched. Right.
  • "That rule binds TODAY only the computed results whose inputs the renderer sees: the grid summary footer ... and the object-metric tile's min / max." Right at the pin. plugin-grid/src/useColumnSummary.ts widestFractionDigits (line 216, applied at 567) rounds the footer's number result to the widest decimal count among the values it read; plugin-dashboard/src/ObjectMetricWidget.tsx inferredFormat returns widthPattern(ownFractionDigits(entered), false) for min / max (line 352). objectui PR Land the verified REST-operations mapping table as a references fact sheet — every fleet operation's channel (REST endpoint / GraphQL-only / git zero-quota), session-verified 2026-08-23 #11375's merge b149617e6f is an ancestor of the pin (git merge-base --is-ancestor, exit 0). The exclusive "only" holds too: the dataset-bound face does not follow the rule (③, out-of-scope reading).
  • "A server-side sum / avg is not one of them ... AnalyticsResultResponseSchema's column metadata (@objectstack/spec/api) carries format / currency / percentScale and no width." Right. data.fields[] on origin/main (api/analytics.zod.ts:94-130) declares name, type, label, format, currency, percentScale, builtinAggregate and nothing else; api/index.ts:66 re-exports analytics.zod, so the @objectstack/spec/api attribution is right. On the producer side, service-analytics's enrichResultColumns and withDeclaredMeasureFormats write label, builtinAggregate, format (copied from the measure), currency, percentScale and type; neither reads a field scale.
  • "The object-metric tile still shows it as a whole number." Right: inferredFormat falls through to '0,0' at line 354 when no format is authored, resolveFieldScale answers undefined, and the aggregate is not min / max.
  • "The dataset-bound tile, given no measure format, prints an integer answer as it is and rounds any other to at most two decimals." Right: DatasetWidget.tsx calls formatMeasure(value, f?.format, f?.currency, f?.percentScale, displayLocale) (lines 1239, 1356, 1734, 1770), and @object-ui/core utils/dataset-format.ts:403 reads Number.isInteger(v) ? String(v) : formatNumberInLocale(v, locale, { maximumFractionDigits: 2 }) on the no-format arm.
  • The ruled end-state A and its trigger. Right, in the ruling's own words: "the analytics result reports the width. Its column metadata carries the widest decimal count among the values the server read, or the declared scale when there is one, and the object-metric tile rounds to that"; "The trigger is the first first-party producer: a tile or dataset measure doing sum / avg over a number that declares no scale and whose author wrote no format. Then A is built in one go. ⛔ No new decision card." The docblock spells the tile as object-metric, which is the card's spelling and the tile the ruling's own reading names (ObjectMetricWidget.tsx:354). The paragraph frames A as "ruled" and says "Nothing is built before its trigger", so no end-state sentence reads as a present-tense delivery.
  • The two declared channels. Right, and scoped per face. A field scale is said to reach the object-metric tile through resolveFieldScale: true (line 342, under fieldUnitApplies, which sum / avg / min / max satisfy). The text does NOT say scale reaches the dataset-bound tile; it names the measure's format as "the only channel that reaches that face", which is exactly what the service copies (analytics-service.ts:2850: if (f.format == null && m.format) f.format = m.format;). No sentence tells an author to declare scale where scale does nothing.
  • Protocol vs runtime. No sentence claims more than the runtime delivers: the paragraph narrows the A′ guarantee to the two faces that honour it, states the gap per face, labels A as ruled and deferred, and names the declared remedies. Each citation (pin 89cad75d5570, ruling 5933283974, batch 261 item 4, letter A deferred, 「其他四张同意」) matches its source. The pin is cited as "measured at .objectui-sha pin 89cad75d5570", which packages/spec/scripts/check-objectui-pin-citations.ts classifies as HISTORICAL (a dated measurement) and not ASSERTING, so a later pin bump does not falsify it and the sentence sets no trap; the sha equals today's pin, and objectui is cited by file and symbol with no line numbers.

② Semver level

  • Clause-②: no, no arm. Right: no accept-set change.
  • skip-changeset is CONFIRMED on this head. The card and the dispatch asked for a patch changeset "since the docblock ships in the published types". That premise is false, measured on three channels, none of them a local run:
    1. packages/spec/package.json files[] is dist, json-schema, liveness, prompts, llms.txt, README.md, src/**/*.zod.ts, CHANGELOG.md, api-surface, spec-changes.json. field-scale.ts is not a *.zod.ts file and there is no .npmignore, so the source file is not packed.
    2. tsup.config.ts sets sourcemap: true under esbuildOptions: dropSourcesContent (scripts/tsup-drop-sources-content.mjs: options.sourcesContent = false), so no shipped map carries source text.
    3. The published @objectstack/spec@17.6.0 tarball (registry time.modified 2026-10-02T03:03Z, read over HTTPS, not built): the pre-existing module-docblock text "What an ABSENT" and "Why a resolver and not a Zod default" is in 0 files; the same file's exported-declaration TSDoc "The decimal places this field displays" is in 2 (dist/data/index.d.ts, dist/data/index.d.mts); no field-scale* file is shipped; 0 maps under dist/ carry sourcesContent. The new paragraph lives in the same detached block as the text that does not ship, so the declaration emit that drops one drops the other. The tracked files[] members on the head (api-surface, llms.txt, prompts, liveness, json-schema, README.md, spec-changes.json) contain none of the docblock text either (git grep on the head); the one other hit for "What an ABSENT" is the barrel comment at src/data/index.ts:289, a paraphrase in a file files[] does not ship.
      So the diff publishes nothing from any released package, which is the one case AGENTS.md Post-Task Checklist step 3 reserves skip-changeset for; a patch entry would have put a CHANGELOG sentence in front of consumers about text none of them receive. The dev's measurement is reproduced independently here, and this at-tier PASS is the confirmation the seat's rule asks for.
  • Check Changeset on this head: success.

③ Boundary flags

  • Changeset removed (dev deviation 1). Answered in ②: the removal is right and skip-changeset is confirmed.
  • Card premise holds on one face only (deviation 2). Confirmed at the pin. The card's and the ruling's "the tile keeps its whole-number pattern" is the object-metric tile (the ruling's own reading is ObjectMetricWidget.tsx:354); the dataset-bound tile with no measure format rounds a non-integer sum / avg to at most two decimals (dataset-format.ts:403). The docblock states each face as the pin does, so the dev corrected the card's generalisation without departing from the ruling.
  • objectui shallow-clone fetch into a private ref (deviation 3). No tracked change in either repo: git status --porcelain is empty in both objectstack and objectui; objectui carries no refs/os-dev/* and no ref naming 21183; its HEAD reflog last moved at 2026-10-01T14:53Z, before the dev's run, and its branch list is unchanged; .git/shallow holds one boundary commit and FETCH_HEAD records the bare-sha fetch of the pin. The object store grew; nothing tracked moved.
  • No merge of main (deviation 4). Confirmed: the three commits since base d7d5b4f96a (535d1d25ab, b79301000c, 53fd35e3e3) touch neither packages/spec/src/data/field-scale.ts, packages/spec/src/api/analytics.zod.ts nor .objectui-sha (git diff --stat over those paths is empty), and the PR is mergeable against 53fd35e3e3.
  • open_questions: none declared, none found.
  • out_of_scope_findings: the dataset-bound tile's min / max round to two decimals. Reading only; ⛔ nothing filed from this seat. Confirmed: formatMeasure does not read the aggregate, so a min / max over a no-fixed-width number with no measure format takes the same maximumFractionDigits: 2 arm, a constant, which is the shape A′ (ruling 5791809146) refuses by name. The docblock already excludes this face ("only ... the object-metric tile's min / max"), so the protocol text does not over-claim. It is the same family as Q2 of ruling 5933283974 (GroupRow's toFixed(2), recorded as "D, a recorded A′ gap" to be shaped when a producer appears), and there is no first-party min / max producer on d7d5b4f96a. Escalated to the seat: decide whether this gap gets a findable carrier (a note on objectui#11254 beside Q2, or a card) rather than living only in the PR's acceptance notes. The ruled end-state A, a width on the analytics column, would cover this face when built.
  • Check-runs on this head, final read 2026-10-02T18:31Z: 39 check-runs, all completed: 32 success, 7 skipped (Auto Label, Check PR Size, Check Changeset duplicate run, Console Pin Gate, Build Docs, and both Packed-tarball smoke (opt-in) runs, each skipped by its path filter or opt-in), 0 failed, 0 still running. The named gates (Lint & Repo Gates, Type Check · source gates, Type Check · workspace, Type Check · consumer gates, Spec property liveness, Governed Surface Queue Guard, Check Changeset, all six Test Core shards) concluded success. These conclusions are the gate verdicts; nothing was run locally.

Implemented-by: claude/issue-21183-field-scale-server-aggregate
Reviewed-by: session_01UtnxvdiN376GF3sgXwAw4d

VERDICT: PASS

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

Labels

protocol:data size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants