Skip to content

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

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11254-metric-widget-resolved-width
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11254-metric-widget-resolved-width

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 says Part of rather than closing the card.

What this changes

Three faces now read a percent or number field's width through resolveFieldScale from @objectstack/spec/data, the metric half and the carriers of ruling A′ (objectstack-ai/objectstack#19628, ruling 5791809146, directed by triage 5917890527). The list cell, the detail chip, the grid footer and the percent edit widget already ask the same function since PR objectui#11246.

Face Before After
object-metric tile, ObjectMetricWidget pattern from the type alone: '0,0%' for percent, '0,0' for number pattern from resolveFieldScale({ type, scale })
Gantt tooltip, percent row in ObjectGantt formatPercent(value, undefined, locale) formatPercent(value, resolveFieldScale({ type, scale }), locale)
Grid mobile card, percent slot in ObjectGrid formatPercent(value, undefined, locale) a percent field's resolved width; any other column unchanged

Measured through the components on the base tree d0fba91aa, then on this branch:

Case Base Now
tile, number with scale: 2, avg 3.75 (the card's repro) 4 3.75
tile, number with scale: 3, sum 1.5 2 1.500
tile, percent with scale: 2, avg 0.1234 12% 12.34%
tile, percent with scale: 1, sum 0.5 50% 50.0%
tile, number with no scale, min 0.99 / max 2.125 1 / 2 0.99 / 2.125
tile, number with no scale, max 0.7 with invert 0 0.3
gantt tooltip, percent with scale: 2, 0.1234 12% 12.34%
mobile card, percent with scale: 2, 0.1234 12% 12.34%

Unchanged on purpose: an undeclared percent (whole percents, the protocol's row), a declared scale: 0, a malformed scale: "2" (no declaration), a count, and an authored format on the tile.

The metric tile, and a number that declares no width

What the tile knows: it already fetches the object schema (getObjectSchema) and resolves the aggregated field's definition before it formats, so the field's type and scale are in hand without a new query.

number has 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:

  • The aggregate value's own decimals. For min / max this 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. With invert, the values that entered 1 - v are 1 and v, so the width is read off v before the inversion (otherwise 1 - 0.7 prints its binary residue). Taken for min / max. For sum it holds only when the server's arithmetic is exact; over a float store 0.1 + 0.2 comes back as 0.30000000000000004. For avg the result's decimals are not bounded by the inputs at all (1, 2, 2 averages to 1.6666666666666667).
  • Reading the inputs client-side. The tile holds none of them; reading them is a new query.
  • No fixed width (print the value as it comes). The same as the first option, with the same failures for sum / avg.

So for sum / avg over an undeclared number, every reading that honours A′ needs the query or the spec to report a width: AnalyticsResultResponseSchema's column metadata carries format, currency and percentScale but 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, the percent tooltip row: on the resolver.

  • ObjectGrid, the mobile card: on the resolver for a percent field, 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's percentKeys). A number named like a rate counts its scale in 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's precision = 0 default in @object-ui/fields: not retired, and @object-ui/fields is not touched. Callers that still reach the default:

    • PercentCellRenderer, for progress and for textual fields promoted by format: 'percent'. The resolver has no row for those types, and progress stays as is: the claim says not to invent a row.
    • The mobile card, for a non-percent column it files under percent by name.

    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's toFixed(2)

Measured on the base tree by rendering GroupRow with one aggregate. It receives { field, type, value } only, so its text depends on the value alone:

  • a percent field's avg of 0.1234 reads avg: 0.12;
  • a number with scale: 3, avg 1.5, reads avg: 1.50;
  • a number with scale: 0, sum 2.75, reads sum: 2.75;
  • an undeclared number summing 1.125 and 2.5 reads sum: 3.63;
  • a USD currency sum of 1234.5 reads sum: 1234.50.

Putting it on the resolver needs the field's type and scale. Neither GroupRow nor the AggregationResult it is handed carries them. The field metadata lives in ObjectGrid, and the unit needs formatPercent / formatCurrency too. Every route therefore adds an input to an exported component or type: GroupRow / GroupRowProps and AggregationResult are 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, and GroupRow.tsx is not edited.

Spec floors and the dependency edge

  • @object-ui/plugin-dashboard now imports @objectstack/spec/data at runtime. @objectstack/spec moves from devDependencies (^17.0.0) to dependencies at ^17.5.0, the first release exporting resolveFieldScale. The package already reached it through @object-ui/fields, whose floor is ^17.5.0.
  • @object-ui/plugin-gantt raises its floor from ^17.0.0 to ^17.5.0.
  • @object-ui/plugin-grid was already at ^17.5.0.
  • The lockfile moves only those importer specifier lines. Resolution is unchanged at 17.5.0.

Tests and gates

All at head 9958d455ed unless 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 a number filed 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=2 gave 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:

  • The dependency closure was built first, with pnpm --workspace-concurrency=2 over the three ^... filters (15 projects, exit 0).
  • type-check for the three packages exited 0.
  • tsconfig.test.json --listFilesOnly lists each new pin in its package.
  • The three packages were then built (exit 0) for the floor gate.

Spec floors: objectui's own check-spec-range-floors.mjs analyze, scoped to the three packages over their fresh dist, reports 0 findings. Its control re-judges the same artifacts with every floor at ^17.0.0 and reports floor-too-low naming resolveFieldScale for each of the three packages. plugin-grid also gets four older symbols there.

eslint on the 6 changed lintable files, the way the package lint scripts run it: 0 errors. Warnings on the edited sources equal BASE, linted through --stdin at the same paths: 11, 81 and 208. The new pins carry only no-explicit-any warnings: 3, 6 and 4.

  • Population: the 6 files, as the JSON output's 6 results show.
  • Invariance: eslint.config.js sets no parserOptions.project or projectService, 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:check
  • check:control-bytes
  • check:new-line-citations (0 new)
  • check:phantom-deps
  • check:unused-deps
  • check:lockfile-integrity
  • check:lockfile-dedupe
  • check:installed-pin-claims
  • check:pending-changeset-literals
  • check:changeset-claims (report-only; it lists 10 pending changesets naming files this touches, and none is made false by it)
  • check:esm-specifiers
  • check:spec-symbols
  • check:test-path-roots
  • the three check:vi-mock-* gates
  • check:unreferenced-sources
  • markdown-test-inputs --audit

NOT 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.
  • The full lint and test farm is CI's.

Acceptance notes

None of these is filed.

  • The gantt tooltip's number row (formatNumber(value, undefined, locale)) renders every number at formatNumber's own two decimals. It ignores a declared scale and 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 sets tooltipFields. Carrier: none.
  • The tile reads no width for the other scale-bearing types (rating, slider, formula): an aggregate over them shows MetricWidget's default whole number. Not widened here. Carrier: none.
  • NumberCellRenderer reads scale off 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.
  • The mobile card's percent slot is chosen by column NAME, as already recorded on the card and on PR objectui#11246. Carrier: none.
  • packages/plugin-dashboard/README.md cites src/ObjectMetricWidget.tsx by 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_0122Knsowci76D2rBWReCzzZ


Generated by Claude Code

claude added 2 commits October 1, 2026 09:58
…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>
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 10 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/4730-retire-dead-locale-key-batch.md

  • names ObjectGrid.tsx → packages/plugin-grid/src/ObjectGrid.tsx — edited by this change

    • Superseded twin vocabularies. cellRender.* and rowAction.* duplicated a grid.* vocabulary that won. RowActionMenu.tsx is fully i18n-wired and reads grid.openMenu / grid.edit / grid.delete; ObjectGrid.tsx reads grid.empty / grid.yes / grid.no / grid.systemFields. The twins had no reader on either side. - Labels that outlived their control. calendar.agenda labelled a view mode b55a34647 retired from CalendarViewMode (now 'month' | 'week' | 'day'). home.quickActions.createApp*, layout.systemNav.createApp, actionDialog.defaultActionTitle / .ok and grid.bulk.selectPlaceholder sit in namespaces whose consumers are live and wired but demonstrably read other siblings. - Surfaces that left the product. map.* is the strongest form: @object-ui/plugin-map declares no @object-ui/i18n dependency and contains no t() call at all, so it cannot consume a locale string. home.stats.* and recordDetail.viewersTooltip name surfaces nothing renders.

.changeset/5793-spec-range-floors.md

  • names pnpm-lock.yaml → pnpm-lock.yaml — edited by this change

    Nothing a consumer installs today changes: normal resolution already picks the newest 17.x, and pnpm-lock.yaml still resolves 17.2.0 on this edge after the bump. The change is to the declared floor only, which is why it is scored patch rather than minor — the same reasoning objectui#5753 used for the other direction on this dependency.

.changeset/6361-spec-floor-17-2-0.md

  • names pnpm-lock.yaml → pnpm-lock.yaml — edited by this change

    Nothing a consumer installs today changes: normal resolution already picks the newest 17.x, and pnpm-lock.yaml still resolves 17.2.0 on both edges after the bump — only the recorded specifier: moves. No source and no behaviour changes, which is why this is scored patch, on the reasoning 111741454 used for the same remediation on @object-ui/plugin-detail.

.changeset/6475-gantt-block-face-declared.md

  • names plugin-gantt/src/ObjectGantt.tsx → packages/plugin-gantt/src/ObjectGantt.tsx — edited by this change

    The gantt nested-block spelling of a gantt config ({ type: 'object-gantt', gantt: { … } }) had no mirror entry at all: it rode through BaseSchema's .passthrough() entirely unvalidated, and the published TypeScript never taught the shape either — an author writing gantt: { … } got no completion, no type checking, no error on a misspelt member. It was the 28th and last of the keys getGanttConfig (plugin-gantt/src/ObjectGantt.tsx) reads off the schema that objectui#6051 (PR feat(types,plugin-gantt): declare the flattened GanttConfig face ObjectGantt reads (#6051) #6472) did not declare — severed into its own card because, unlike the other 27, declaring it changes what a published tool refuses.

.changeset/6597-retire-fieldmeta-referenceto.md

  • names plugin-grid/src/ObjectGrid.tsx → packages/plugin-grid/src/ObjectGrid.tsx — edited by this change

    No authoring story survived the search either. ObjectGrid's own relational-meta pass-through (applyRelationalMeta, plugin-grid/src/ObjectGrid.tsx) copies reference_to / reference / display_field / etc. from the SCHEMA field def only, at all three of its call sites — never from an authored column override. No doc, example, or fixture in this repo shows a table column pinning a lookup's target away from what its schema field already says. Under the maintainer's standing startup-stage rule (2026-08-27: deprecated/alias spellings retire immediately, no transition windows), no measured demand selects withdraw.

.changeset/6939-objectql-record-source-refinement.md

  • names plugin-gantt/src/ObjectGantt.tsx → packages/plugin-gantt/src/ObjectGantt.tsx — edited by this change

    Both renderers resolve their records from one of three keys, in this order — getDataConfig in plugin-map/src/ObjectMap.tsx and plugin-gantt/src/ObjectGantt.tsx: data, then staticData, then objectName. Both mirrors required objectName alone, so a document authored on staticData drew correctly and was refused by safeValidateSchema — six catalog entries, three per component.

.changeset/7122-objectstack-family-17-3-0.md

  • names pnpm-lock.yaml → pnpm-lock.yaml — edited by this change

    @objectstack/client, core, formula and lint each pin @objectstack/spec EXACTLY, so resolving the spec alone to 17.3.0 left the console bundling TWO copies of it. Moving the family with it in pnpm-lock.yaml collapses the duplicate; every declared range already admitted 17.3.0, so no manifest moved.

.changeset/7762-object-grid-export-options-bare-array-refusal.md

  • names ObjectGrid.tsx → packages/plugin-grid/src/ObjectGrid.tsx — edited by this change

    What was measured, on this branch's base. The mirror declared NO exportOptions member at all, and BaseSchema is .passthrough(), so ObjectGridSchema.safeParse({ type: 'object-grid', objectName: 'accounts', exportOptions: ['csv', 'xlsx'] }) returned success: true with the array back VERBATIM — as did { formats: ['csv', 'pdf'], compression: 'gzip' }. Nothing on the render path parses, and ObjectGrid.tsx reads schema.exportOptions?.formats and only that, so the authored array then lost SILENTLY to the ['csv', 'json'] default: the useEffect that warns about dropped formats reads .formats too and returns early when it is absent, while !!schema.exportOptions kept the export button on screen. An author declared ['csv', 'xlsx'] and got csv/json with no error, no warning and no console line. The two authoring faces disagreed in the direction opposite to objectui#6956's: the TypeScript interface already declared the object form only, so TS refused what zod admitted.

.changeset/9722-bulk-executor-datasource-face.md

  • names ObjectGrid.tsx → packages/plugin-grid/src/ObjectGrid.tsx — edited by this change

    • ObjectGrid.tsx → RecordDetailPanel: removing it type-checks GREEN. It was paying for nothing — both sides already declare the same DataSource | undefined from @object-ui/types. Removed outright. - ObjectGrid.tsx → BulkActionDialog: removing it reddens, and it was paying for TWO things at once — the optional-vs-required arm (the grid declares dataSource?: DataSource, the dialog demands one) AND the bulk-door contravariance. Only the first still needs erasing, so it is now a non-null assertion: today's runtime behaviour is preserved exactly, and any future drift of the face reddens at that site instead of passing silently.

.changeset/9853-grid-non-positive-page-size.md

  • names ObjectGrid.tsx → packages/plugin-grid/src/ObjectGrid.tsx — edited by this change

    Refuse a non-positive pageSize at all three of ObjectGrid.tsx's read points, instead of giving two different answers for one authored value (objectui#9853).

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with c82ff391f (merge-base with origin/main): 9 file(s) changed outside .changeset/, read against 1920 pending declaration(s) that publish a body (2538 pending in total). · run

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3552.4 KB 3574.6 KB
Main entry chunk (gzip) 150.4 KB 350 KB
Entry file index-B4MTwpKh.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 570.62KB 136.73KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 229.96KB 63.80KB
fields (index.js) 260.89KB 66.19KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.49KB 9.23KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.93KB 11.29KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 53.15KB 15.45KB
plugin-charts (index.js) 84.07KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 139.70KB 37.42KB
plugin-designer (index.js) 216.54KB 44.64KB
plugin-detail (index.js) 245.45KB 64.47KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 173.16KB 44.58KB
plugin-gantt (index.js) 173.23KB 43.13KB
plugin-grid (index.js) 231.72KB 63.65KB
plugin-kanban (index.js) 49.49KB 15.54KB
plugin-list (index.js) 116.63KB 28.97KB
plugin-map (index.js) 23.54KB 7.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 33.91KB 10.00KB
plugin-tree (index.js) 11.20KB 3.89KB
plugin-view (index.js) 90.32KB 22.76KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 119.55KB 39.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.06KB 2.68KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 21.42KB 7.05KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.26KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 21.59KB 7.71KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9958d455ed229bbbe530393f47528f28e546b242
Local-runs: none

Inputs: card objectui#11254 (body, triage 5917890527, claim 5928905073, report 5929529084), PR objectui#11375 (body, 12-file list, net diff against merge-base d0fba91aa, two commits ddadc32a4a pins → 9958d455ed fix), the 44 check-runs on the head, and read-only PR objectui#11246 with the A′ pointer 5791883815 on objectui#9843. The PR is a partial delivery (Part of #11254); judged on what it ships and whether it is safe to land alone.

① Derived judgments

  1. The object-metric tile is on the resolver — right. ObjectMetricWidget.tsx:342 calls resolveFieldScale({ type, scale: valueFieldDef.scale }), the call shape PR objectui#11246 established (fields/src/index.tsx:895, DetailView.tsx:1288, useColumnSummary.ts:462 / :477, PercentField.tsx:124). No private ?? N. A declared width becomes widthPattern(width, percent); an undeclared percent gets the protocol's 0 (the resolver's one row), so 0,0% is now the protocol's answer, not an inferred one. The type-only '0,0%' / '0,0' inference is gone from the declared and percent arms. Bytes: scale: 2 avg 3.75 → 3.75; percent scale: 2 → 12.34%; scale: 3 pads to 1.500; a malformed scale: "2" is no declaration and falls to the protocol row — each pinned against the real PercentCellRenderer / NumberCellRenderer output for the same value and field.
  2. A′ on min / max over an undeclared number — right. The resolver answers undefined (no fixed width); the tile then prints the fetched value at its own decimal count (ownFractionDigits, which is the footer's widestFractionDigits over one value, capped at 20). A′'s rule is "round the computed result to the widest decimal count among its inputs": a min / max IS one of its inputs, so its own digit count is at most that widest count, and rounding it there changes nothing. The claim that the grid footer prints the same bytes holds: useColumnSummary.ts:478-483 formats a no-fixed-width result with maximumFractionDigits: widest and no minimum, so 0.5 over rows 0.5, 0.125 prints 0.5 on both faces, and 0.125 prints 0.125. With invert, displayValue is derived from fetchedValue at :519-524 while the memo at :347 reads fetchedValue, so the width is 0.7's before 1 − 0.7 is taken and the tile prints 0.3, not the binary residue — the values that entered the inversion are 1 and v. Pinned (0.99, 2.125, whole 3, inverted 0.3, malformed scale: "0" → 0.99).
  3. sum / avg over an undeclared number is byte-identical to main — right, and the stop line is real. The arm returns '0,0' at :354, the same literal the base memo returned for every number / integer. The premise for stopping checks out in the spec: AnalyticsResultResponseSchema's field metadata carries format, currency and percentScale (a fraction-versus-whole convention, not a width) and no width member (analytics.zod.ts:118), so any A′-conformant reading needs the query or the spec to report one — triage's stop line. The constant is pre-existing, not introduced; the code comment, the changeset and the PR body all say it is kept, not conformed. Landing this moves nothing Q1 still has to decide.
  4. integer rides the same branch as on main. The resolver has no integer row either; a whole value has zero own digits, so min / max print '0,0' as before, and sum / avg keep '0,0'. No behaviour change for integer-valued data.
  5. Controls unchanged. count fails answersInFieldUnit → no pattern (a number of rows); an authored format short-circuits the memo. Both pinned.
  6. The gantt tooltip percent row — right. ObjectGantt.tsx:1395-1400: type is def?.type (:1331), so the percent case always has the def in hand, and the call is the established shape. An undeclared percent resolves to 0, which equals formatPercent's own default, so only a declared width moves (12% → 12.34%); pinned against the real cell.
  7. The grid mobile card's percent slot — right, and correctly narrowed. ObjectGrid.tsx:6051-6060 reads the width off objectSchema.fields[key] only when that def's type === 'percent', the same def the amount slot reads its currency from. A column that classify (:5911-5917) filed under percent by NAME but whose field is a number passes undefined, i.e. the base behaviour: a number's scale counts the stored value's decimals, not the percentage points the card prints, so scale: 2 on 0.25 must not read 25.00% — the negative pin guards exactly that. The name classification is untouched; it is not this card's subject and is already recorded on the card and on PR objectui#11246.
  8. What is left exactly as on main — confirmed by diff. packages/plugin-grid/src/GroupRow.tsx is not in the diff; toFixed(2) stands at :106. @object-ui/fields is not in the diff; formatPercent(value, precision = 0, locale) stands at index.tsx:670, still reached by PercentCellRenderer for progress and format-promoted text (no protocol row; triage said not to invent one) and by the mobile card's name-filed non-percent column. Each is the state a decision still owns, untouched.
  9. Manifests and lockfile — right and minimal. On the base tree plugin-dashboard/src had no runtime import of @objectstack/spec (grep empty), so devDependencies ^17.0.0 was correct then; the new runtime import requires the dependencies entry, and ^17.5.0 is the floor PR objectui#11246 set for the same symbol on fields, plugin-detail and plugin-grid. plugin-gantt already carried a runtime edge → floor raise only. plugin-grid (^17.5.0 in both dependencies and devDependencies) and plugin-detail (^17.5.0) need nothing. pnpm-lock.yaml: three hunks, importer specifier / placement lines only, resolution 17.5.0(ai@7.0.65(zod@4.6.5)) unchanged. Pre-Install Import Graph Check, Lockfile Integrity Check and Lockfile Dedupe Check concluded success. The floors gate itself (Spec Main Shape Gate) is in progress — item ③9.
  10. Pins and the before leg — adequate. No pin mocks @objectstack/spec or the resolver. The dashboard pin mounts the real tile over a mocked data source and asserts against the real list cell; the gantt pin mocks only ./GanttView (presentation) and reads what the real formatFieldValue produced; the grid pin mounts the real ObjectGrid at 390px. The directions predicted in each file header sum to 8 red / 6 green (tile) and 1 red / 2 green twice (gantt, card) — exactly the reported 8 failed | 6 passed and 2 failed | 5 passed, each failure the predicted byte (12% for 12.34%, 4 for 3.75, …). The red-first run on d0fba91aa is the same pins with the fix absent, which is what an in-tree ablation would reproduce; nothing is gained by a second one.
  11. No over-claim found. The metric changeset's "the same answer the list cell, the record header chip, the grid footer and the percent edit widget use" names four callers that all exist on this head (item 1). The "same bytes as the footer" claim for min / max holds (item 2). The sum / avg paragraph states the gap rather than conformance. Part of #11254 is the right keyword while Q1 and Q2 have no successor card. The three manifests, the lockfile and the unedited GroupRow.tsx are each declared as deviations in the report.

② Semver level

Three patch changesets — @object-ui/plugin-dashboard, @object-ui/plugin-gantt, @object-ui/plugin-grid — match the diff. Each is a display-width fix behind existing props; the new helpers (widthPattern, ownFractionDigits) are module-private, the mobile card's IIFE is render-local, and no prop, export, type or accepted set moves. The dependency edges are additive and already satisfied in any consumer tree: plugin-dashboard's new runtime dependency on @objectstack/spec ^17.5.0 was already reached through @object-ui/fields (^17.5.0), and plugin-gantt's floor raise is scored patch on the repo's own precedent for the same dependency (pending changesets 5793 and 6361). @object-ui/fields is untouched, so no fourth changeset is owed. Changeset Bump Policy, Changeset Declaration and Changeset Fixed Group Check concluded success.

Clause-②: no — right. The claim's rationale ("reads the protocol's declared or resolved width through the existing published resolver; no prop, export, type or accept set changes") holds on the diff, and the one carrier that would breach it (GroupRow) was stopped rather than done.

③ Boundary flags

  1. Deviation — Part of #11254, not the order's Fixes. Accepted. Two parts of the card are returned undecided with no successor cards yet; a closing keyword would close a card that still owns Q1 and Q2. The seat switches it once the decision cards exist.
  2. Deviation — GroupRow.tsx in the file surface but not edited. Accepted: it is handed { field, type, value } only, and every resolver route adds an input to GroupRow / GroupRowProps / AggregationResult, all exported from @object-ui/plugin-grid, which breaches the claim's Clause-②: no. Stopping is what the claim's own line ("the dev stops on a breach") directs.
  3. Deviation — three manifest / lockfile files beyond the surface. Accepted (①9); the seat should supplement the claim's file surface with packages/plugin-dashboard/package.json, packages/plugin-gantt/package.json and pnpm-lock.yaml.
  4. Deviation — no @object-ui/fields changeset. Accepted; the package is not in the diff.
  5. Deviation — a scratch GroupRow probe created and deleted before any commit. Process note; not in the diff.
  6. Open question Q1 (the tile's sum / avg over an undeclared number) — genuinely outside seat discretion, correctly framed; not answered here. Every A′-conformant reading needs a width the aggregate answer does not carry (①3), which is triage's stop line verbatim, and the only no-change option (D, keep the whole-number pattern) is a constant that A′ refuses by name, so it needs the maintainer. Four options on the four axes, with a recommendation. Routed to triage, which opens the decision card per its own direction.
  7. Open question Q2 (GroupRow's toFixed(2)) — genuinely outside seat discretion, correctly framed; not answered here. Ruled in scope on the resolver, but every route is a new public input on an exported component or type — a contract shape, Clause-②: yes, which the claim did not carry. Four options with axes; the server-grouped arm correctly notes its dependence on Q1.
  8. Out-of-scope notes, graded. (a) The gantt tooltip number row (ObjectGantt.tsx:1370, formatNumber(value, undefined, locale) → two decimals regardless of a declared scale): same A′ class as this card, outside the named surface, no first-party tooltipFields producer — fileable as a low-grade ruled remainder on the same reasoning this card used to file itself, so the ruling's "every consumer" is tracked; a seat decision, not a blocker. (b) rating / slider / formula aggregates on the tile (no pattern, MetricWidget's whole-number default): acceptance note; A′ names number and percent, the protocol has no row for these, and no producer — nothing required, fileable only if the seat wants the tile's inferredFormat widened on its own card. (c) NumberCellRenderer reads typeof numField.scale === 'number' ? numField.scale : undefined (index.tsx:787), not the resolver; it differs only for a non-integer or negative numeric scale, which 17.5.0 refuses at parse — fileable at low grade as a resolver bypass on a number face (the pointer's "every consumer"), not a blocker. (d) The mobile card's name-based percent slot: already recorded on the card and on PR objectui#11246 — nothing new. (e) packages/plugin-dashboard/README.md citing ObjectMetricWidget.tsx by a line number already wrong on the base: nothing; the no-sweep rule applies and the next README edit corrects it.
  9. CI on the head — 44 check-runs: 31 success, 3 skipped (Test (coverage), its shard matrix, dependabot), 10 in progress, 0 failed. Not concluded, named and not treated as pass or fail: Type Check, Spec Main Shape Gate, Test (shard 1/8) through Test (shard 8/8). Concluded success includes Lint, Build & E2E, Test (dist pins), README Export Check, Docs Route Eager Closure Check, the console performance budget (eager closure 3552.4 KB under 3574.6 KB), the three changeset gates, both lockfile gates, Pre-Install Import Graph Check, Line Citation Gate, Control Byte Scan, Changeset Claim Re-read (its 10 named pending bodies are not made false by this diff — none describes the lines it moves). The dev's local type-check, package suites and scoped floors analyze are the dev's claims, not gate verdicts. This verdict stands on the diff and the concluded runs; a failing conclusion on any of the ten named runs reopens it before merge.
  10. Safe to land as a partial. The three delivered faces are on the resolver with no private constant; the arms a decision still owns (sum / avg undeclared, GroupRow, formatPercent's default) are byte-identical to main; the manifests follow the import; the card stays open under Part of. Nothing in this diff pre-empts Q1 or Q2.

Implemented-by: claude/issue-11254-metric-widget-resolved-width
Reviewed-by: session_0122Knsowci76D2rBWReCzzZ

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 1, 2026 10:42
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 1, 2026 10:42
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 1, 2026
Merged via the queue into main with commit b149617 Oct 1, 2026
47 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11254-metric-widget-resolved-width branch October 1, 2026 10:57
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants