fix(plugin-charts): refuse a second scatter series instead of drawing it at the first one's y - #7400

Draft
os-project-manager wants to merge 3 commits into
mainfrom
claude/issue-7194-scatter-second-series-refusal
Draft

fix(plugin-charts): refuse a second scatter series instead of drawing it at the first one's y#7400
os-project-manager wants to merge 3 commits into
mainfrom
claude/issue-7194-scatter-second-series-refusal

Conversation

@os-project-manager

@os-project-manageros-project-manager commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes#7194

Maintainer ruling B (comment 5507673384, 2026-09-02): a chartType: 'scatter' with two or more series renders the file's existing refusal shell, stating that a scatter binds one measure and naming the authored fix; a single-series scatter is unchanged; no multi-measure projection (option A waits for a real caller); render-time only — the authoring-time (zod) rejection follows #7113 when that card rules. Clause-② yes ⇒ this PR is a draft; it is not flipped ready and not enqueued.

What changed

  • packages/plugin-charts/src/AdvancedChartImpl.tsx — the scatter arm checks series.length > 1first (a binding fault is true of the spec whatever the rows carry, so it outranks the finding(plugin-charts): scatter is NOT MEASURED for the degenerate-magnitude class — its all-positive CONTROL drew zero marks, so the sweep's zeros say nothing #7171 positional refusal the same way the missing-category-key guard outranks the series guard) and renders SeriesArityRefusal: the existing ChartRefusal shell under data-chart-error="scatter-multi-series", role="status", no console warning (matching the three scatter/magnitude answers already in the file). The shell fits without modification. The sentence is read from chart.scatterOneMeasure through useSafeTranslate — the hook ObjectChart already imports from @object-ui/i18n — so no module-scope createSafeTranslation map is added and no partial i18n mock is disturbed (census: 91 tests mock @object-ui/i18n; none of the partial ones reaches this renderer). The series keys are rendered as data after the sentence, as code elements, outside the translated copy.
  • packages/i18n/src/locales/{en,zh,ja,ko,de,fr,es,pt,ru,ar}.ts — one key, chart.scatterOneMeasure. en: A scatter plots one measure. Keep exactly one series: — states one measure, names the fix; the keys follow as data.
  • packages/plugin-charts/src/AdvancedChartImpl.scatterSeriesArity.test.tsx — 13 pins: single-series control draws 2 marks / no refusal / no note; series: [] and no series still reach the chart through the value fallback; two series ⇒ refusal, no chart container, 0 symbols, no legend; copy contains the sentence and lists both keys; three series refuse; a variant: 'comparison' second series refuses; the binding refusal wins over no-plottable-points; holds with zero rows; no console warning; a two-series bar still draws both series; en pack value equals the inline fallback; every pack defines it and none copies the English; under an I18nProvider (zh) the pack value renders and the keys stay untranslated.
  • .changeset/7194-scatter-one-measure.md — patch for @object-ui/plugin-charts and @object-ui/i18n.

A consequence the contract review looked at: compareTo on scatter

ObjectChart.supportsCompareTo excludes only pie / donut / funnel, and DatasetWidget synthesises a variant: 'comparison' series for every compared measure — so a compare-to scatter is a two-series scatter in the product. Today its overlay is painted at the primary's y values (the overlay never reads its own __comparison column — the same mechanism this card fixes). Under the guard it now refuses, and the pin records that. ⚠️ That behaviour is not a ruled one — the ruling names only "two or more series" and never mentions compareTo or variant — and objectui#7402 is open on whether refusing is the right answer on this path. This PR does not pre-judge it: if that card excludes scatter from compareTo (its shape (b), as pie / donut / funnel already are), the pin inverts, and that is the decision landing rather than a regression. Exempting the overlay would be a one-line change to the predicate, and the picture it would restore is the wrong one.

⚠️ Also raised by the review and recorded on #7402, not fixed here because its remedy depends on how that card rules: on the compare-to path the refusal names a fix the author cannot act on — it says "Keep exactly one series: amount, amount__comparison" while the author wrote exactly one series and never wrote amount__comparison.

Byte cost — measured, both halves, against a control build of the base

Control: origin/main at a6e5f7050 built in a second worktree (pnpm --filter @object-ui/console exec vite build, then node scripts/check-eager-closure-budget.mjs), the same recipe for all three builds; per-chunk numbers read from apps/console/dist/eager-closure.json.

buildframework gzip bytesdelta vs controlcheck:eager-closure
control (a6e5f7050)523,823exit 0 — ✅ framework 511.5 KB / 511.7 KB ceiling (headroom 0.2 KB)
mechanism only (renderer + test, packs at base)523,8230exit 0 — same line
mechanism + copy (eacc55913, unchanged at e090fd866)524,243+420exit 1 — ❌ framework 512.0 KB / 511.7 KB ceiling (OVER by 0.2 KB)

Ceiling PER_CHUNK_GZIP_CEILINGS['framework'] = 524,000; headroom on the control is 177 bytes; this branch is over by 243. The code costs the eager closure nothing (plugin-charts is lazy and not in the closure); the ten-pack copy costs 420 bytes, ~42 per pack, for one sentence already at the ruling's floor (one measure + the fix). ⛔ The ceiling is not raised here and the copy is not weakened below the ruling. This is the fork the dispatch anticipated (PM comment 5513454764, zone 2b): a maintainer budget decision, filed as #7399, one card covering #7194 and #7210. This PR stays draft until it rules; landing order against #7391 is the PM's.

Premise re-checks against the merged ref

  • Defect on origin/maina6e5f7050: yes — scatterYKey = series[0]?.dataKey || 'value' and the legend named off series[0]. fix(plugin-charts): draw every categorical x-axis label on short axes #7385 (d6fe1e1f2) touched only the categorical x-axis path (xAxisCommonProps, label rotation and ellipsis); scatter's numeric axes and the refusal surface were untouched.
  • Authored scatters: 0 in examples / content/docs / apps / packages/*/examples (control bar: 3 files). ⭐ The contract review widened this census to the dashboard-widget spelling type: 'scatter' | 'bubble', which the regex above cannot see: still 0, with control type: 'bar' firing at 10 files including five examples/schema-catalog/**/plugin-dashboard/*.json. The zero holds on a stronger basis than first claimed. One two-series scatter exists in a test corpus: packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx renders plugin-charts:scatter-chart with the shared two-series CHART_SERIES. The sweep stays green on this branch (202/202) because that entry never reaches the scatter arm — the SDUI path does not apply a registration's defaultProps, so it renders with no chartType (filed as bug(plugin-charts): a registration's defaultProps.chartType is inert on the SDUI path — scatter-chart, pie-chart, donut-chart, radar-chart all render as a BAR chart #7401). Not an authored product scatter; the ruling's premise holds. No file outside the declared surface was touched.

Overlap

Verification — mechanism at eacc55913, amendments at e090fd866

  • pnpm exec vitest run packages/plugin-charts/ packages/i18n/ (root, under the shared verify lock): Test Files 100 passed (100) / Tests 1323 passed (1323), exit 0.
  • pnpm exec vitest run packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx: 202 passed on this branch and on the control.
  • Reverse verification (fix committed first): with the base's AdvancedChartImpl.tsx checked out over the commit, the new file reads Tests 8 failed | 5 passed (13) — the 8 refusal/pack pins red, the 5 controls green; restored with git checkout HEAD -- FILE, git diff HEAD empty, blob hash 624cba8… on both sides.
  • pnpm --filter @object-ui/plugin-charts run type-check exit 0 (the new test file is in --listFiles); pnpm --filter @object-ui/i18n run type-check exit 0. Dependency closure built first.
  • Gates: check:i18n-keys 0 · check:i18n-drift 0 (1 key added, 0 en values changed) · check:control-bytes 0 · check:vi-mock-specifiers 0 · check:vi-mock-inherit 0 · check:shell-escape-residue 0 · check-changeset-presence 0 · check:eager-closure1 (above, expected). eslint on the changed files: 0 errors, warnings are the pre-existing no-explicit-any pattern the sibling chart pins also carry.
  • pnpm lint (repo-wide) was not run locally; CI owns it.

Contract review (Clause-②) — PASS, amendments applied at e090fd866

Reviewed at CONTRACT_REVIEW_TIER. Verdict PASS WITH REQUIRED AMENDMENTS; both are applied and verified, so the gate label is off.

  • R1 — the changeset and the compareTo pin comment no longer attribute that behaviour to ruling B; both now describe it as current and unruled, name [Decision] Does #7194's ruling B reach a compareTo scatter — a published capability it never named? The refusal currently swallows it and names a fix the author cannot act on #7402, and state that shape (b) there inverts the pin as a decision landing, not a regression. The pin itself stays: it records what ships. This section of the PR body was corrected on the same grounds.
  • R2 — a test comment claimed check:i18n-keys also enforces the inline-fallback / en-pack equality. ⭐ Verified false by mutation, twice — with the fallback changed to Keep ONE series: all three i18n gates still exit 0, because inlineDefaultValue reads only a defaultValue property off an options object and interpolationOptions skips a positional string. The toBe(EN_COPY) assertion is the sole guard, and the comment now says so. Mutation proven on disk by blob hash and anchored token counts; restore proven by state (git diff HEAD empty, blob back to 624cba845).

Amendments are prose only — 2 files, 35 insertions, 8 deletions, no renderer and no locale packs. Eager-closure figures byte-identical across the amendment: framework 524,243 at both eacc55913 and e090fd866.

Not done here

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

… it at the first one's y
A scatter binds one measure: `series[0].dataKey` is the YAxis key and every
`<Scatter>` reads the same rows through it, so a second series added a colour
and a legend entry and nothing else. Measured: two series over two rows painted
four symbols at two positions, each drawn twice, and the second measure's values
appeared nowhere on the plot. The data was valid and the picture was confidently
wrong — no existing refusal could see it.
Ruled B (maintainer, 2026-09-02): a `chartType: 'scatter'` with two or more
`series` now renders the file's refusal shell under
`data-chart-error="scatter-multi-series"`, stating that a scatter plots one
measure, naming the fix (keep exactly one series) and listing the series keys as
data. A single-series scatter is unchanged. No multi-measure projection is built;
render-time only (authoring-time rejection follows #7113). A `compareTo` overlay
counts as a second series — it was painted at the primary's y by the same
mechanism.
The sentence resolves from `chart.scatterOneMeasure` in all ten locale packs via
`useSafeTranslate`, the hook ObjectChart already uses (no module-scope defaults
map). Measured against a control build of the base: +420 gzipped bytes in the
console's eager `framework` chunk, 243 over its ceiling. The ceiling is NOT
raised here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3177.5 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-CsrWIwFh.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.83KB19.71KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

Standing down on check:eager-closure — the failure is real, it is the maintainer's to resolve, and I am not making it green

PM record from the domain:ui execution seat (session session_01EMrWaQw3XS5DxTHxp4yRyC). This PR is complete and parked as draft. Saying once what is red and why no fix is coming.

What is failing

Bundle Analysis, job 100360098939, head eacc55913. One verdict of four — aggregate closure, ceiling sensitivity and ceiling freshness all pass; only the framework per-chunk ceiling fails, 512.0 KB / 511.7 KB, over by 243 bytes.

⭐ The measurement that matters, and it differs from the sibling fork

This PR's implementer split the two halves against a control build of the base, which is exactly what makes this decidable:

buildframework gzipdeltagate
control (a6e5f7050)523,823 Bexit 0 (headroom 177 B)
mechanism only — renderer + tests, packs at base523,823 B0exit 0
mechanism + ten-pack copy (eacc55913)524,243 B+420exit 1

The mechanism costs the eager closure nothingplugin-charts is lazy and not in the closure. The entire overage is the ten-pack copy, ~42 bytes per pack, for one sentence already at the ruling's floor (states one measure, names the fix).

⚠️ This is a materially different shape from PR #7391's fork, and the difference is load-bearing for the decision. There, the mechanism's own code cost 230 B against 177 B of headroom, so no version fitted, footnote deleted or not. Here the mechanism fits with room to spare and only the translations do not. ⇒ The two forks are not interchangeable, and an option that resolves one may not resolve the other. I have recorded that asymmetry on the decision card.

Why I am not making it green

  • Raising PER_CHUNK_GZIP_CEILINGS['framework'] is 门禁削弱 — the maintainer's floor, not an execution lane's, however well-precedented.
  • Shipping the mechanism with English only and deferring the nine translations delivers less than ruling B specifies. It is a real option and it is on the decision card as one — but it is the maintainer's to choose, not this lane's to take because it happens to be the one that compiles.

Where the decision lives

#7399 — one maintainer decision covering this PR and PR #7391 (#7210 / #7225), since both need ruled user-facing copy in the same ten locale packs and both meet the same 177 bytes. It now carries both forks' measurements and the asymmetry between them.

#7194 is pm:blocked with Blocked-by: #7399 and Unlock-action: re-check PR #7400. This PR stays draft, keeps needs:contract-review, and is not enqueued.

Separately, and not blocked by any of the above

Clause-② is yes on this card, so an in-seat contract review at CONTRACT_REVIEW_TIER is owed before needs:contract-review comes off. This seat runs opus and is not at tier, so a claude-fable-5 reviewer has been dispatched — as was done for #7182. That review runs now, in parallel with the budget decision, so this PR is ready the moment #7399 rules. ⚠️ The reviewer has been pointed specifically at the compareTo consequence this PR's description raises: supportsCompareTo excludes only pie / donut / funnel, so a compare-to scatter is a two-series scatter in the product and now refuses. That was surfaced by the implementer rather than buried, which is the right instinct.

⚠️mergeable_state reads behind; the branch needs updating before any enqueue. Not done now — there is no point rebuilding against a moving base while the PR is parked.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review complete — PASS WITH REQUIRED AMENDMENTS. needs:contract-review stays on.

Clause-② in-seat contract review at CONTRACT_REVIEW_TIER (claude-fable-5), commissioned by the domain:ui seat (PM session session_01EMrWaQw3XS5DxTHxp4yRyC). This seat runs opus and is not at tier, so it commissioned the review rather than performing it — the same route used for #7182.

The mechanism implements ruling B faithfully for authored two-series scatters. The gate label cannot come off for two reasons.

⭐ The finding that justifies the tier — filed as #7402

The PR pins and publishes, as though ruled, a behaviour on the compareTo path that ruling B never contemplated. The reviewer raised it explicitly as the maintainer's question, not the PM's and not its own — and that restraint is the right call.

The ruling's literal predicate ("two or more series") reaches a compare-to scatter. Its stated premise does not: "zero authored scatters in-repo" was measured on chartType: 'scatter' literals, and a compare-to scatter is one authored series plus one synthesised overlay from a documented, published product featureCHANGELOG.md:1727 publishes that only pie / donut / funnel ignore compareTo, and comparisonStyle(s, 'scatter') has a scatter-specific branch. The census that justified B cannot see it.

⚠️A second defect on that path, independent of how #7402 rules: ruling B requires the refusal name "the authored fix". There the tile says "Keep exactly one series: amount, amount__comparison" — the author has exactly one series in their spec and never wrote amount__comparison. The refusal names a fix that cannot be acted on, and the real fix is unknowable at the renderer, which only sees series. A refusal that misdirects is worse than the wrong picture it replaces.

Required amendments

R1 — do not land the compareTo behaviour as decided. The changeset sentence "This applies to a compareTo overlay too — it is a second series…" and the pin comment "The ruling counts series, not variants" (test file 141–155) attribute to the ruling a scope it did not state. The pin may stay — it records what the code actually does today — but it must point at #7402 as open rather than at the ruling as settled.

R2 — correct a false claim about a gate (AdvancedChartImpl.scatterSeriesArity.test.tsx:196): "The gate check:i18n-keys enforces this equality too."Verified false by mutation — with the inline fallback changed to Keep ONE series: the gate still exits 0, because inlineDefaultValue (scripts/check-i18n-call-site-keys.mjs:1368) reads only a defaultValue property in an options object and interpolationOptions explicitly skips a positional string. The toBe(EN_COPY) pin is the only thing holding the fallback and the pack together — the comment must say so, or the next reader deletes the pin as redundant.

A dev is dispatched for both. They are text-only and do not touch the mechanism.

What the review confirmed

  • Precedence is correct and masks nothing. Scatter is excluded from both CATEGORY_AXIS_CHART_TYPES and SERIES_ONLY_CHART_TYPES "on purpose", so the arity check is genuinely the first guard on the scatter path; the binding wins over positional pin goes red on the base. Observation: the cited precedent's actual reasoning is "the more fundamental failure and the more specific message", not "spec fault beats row fault" — same outcome, looser analogy than the comment claims.
  • No new export.SeriesArityRefusal is module-local, packages/plugin-charts/src/index.tsx is unchanged, @object-ui/i18n was already a dependency. ⚠️data-chart-error="scatter-multi-series"is a de-facto contract — the five existing values are pinned across 8 test files and published in the CHANGELOG; this changeset publishes the sixth. chart.scatterOneMeasure widens TranslationKeys; patch is the right bump per standing convention.
  • 13/13 pins green; reverse verification reproduced exactly — base renderer over head gives 8 red / 5 green, file restored to blob 624cba845.
  • The census holds on a stronger basis than claimed. The PR's bar control (3 files) fires only in content/docs, so it proved the regex rather than the coverage. Widened to the dashboard spelling type: 'scatter' | 'bubble' — which the PR's regex cannot see — still 0, with control type: 'bar' at 10 files including five plugin-dashboard fixtures.
  • ⚠️ None of the four suites the implementer did not run (251 tests, all green on the head) exercises a scatter with compareTo — so nothing pinned the old wrong picture, and nothing outside this PR pins the new refusal on that path.
  • check:i18n-keys and check:i18n-drift both exit 0, reproduced. check:eager-closure was out of scope by dispatch ([Decision] The framework per-chunk eager-closure ceiling leaves 177 bytes for the whole repo — two ruled user-facing fixes cannot land, and no wording of either fits #7399).

Where this leaves the PR

⚠️Two independent decisions now block it, and neither alone releases it:

decisionabout
#7399ruled? nothe framework byte ceiling
#7402ruled? nowhether ruling B reaches a compareTo scatter

Draft, needs:contract-review retained, not enqueued.


Generated by Claude Code

…nd name the pin's real guard
Prose only — no behaviour, no pins added or removed, no locale copy touched.
Two amendments required by the Clause-② contract review of this PR.
R1 — the changeset and the `compareTo` pin comment both presented the overlay's
refusal as settled by the maintainer ruling. The ruling's words are "a
`chartType: 'scatter'` with two or more `series`"; it never mentions `compareTo`
or `variant`, and "the ruling counts series, not variants" was the implementer's
reading of it. Both now describe the behaviour as current and unruled, pointing
at objectui#7402, which is open on whether refusing is right on that path. The
pin stays — it records what ships — and its comment now says that shape (b) on
that card would invert it as a decision landing, not as a regression.
R2 — the locale pin carried "The gate `check:i18n-keys` enforces this equality
too", which is false. `inlineDefaultValue` reads a `defaultValue` property off an
options object and `interpolationOptions` skips a positional string, and this
call site passes the English positionally. Re-measured on this head: with the
inline fallback mutated to `Keep ONE series:`, all three i18n gates still exit 0
(`check:i18n-keys`, `check:i18n-drift`, `check:i18n-dead-keys`), and the mutation
was proven on disk by blob hash before each run and the restore by blob equality
plus an empty `git diff HEAD`. The comment now says the assertion is the sole
guard and why no gate sees the pair.
Verified: 13/13 pins in AdvancedChartImpl.scatterSeriesArity.test.tsx pass;
check-changeset-presence and all three i18n gates exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3177.5 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-BFq2aqIU.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.86KB19.72KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.6 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-DLTVLYV9.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.86KB19.72KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

Sign up for freeto 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.

bug(plugin-charts): a multi-series scatter draws every series at the FIRST series' y values — two measures, one cloud painted twice

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(plugin-charts): refuse a second scatter series instead of drawing it at the first one's y - #7400

Draft
os-project-manager wants to merge 3 commits into
mainfrom
claude/issue-7194-scatter-second-series-refusal
Draft

fix(plugin-charts): refuse a second scatter series instead of drawing it at the first one's y#7400
os-project-manager wants to merge 3 commits into
mainfrom
claude/issue-7194-scatter-second-series-refusal

Conversation

@os-project-manager

@os-project-manageros-project-manager commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes#7194

Maintainer ruling B (comment 5507673384, 2026-09-02): a chartType: 'scatter' with two or more series renders the file's existing refusal shell, stating that a scatter binds one measure and naming the authored fix; a single-series scatter is unchanged; no multi-measure projection (option A waits for a real caller); render-time only — the authoring-time (zod) rejection follows #7113 when that card rules. Clause-② yes ⇒ this PR is a draft; it is not flipped ready and not enqueued.

What changed

  • packages/plugin-charts/src/AdvancedChartImpl.tsx — the scatter arm checks series.length > 1first (a binding fault is true of the spec whatever the rows carry, so it outranks the finding(plugin-charts): scatter is NOT MEASURED for the degenerate-magnitude class — its all-positive CONTROL drew zero marks, so the sweep's zeros say nothing #7171 positional refusal the same way the missing-category-key guard outranks the series guard) and renders SeriesArityRefusal: the existing ChartRefusal shell under data-chart-error="scatter-multi-series", role="status", no console warning (matching the three scatter/magnitude answers already in the file). The shell fits without modification. The sentence is read from chart.scatterOneMeasure through useSafeTranslate — the hook ObjectChart already imports from @object-ui/i18n — so no module-scope createSafeTranslation map is added and no partial i18n mock is disturbed (census: 91 tests mock @object-ui/i18n; none of the partial ones reaches this renderer). The series keys are rendered as data after the sentence, as code elements, outside the translated copy.
  • packages/i18n/src/locales/{en,zh,ja,ko,de,fr,es,pt,ru,ar}.ts — one key, chart.scatterOneMeasure. en: A scatter plots one measure. Keep exactly one series: — states one measure, names the fix; the keys follow as data.
  • packages/plugin-charts/src/AdvancedChartImpl.scatterSeriesArity.test.tsx — 13 pins: single-series control draws 2 marks / no refusal / no note; series: [] and no series still reach the chart through the value fallback; two series ⇒ refusal, no chart container, 0 symbols, no legend; copy contains the sentence and lists both keys; three series refuse; a variant: 'comparison' second series refuses; the binding refusal wins over no-plottable-points; holds with zero rows; no console warning; a two-series bar still draws both series; en pack value equals the inline fallback; every pack defines it and none copies the English; under an I18nProvider (zh) the pack value renders and the keys stay untranslated.
  • .changeset/7194-scatter-one-measure.md — patch for @object-ui/plugin-charts and @object-ui/i18n.

A consequence the contract review looked at: compareTo on scatter

ObjectChart.supportsCompareTo excludes only pie / donut / funnel, and DatasetWidget synthesises a variant: 'comparison' series for every compared measure — so a compare-to scatter is a two-series scatter in the product. Today its overlay is painted at the primary's y values (the overlay never reads its own __comparison column — the same mechanism this card fixes). Under the guard it now refuses, and the pin records that. ⚠️ That behaviour is not a ruled one — the ruling names only "two or more series" and never mentions compareTo or variant — and objectui#7402 is open on whether refusing is the right answer on this path. This PR does not pre-judge it: if that card excludes scatter from compareTo (its shape (b), as pie / donut / funnel already are), the pin inverts, and that is the decision landing rather than a regression. Exempting the overlay would be a one-line change to the predicate, and the picture it would restore is the wrong one.

⚠️ Also raised by the review and recorded on #7402, not fixed here because its remedy depends on how that card rules: on the compare-to path the refusal names a fix the author cannot act on — it says "Keep exactly one series: amount, amount__comparison" while the author wrote exactly one series and never wrote amount__comparison.

Byte cost — measured, both halves, against a control build of the base

Control: origin/main at a6e5f7050 built in a second worktree (pnpm --filter @object-ui/console exec vite build, then node scripts/check-eager-closure-budget.mjs), the same recipe for all three builds; per-chunk numbers read from apps/console/dist/eager-closure.json.

buildframework gzip bytesdelta vs controlcheck:eager-closure
control (a6e5f7050)523,823exit 0 — ✅ framework 511.5 KB / 511.7 KB ceiling (headroom 0.2 KB)
mechanism only (renderer + test, packs at base)523,8230exit 0 — same line
mechanism + copy (eacc55913, unchanged at e090fd866)524,243+420exit 1 — ❌ framework 512.0 KB / 511.7 KB ceiling (OVER by 0.2 KB)

Ceiling PER_CHUNK_GZIP_CEILINGS['framework'] = 524,000; headroom on the control is 177 bytes; this branch is over by 243. The code costs the eager closure nothing (plugin-charts is lazy and not in the closure); the ten-pack copy costs 420 bytes, ~42 per pack, for one sentence already at the ruling's floor (one measure + the fix). ⛔ The ceiling is not raised here and the copy is not weakened below the ruling. This is the fork the dispatch anticipated (PM comment 5513454764, zone 2b): a maintainer budget decision, filed as #7399, one card covering #7194 and #7210. This PR stays draft until it rules; landing order against #7391 is the PM's.

Premise re-checks against the merged ref

  • Defect on origin/maina6e5f7050: yes — scatterYKey = series[0]?.dataKey || 'value' and the legend named off series[0]. fix(plugin-charts): draw every categorical x-axis label on short axes #7385 (d6fe1e1f2) touched only the categorical x-axis path (xAxisCommonProps, label rotation and ellipsis); scatter's numeric axes and the refusal surface were untouched.
  • Authored scatters: 0 in examples / content/docs / apps / packages/*/examples (control bar: 3 files). ⭐ The contract review widened this census to the dashboard-widget spelling type: 'scatter' | 'bubble', which the regex above cannot see: still 0, with control type: 'bar' firing at 10 files including five examples/schema-catalog/**/plugin-dashboard/*.json. The zero holds on a stronger basis than first claimed. One two-series scatter exists in a test corpus: packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx renders plugin-charts:scatter-chart with the shared two-series CHART_SERIES. The sweep stays green on this branch (202/202) because that entry never reaches the scatter arm — the SDUI path does not apply a registration's defaultProps, so it renders with no chartType (filed as bug(plugin-charts): a registration's defaultProps.chartType is inert on the SDUI path — scatter-chart, pie-chart, donut-chart, radar-chart all render as a BAR chart #7401). Not an authored product scatter; the ruling's premise holds. No file outside the declared surface was touched.

Overlap

Verification — mechanism at eacc55913, amendments at e090fd866

  • pnpm exec vitest run packages/plugin-charts/ packages/i18n/ (root, under the shared verify lock): Test Files 100 passed (100) / Tests 1323 passed (1323), exit 0.
  • pnpm exec vitest run packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx: 202 passed on this branch and on the control.
  • Reverse verification (fix committed first): with the base's AdvancedChartImpl.tsx checked out over the commit, the new file reads Tests 8 failed | 5 passed (13) — the 8 refusal/pack pins red, the 5 controls green; restored with git checkout HEAD -- FILE, git diff HEAD empty, blob hash 624cba8… on both sides.
  • pnpm --filter @object-ui/plugin-charts run type-check exit 0 (the new test file is in --listFiles); pnpm --filter @object-ui/i18n run type-check exit 0. Dependency closure built first.
  • Gates: check:i18n-keys 0 · check:i18n-drift 0 (1 key added, 0 en values changed) · check:control-bytes 0 · check:vi-mock-specifiers 0 · check:vi-mock-inherit 0 · check:shell-escape-residue 0 · check-changeset-presence 0 · check:eager-closure1 (above, expected). eslint on the changed files: 0 errors, warnings are the pre-existing no-explicit-any pattern the sibling chart pins also carry.
  • pnpm lint (repo-wide) was not run locally; CI owns it.

Contract review (Clause-②) — PASS, amendments applied at e090fd866

Reviewed at CONTRACT_REVIEW_TIER. Verdict PASS WITH REQUIRED AMENDMENTS; both are applied and verified, so the gate label is off.

  • R1 — the changeset and the compareTo pin comment no longer attribute that behaviour to ruling B; both now describe it as current and unruled, name [Decision] Does #7194's ruling B reach a compareTo scatter — a published capability it never named? The refusal currently swallows it and names a fix the author cannot act on #7402, and state that shape (b) there inverts the pin as a decision landing, not a regression. The pin itself stays: it records what ships. This section of the PR body was corrected on the same grounds.
  • R2 — a test comment claimed check:i18n-keys also enforces the inline-fallback / en-pack equality. ⭐ Verified false by mutation, twice — with the fallback changed to Keep ONE series: all three i18n gates still exit 0, because inlineDefaultValue reads only a defaultValue property off an options object and interpolationOptions skips a positional string. The toBe(EN_COPY) assertion is the sole guard, and the comment now says so. Mutation proven on disk by blob hash and anchored token counts; restore proven by state (git diff HEAD empty, blob back to 624cba845).

Amendments are prose only — 2 files, 35 insertions, 8 deletions, no renderer and no locale packs. Eager-closure figures byte-identical across the amendment: framework 524,243 at both eacc55913 and e090fd866.

Not done here

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

… it at the first one's y
A scatter binds one measure: `series[0].dataKey` is the YAxis key and every
`<Scatter>` reads the same rows through it, so a second series added a colour
and a legend entry and nothing else. Measured: two series over two rows painted
four symbols at two positions, each drawn twice, and the second measure's values
appeared nowhere on the plot. The data was valid and the picture was confidently
wrong — no existing refusal could see it.
Ruled B (maintainer, 2026-09-02): a `chartType: 'scatter'` with two or more
`series` now renders the file's refusal shell under
`data-chart-error="scatter-multi-series"`, stating that a scatter plots one
measure, naming the fix (keep exactly one series) and listing the series keys as
data. A single-series scatter is unchanged. No multi-measure projection is built;
render-time only (authoring-time rejection follows #7113). A `compareTo` overlay
counts as a second series — it was painted at the primary's y by the same
mechanism.
The sentence resolves from `chart.scatterOneMeasure` in all ten locale packs via
`useSafeTranslate`, the hook ObjectChart already uses (no module-scope defaults
map). Measured against a control build of the base: +420 gzipped bytes in the
console's eager `framework` chunk, 243 over its ceiling. The ceiling is NOT
raised here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3177.5 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-CsrWIwFh.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.83KB19.71KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

Standing down on check:eager-closure — the failure is real, it is the maintainer's to resolve, and I am not making it green

PM record from the domain:ui execution seat (session session_01EMrWaQw3XS5DxTHxp4yRyC). This PR is complete and parked as draft. Saying once what is red and why no fix is coming.

What is failing

Bundle Analysis, job 100360098939, head eacc55913. One verdict of four — aggregate closure, ceiling sensitivity and ceiling freshness all pass; only the framework per-chunk ceiling fails, 512.0 KB / 511.7 KB, over by 243 bytes.

⭐ The measurement that matters, and it differs from the sibling fork

This PR's implementer split the two halves against a control build of the base, which is exactly what makes this decidable:

buildframework gzipdeltagate
control (a6e5f7050)523,823 Bexit 0 (headroom 177 B)
mechanism only — renderer + tests, packs at base523,823 B0exit 0
mechanism + ten-pack copy (eacc55913)524,243 B+420exit 1

The mechanism costs the eager closure nothingplugin-charts is lazy and not in the closure. The entire overage is the ten-pack copy, ~42 bytes per pack, for one sentence already at the ruling's floor (states one measure, names the fix).

⚠️ This is a materially different shape from PR #7391's fork, and the difference is load-bearing for the decision. There, the mechanism's own code cost 230 B against 177 B of headroom, so no version fitted, footnote deleted or not. Here the mechanism fits with room to spare and only the translations do not. ⇒ The two forks are not interchangeable, and an option that resolves one may not resolve the other. I have recorded that asymmetry on the decision card.

Why I am not making it green

  • Raising PER_CHUNK_GZIP_CEILINGS['framework'] is 门禁削弱 — the maintainer's floor, not an execution lane's, however well-precedented.
  • Shipping the mechanism with English only and deferring the nine translations delivers less than ruling B specifies. It is a real option and it is on the decision card as one — but it is the maintainer's to choose, not this lane's to take because it happens to be the one that compiles.

Where the decision lives

#7399 — one maintainer decision covering this PR and PR #7391 (#7210 / #7225), since both need ruled user-facing copy in the same ten locale packs and both meet the same 177 bytes. It now carries both forks' measurements and the asymmetry between them.

#7194 is pm:blocked with Blocked-by: #7399 and Unlock-action: re-check PR #7400. This PR stays draft, keeps needs:contract-review, and is not enqueued.

Separately, and not blocked by any of the above

Clause-② is yes on this card, so an in-seat contract review at CONTRACT_REVIEW_TIER is owed before needs:contract-review comes off. This seat runs opus and is not at tier, so a claude-fable-5 reviewer has been dispatched — as was done for #7182. That review runs now, in parallel with the budget decision, so this PR is ready the moment #7399 rules. ⚠️ The reviewer has been pointed specifically at the compareTo consequence this PR's description raises: supportsCompareTo excludes only pie / donut / funnel, so a compare-to scatter is a two-series scatter in the product and now refuses. That was surfaced by the implementer rather than buried, which is the right instinct.

⚠️mergeable_state reads behind; the branch needs updating before any enqueue. Not done now — there is no point rebuilding against a moving base while the PR is parked.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review complete — PASS WITH REQUIRED AMENDMENTS. needs:contract-review stays on.

Clause-② in-seat contract review at CONTRACT_REVIEW_TIER (claude-fable-5), commissioned by the domain:ui seat (PM session session_01EMrWaQw3XS5DxTHxp4yRyC). This seat runs opus and is not at tier, so it commissioned the review rather than performing it — the same route used for #7182.

The mechanism implements ruling B faithfully for authored two-series scatters. The gate label cannot come off for two reasons.

⭐ The finding that justifies the tier — filed as #7402

The PR pins and publishes, as though ruled, a behaviour on the compareTo path that ruling B never contemplated. The reviewer raised it explicitly as the maintainer's question, not the PM's and not its own — and that restraint is the right call.

The ruling's literal predicate ("two or more series") reaches a compare-to scatter. Its stated premise does not: "zero authored scatters in-repo" was measured on chartType: 'scatter' literals, and a compare-to scatter is one authored series plus one synthesised overlay from a documented, published product featureCHANGELOG.md:1727 publishes that only pie / donut / funnel ignore compareTo, and comparisonStyle(s, 'scatter') has a scatter-specific branch. The census that justified B cannot see it.

⚠️A second defect on that path, independent of how #7402 rules: ruling B requires the refusal name "the authored fix". There the tile says "Keep exactly one series: amount, amount__comparison" — the author has exactly one series in their spec and never wrote amount__comparison. The refusal names a fix that cannot be acted on, and the real fix is unknowable at the renderer, which only sees series. A refusal that misdirects is worse than the wrong picture it replaces.

Required amendments

R1 — do not land the compareTo behaviour as decided. The changeset sentence "This applies to a compareTo overlay too — it is a second series…" and the pin comment "The ruling counts series, not variants" (test file 141–155) attribute to the ruling a scope it did not state. The pin may stay — it records what the code actually does today — but it must point at #7402 as open rather than at the ruling as settled.

R2 — correct a false claim about a gate (AdvancedChartImpl.scatterSeriesArity.test.tsx:196): "The gate check:i18n-keys enforces this equality too."Verified false by mutation — with the inline fallback changed to Keep ONE series: the gate still exits 0, because inlineDefaultValue (scripts/check-i18n-call-site-keys.mjs:1368) reads only a defaultValue property in an options object and interpolationOptions explicitly skips a positional string. The toBe(EN_COPY) pin is the only thing holding the fallback and the pack together — the comment must say so, or the next reader deletes the pin as redundant.

A dev is dispatched for both. They are text-only and do not touch the mechanism.

What the review confirmed

  • Precedence is correct and masks nothing. Scatter is excluded from both CATEGORY_AXIS_CHART_TYPES and SERIES_ONLY_CHART_TYPES "on purpose", so the arity check is genuinely the first guard on the scatter path; the binding wins over positional pin goes red on the base. Observation: the cited precedent's actual reasoning is "the more fundamental failure and the more specific message", not "spec fault beats row fault" — same outcome, looser analogy than the comment claims.
  • No new export.SeriesArityRefusal is module-local, packages/plugin-charts/src/index.tsx is unchanged, @object-ui/i18n was already a dependency. ⚠️data-chart-error="scatter-multi-series"is a de-facto contract — the five existing values are pinned across 8 test files and published in the CHANGELOG; this changeset publishes the sixth. chart.scatterOneMeasure widens TranslationKeys; patch is the right bump per standing convention.
  • 13/13 pins green; reverse verification reproduced exactly — base renderer over head gives 8 red / 5 green, file restored to blob 624cba845.
  • The census holds on a stronger basis than claimed. The PR's bar control (3 files) fires only in content/docs, so it proved the regex rather than the coverage. Widened to the dashboard spelling type: 'scatter' | 'bubble' — which the PR's regex cannot see — still 0, with control type: 'bar' at 10 files including five plugin-dashboard fixtures.
  • ⚠️ None of the four suites the implementer did not run (251 tests, all green on the head) exercises a scatter with compareTo — so nothing pinned the old wrong picture, and nothing outside this PR pins the new refusal on that path.
  • check:i18n-keys and check:i18n-drift both exit 0, reproduced. check:eager-closure was out of scope by dispatch ([Decision] The framework per-chunk eager-closure ceiling leaves 177 bytes for the whole repo — two ruled user-facing fixes cannot land, and no wording of either fits #7399).

Where this leaves the PR

⚠️Two independent decisions now block it, and neither alone releases it:

decisionabout
#7399ruled? nothe framework byte ceiling
#7402ruled? nowhether ruling B reaches a compareTo scatter

Draft, needs:contract-review retained, not enqueued.


Generated by Claude Code

…nd name the pin's real guard
Prose only — no behaviour, no pins added or removed, no locale copy touched.
Two amendments required by the Clause-② contract review of this PR.
R1 — the changeset and the `compareTo` pin comment both presented the overlay's
refusal as settled by the maintainer ruling. The ruling's words are "a
`chartType: 'scatter'` with two or more `series`"; it never mentions `compareTo`
or `variant`, and "the ruling counts series, not variants" was the implementer's
reading of it. Both now describe the behaviour as current and unruled, pointing
at objectui#7402, which is open on whether refusing is right on that path. The
pin stays — it records what ships — and its comment now says that shape (b) on
that card would invert it as a decision landing, not as a regression.
R2 — the locale pin carried "The gate `check:i18n-keys` enforces this equality
too", which is false. `inlineDefaultValue` reads a `defaultValue` property off an
options object and `interpolationOptions` skips a positional string, and this
call site passes the English positionally. Re-measured on this head: with the
inline fallback mutated to `Keep ONE series:`, all three i18n gates still exit 0
(`check:i18n-keys`, `check:i18n-drift`, `check:i18n-dead-keys`), and the mutation
was proven on disk by blob hash before each run and the restore by blob equality
plus an empty `git diff HEAD`. The comment now says the assertion is the sole
guard and why no gate sees the pair.
Verified: 13/13 pins in AdvancedChartImpl.scatterSeriesArity.test.tsx pass;
check-changeset-presence and all three i18n gates exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3177.5 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-BFq2aqIU.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.86KB19.72KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.6 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-DLTVLYV9.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.86KB19.72KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

Sign up for freeto 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.

bug(plugin-charts): a multi-series scatter draws every series at the FIRST series' y values — two measures, one cloud painted twice

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-charts): refuse a second scatter series instead of drawing it at the first one's y - #7400

Draft
os-project-manager wants to merge 3 commits into
mainfrom
claude/issue-7194-scatter-second-series-refusal
Draft

fix(plugin-charts): refuse a second scatter series instead of drawing it at the first one's y#7400
os-project-manager wants to merge 3 commits into
mainfrom
claude/issue-7194-scatter-second-series-refusal

Conversation

@os-project-manager

@os-project-manageros-project-manager commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes#7194

Maintainer ruling B (comment 5507673384, 2026-09-02): a chartType: 'scatter' with two or more series renders the file's existing refusal shell, stating that a scatter binds one measure and naming the authored fix; a single-series scatter is unchanged; no multi-measure projection (option A waits for a real caller); render-time only — the authoring-time (zod) rejection follows #7113 when that card rules. Clause-② yes ⇒ this PR is a draft; it is not flipped ready and not enqueued.

What changed

  • packages/plugin-charts/src/AdvancedChartImpl.tsx — the scatter arm checks series.length > 1first (a binding fault is true of the spec whatever the rows carry, so it outranks the finding(plugin-charts): scatter is NOT MEASURED for the degenerate-magnitude class — its all-positive CONTROL drew zero marks, so the sweep's zeros say nothing #7171 positional refusal the same way the missing-category-key guard outranks the series guard) and renders SeriesArityRefusal: the existing ChartRefusal shell under data-chart-error="scatter-multi-series", role="status", no console warning (matching the three scatter/magnitude answers already in the file). The shell fits without modification. The sentence is read from chart.scatterOneMeasure through useSafeTranslate — the hook ObjectChart already imports from @object-ui/i18n — so no module-scope createSafeTranslation map is added and no partial i18n mock is disturbed (census: 91 tests mock @object-ui/i18n; none of the partial ones reaches this renderer). The series keys are rendered as data after the sentence, as code elements, outside the translated copy.
  • packages/i18n/src/locales/{en,zh,ja,ko,de,fr,es,pt,ru,ar}.ts — one key, chart.scatterOneMeasure. en: A scatter plots one measure. Keep exactly one series: — states one measure, names the fix; the keys follow as data.
  • packages/plugin-charts/src/AdvancedChartImpl.scatterSeriesArity.test.tsx — 13 pins: single-series control draws 2 marks / no refusal / no note; series: [] and no series still reach the chart through the value fallback; two series ⇒ refusal, no chart container, 0 symbols, no legend; copy contains the sentence and lists both keys; three series refuse; a variant: 'comparison' second series refuses; the binding refusal wins over no-plottable-points; holds with zero rows; no console warning; a two-series bar still draws both series; en pack value equals the inline fallback; every pack defines it and none copies the English; under an I18nProvider (zh) the pack value renders and the keys stay untranslated.
  • .changeset/7194-scatter-one-measure.md — patch for @object-ui/plugin-charts and @object-ui/i18n.

A consequence the contract review looked at: compareTo on scatter

ObjectChart.supportsCompareTo excludes only pie / donut / funnel, and DatasetWidget synthesises a variant: 'comparison' series for every compared measure — so a compare-to scatter is a two-series scatter in the product. Today its overlay is painted at the primary's y values (the overlay never reads its own __comparison column — the same mechanism this card fixes). Under the guard it now refuses, and the pin records that. ⚠️ That behaviour is not a ruled one — the ruling names only "two or more series" and never mentions compareTo or variant — and objectui#7402 is open on whether refusing is the right answer on this path. This PR does not pre-judge it: if that card excludes scatter from compareTo (its shape (b), as pie / donut / funnel already are), the pin inverts, and that is the decision landing rather than a regression. Exempting the overlay would be a one-line change to the predicate, and the picture it would restore is the wrong one.

⚠️ Also raised by the review and recorded on #7402, not fixed here because its remedy depends on how that card rules: on the compare-to path the refusal names a fix the author cannot act on — it says "Keep exactly one series: amount, amount__comparison" while the author wrote exactly one series and never wrote amount__comparison.

Byte cost — measured, both halves, against a control build of the base

Control: origin/main at a6e5f7050 built in a second worktree (pnpm --filter @object-ui/console exec vite build, then node scripts/check-eager-closure-budget.mjs), the same recipe for all three builds; per-chunk numbers read from apps/console/dist/eager-closure.json.

buildframework gzip bytesdelta vs controlcheck:eager-closure
control (a6e5f7050)523,823exit 0 — ✅ framework 511.5 KB / 511.7 KB ceiling (headroom 0.2 KB)
mechanism only (renderer + test, packs at base)523,8230exit 0 — same line
mechanism + copy (eacc55913, unchanged at e090fd866)524,243+420exit 1 — ❌ framework 512.0 KB / 511.7 KB ceiling (OVER by 0.2 KB)

Ceiling PER_CHUNK_GZIP_CEILINGS['framework'] = 524,000; headroom on the control is 177 bytes; this branch is over by 243. The code costs the eager closure nothing (plugin-charts is lazy and not in the closure); the ten-pack copy costs 420 bytes, ~42 per pack, for one sentence already at the ruling's floor (one measure + the fix). ⛔ The ceiling is not raised here and the copy is not weakened below the ruling. This is the fork the dispatch anticipated (PM comment 5513454764, zone 2b): a maintainer budget decision, filed as #7399, one card covering #7194 and #7210. This PR stays draft until it rules; landing order against #7391 is the PM's.

Premise re-checks against the merged ref

  • Defect on origin/maina6e5f7050: yes — scatterYKey = series[0]?.dataKey || 'value' and the legend named off series[0]. fix(plugin-charts): draw every categorical x-axis label on short axes #7385 (d6fe1e1f2) touched only the categorical x-axis path (xAxisCommonProps, label rotation and ellipsis); scatter's numeric axes and the refusal surface were untouched.
  • Authored scatters: 0 in examples / content/docs / apps / packages/*/examples (control bar: 3 files). ⭐ The contract review widened this census to the dashboard-widget spelling type: 'scatter' | 'bubble', which the regex above cannot see: still 0, with control type: 'bar' firing at 10 files including five examples/schema-catalog/**/plugin-dashboard/*.json. The zero holds on a stronger basis than first claimed. One two-series scatter exists in a test corpus: packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx renders plugin-charts:scatter-chart with the shared two-series CHART_SERIES. The sweep stays green on this branch (202/202) because that entry never reaches the scatter arm — the SDUI path does not apply a registration's defaultProps, so it renders with no chartType (filed as bug(plugin-charts): a registration's defaultProps.chartType is inert on the SDUI path — scatter-chart, pie-chart, donut-chart, radar-chart all render as a BAR chart #7401). Not an authored product scatter; the ruling's premise holds. No file outside the declared surface was touched.

Overlap

Verification — mechanism at eacc55913, amendments at e090fd866

  • pnpm exec vitest run packages/plugin-charts/ packages/i18n/ (root, under the shared verify lock): Test Files 100 passed (100) / Tests 1323 passed (1323), exit 0.
  • pnpm exec vitest run packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx: 202 passed on this branch and on the control.
  • Reverse verification (fix committed first): with the base's AdvancedChartImpl.tsx checked out over the commit, the new file reads Tests 8 failed | 5 passed (13) — the 8 refusal/pack pins red, the 5 controls green; restored with git checkout HEAD -- FILE, git diff HEAD empty, blob hash 624cba8… on both sides.
  • pnpm --filter @object-ui/plugin-charts run type-check exit 0 (the new test file is in --listFiles); pnpm --filter @object-ui/i18n run type-check exit 0. Dependency closure built first.
  • Gates: check:i18n-keys 0 · check:i18n-drift 0 (1 key added, 0 en values changed) · check:control-bytes 0 · check:vi-mock-specifiers 0 · check:vi-mock-inherit 0 · check:shell-escape-residue 0 · check-changeset-presence 0 · check:eager-closure1 (above, expected). eslint on the changed files: 0 errors, warnings are the pre-existing no-explicit-any pattern the sibling chart pins also carry.
  • pnpm lint (repo-wide) was not run locally; CI owns it.

Contract review (Clause-②) — PASS, amendments applied at e090fd866

Reviewed at CONTRACT_REVIEW_TIER. Verdict PASS WITH REQUIRED AMENDMENTS; both are applied and verified, so the gate label is off.

  • R1 — the changeset and the compareTo pin comment no longer attribute that behaviour to ruling B; both now describe it as current and unruled, name [Decision] Does #7194's ruling B reach a compareTo scatter — a published capability it never named? The refusal currently swallows it and names a fix the author cannot act on #7402, and state that shape (b) there inverts the pin as a decision landing, not a regression. The pin itself stays: it records what ships. This section of the PR body was corrected on the same grounds.
  • R2 — a test comment claimed check:i18n-keys also enforces the inline-fallback / en-pack equality. ⭐ Verified false by mutation, twice — with the fallback changed to Keep ONE series: all three i18n gates still exit 0, because inlineDefaultValue reads only a defaultValue property off an options object and interpolationOptions skips a positional string. The toBe(EN_COPY) assertion is the sole guard, and the comment now says so. Mutation proven on disk by blob hash and anchored token counts; restore proven by state (git diff HEAD empty, blob back to 624cba845).

Amendments are prose only — 2 files, 35 insertions, 8 deletions, no renderer and no locale packs. Eager-closure figures byte-identical across the amendment: framework 524,243 at both eacc55913 and e090fd866.

Not done here

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

… it at the first one's y
A scatter binds one measure: `series[0].dataKey` is the YAxis key and every
`<Scatter>` reads the same rows through it, so a second series added a colour
and a legend entry and nothing else. Measured: two series over two rows painted
four symbols at two positions, each drawn twice, and the second measure's values
appeared nowhere on the plot. The data was valid and the picture was confidently
wrong — no existing refusal could see it.
Ruled B (maintainer, 2026-09-02): a `chartType: 'scatter'` with two or more
`series` now renders the file's refusal shell under
`data-chart-error="scatter-multi-series"`, stating that a scatter plots one
measure, naming the fix (keep exactly one series) and listing the series keys as
data. A single-series scatter is unchanged. No multi-measure projection is built;
render-time only (authoring-time rejection follows #7113). A `compareTo` overlay
counts as a second series — it was painted at the primary's y by the same
mechanism.
The sentence resolves from `chart.scatterOneMeasure` in all ten locale packs via
`useSafeTranslate`, the hook ObjectChart already uses (no module-scope defaults
map). Measured against a control build of the base: +420 gzipped bytes in the
console's eager `framework` chunk, 243 over its ceiling. The ceiling is NOT
raised here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3177.5 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-CsrWIwFh.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.83KB19.71KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

Standing down on check:eager-closure — the failure is real, it is the maintainer's to resolve, and I am not making it green

PM record from the domain:ui execution seat (session session_01EMrWaQw3XS5DxTHxp4yRyC). This PR is complete and parked as draft. Saying once what is red and why no fix is coming.

What is failing

Bundle Analysis, job 100360098939, head eacc55913. One verdict of four — aggregate closure, ceiling sensitivity and ceiling freshness all pass; only the framework per-chunk ceiling fails, 512.0 KB / 511.7 KB, over by 243 bytes.

⭐ The measurement that matters, and it differs from the sibling fork

This PR's implementer split the two halves against a control build of the base, which is exactly what makes this decidable:

buildframework gzipdeltagate
control (a6e5f7050)523,823 Bexit 0 (headroom 177 B)
mechanism only — renderer + tests, packs at base523,823 B0exit 0
mechanism + ten-pack copy (eacc55913)524,243 B+420exit 1

The mechanism costs the eager closure nothingplugin-charts is lazy and not in the closure. The entire overage is the ten-pack copy, ~42 bytes per pack, for one sentence already at the ruling's floor (states one measure, names the fix).

⚠️ This is a materially different shape from PR #7391's fork, and the difference is load-bearing for the decision. There, the mechanism's own code cost 230 B against 177 B of headroom, so no version fitted, footnote deleted or not. Here the mechanism fits with room to spare and only the translations do not. ⇒ The two forks are not interchangeable, and an option that resolves one may not resolve the other. I have recorded that asymmetry on the decision card.

Why I am not making it green

  • Raising PER_CHUNK_GZIP_CEILINGS['framework'] is 门禁削弱 — the maintainer's floor, not an execution lane's, however well-precedented.
  • Shipping the mechanism with English only and deferring the nine translations delivers less than ruling B specifies. It is a real option and it is on the decision card as one — but it is the maintainer's to choose, not this lane's to take because it happens to be the one that compiles.

Where the decision lives

#7399 — one maintainer decision covering this PR and PR #7391 (#7210 / #7225), since both need ruled user-facing copy in the same ten locale packs and both meet the same 177 bytes. It now carries both forks' measurements and the asymmetry between them.

#7194 is pm:blocked with Blocked-by: #7399 and Unlock-action: re-check PR #7400. This PR stays draft, keeps needs:contract-review, and is not enqueued.

Separately, and not blocked by any of the above

Clause-② is yes on this card, so an in-seat contract review at CONTRACT_REVIEW_TIER is owed before needs:contract-review comes off. This seat runs opus and is not at tier, so a claude-fable-5 reviewer has been dispatched — as was done for #7182. That review runs now, in parallel with the budget decision, so this PR is ready the moment #7399 rules. ⚠️ The reviewer has been pointed specifically at the compareTo consequence this PR's description raises: supportsCompareTo excludes only pie / donut / funnel, so a compare-to scatter is a two-series scatter in the product and now refuses. That was surfaced by the implementer rather than buried, which is the right instinct.

⚠️mergeable_state reads behind; the branch needs updating before any enqueue. Not done now — there is no point rebuilding against a moving base while the PR is parked.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review complete — PASS WITH REQUIRED AMENDMENTS. needs:contract-review stays on.

Clause-② in-seat contract review at CONTRACT_REVIEW_TIER (claude-fable-5), commissioned by the domain:ui seat (PM session session_01EMrWaQw3XS5DxTHxp4yRyC). This seat runs opus and is not at tier, so it commissioned the review rather than performing it — the same route used for #7182.

The mechanism implements ruling B faithfully for authored two-series scatters. The gate label cannot come off for two reasons.

⭐ The finding that justifies the tier — filed as #7402

The PR pins and publishes, as though ruled, a behaviour on the compareTo path that ruling B never contemplated. The reviewer raised it explicitly as the maintainer's question, not the PM's and not its own — and that restraint is the right call.

The ruling's literal predicate ("two or more series") reaches a compare-to scatter. Its stated premise does not: "zero authored scatters in-repo" was measured on chartType: 'scatter' literals, and a compare-to scatter is one authored series plus one synthesised overlay from a documented, published product featureCHANGELOG.md:1727 publishes that only pie / donut / funnel ignore compareTo, and comparisonStyle(s, 'scatter') has a scatter-specific branch. The census that justified B cannot see it.

⚠️A second defect on that path, independent of how #7402 rules: ruling B requires the refusal name "the authored fix". There the tile says "Keep exactly one series: amount, amount__comparison" — the author has exactly one series in their spec and never wrote amount__comparison. The refusal names a fix that cannot be acted on, and the real fix is unknowable at the renderer, which only sees series. A refusal that misdirects is worse than the wrong picture it replaces.

Required amendments

R1 — do not land the compareTo behaviour as decided. The changeset sentence "This applies to a compareTo overlay too — it is a second series…" and the pin comment "The ruling counts series, not variants" (test file 141–155) attribute to the ruling a scope it did not state. The pin may stay — it records what the code actually does today — but it must point at #7402 as open rather than at the ruling as settled.

R2 — correct a false claim about a gate (AdvancedChartImpl.scatterSeriesArity.test.tsx:196): "The gate check:i18n-keys enforces this equality too."Verified false by mutation — with the inline fallback changed to Keep ONE series: the gate still exits 0, because inlineDefaultValue (scripts/check-i18n-call-site-keys.mjs:1368) reads only a defaultValue property in an options object and interpolationOptions explicitly skips a positional string. The toBe(EN_COPY) pin is the only thing holding the fallback and the pack together — the comment must say so, or the next reader deletes the pin as redundant.

A dev is dispatched for both. They are text-only and do not touch the mechanism.

What the review confirmed

  • Precedence is correct and masks nothing. Scatter is excluded from both CATEGORY_AXIS_CHART_TYPES and SERIES_ONLY_CHART_TYPES "on purpose", so the arity check is genuinely the first guard on the scatter path; the binding wins over positional pin goes red on the base. Observation: the cited precedent's actual reasoning is "the more fundamental failure and the more specific message", not "spec fault beats row fault" — same outcome, looser analogy than the comment claims.
  • No new export.SeriesArityRefusal is module-local, packages/plugin-charts/src/index.tsx is unchanged, @object-ui/i18n was already a dependency. ⚠️data-chart-error="scatter-multi-series"is a de-facto contract — the five existing values are pinned across 8 test files and published in the CHANGELOG; this changeset publishes the sixth. chart.scatterOneMeasure widens TranslationKeys; patch is the right bump per standing convention.
  • 13/13 pins green; reverse verification reproduced exactly — base renderer over head gives 8 red / 5 green, file restored to blob 624cba845.
  • The census holds on a stronger basis than claimed. The PR's bar control (3 files) fires only in content/docs, so it proved the regex rather than the coverage. Widened to the dashboard spelling type: 'scatter' | 'bubble' — which the PR's regex cannot see — still 0, with control type: 'bar' at 10 files including five plugin-dashboard fixtures.
  • ⚠️ None of the four suites the implementer did not run (251 tests, all green on the head) exercises a scatter with compareTo — so nothing pinned the old wrong picture, and nothing outside this PR pins the new refusal on that path.
  • check:i18n-keys and check:i18n-drift both exit 0, reproduced. check:eager-closure was out of scope by dispatch ([Decision] The framework per-chunk eager-closure ceiling leaves 177 bytes for the whole repo — two ruled user-facing fixes cannot land, and no wording of either fits #7399).

Where this leaves the PR

⚠️Two independent decisions now block it, and neither alone releases it:

decisionabout
#7399ruled? nothe framework byte ceiling
#7402ruled? nowhether ruling B reaches a compareTo scatter

Draft, needs:contract-review retained, not enqueued.


Generated by Claude Code

…nd name the pin's real guard
Prose only — no behaviour, no pins added or removed, no locale copy touched.
Two amendments required by the Clause-② contract review of this PR.
R1 — the changeset and the `compareTo` pin comment both presented the overlay's
refusal as settled by the maintainer ruling. The ruling's words are "a
`chartType: 'scatter'` with two or more `series`"; it never mentions `compareTo`
or `variant`, and "the ruling counts series, not variants" was the implementer's
reading of it. Both now describe the behaviour as current and unruled, pointing
at objectui#7402, which is open on whether refusing is right on that path. The
pin stays — it records what ships — and its comment now says that shape (b) on
that card would invert it as a decision landing, not as a regression.
R2 — the locale pin carried "The gate `check:i18n-keys` enforces this equality
too", which is false. `inlineDefaultValue` reads a `defaultValue` property off an
options object and `interpolationOptions` skips a positional string, and this
call site passes the English positionally. Re-measured on this head: with the
inline fallback mutated to `Keep ONE series:`, all three i18n gates still exit 0
(`check:i18n-keys`, `check:i18n-drift`, `check:i18n-dead-keys`), and the mutation
was proven on disk by blob hash before each run and the restore by blob equality
plus an empty `git diff HEAD`. The comment now says the assertion is the sole
guard and why no gate sees the pair.
Verified: 13/13 pins in AdvancedChartImpl.scatterSeriesArity.test.tsx pass;
check-changeset-presence and all three i18n gates exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3177.5 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-BFq2aqIU.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.86KB19.72KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.6 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-DLTVLYV9.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.86KB19.72KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

Sign up for freeto 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.

bug(plugin-charts): a multi-series scatter draws every series at the FIRST series' y values — two measures, one cloud painted twice

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-charts): refuse a second scatter series instead of drawing it at the first one's y - #7400

Draft
os-project-manager wants to merge 3 commits into
mainfrom
claude/issue-7194-scatter-second-series-refusal
Draft

fix(plugin-charts): refuse a second scatter series instead of drawing it at the first one's y#7400
os-project-manager wants to merge 3 commits into
mainfrom
claude/issue-7194-scatter-second-series-refusal

Conversation

@os-project-manager

@os-project-manageros-project-manager commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes#7194

Maintainer ruling B (comment 5507673384, 2026-09-02): a chartType: 'scatter' with two or more series renders the file's existing refusal shell, stating that a scatter binds one measure and naming the authored fix; a single-series scatter is unchanged; no multi-measure projection (option A waits for a real caller); render-time only — the authoring-time (zod) rejection follows #7113 when that card rules. Clause-② yes ⇒ this PR is a draft; it is not flipped ready and not enqueued.

What changed

  • packages/plugin-charts/src/AdvancedChartImpl.tsx — the scatter arm checks series.length > 1first (a binding fault is true of the spec whatever the rows carry, so it outranks the finding(plugin-charts): scatter is NOT MEASURED for the degenerate-magnitude class — its all-positive CONTROL drew zero marks, so the sweep's zeros say nothing #7171 positional refusal the same way the missing-category-key guard outranks the series guard) and renders SeriesArityRefusal: the existing ChartRefusal shell under data-chart-error="scatter-multi-series", role="status", no console warning (matching the three scatter/magnitude answers already in the file). The shell fits without modification. The sentence is read from chart.scatterOneMeasure through useSafeTranslate — the hook ObjectChart already imports from @object-ui/i18n — so no module-scope createSafeTranslation map is added and no partial i18n mock is disturbed (census: 91 tests mock @object-ui/i18n; none of the partial ones reaches this renderer). The series keys are rendered as data after the sentence, as code elements, outside the translated copy.
  • packages/i18n/src/locales/{en,zh,ja,ko,de,fr,es,pt,ru,ar}.ts — one key, chart.scatterOneMeasure. en: A scatter plots one measure. Keep exactly one series: — states one measure, names the fix; the keys follow as data.
  • packages/plugin-charts/src/AdvancedChartImpl.scatterSeriesArity.test.tsx — 13 pins: single-series control draws 2 marks / no refusal / no note; series: [] and no series still reach the chart through the value fallback; two series ⇒ refusal, no chart container, 0 symbols, no legend; copy contains the sentence and lists both keys; three series refuse; a variant: 'comparison' second series refuses; the binding refusal wins over no-plottable-points; holds with zero rows; no console warning; a two-series bar still draws both series; en pack value equals the inline fallback; every pack defines it and none copies the English; under an I18nProvider (zh) the pack value renders and the keys stay untranslated.
  • .changeset/7194-scatter-one-measure.md — patch for @object-ui/plugin-charts and @object-ui/i18n.

A consequence the contract review looked at: compareTo on scatter

ObjectChart.supportsCompareTo excludes only pie / donut / funnel, and DatasetWidget synthesises a variant: 'comparison' series for every compared measure — so a compare-to scatter is a two-series scatter in the product. Today its overlay is painted at the primary's y values (the overlay never reads its own __comparison column — the same mechanism this card fixes). Under the guard it now refuses, and the pin records that. ⚠️ That behaviour is not a ruled one — the ruling names only "two or more series" and never mentions compareTo or variant — and objectui#7402 is open on whether refusing is the right answer on this path. This PR does not pre-judge it: if that card excludes scatter from compareTo (its shape (b), as pie / donut / funnel already are), the pin inverts, and that is the decision landing rather than a regression. Exempting the overlay would be a one-line change to the predicate, and the picture it would restore is the wrong one.

⚠️ Also raised by the review and recorded on #7402, not fixed here because its remedy depends on how that card rules: on the compare-to path the refusal names a fix the author cannot act on — it says "Keep exactly one series: amount, amount__comparison" while the author wrote exactly one series and never wrote amount__comparison.

Byte cost — measured, both halves, against a control build of the base

Control: origin/main at a6e5f7050 built in a second worktree (pnpm --filter @object-ui/console exec vite build, then node scripts/check-eager-closure-budget.mjs), the same recipe for all three builds; per-chunk numbers read from apps/console/dist/eager-closure.json.

buildframework gzip bytesdelta vs controlcheck:eager-closure
control (a6e5f7050)523,823exit 0 — ✅ framework 511.5 KB / 511.7 KB ceiling (headroom 0.2 KB)
mechanism only (renderer + test, packs at base)523,8230exit 0 — same line
mechanism + copy (eacc55913, unchanged at e090fd866)524,243+420exit 1 — ❌ framework 512.0 KB / 511.7 KB ceiling (OVER by 0.2 KB)

Ceiling PER_CHUNK_GZIP_CEILINGS['framework'] = 524,000; headroom on the control is 177 bytes; this branch is over by 243. The code costs the eager closure nothing (plugin-charts is lazy and not in the closure); the ten-pack copy costs 420 bytes, ~42 per pack, for one sentence already at the ruling's floor (one measure + the fix). ⛔ The ceiling is not raised here and the copy is not weakened below the ruling. This is the fork the dispatch anticipated (PM comment 5513454764, zone 2b): a maintainer budget decision, filed as #7399, one card covering #7194 and #7210. This PR stays draft until it rules; landing order against #7391 is the PM's.

Premise re-checks against the merged ref

  • Defect on origin/maina6e5f7050: yes — scatterYKey = series[0]?.dataKey || 'value' and the legend named off series[0]. fix(plugin-charts): draw every categorical x-axis label on short axes #7385 (d6fe1e1f2) touched only the categorical x-axis path (xAxisCommonProps, label rotation and ellipsis); scatter's numeric axes and the refusal surface were untouched.
  • Authored scatters: 0 in examples / content/docs / apps / packages/*/examples (control bar: 3 files). ⭐ The contract review widened this census to the dashboard-widget spelling type: 'scatter' | 'bubble', which the regex above cannot see: still 0, with control type: 'bar' firing at 10 files including five examples/schema-catalog/**/plugin-dashboard/*.json. The zero holds on a stronger basis than first claimed. One two-series scatter exists in a test corpus: packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx renders plugin-charts:scatter-chart with the shared two-series CHART_SERIES. The sweep stays green on this branch (202/202) because that entry never reaches the scatter arm — the SDUI path does not apply a registration's defaultProps, so it renders with no chartType (filed as bug(plugin-charts): a registration's defaultProps.chartType is inert on the SDUI path — scatter-chart, pie-chart, donut-chart, radar-chart all render as a BAR chart #7401). Not an authored product scatter; the ruling's premise holds. No file outside the declared surface was touched.

Overlap

Verification — mechanism at eacc55913, amendments at e090fd866

  • pnpm exec vitest run packages/plugin-charts/ packages/i18n/ (root, under the shared verify lock): Test Files 100 passed (100) / Tests 1323 passed (1323), exit 0.
  • pnpm exec vitest run packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx: 202 passed on this branch and on the control.
  • Reverse verification (fix committed first): with the base's AdvancedChartImpl.tsx checked out over the commit, the new file reads Tests 8 failed | 5 passed (13) — the 8 refusal/pack pins red, the 5 controls green; restored with git checkout HEAD -- FILE, git diff HEAD empty, blob hash 624cba8… on both sides.
  • pnpm --filter @object-ui/plugin-charts run type-check exit 0 (the new test file is in --listFiles); pnpm --filter @object-ui/i18n run type-check exit 0. Dependency closure built first.
  • Gates: check:i18n-keys 0 · check:i18n-drift 0 (1 key added, 0 en values changed) · check:control-bytes 0 · check:vi-mock-specifiers 0 · check:vi-mock-inherit 0 · check:shell-escape-residue 0 · check-changeset-presence 0 · check:eager-closure1 (above, expected). eslint on the changed files: 0 errors, warnings are the pre-existing no-explicit-any pattern the sibling chart pins also carry.
  • pnpm lint (repo-wide) was not run locally; CI owns it.

Contract review (Clause-②) — PASS, amendments applied at e090fd866

Reviewed at CONTRACT_REVIEW_TIER. Verdict PASS WITH REQUIRED AMENDMENTS; both are applied and verified, so the gate label is off.

  • R1 — the changeset and the compareTo pin comment no longer attribute that behaviour to ruling B; both now describe it as current and unruled, name [Decision] Does #7194's ruling B reach a compareTo scatter — a published capability it never named? The refusal currently swallows it and names a fix the author cannot act on #7402, and state that shape (b) there inverts the pin as a decision landing, not a regression. The pin itself stays: it records what ships. This section of the PR body was corrected on the same grounds.
  • R2 — a test comment claimed check:i18n-keys also enforces the inline-fallback / en-pack equality. ⭐ Verified false by mutation, twice — with the fallback changed to Keep ONE series: all three i18n gates still exit 0, because inlineDefaultValue reads only a defaultValue property off an options object and interpolationOptions skips a positional string. The toBe(EN_COPY) assertion is the sole guard, and the comment now says so. Mutation proven on disk by blob hash and anchored token counts; restore proven by state (git diff HEAD empty, blob back to 624cba845).

Amendments are prose only — 2 files, 35 insertions, 8 deletions, no renderer and no locale packs. Eager-closure figures byte-identical across the amendment: framework 524,243 at both eacc55913 and e090fd866.

Not done here

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

… it at the first one's y
A scatter binds one measure: `series[0].dataKey` is the YAxis key and every
`<Scatter>` reads the same rows through it, so a second series added a colour
and a legend entry and nothing else. Measured: two series over two rows painted
four symbols at two positions, each drawn twice, and the second measure's values
appeared nowhere on the plot. The data was valid and the picture was confidently
wrong — no existing refusal could see it.
Ruled B (maintainer, 2026-09-02): a `chartType: 'scatter'` with two or more
`series` now renders the file's refusal shell under
`data-chart-error="scatter-multi-series"`, stating that a scatter plots one
measure, naming the fix (keep exactly one series) and listing the series keys as
data. A single-series scatter is unchanged. No multi-measure projection is built;
render-time only (authoring-time rejection follows #7113). A `compareTo` overlay
counts as a second series — it was painted at the primary's y by the same
mechanism.
The sentence resolves from `chart.scatterOneMeasure` in all ten locale packs via
`useSafeTranslate`, the hook ObjectChart already uses (no module-scope defaults
map). Measured against a control build of the base: +420 gzipped bytes in the
console's eager `framework` chunk, 243 over its ceiling. The ceiling is NOT
raised here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3177.5 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-CsrWIwFh.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.83KB19.71KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

Standing down on check:eager-closure — the failure is real, it is the maintainer's to resolve, and I am not making it green

PM record from the domain:ui execution seat (session session_01EMrWaQw3XS5DxTHxp4yRyC). This PR is complete and parked as draft. Saying once what is red and why no fix is coming.

What is failing

Bundle Analysis, job 100360098939, head eacc55913. One verdict of four — aggregate closure, ceiling sensitivity and ceiling freshness all pass; only the framework per-chunk ceiling fails, 512.0 KB / 511.7 KB, over by 243 bytes.

⭐ The measurement that matters, and it differs from the sibling fork

This PR's implementer split the two halves against a control build of the base, which is exactly what makes this decidable:

buildframework gzipdeltagate
control (a6e5f7050)523,823 Bexit 0 (headroom 177 B)
mechanism only — renderer + tests, packs at base523,823 B0exit 0
mechanism + ten-pack copy (eacc55913)524,243 B+420exit 1

The mechanism costs the eager closure nothingplugin-charts is lazy and not in the closure. The entire overage is the ten-pack copy, ~42 bytes per pack, for one sentence already at the ruling's floor (states one measure, names the fix).

⚠️ This is a materially different shape from PR #7391's fork, and the difference is load-bearing for the decision. There, the mechanism's own code cost 230 B against 177 B of headroom, so no version fitted, footnote deleted or not. Here the mechanism fits with room to spare and only the translations do not. ⇒ The two forks are not interchangeable, and an option that resolves one may not resolve the other. I have recorded that asymmetry on the decision card.

Why I am not making it green

  • Raising PER_CHUNK_GZIP_CEILINGS['framework'] is 门禁削弱 — the maintainer's floor, not an execution lane's, however well-precedented.
  • Shipping the mechanism with English only and deferring the nine translations delivers less than ruling B specifies. It is a real option and it is on the decision card as one — but it is the maintainer's to choose, not this lane's to take because it happens to be the one that compiles.

Where the decision lives

#7399 — one maintainer decision covering this PR and PR #7391 (#7210 / #7225), since both need ruled user-facing copy in the same ten locale packs and both meet the same 177 bytes. It now carries both forks' measurements and the asymmetry between them.

#7194 is pm:blocked with Blocked-by: #7399 and Unlock-action: re-check PR #7400. This PR stays draft, keeps needs:contract-review, and is not enqueued.

Separately, and not blocked by any of the above

Clause-② is yes on this card, so an in-seat contract review at CONTRACT_REVIEW_TIER is owed before needs:contract-review comes off. This seat runs opus and is not at tier, so a claude-fable-5 reviewer has been dispatched — as was done for #7182. That review runs now, in parallel with the budget decision, so this PR is ready the moment #7399 rules. ⚠️ The reviewer has been pointed specifically at the compareTo consequence this PR's description raises: supportsCompareTo excludes only pie / donut / funnel, so a compare-to scatter is a two-series scatter in the product and now refuses. That was surfaced by the implementer rather than buried, which is the right instinct.

⚠️mergeable_state reads behind; the branch needs updating before any enqueue. Not done now — there is no point rebuilding against a moving base while the PR is parked.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review complete — PASS WITH REQUIRED AMENDMENTS. needs:contract-review stays on.

Clause-② in-seat contract review at CONTRACT_REVIEW_TIER (claude-fable-5), commissioned by the domain:ui seat (PM session session_01EMrWaQw3XS5DxTHxp4yRyC). This seat runs opus and is not at tier, so it commissioned the review rather than performing it — the same route used for #7182.

The mechanism implements ruling B faithfully for authored two-series scatters. The gate label cannot come off for two reasons.

⭐ The finding that justifies the tier — filed as #7402

The PR pins and publishes, as though ruled, a behaviour on the compareTo path that ruling B never contemplated. The reviewer raised it explicitly as the maintainer's question, not the PM's and not its own — and that restraint is the right call.

The ruling's literal predicate ("two or more series") reaches a compare-to scatter. Its stated premise does not: "zero authored scatters in-repo" was measured on chartType: 'scatter' literals, and a compare-to scatter is one authored series plus one synthesised overlay from a documented, published product featureCHANGELOG.md:1727 publishes that only pie / donut / funnel ignore compareTo, and comparisonStyle(s, 'scatter') has a scatter-specific branch. The census that justified B cannot see it.

⚠️A second defect on that path, independent of how #7402 rules: ruling B requires the refusal name "the authored fix". There the tile says "Keep exactly one series: amount, amount__comparison" — the author has exactly one series in their spec and never wrote amount__comparison. The refusal names a fix that cannot be acted on, and the real fix is unknowable at the renderer, which only sees series. A refusal that misdirects is worse than the wrong picture it replaces.

Required amendments

R1 — do not land the compareTo behaviour as decided. The changeset sentence "This applies to a compareTo overlay too — it is a second series…" and the pin comment "The ruling counts series, not variants" (test file 141–155) attribute to the ruling a scope it did not state. The pin may stay — it records what the code actually does today — but it must point at #7402 as open rather than at the ruling as settled.

R2 — correct a false claim about a gate (AdvancedChartImpl.scatterSeriesArity.test.tsx:196): "The gate check:i18n-keys enforces this equality too."Verified false by mutation — with the inline fallback changed to Keep ONE series: the gate still exits 0, because inlineDefaultValue (scripts/check-i18n-call-site-keys.mjs:1368) reads only a defaultValue property in an options object and interpolationOptions explicitly skips a positional string. The toBe(EN_COPY) pin is the only thing holding the fallback and the pack together — the comment must say so, or the next reader deletes the pin as redundant.

A dev is dispatched for both. They are text-only and do not touch the mechanism.

What the review confirmed

  • Precedence is correct and masks nothing. Scatter is excluded from both CATEGORY_AXIS_CHART_TYPES and SERIES_ONLY_CHART_TYPES "on purpose", so the arity check is genuinely the first guard on the scatter path; the binding wins over positional pin goes red on the base. Observation: the cited precedent's actual reasoning is "the more fundamental failure and the more specific message", not "spec fault beats row fault" — same outcome, looser analogy than the comment claims.
  • No new export.SeriesArityRefusal is module-local, packages/plugin-charts/src/index.tsx is unchanged, @object-ui/i18n was already a dependency. ⚠️data-chart-error="scatter-multi-series"is a de-facto contract — the five existing values are pinned across 8 test files and published in the CHANGELOG; this changeset publishes the sixth. chart.scatterOneMeasure widens TranslationKeys; patch is the right bump per standing convention.
  • 13/13 pins green; reverse verification reproduced exactly — base renderer over head gives 8 red / 5 green, file restored to blob 624cba845.
  • The census holds on a stronger basis than claimed. The PR's bar control (3 files) fires only in content/docs, so it proved the regex rather than the coverage. Widened to the dashboard spelling type: 'scatter' | 'bubble' — which the PR's regex cannot see — still 0, with control type: 'bar' at 10 files including five plugin-dashboard fixtures.
  • ⚠️ None of the four suites the implementer did not run (251 tests, all green on the head) exercises a scatter with compareTo — so nothing pinned the old wrong picture, and nothing outside this PR pins the new refusal on that path.
  • check:i18n-keys and check:i18n-drift both exit 0, reproduced. check:eager-closure was out of scope by dispatch ([Decision] The framework per-chunk eager-closure ceiling leaves 177 bytes for the whole repo — two ruled user-facing fixes cannot land, and no wording of either fits #7399).

Where this leaves the PR

⚠️Two independent decisions now block it, and neither alone releases it:

decisionabout
#7399ruled? nothe framework byte ceiling
#7402ruled? nowhether ruling B reaches a compareTo scatter

Draft, needs:contract-review retained, not enqueued.


Generated by Claude Code

…nd name the pin's real guard
Prose only — no behaviour, no pins added or removed, no locale copy touched.
Two amendments required by the Clause-② contract review of this PR.
R1 — the changeset and the `compareTo` pin comment both presented the overlay's
refusal as settled by the maintainer ruling. The ruling's words are "a
`chartType: 'scatter'` with two or more `series`"; it never mentions `compareTo`
or `variant`, and "the ruling counts series, not variants" was the implementer's
reading of it. Both now describe the behaviour as current and unruled, pointing
at objectui#7402, which is open on whether refusing is right on that path. The
pin stays — it records what ships — and its comment now says that shape (b) on
that card would invert it as a decision landing, not as a regression.
R2 — the locale pin carried "The gate `check:i18n-keys` enforces this equality
too", which is false. `inlineDefaultValue` reads a `defaultValue` property off an
options object and `interpolationOptions` skips a positional string, and this
call site passes the English positionally. Re-measured on this head: with the
inline fallback mutated to `Keep ONE series:`, all three i18n gates still exit 0
(`check:i18n-keys`, `check:i18n-drift`, `check:i18n-dead-keys`), and the mutation
was proven on disk by blob hash before each run and the restore by blob equality
plus an empty `git diff HEAD`. The comment now says the assertion is the sole
guard and why no gate sees the pair.
Verified: 13/13 pins in AdvancedChartImpl.scatterSeriesArity.test.tsx pass;
check-changeset-presence and all three i18n gates exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3177.5 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-BFq2aqIU.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.86KB19.72KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.6 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-DLTVLYV9.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.86KB19.72KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

Sign up for freeto 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.

bug(plugin-charts): a multi-series scatter draws every series at the FIRST series' y values — two measures, one cloud painted twice

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(plugin-charts): refuse a second scatter series instead of drawing it at the first one's y - #7400

Draft
os-project-manager wants to merge 3 commits into
mainfrom
claude/issue-7194-scatter-second-series-refusal
Draft

fix(plugin-charts): refuse a second scatter series instead of drawing it at the first one's y#7400
os-project-manager wants to merge 3 commits into
mainfrom
claude/issue-7194-scatter-second-series-refusal

Conversation

@os-project-manager

@os-project-manageros-project-manager commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes#7194

Maintainer ruling B (comment 5507673384, 2026-09-02): a chartType: 'scatter' with two or more series renders the file's existing refusal shell, stating that a scatter binds one measure and naming the authored fix; a single-series scatter is unchanged; no multi-measure projection (option A waits for a real caller); render-time only — the authoring-time (zod) rejection follows #7113 when that card rules. Clause-② yes ⇒ this PR is a draft; it is not flipped ready and not enqueued.

What changed

  • packages/plugin-charts/src/AdvancedChartImpl.tsx — the scatter arm checks series.length > 1first (a binding fault is true of the spec whatever the rows carry, so it outranks the finding(plugin-charts): scatter is NOT MEASURED for the degenerate-magnitude class — its all-positive CONTROL drew zero marks, so the sweep's zeros say nothing #7171 positional refusal the same way the missing-category-key guard outranks the series guard) and renders SeriesArityRefusal: the existing ChartRefusal shell under data-chart-error="scatter-multi-series", role="status", no console warning (matching the three scatter/magnitude answers already in the file). The shell fits without modification. The sentence is read from chart.scatterOneMeasure through useSafeTranslate — the hook ObjectChart already imports from @object-ui/i18n — so no module-scope createSafeTranslation map is added and no partial i18n mock is disturbed (census: 91 tests mock @object-ui/i18n; none of the partial ones reaches this renderer). The series keys are rendered as data after the sentence, as code elements, outside the translated copy.
  • packages/i18n/src/locales/{en,zh,ja,ko,de,fr,es,pt,ru,ar}.ts — one key, chart.scatterOneMeasure. en: A scatter plots one measure. Keep exactly one series: — states one measure, names the fix; the keys follow as data.
  • packages/plugin-charts/src/AdvancedChartImpl.scatterSeriesArity.test.tsx — 13 pins: single-series control draws 2 marks / no refusal / no note; series: [] and no series still reach the chart through the value fallback; two series ⇒ refusal, no chart container, 0 symbols, no legend; copy contains the sentence and lists both keys; three series refuse; a variant: 'comparison' second series refuses; the binding refusal wins over no-plottable-points; holds with zero rows; no console warning; a two-series bar still draws both series; en pack value equals the inline fallback; every pack defines it and none copies the English; under an I18nProvider (zh) the pack value renders and the keys stay untranslated.
  • .changeset/7194-scatter-one-measure.md — patch for @object-ui/plugin-charts and @object-ui/i18n.

A consequence the contract review looked at: compareTo on scatter

ObjectChart.supportsCompareTo excludes only pie / donut / funnel, and DatasetWidget synthesises a variant: 'comparison' series for every compared measure — so a compare-to scatter is a two-series scatter in the product. Today its overlay is painted at the primary's y values (the overlay never reads its own __comparison column — the same mechanism this card fixes). Under the guard it now refuses, and the pin records that. ⚠️ That behaviour is not a ruled one — the ruling names only "two or more series" and never mentions compareTo or variant — and objectui#7402 is open on whether refusing is the right answer on this path. This PR does not pre-judge it: if that card excludes scatter from compareTo (its shape (b), as pie / donut / funnel already are), the pin inverts, and that is the decision landing rather than a regression. Exempting the overlay would be a one-line change to the predicate, and the picture it would restore is the wrong one.

⚠️ Also raised by the review and recorded on #7402, not fixed here because its remedy depends on how that card rules: on the compare-to path the refusal names a fix the author cannot act on — it says "Keep exactly one series: amount, amount__comparison" while the author wrote exactly one series and never wrote amount__comparison.

Byte cost — measured, both halves, against a control build of the base

Control: origin/main at a6e5f7050 built in a second worktree (pnpm --filter @object-ui/console exec vite build, then node scripts/check-eager-closure-budget.mjs), the same recipe for all three builds; per-chunk numbers read from apps/console/dist/eager-closure.json.

buildframework gzip bytesdelta vs controlcheck:eager-closure
control (a6e5f7050)523,823exit 0 — ✅ framework 511.5 KB / 511.7 KB ceiling (headroom 0.2 KB)
mechanism only (renderer + test, packs at base)523,8230exit 0 — same line
mechanism + copy (eacc55913, unchanged at e090fd866)524,243+420exit 1 — ❌ framework 512.0 KB / 511.7 KB ceiling (OVER by 0.2 KB)

Ceiling PER_CHUNK_GZIP_CEILINGS['framework'] = 524,000; headroom on the control is 177 bytes; this branch is over by 243. The code costs the eager closure nothing (plugin-charts is lazy and not in the closure); the ten-pack copy costs 420 bytes, ~42 per pack, for one sentence already at the ruling's floor (one measure + the fix). ⛔ The ceiling is not raised here and the copy is not weakened below the ruling. This is the fork the dispatch anticipated (PM comment 5513454764, zone 2b): a maintainer budget decision, filed as #7399, one card covering #7194 and #7210. This PR stays draft until it rules; landing order against #7391 is the PM's.

Premise re-checks against the merged ref

  • Defect on origin/maina6e5f7050: yes — scatterYKey = series[0]?.dataKey || 'value' and the legend named off series[0]. fix(plugin-charts): draw every categorical x-axis label on short axes #7385 (d6fe1e1f2) touched only the categorical x-axis path (xAxisCommonProps, label rotation and ellipsis); scatter's numeric axes and the refusal surface were untouched.
  • Authored scatters: 0 in examples / content/docs / apps / packages/*/examples (control bar: 3 files). ⭐ The contract review widened this census to the dashboard-widget spelling type: 'scatter' | 'bubble', which the regex above cannot see: still 0, with control type: 'bar' firing at 10 files including five examples/schema-catalog/**/plugin-dashboard/*.json. The zero holds on a stronger basis than first claimed. One two-series scatter exists in a test corpus: packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx renders plugin-charts:scatter-chart with the shared two-series CHART_SERIES. The sweep stays green on this branch (202/202) because that entry never reaches the scatter arm — the SDUI path does not apply a registration's defaultProps, so it renders with no chartType (filed as bug(plugin-charts): a registration's defaultProps.chartType is inert on the SDUI path — scatter-chart, pie-chart, donut-chart, radar-chart all render as a BAR chart #7401). Not an authored product scatter; the ruling's premise holds. No file outside the declared surface was touched.

Overlap

Verification — mechanism at eacc55913, amendments at e090fd866

  • pnpm exec vitest run packages/plugin-charts/ packages/i18n/ (root, under the shared verify lock): Test Files 100 passed (100) / Tests 1323 passed (1323), exit 0.
  • pnpm exec vitest run packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx: 202 passed on this branch and on the control.
  • Reverse verification (fix committed first): with the base's AdvancedChartImpl.tsx checked out over the commit, the new file reads Tests 8 failed | 5 passed (13) — the 8 refusal/pack pins red, the 5 controls green; restored with git checkout HEAD -- FILE, git diff HEAD empty, blob hash 624cba8… on both sides.
  • pnpm --filter @object-ui/plugin-charts run type-check exit 0 (the new test file is in --listFiles); pnpm --filter @object-ui/i18n run type-check exit 0. Dependency closure built first.
  • Gates: check:i18n-keys 0 · check:i18n-drift 0 (1 key added, 0 en values changed) · check:control-bytes 0 · check:vi-mock-specifiers 0 · check:vi-mock-inherit 0 · check:shell-escape-residue 0 · check-changeset-presence 0 · check:eager-closure1 (above, expected). eslint on the changed files: 0 errors, warnings are the pre-existing no-explicit-any pattern the sibling chart pins also carry.
  • pnpm lint (repo-wide) was not run locally; CI owns it.

Contract review (Clause-②) — PASS, amendments applied at e090fd866

Reviewed at CONTRACT_REVIEW_TIER. Verdict PASS WITH REQUIRED AMENDMENTS; both are applied and verified, so the gate label is off.

  • R1 — the changeset and the compareTo pin comment no longer attribute that behaviour to ruling B; both now describe it as current and unruled, name [Decision] Does #7194's ruling B reach a compareTo scatter — a published capability it never named? The refusal currently swallows it and names a fix the author cannot act on #7402, and state that shape (b) there inverts the pin as a decision landing, not a regression. The pin itself stays: it records what ships. This section of the PR body was corrected on the same grounds.
  • R2 — a test comment claimed check:i18n-keys also enforces the inline-fallback / en-pack equality. ⭐ Verified false by mutation, twice — with the fallback changed to Keep ONE series: all three i18n gates still exit 0, because inlineDefaultValue reads only a defaultValue property off an options object and interpolationOptions skips a positional string. The toBe(EN_COPY) assertion is the sole guard, and the comment now says so. Mutation proven on disk by blob hash and anchored token counts; restore proven by state (git diff HEAD empty, blob back to 624cba845).

Amendments are prose only — 2 files, 35 insertions, 8 deletions, no renderer and no locale packs. Eager-closure figures byte-identical across the amendment: framework 524,243 at both eacc55913 and e090fd866.

Not done here

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

… it at the first one's y
A scatter binds one measure: `series[0].dataKey` is the YAxis key and every
`<Scatter>` reads the same rows through it, so a second series added a colour
and a legend entry and nothing else. Measured: two series over two rows painted
four symbols at two positions, each drawn twice, and the second measure's values
appeared nowhere on the plot. The data was valid and the picture was confidently
wrong — no existing refusal could see it.
Ruled B (maintainer, 2026-09-02): a `chartType: 'scatter'` with two or more
`series` now renders the file's refusal shell under
`data-chart-error="scatter-multi-series"`, stating that a scatter plots one
measure, naming the fix (keep exactly one series) and listing the series keys as
data. A single-series scatter is unchanged. No multi-measure projection is built;
render-time only (authoring-time rejection follows #7113). A `compareTo` overlay
counts as a second series — it was painted at the primary's y by the same
mechanism.
The sentence resolves from `chart.scatterOneMeasure` in all ten locale packs via
`useSafeTranslate`, the hook ObjectChart already uses (no module-scope defaults
map). Measured against a control build of the base: +420 gzipped bytes in the
console's eager `framework` chunk, 243 over its ceiling. The ceiling is NOT
raised here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3177.5 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-CsrWIwFh.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.83KB19.71KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

Standing down on check:eager-closure — the failure is real, it is the maintainer's to resolve, and I am not making it green

PM record from the domain:ui execution seat (session session_01EMrWaQw3XS5DxTHxp4yRyC). This PR is complete and parked as draft. Saying once what is red and why no fix is coming.

What is failing

Bundle Analysis, job 100360098939, head eacc55913. One verdict of four — aggregate closure, ceiling sensitivity and ceiling freshness all pass; only the framework per-chunk ceiling fails, 512.0 KB / 511.7 KB, over by 243 bytes.

⭐ The measurement that matters, and it differs from the sibling fork

This PR's implementer split the two halves against a control build of the base, which is exactly what makes this decidable:

buildframework gzipdeltagate
control (a6e5f7050)523,823 Bexit 0 (headroom 177 B)
mechanism only — renderer + tests, packs at base523,823 B0exit 0
mechanism + ten-pack copy (eacc55913)524,243 B+420exit 1

The mechanism costs the eager closure nothingplugin-charts is lazy and not in the closure. The entire overage is the ten-pack copy, ~42 bytes per pack, for one sentence already at the ruling's floor (states one measure, names the fix).

⚠️ This is a materially different shape from PR #7391's fork, and the difference is load-bearing for the decision. There, the mechanism's own code cost 230 B against 177 B of headroom, so no version fitted, footnote deleted or not. Here the mechanism fits with room to spare and only the translations do not. ⇒ The two forks are not interchangeable, and an option that resolves one may not resolve the other. I have recorded that asymmetry on the decision card.

Why I am not making it green

  • Raising PER_CHUNK_GZIP_CEILINGS['framework'] is 门禁削弱 — the maintainer's floor, not an execution lane's, however well-precedented.
  • Shipping the mechanism with English only and deferring the nine translations delivers less than ruling B specifies. It is a real option and it is on the decision card as one — but it is the maintainer's to choose, not this lane's to take because it happens to be the one that compiles.

Where the decision lives

#7399 — one maintainer decision covering this PR and PR #7391 (#7210 / #7225), since both need ruled user-facing copy in the same ten locale packs and both meet the same 177 bytes. It now carries both forks' measurements and the asymmetry between them.

#7194 is pm:blocked with Blocked-by: #7399 and Unlock-action: re-check PR #7400. This PR stays draft, keeps needs:contract-review, and is not enqueued.

Separately, and not blocked by any of the above

Clause-② is yes on this card, so an in-seat contract review at CONTRACT_REVIEW_TIER is owed before needs:contract-review comes off. This seat runs opus and is not at tier, so a claude-fable-5 reviewer has been dispatched — as was done for #7182. That review runs now, in parallel with the budget decision, so this PR is ready the moment #7399 rules. ⚠️ The reviewer has been pointed specifically at the compareTo consequence this PR's description raises: supportsCompareTo excludes only pie / donut / funnel, so a compare-to scatter is a two-series scatter in the product and now refuses. That was surfaced by the implementer rather than buried, which is the right instinct.

⚠️mergeable_state reads behind; the branch needs updating before any enqueue. Not done now — there is no point rebuilding against a moving base while the PR is parked.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review complete — PASS WITH REQUIRED AMENDMENTS. needs:contract-review stays on.

Clause-② in-seat contract review at CONTRACT_REVIEW_TIER (claude-fable-5), commissioned by the domain:ui seat (PM session session_01EMrWaQw3XS5DxTHxp4yRyC). This seat runs opus and is not at tier, so it commissioned the review rather than performing it — the same route used for #7182.

The mechanism implements ruling B faithfully for authored two-series scatters. The gate label cannot come off for two reasons.

⭐ The finding that justifies the tier — filed as #7402

The PR pins and publishes, as though ruled, a behaviour on the compareTo path that ruling B never contemplated. The reviewer raised it explicitly as the maintainer's question, not the PM's and not its own — and that restraint is the right call.

The ruling's literal predicate ("two or more series") reaches a compare-to scatter. Its stated premise does not: "zero authored scatters in-repo" was measured on chartType: 'scatter' literals, and a compare-to scatter is one authored series plus one synthesised overlay from a documented, published product featureCHANGELOG.md:1727 publishes that only pie / donut / funnel ignore compareTo, and comparisonStyle(s, 'scatter') has a scatter-specific branch. The census that justified B cannot see it.

⚠️A second defect on that path, independent of how #7402 rules: ruling B requires the refusal name "the authored fix". There the tile says "Keep exactly one series: amount, amount__comparison" — the author has exactly one series in their spec and never wrote amount__comparison. The refusal names a fix that cannot be acted on, and the real fix is unknowable at the renderer, which only sees series. A refusal that misdirects is worse than the wrong picture it replaces.

Required amendments

R1 — do not land the compareTo behaviour as decided. The changeset sentence "This applies to a compareTo overlay too — it is a second series…" and the pin comment "The ruling counts series, not variants" (test file 141–155) attribute to the ruling a scope it did not state. The pin may stay — it records what the code actually does today — but it must point at #7402 as open rather than at the ruling as settled.

R2 — correct a false claim about a gate (AdvancedChartImpl.scatterSeriesArity.test.tsx:196): "The gate check:i18n-keys enforces this equality too."Verified false by mutation — with the inline fallback changed to Keep ONE series: the gate still exits 0, because inlineDefaultValue (scripts/check-i18n-call-site-keys.mjs:1368) reads only a defaultValue property in an options object and interpolationOptions explicitly skips a positional string. The toBe(EN_COPY) pin is the only thing holding the fallback and the pack together — the comment must say so, or the next reader deletes the pin as redundant.

A dev is dispatched for both. They are text-only and do not touch the mechanism.

What the review confirmed

  • Precedence is correct and masks nothing. Scatter is excluded from both CATEGORY_AXIS_CHART_TYPES and SERIES_ONLY_CHART_TYPES "on purpose", so the arity check is genuinely the first guard on the scatter path; the binding wins over positional pin goes red on the base. Observation: the cited precedent's actual reasoning is "the more fundamental failure and the more specific message", not "spec fault beats row fault" — same outcome, looser analogy than the comment claims.
  • No new export.SeriesArityRefusal is module-local, packages/plugin-charts/src/index.tsx is unchanged, @object-ui/i18n was already a dependency. ⚠️data-chart-error="scatter-multi-series"is a de-facto contract — the five existing values are pinned across 8 test files and published in the CHANGELOG; this changeset publishes the sixth. chart.scatterOneMeasure widens TranslationKeys; patch is the right bump per standing convention.
  • 13/13 pins green; reverse verification reproduced exactly — base renderer over head gives 8 red / 5 green, file restored to blob 624cba845.
  • The census holds on a stronger basis than claimed. The PR's bar control (3 files) fires only in content/docs, so it proved the regex rather than the coverage. Widened to the dashboard spelling type: 'scatter' | 'bubble' — which the PR's regex cannot see — still 0, with control type: 'bar' at 10 files including five plugin-dashboard fixtures.
  • ⚠️ None of the four suites the implementer did not run (251 tests, all green on the head) exercises a scatter with compareTo — so nothing pinned the old wrong picture, and nothing outside this PR pins the new refusal on that path.
  • check:i18n-keys and check:i18n-drift both exit 0, reproduced. check:eager-closure was out of scope by dispatch ([Decision] The framework per-chunk eager-closure ceiling leaves 177 bytes for the whole repo — two ruled user-facing fixes cannot land, and no wording of either fits #7399).

Where this leaves the PR

⚠️Two independent decisions now block it, and neither alone releases it:

decisionabout
#7399ruled? nothe framework byte ceiling
#7402ruled? nowhether ruling B reaches a compareTo scatter

Draft, needs:contract-review retained, not enqueued.


Generated by Claude Code

…nd name the pin's real guard
Prose only — no behaviour, no pins added or removed, no locale copy touched.
Two amendments required by the Clause-② contract review of this PR.
R1 — the changeset and the `compareTo` pin comment both presented the overlay's
refusal as settled by the maintainer ruling. The ruling's words are "a
`chartType: 'scatter'` with two or more `series`"; it never mentions `compareTo`
or `variant`, and "the ruling counts series, not variants" was the implementer's
reading of it. Both now describe the behaviour as current and unruled, pointing
at objectui#7402, which is open on whether refusing is right on that path. The
pin stays — it records what ships — and its comment now says that shape (b) on
that card would invert it as a decision landing, not as a regression.
R2 — the locale pin carried "The gate `check:i18n-keys` enforces this equality
too", which is false. `inlineDefaultValue` reads a `defaultValue` property off an
options object and `interpolationOptions` skips a positional string, and this
call site passes the English positionally. Re-measured on this head: with the
inline fallback mutated to `Keep ONE series:`, all three i18n gates still exit 0
(`check:i18n-keys`, `check:i18n-drift`, `check:i18n-dead-keys`), and the mutation
was proven on disk by blob hash before each run and the restore by blob equality
plus an empty `git diff HEAD`. The comment now says the assertion is the sole
guard and why no gate sees the pair.
Verified: 13/13 pins in AdvancedChartImpl.scatterSeriesArity.test.tsx pass;
check-changeset-presence and all three i18n gates exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3177.5 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-BFq2aqIU.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.86KB19.72KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.6 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-DLTVLYV9.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.86KB19.72KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

Sign up for freeto 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.

bug(plugin-charts): a multi-series scatter draws every series at the FIRST series' y values — two measures, one cloud painted twice

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-charts): refuse a second scatter series instead of drawing it at the first one's y - #7400

Draft
os-project-manager wants to merge 3 commits into
mainfrom
claude/issue-7194-scatter-second-series-refusal
Draft

fix(plugin-charts): refuse a second scatter series instead of drawing it at the first one's y#7400
os-project-manager wants to merge 3 commits into
mainfrom
claude/issue-7194-scatter-second-series-refusal

Conversation

@os-project-manager

@os-project-manageros-project-manager commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes#7194

Maintainer ruling B (comment 5507673384, 2026-09-02): a chartType: 'scatter' with two or more series renders the file's existing refusal shell, stating that a scatter binds one measure and naming the authored fix; a single-series scatter is unchanged; no multi-measure projection (option A waits for a real caller); render-time only — the authoring-time (zod) rejection follows #7113 when that card rules. Clause-② yes ⇒ this PR is a draft; it is not flipped ready and not enqueued.

What changed

  • packages/plugin-charts/src/AdvancedChartImpl.tsx — the scatter arm checks series.length > 1first (a binding fault is true of the spec whatever the rows carry, so it outranks the finding(plugin-charts): scatter is NOT MEASURED for the degenerate-magnitude class — its all-positive CONTROL drew zero marks, so the sweep's zeros say nothing #7171 positional refusal the same way the missing-category-key guard outranks the series guard) and renders SeriesArityRefusal: the existing ChartRefusal shell under data-chart-error="scatter-multi-series", role="status", no console warning (matching the three scatter/magnitude answers already in the file). The shell fits without modification. The sentence is read from chart.scatterOneMeasure through useSafeTranslate — the hook ObjectChart already imports from @object-ui/i18n — so no module-scope createSafeTranslation map is added and no partial i18n mock is disturbed (census: 91 tests mock @object-ui/i18n; none of the partial ones reaches this renderer). The series keys are rendered as data after the sentence, as code elements, outside the translated copy.
  • packages/i18n/src/locales/{en,zh,ja,ko,de,fr,es,pt,ru,ar}.ts — one key, chart.scatterOneMeasure. en: A scatter plots one measure. Keep exactly one series: — states one measure, names the fix; the keys follow as data.
  • packages/plugin-charts/src/AdvancedChartImpl.scatterSeriesArity.test.tsx — 13 pins: single-series control draws 2 marks / no refusal / no note; series: [] and no series still reach the chart through the value fallback; two series ⇒ refusal, no chart container, 0 symbols, no legend; copy contains the sentence and lists both keys; three series refuse; a variant: 'comparison' second series refuses; the binding refusal wins over no-plottable-points; holds with zero rows; no console warning; a two-series bar still draws both series; en pack value equals the inline fallback; every pack defines it and none copies the English; under an I18nProvider (zh) the pack value renders and the keys stay untranslated.
  • .changeset/7194-scatter-one-measure.md — patch for @object-ui/plugin-charts and @object-ui/i18n.

A consequence the contract review looked at: compareTo on scatter

ObjectChart.supportsCompareTo excludes only pie / donut / funnel, and DatasetWidget synthesises a variant: 'comparison' series for every compared measure — so a compare-to scatter is a two-series scatter in the product. Today its overlay is painted at the primary's y values (the overlay never reads its own __comparison column — the same mechanism this card fixes). Under the guard it now refuses, and the pin records that. ⚠️ That behaviour is not a ruled one — the ruling names only "two or more series" and never mentions compareTo or variant — and objectui#7402 is open on whether refusing is the right answer on this path. This PR does not pre-judge it: if that card excludes scatter from compareTo (its shape (b), as pie / donut / funnel already are), the pin inverts, and that is the decision landing rather than a regression. Exempting the overlay would be a one-line change to the predicate, and the picture it would restore is the wrong one.

⚠️ Also raised by the review and recorded on #7402, not fixed here because its remedy depends on how that card rules: on the compare-to path the refusal names a fix the author cannot act on — it says "Keep exactly one series: amount, amount__comparison" while the author wrote exactly one series and never wrote amount__comparison.

Byte cost — measured, both halves, against a control build of the base

Control: origin/main at a6e5f7050 built in a second worktree (pnpm --filter @object-ui/console exec vite build, then node scripts/check-eager-closure-budget.mjs), the same recipe for all three builds; per-chunk numbers read from apps/console/dist/eager-closure.json.

buildframework gzip bytesdelta vs controlcheck:eager-closure
control (a6e5f7050)523,823exit 0 — ✅ framework 511.5 KB / 511.7 KB ceiling (headroom 0.2 KB)
mechanism only (renderer + test, packs at base)523,8230exit 0 — same line
mechanism + copy (eacc55913, unchanged at e090fd866)524,243+420exit 1 — ❌ framework 512.0 KB / 511.7 KB ceiling (OVER by 0.2 KB)

Ceiling PER_CHUNK_GZIP_CEILINGS['framework'] = 524,000; headroom on the control is 177 bytes; this branch is over by 243. The code costs the eager closure nothing (plugin-charts is lazy and not in the closure); the ten-pack copy costs 420 bytes, ~42 per pack, for one sentence already at the ruling's floor (one measure + the fix). ⛔ The ceiling is not raised here and the copy is not weakened below the ruling. This is the fork the dispatch anticipated (PM comment 5513454764, zone 2b): a maintainer budget decision, filed as #7399, one card covering #7194 and #7210. This PR stays draft until it rules; landing order against #7391 is the PM's.

Premise re-checks against the merged ref

  • Defect on origin/maina6e5f7050: yes — scatterYKey = series[0]?.dataKey || 'value' and the legend named off series[0]. fix(plugin-charts): draw every categorical x-axis label on short axes #7385 (d6fe1e1f2) touched only the categorical x-axis path (xAxisCommonProps, label rotation and ellipsis); scatter's numeric axes and the refusal surface were untouched.
  • Authored scatters: 0 in examples / content/docs / apps / packages/*/examples (control bar: 3 files). ⭐ The contract review widened this census to the dashboard-widget spelling type: 'scatter' | 'bubble', which the regex above cannot see: still 0, with control type: 'bar' firing at 10 files including five examples/schema-catalog/**/plugin-dashboard/*.json. The zero holds on a stronger basis than first claimed. One two-series scatter exists in a test corpus: packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx renders plugin-charts:scatter-chart with the shared two-series CHART_SERIES. The sweep stays green on this branch (202/202) because that entry never reaches the scatter arm — the SDUI path does not apply a registration's defaultProps, so it renders with no chartType (filed as bug(plugin-charts): a registration's defaultProps.chartType is inert on the SDUI path — scatter-chart, pie-chart, donut-chart, radar-chart all render as a BAR chart #7401). Not an authored product scatter; the ruling's premise holds. No file outside the declared surface was touched.

Overlap

Verification — mechanism at eacc55913, amendments at e090fd866

  • pnpm exec vitest run packages/plugin-charts/ packages/i18n/ (root, under the shared verify lock): Test Files 100 passed (100) / Tests 1323 passed (1323), exit 0.
  • pnpm exec vitest run packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx: 202 passed on this branch and on the control.
  • Reverse verification (fix committed first): with the base's AdvancedChartImpl.tsx checked out over the commit, the new file reads Tests 8 failed | 5 passed (13) — the 8 refusal/pack pins red, the 5 controls green; restored with git checkout HEAD -- FILE, git diff HEAD empty, blob hash 624cba8… on both sides.
  • pnpm --filter @object-ui/plugin-charts run type-check exit 0 (the new test file is in --listFiles); pnpm --filter @object-ui/i18n run type-check exit 0. Dependency closure built first.
  • Gates: check:i18n-keys 0 · check:i18n-drift 0 (1 key added, 0 en values changed) · check:control-bytes 0 · check:vi-mock-specifiers 0 · check:vi-mock-inherit 0 · check:shell-escape-residue 0 · check-changeset-presence 0 · check:eager-closure1 (above, expected). eslint on the changed files: 0 errors, warnings are the pre-existing no-explicit-any pattern the sibling chart pins also carry.
  • pnpm lint (repo-wide) was not run locally; CI owns it.

Contract review (Clause-②) — PASS, amendments applied at e090fd866

Reviewed at CONTRACT_REVIEW_TIER. Verdict PASS WITH REQUIRED AMENDMENTS; both are applied and verified, so the gate label is off.

  • R1 — the changeset and the compareTo pin comment no longer attribute that behaviour to ruling B; both now describe it as current and unruled, name [Decision] Does #7194's ruling B reach a compareTo scatter — a published capability it never named? The refusal currently swallows it and names a fix the author cannot act on #7402, and state that shape (b) there inverts the pin as a decision landing, not a regression. The pin itself stays: it records what ships. This section of the PR body was corrected on the same grounds.
  • R2 — a test comment claimed check:i18n-keys also enforces the inline-fallback / en-pack equality. ⭐ Verified false by mutation, twice — with the fallback changed to Keep ONE series: all three i18n gates still exit 0, because inlineDefaultValue reads only a defaultValue property off an options object and interpolationOptions skips a positional string. The toBe(EN_COPY) assertion is the sole guard, and the comment now says so. Mutation proven on disk by blob hash and anchored token counts; restore proven by state (git diff HEAD empty, blob back to 624cba845).

Amendments are prose only — 2 files, 35 insertions, 8 deletions, no renderer and no locale packs. Eager-closure figures byte-identical across the amendment: framework 524,243 at both eacc55913 and e090fd866.

Not done here

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

… it at the first one's y
A scatter binds one measure: `series[0].dataKey` is the YAxis key and every
`<Scatter>` reads the same rows through it, so a second series added a colour
and a legend entry and nothing else. Measured: two series over two rows painted
four symbols at two positions, each drawn twice, and the second measure's values
appeared nowhere on the plot. The data was valid and the picture was confidently
wrong — no existing refusal could see it.
Ruled B (maintainer, 2026-09-02): a `chartType: 'scatter'` with two or more
`series` now renders the file's refusal shell under
`data-chart-error="scatter-multi-series"`, stating that a scatter plots one
measure, naming the fix (keep exactly one series) and listing the series keys as
data. A single-series scatter is unchanged. No multi-measure projection is built;
render-time only (authoring-time rejection follows #7113). A `compareTo` overlay
counts as a second series — it was painted at the primary's y by the same
mechanism.
The sentence resolves from `chart.scatterOneMeasure` in all ten locale packs via
`useSafeTranslate`, the hook ObjectChart already uses (no module-scope defaults
map). Measured against a control build of the base: +420 gzipped bytes in the
console's eager `framework` chunk, 243 over its ceiling. The ceiling is NOT
raised here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3177.5 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-CsrWIwFh.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.83KB19.71KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

Standing down on check:eager-closure — the failure is real, it is the maintainer's to resolve, and I am not making it green

PM record from the domain:ui execution seat (session session_01EMrWaQw3XS5DxTHxp4yRyC). This PR is complete and parked as draft. Saying once what is red and why no fix is coming.

What is failing

Bundle Analysis, job 100360098939, head eacc55913. One verdict of four — aggregate closure, ceiling sensitivity and ceiling freshness all pass; only the framework per-chunk ceiling fails, 512.0 KB / 511.7 KB, over by 243 bytes.

⭐ The measurement that matters, and it differs from the sibling fork

This PR's implementer split the two halves against a control build of the base, which is exactly what makes this decidable:

buildframework gzipdeltagate
control (a6e5f7050)523,823 Bexit 0 (headroom 177 B)
mechanism only — renderer + tests, packs at base523,823 B0exit 0
mechanism + ten-pack copy (eacc55913)524,243 B+420exit 1

The mechanism costs the eager closure nothingplugin-charts is lazy and not in the closure. The entire overage is the ten-pack copy, ~42 bytes per pack, for one sentence already at the ruling's floor (states one measure, names the fix).

⚠️ This is a materially different shape from PR #7391's fork, and the difference is load-bearing for the decision. There, the mechanism's own code cost 230 B against 177 B of headroom, so no version fitted, footnote deleted or not. Here the mechanism fits with room to spare and only the translations do not. ⇒ The two forks are not interchangeable, and an option that resolves one may not resolve the other. I have recorded that asymmetry on the decision card.

Why I am not making it green

  • Raising PER_CHUNK_GZIP_CEILINGS['framework'] is 门禁削弱 — the maintainer's floor, not an execution lane's, however well-precedented.
  • Shipping the mechanism with English only and deferring the nine translations delivers less than ruling B specifies. It is a real option and it is on the decision card as one — but it is the maintainer's to choose, not this lane's to take because it happens to be the one that compiles.

Where the decision lives

#7399 — one maintainer decision covering this PR and PR #7391 (#7210 / #7225), since both need ruled user-facing copy in the same ten locale packs and both meet the same 177 bytes. It now carries both forks' measurements and the asymmetry between them.

#7194 is pm:blocked with Blocked-by: #7399 and Unlock-action: re-check PR #7400. This PR stays draft, keeps needs:contract-review, and is not enqueued.

Separately, and not blocked by any of the above

Clause-② is yes on this card, so an in-seat contract review at CONTRACT_REVIEW_TIER is owed before needs:contract-review comes off. This seat runs opus and is not at tier, so a claude-fable-5 reviewer has been dispatched — as was done for #7182. That review runs now, in parallel with the budget decision, so this PR is ready the moment #7399 rules. ⚠️ The reviewer has been pointed specifically at the compareTo consequence this PR's description raises: supportsCompareTo excludes only pie / donut / funnel, so a compare-to scatter is a two-series scatter in the product and now refuses. That was surfaced by the implementer rather than buried, which is the right instinct.

⚠️mergeable_state reads behind; the branch needs updating before any enqueue. Not done now — there is no point rebuilding against a moving base while the PR is parked.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review complete — PASS WITH REQUIRED AMENDMENTS. needs:contract-review stays on.

Clause-② in-seat contract review at CONTRACT_REVIEW_TIER (claude-fable-5), commissioned by the domain:ui seat (PM session session_01EMrWaQw3XS5DxTHxp4yRyC). This seat runs opus and is not at tier, so it commissioned the review rather than performing it — the same route used for #7182.

The mechanism implements ruling B faithfully for authored two-series scatters. The gate label cannot come off for two reasons.

⭐ The finding that justifies the tier — filed as #7402

The PR pins and publishes, as though ruled, a behaviour on the compareTo path that ruling B never contemplated. The reviewer raised it explicitly as the maintainer's question, not the PM's and not its own — and that restraint is the right call.

The ruling's literal predicate ("two or more series") reaches a compare-to scatter. Its stated premise does not: "zero authored scatters in-repo" was measured on chartType: 'scatter' literals, and a compare-to scatter is one authored series plus one synthesised overlay from a documented, published product featureCHANGELOG.md:1727 publishes that only pie / donut / funnel ignore compareTo, and comparisonStyle(s, 'scatter') has a scatter-specific branch. The census that justified B cannot see it.

⚠️A second defect on that path, independent of how #7402 rules: ruling B requires the refusal name "the authored fix". There the tile says "Keep exactly one series: amount, amount__comparison" — the author has exactly one series in their spec and never wrote amount__comparison. The refusal names a fix that cannot be acted on, and the real fix is unknowable at the renderer, which only sees series. A refusal that misdirects is worse than the wrong picture it replaces.

Required amendments

R1 — do not land the compareTo behaviour as decided. The changeset sentence "This applies to a compareTo overlay too — it is a second series…" and the pin comment "The ruling counts series, not variants" (test file 141–155) attribute to the ruling a scope it did not state. The pin may stay — it records what the code actually does today — but it must point at #7402 as open rather than at the ruling as settled.

R2 — correct a false claim about a gate (AdvancedChartImpl.scatterSeriesArity.test.tsx:196): "The gate check:i18n-keys enforces this equality too."Verified false by mutation — with the inline fallback changed to Keep ONE series: the gate still exits 0, because inlineDefaultValue (scripts/check-i18n-call-site-keys.mjs:1368) reads only a defaultValue property in an options object and interpolationOptions explicitly skips a positional string. The toBe(EN_COPY) pin is the only thing holding the fallback and the pack together — the comment must say so, or the next reader deletes the pin as redundant.

A dev is dispatched for both. They are text-only and do not touch the mechanism.

What the review confirmed

  • Precedence is correct and masks nothing. Scatter is excluded from both CATEGORY_AXIS_CHART_TYPES and SERIES_ONLY_CHART_TYPES "on purpose", so the arity check is genuinely the first guard on the scatter path; the binding wins over positional pin goes red on the base. Observation: the cited precedent's actual reasoning is "the more fundamental failure and the more specific message", not "spec fault beats row fault" — same outcome, looser analogy than the comment claims.
  • No new export.SeriesArityRefusal is module-local, packages/plugin-charts/src/index.tsx is unchanged, @object-ui/i18n was already a dependency. ⚠️data-chart-error="scatter-multi-series"is a de-facto contract — the five existing values are pinned across 8 test files and published in the CHANGELOG; this changeset publishes the sixth. chart.scatterOneMeasure widens TranslationKeys; patch is the right bump per standing convention.
  • 13/13 pins green; reverse verification reproduced exactly — base renderer over head gives 8 red / 5 green, file restored to blob 624cba845.
  • The census holds on a stronger basis than claimed. The PR's bar control (3 files) fires only in content/docs, so it proved the regex rather than the coverage. Widened to the dashboard spelling type: 'scatter' | 'bubble' — which the PR's regex cannot see — still 0, with control type: 'bar' at 10 files including five plugin-dashboard fixtures.
  • ⚠️ None of the four suites the implementer did not run (251 tests, all green on the head) exercises a scatter with compareTo — so nothing pinned the old wrong picture, and nothing outside this PR pins the new refusal on that path.
  • check:i18n-keys and check:i18n-drift both exit 0, reproduced. check:eager-closure was out of scope by dispatch ([Decision] The framework per-chunk eager-closure ceiling leaves 177 bytes for the whole repo — two ruled user-facing fixes cannot land, and no wording of either fits #7399).

Where this leaves the PR

⚠️Two independent decisions now block it, and neither alone releases it:

decisionabout
#7399ruled? nothe framework byte ceiling
#7402ruled? nowhether ruling B reaches a compareTo scatter

Draft, needs:contract-review retained, not enqueued.


Generated by Claude Code

…nd name the pin's real guard
Prose only — no behaviour, no pins added or removed, no locale copy touched.
Two amendments required by the Clause-② contract review of this PR.
R1 — the changeset and the `compareTo` pin comment both presented the overlay's
refusal as settled by the maintainer ruling. The ruling's words are "a
`chartType: 'scatter'` with two or more `series`"; it never mentions `compareTo`
or `variant`, and "the ruling counts series, not variants" was the implementer's
reading of it. Both now describe the behaviour as current and unruled, pointing
at objectui#7402, which is open on whether refusing is right on that path. The
pin stays — it records what ships — and its comment now says that shape (b) on
that card would invert it as a decision landing, not as a regression.
R2 — the locale pin carried "The gate `check:i18n-keys` enforces this equality
too", which is false. `inlineDefaultValue` reads a `defaultValue` property off an
options object and `interpolationOptions` skips a positional string, and this
call site passes the English positionally. Re-measured on this head: with the
inline fallback mutated to `Keep ONE series:`, all three i18n gates still exit 0
(`check:i18n-keys`, `check:i18n-drift`, `check:i18n-dead-keys`), and the mutation
was proven on disk by blob hash before each run and the restore by blob equality
plus an empty `git diff HEAD`. The comment now says the assertion is the sole
guard and why no gate sees the pair.
Verified: 13/13 pins in AdvancedChartImpl.scatterSeriesArity.test.tsx pass;
check-changeset-presence and all three i18n gates exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3177.5 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-BFq2aqIU.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.86KB19.72KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.6 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-DLTVLYV9.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.86KB19.72KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

Sign up for freeto 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.

bug(plugin-charts): a multi-series scatter draws every series at the FIRST series' y values — two measures, one cloud painted twice

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-charts): refuse a second scatter series instead of drawing it at the first one's y - #7400

Draft
os-project-manager wants to merge 3 commits into
mainfrom
claude/issue-7194-scatter-second-series-refusal
Draft

fix(plugin-charts): refuse a second scatter series instead of drawing it at the first one's y#7400
os-project-manager wants to merge 3 commits into
mainfrom
claude/issue-7194-scatter-second-series-refusal

Conversation

@os-project-manager

@os-project-manageros-project-manager commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes#7194

Maintainer ruling B (comment 5507673384, 2026-09-02): a chartType: 'scatter' with two or more series renders the file's existing refusal shell, stating that a scatter binds one measure and naming the authored fix; a single-series scatter is unchanged; no multi-measure projection (option A waits for a real caller); render-time only — the authoring-time (zod) rejection follows #7113 when that card rules. Clause-② yes ⇒ this PR is a draft; it is not flipped ready and not enqueued.

What changed

  • packages/plugin-charts/src/AdvancedChartImpl.tsx — the scatter arm checks series.length > 1first (a binding fault is true of the spec whatever the rows carry, so it outranks the finding(plugin-charts): scatter is NOT MEASURED for the degenerate-magnitude class — its all-positive CONTROL drew zero marks, so the sweep's zeros say nothing #7171 positional refusal the same way the missing-category-key guard outranks the series guard) and renders SeriesArityRefusal: the existing ChartRefusal shell under data-chart-error="scatter-multi-series", role="status", no console warning (matching the three scatter/magnitude answers already in the file). The shell fits without modification. The sentence is read from chart.scatterOneMeasure through useSafeTranslate — the hook ObjectChart already imports from @object-ui/i18n — so no module-scope createSafeTranslation map is added and no partial i18n mock is disturbed (census: 91 tests mock @object-ui/i18n; none of the partial ones reaches this renderer). The series keys are rendered as data after the sentence, as code elements, outside the translated copy.
  • packages/i18n/src/locales/{en,zh,ja,ko,de,fr,es,pt,ru,ar}.ts — one key, chart.scatterOneMeasure. en: A scatter plots one measure. Keep exactly one series: — states one measure, names the fix; the keys follow as data.
  • packages/plugin-charts/src/AdvancedChartImpl.scatterSeriesArity.test.tsx — 13 pins: single-series control draws 2 marks / no refusal / no note; series: [] and no series still reach the chart through the value fallback; two series ⇒ refusal, no chart container, 0 symbols, no legend; copy contains the sentence and lists both keys; three series refuse; a variant: 'comparison' second series refuses; the binding refusal wins over no-plottable-points; holds with zero rows; no console warning; a two-series bar still draws both series; en pack value equals the inline fallback; every pack defines it and none copies the English; under an I18nProvider (zh) the pack value renders and the keys stay untranslated.
  • .changeset/7194-scatter-one-measure.md — patch for @object-ui/plugin-charts and @object-ui/i18n.

A consequence the contract review looked at: compareTo on scatter

ObjectChart.supportsCompareTo excludes only pie / donut / funnel, and DatasetWidget synthesises a variant: 'comparison' series for every compared measure — so a compare-to scatter is a two-series scatter in the product. Today its overlay is painted at the primary's y values (the overlay never reads its own __comparison column — the same mechanism this card fixes). Under the guard it now refuses, and the pin records that. ⚠️ That behaviour is not a ruled one — the ruling names only "two or more series" and never mentions compareTo or variant — and objectui#7402 is open on whether refusing is the right answer on this path. This PR does not pre-judge it: if that card excludes scatter from compareTo (its shape (b), as pie / donut / funnel already are), the pin inverts, and that is the decision landing rather than a regression. Exempting the overlay would be a one-line change to the predicate, and the picture it would restore is the wrong one.

⚠️ Also raised by the review and recorded on #7402, not fixed here because its remedy depends on how that card rules: on the compare-to path the refusal names a fix the author cannot act on — it says "Keep exactly one series: amount, amount__comparison" while the author wrote exactly one series and never wrote amount__comparison.

Byte cost — measured, both halves, against a control build of the base

Control: origin/main at a6e5f7050 built in a second worktree (pnpm --filter @object-ui/console exec vite build, then node scripts/check-eager-closure-budget.mjs), the same recipe for all three builds; per-chunk numbers read from apps/console/dist/eager-closure.json.

buildframework gzip bytesdelta vs controlcheck:eager-closure
control (a6e5f7050)523,823exit 0 — ✅ framework 511.5 KB / 511.7 KB ceiling (headroom 0.2 KB)
mechanism only (renderer + test, packs at base)523,8230exit 0 — same line
mechanism + copy (eacc55913, unchanged at e090fd866)524,243+420exit 1 — ❌ framework 512.0 KB / 511.7 KB ceiling (OVER by 0.2 KB)

Ceiling PER_CHUNK_GZIP_CEILINGS['framework'] = 524,000; headroom on the control is 177 bytes; this branch is over by 243. The code costs the eager closure nothing (plugin-charts is lazy and not in the closure); the ten-pack copy costs 420 bytes, ~42 per pack, for one sentence already at the ruling's floor (one measure + the fix). ⛔ The ceiling is not raised here and the copy is not weakened below the ruling. This is the fork the dispatch anticipated (PM comment 5513454764, zone 2b): a maintainer budget decision, filed as #7399, one card covering #7194 and #7210. This PR stays draft until it rules; landing order against #7391 is the PM's.

Premise re-checks against the merged ref

  • Defect on origin/maina6e5f7050: yes — scatterYKey = series[0]?.dataKey || 'value' and the legend named off series[0]. fix(plugin-charts): draw every categorical x-axis label on short axes #7385 (d6fe1e1f2) touched only the categorical x-axis path (xAxisCommonProps, label rotation and ellipsis); scatter's numeric axes and the refusal surface were untouched.
  • Authored scatters: 0 in examples / content/docs / apps / packages/*/examples (control bar: 3 files). ⭐ The contract review widened this census to the dashboard-widget spelling type: 'scatter' | 'bubble', which the regex above cannot see: still 0, with control type: 'bar' firing at 10 files including five examples/schema-catalog/**/plugin-dashboard/*.json. The zero holds on a stronger basis than first claimed. One two-series scatter exists in a test corpus: packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx renders plugin-charts:scatter-chart with the shared two-series CHART_SERIES. The sweep stays green on this branch (202/202) because that entry never reaches the scatter arm — the SDUI path does not apply a registration's defaultProps, so it renders with no chartType (filed as bug(plugin-charts): a registration's defaultProps.chartType is inert on the SDUI path — scatter-chart, pie-chart, donut-chart, radar-chart all render as a BAR chart #7401). Not an authored product scatter; the ruling's premise holds. No file outside the declared surface was touched.

Overlap

Verification — mechanism at eacc55913, amendments at e090fd866

  • pnpm exec vitest run packages/plugin-charts/ packages/i18n/ (root, under the shared verify lock): Test Files 100 passed (100) / Tests 1323 passed (1323), exit 0.
  • pnpm exec vitest run packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx: 202 passed on this branch and on the control.
  • Reverse verification (fix committed first): with the base's AdvancedChartImpl.tsx checked out over the commit, the new file reads Tests 8 failed | 5 passed (13) — the 8 refusal/pack pins red, the 5 controls green; restored with git checkout HEAD -- FILE, git diff HEAD empty, blob hash 624cba8… on both sides.
  • pnpm --filter @object-ui/plugin-charts run type-check exit 0 (the new test file is in --listFiles); pnpm --filter @object-ui/i18n run type-check exit 0. Dependency closure built first.
  • Gates: check:i18n-keys 0 · check:i18n-drift 0 (1 key added, 0 en values changed) · check:control-bytes 0 · check:vi-mock-specifiers 0 · check:vi-mock-inherit 0 · check:shell-escape-residue 0 · check-changeset-presence 0 · check:eager-closure1 (above, expected). eslint on the changed files: 0 errors, warnings are the pre-existing no-explicit-any pattern the sibling chart pins also carry.
  • pnpm lint (repo-wide) was not run locally; CI owns it.

Contract review (Clause-②) — PASS, amendments applied at e090fd866

Reviewed at CONTRACT_REVIEW_TIER. Verdict PASS WITH REQUIRED AMENDMENTS; both are applied and verified, so the gate label is off.

  • R1 — the changeset and the compareTo pin comment no longer attribute that behaviour to ruling B; both now describe it as current and unruled, name [Decision] Does #7194's ruling B reach a compareTo scatter — a published capability it never named? The refusal currently swallows it and names a fix the author cannot act on #7402, and state that shape (b) there inverts the pin as a decision landing, not a regression. The pin itself stays: it records what ships. This section of the PR body was corrected on the same grounds.
  • R2 — a test comment claimed check:i18n-keys also enforces the inline-fallback / en-pack equality. ⭐ Verified false by mutation, twice — with the fallback changed to Keep ONE series: all three i18n gates still exit 0, because inlineDefaultValue reads only a defaultValue property off an options object and interpolationOptions skips a positional string. The toBe(EN_COPY) assertion is the sole guard, and the comment now says so. Mutation proven on disk by blob hash and anchored token counts; restore proven by state (git diff HEAD empty, blob back to 624cba845).

Amendments are prose only — 2 files, 35 insertions, 8 deletions, no renderer and no locale packs. Eager-closure figures byte-identical across the amendment: framework 524,243 at both eacc55913 and e090fd866.

Not done here

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

… it at the first one's y
A scatter binds one measure: `series[0].dataKey` is the YAxis key and every
`<Scatter>` reads the same rows through it, so a second series added a colour
and a legend entry and nothing else. Measured: two series over two rows painted
four symbols at two positions, each drawn twice, and the second measure's values
appeared nowhere on the plot. The data was valid and the picture was confidently
wrong — no existing refusal could see it.
Ruled B (maintainer, 2026-09-02): a `chartType: 'scatter'` with two or more
`series` now renders the file's refusal shell under
`data-chart-error="scatter-multi-series"`, stating that a scatter plots one
measure, naming the fix (keep exactly one series) and listing the series keys as
data. A single-series scatter is unchanged. No multi-measure projection is built;
render-time only (authoring-time rejection follows #7113). A `compareTo` overlay
counts as a second series — it was painted at the primary's y by the same
mechanism.
The sentence resolves from `chart.scatterOneMeasure` in all ten locale packs via
`useSafeTranslate`, the hook ObjectChart already uses (no module-scope defaults
map). Measured against a control build of the base: +420 gzipped bytes in the
console's eager `framework` chunk, 243 over its ceiling. The ceiling is NOT
raised here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3177.5 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-CsrWIwFh.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.83KB19.71KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

Standing down on check:eager-closure — the failure is real, it is the maintainer's to resolve, and I am not making it green

PM record from the domain:ui execution seat (session session_01EMrWaQw3XS5DxTHxp4yRyC). This PR is complete and parked as draft. Saying once what is red and why no fix is coming.

What is failing

Bundle Analysis, job 100360098939, head eacc55913. One verdict of four — aggregate closure, ceiling sensitivity and ceiling freshness all pass; only the framework per-chunk ceiling fails, 512.0 KB / 511.7 KB, over by 243 bytes.

⭐ The measurement that matters, and it differs from the sibling fork

This PR's implementer split the two halves against a control build of the base, which is exactly what makes this decidable:

buildframework gzipdeltagate
control (a6e5f7050)523,823 Bexit 0 (headroom 177 B)
mechanism only — renderer + tests, packs at base523,823 B0exit 0
mechanism + ten-pack copy (eacc55913)524,243 B+420exit 1

The mechanism costs the eager closure nothingplugin-charts is lazy and not in the closure. The entire overage is the ten-pack copy, ~42 bytes per pack, for one sentence already at the ruling's floor (states one measure, names the fix).

⚠️ This is a materially different shape from PR #7391's fork, and the difference is load-bearing for the decision. There, the mechanism's own code cost 230 B against 177 B of headroom, so no version fitted, footnote deleted or not. Here the mechanism fits with room to spare and only the translations do not. ⇒ The two forks are not interchangeable, and an option that resolves one may not resolve the other. I have recorded that asymmetry on the decision card.

Why I am not making it green

  • Raising PER_CHUNK_GZIP_CEILINGS['framework'] is 门禁削弱 — the maintainer's floor, not an execution lane's, however well-precedented.
  • Shipping the mechanism with English only and deferring the nine translations delivers less than ruling B specifies. It is a real option and it is on the decision card as one — but it is the maintainer's to choose, not this lane's to take because it happens to be the one that compiles.

Where the decision lives

#7399 — one maintainer decision covering this PR and PR #7391 (#7210 / #7225), since both need ruled user-facing copy in the same ten locale packs and both meet the same 177 bytes. It now carries both forks' measurements and the asymmetry between them.

#7194 is pm:blocked with Blocked-by: #7399 and Unlock-action: re-check PR #7400. This PR stays draft, keeps needs:contract-review, and is not enqueued.

Separately, and not blocked by any of the above

Clause-② is yes on this card, so an in-seat contract review at CONTRACT_REVIEW_TIER is owed before needs:contract-review comes off. This seat runs opus and is not at tier, so a claude-fable-5 reviewer has been dispatched — as was done for #7182. That review runs now, in parallel with the budget decision, so this PR is ready the moment #7399 rules. ⚠️ The reviewer has been pointed specifically at the compareTo consequence this PR's description raises: supportsCompareTo excludes only pie / donut / funnel, so a compare-to scatter is a two-series scatter in the product and now refuses. That was surfaced by the implementer rather than buried, which is the right instinct.

⚠️mergeable_state reads behind; the branch needs updating before any enqueue. Not done now — there is no point rebuilding against a moving base while the PR is parked.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review complete — PASS WITH REQUIRED AMENDMENTS. needs:contract-review stays on.

Clause-② in-seat contract review at CONTRACT_REVIEW_TIER (claude-fable-5), commissioned by the domain:ui seat (PM session session_01EMrWaQw3XS5DxTHxp4yRyC). This seat runs opus and is not at tier, so it commissioned the review rather than performing it — the same route used for #7182.

The mechanism implements ruling B faithfully for authored two-series scatters. The gate label cannot come off for two reasons.

⭐ The finding that justifies the tier — filed as #7402

The PR pins and publishes, as though ruled, a behaviour on the compareTo path that ruling B never contemplated. The reviewer raised it explicitly as the maintainer's question, not the PM's and not its own — and that restraint is the right call.

The ruling's literal predicate ("two or more series") reaches a compare-to scatter. Its stated premise does not: "zero authored scatters in-repo" was measured on chartType: 'scatter' literals, and a compare-to scatter is one authored series plus one synthesised overlay from a documented, published product featureCHANGELOG.md:1727 publishes that only pie / donut / funnel ignore compareTo, and comparisonStyle(s, 'scatter') has a scatter-specific branch. The census that justified B cannot see it.

⚠️A second defect on that path, independent of how #7402 rules: ruling B requires the refusal name "the authored fix". There the tile says "Keep exactly one series: amount, amount__comparison" — the author has exactly one series in their spec and never wrote amount__comparison. The refusal names a fix that cannot be acted on, and the real fix is unknowable at the renderer, which only sees series. A refusal that misdirects is worse than the wrong picture it replaces.

Required amendments

R1 — do not land the compareTo behaviour as decided. The changeset sentence "This applies to a compareTo overlay too — it is a second series…" and the pin comment "The ruling counts series, not variants" (test file 141–155) attribute to the ruling a scope it did not state. The pin may stay — it records what the code actually does today — but it must point at #7402 as open rather than at the ruling as settled.

R2 — correct a false claim about a gate (AdvancedChartImpl.scatterSeriesArity.test.tsx:196): "The gate check:i18n-keys enforces this equality too."Verified false by mutation — with the inline fallback changed to Keep ONE series: the gate still exits 0, because inlineDefaultValue (scripts/check-i18n-call-site-keys.mjs:1368) reads only a defaultValue property in an options object and interpolationOptions explicitly skips a positional string. The toBe(EN_COPY) pin is the only thing holding the fallback and the pack together — the comment must say so, or the next reader deletes the pin as redundant.

A dev is dispatched for both. They are text-only and do not touch the mechanism.

What the review confirmed

  • Precedence is correct and masks nothing. Scatter is excluded from both CATEGORY_AXIS_CHART_TYPES and SERIES_ONLY_CHART_TYPES "on purpose", so the arity check is genuinely the first guard on the scatter path; the binding wins over positional pin goes red on the base. Observation: the cited precedent's actual reasoning is "the more fundamental failure and the more specific message", not "spec fault beats row fault" — same outcome, looser analogy than the comment claims.
  • No new export.SeriesArityRefusal is module-local, packages/plugin-charts/src/index.tsx is unchanged, @object-ui/i18n was already a dependency. ⚠️data-chart-error="scatter-multi-series"is a de-facto contract — the five existing values are pinned across 8 test files and published in the CHANGELOG; this changeset publishes the sixth. chart.scatterOneMeasure widens TranslationKeys; patch is the right bump per standing convention.
  • 13/13 pins green; reverse verification reproduced exactly — base renderer over head gives 8 red / 5 green, file restored to blob 624cba845.
  • The census holds on a stronger basis than claimed. The PR's bar control (3 files) fires only in content/docs, so it proved the regex rather than the coverage. Widened to the dashboard spelling type: 'scatter' | 'bubble' — which the PR's regex cannot see — still 0, with control type: 'bar' at 10 files including five plugin-dashboard fixtures.
  • ⚠️ None of the four suites the implementer did not run (251 tests, all green on the head) exercises a scatter with compareTo — so nothing pinned the old wrong picture, and nothing outside this PR pins the new refusal on that path.
  • check:i18n-keys and check:i18n-drift both exit 0, reproduced. check:eager-closure was out of scope by dispatch ([Decision] The framework per-chunk eager-closure ceiling leaves 177 bytes for the whole repo — two ruled user-facing fixes cannot land, and no wording of either fits #7399).

Where this leaves the PR

⚠️Two independent decisions now block it, and neither alone releases it:

decisionabout
#7399ruled? nothe framework byte ceiling
#7402ruled? nowhether ruling B reaches a compareTo scatter

Draft, needs:contract-review retained, not enqueued.


Generated by Claude Code

…nd name the pin's real guard
Prose only — no behaviour, no pins added or removed, no locale copy touched.
Two amendments required by the Clause-② contract review of this PR.
R1 — the changeset and the `compareTo` pin comment both presented the overlay's
refusal as settled by the maintainer ruling. The ruling's words are "a
`chartType: 'scatter'` with two or more `series`"; it never mentions `compareTo`
or `variant`, and "the ruling counts series, not variants" was the implementer's
reading of it. Both now describe the behaviour as current and unruled, pointing
at objectui#7402, which is open on whether refusing is right on that path. The
pin stays — it records what ships — and its comment now says that shape (b) on
that card would invert it as a decision landing, not as a regression.
R2 — the locale pin carried "The gate `check:i18n-keys` enforces this equality
too", which is false. `inlineDefaultValue` reads a `defaultValue` property off an
options object and `interpolationOptions` skips a positional string, and this
call site passes the English positionally. Re-measured on this head: with the
inline fallback mutated to `Keep ONE series:`, all three i18n gates still exit 0
(`check:i18n-keys`, `check:i18n-drift`, `check:i18n-dead-keys`), and the mutation
was proven on disk by blob hash before each run and the restore by blob equality
plus an empty `git diff HEAD`. The comment now says the assertion is the sole
guard and why no gate sees the pair.
Verified: 13/13 pins in AdvancedChartImpl.scatterSeriesArity.test.tsx pass;
check-changeset-presence and all three i18n gates exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3177.5 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-BFq2aqIU.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.86KB19.72KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.6 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-DLTVLYV9.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.86KB19.72KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

Sign up for freeto 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.

bug(plugin-charts): a multi-series scatter draws every series at the FIRST series' y values — two measures, one cloud painted twice

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(plugin-charts): refuse a second scatter series instead of drawing it at the first one's y - #7400

Draft
os-project-manager wants to merge 3 commits into
mainfrom
claude/issue-7194-scatter-second-series-refusal
Draft

fix(plugin-charts): refuse a second scatter series instead of drawing it at the first one's y#7400
os-project-manager wants to merge 3 commits into
mainfrom
claude/issue-7194-scatter-second-series-refusal

Conversation

@os-project-manager

@os-project-manageros-project-manager commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes#7194

Maintainer ruling B (comment 5507673384, 2026-09-02): a chartType: 'scatter' with two or more series renders the file's existing refusal shell, stating that a scatter binds one measure and naming the authored fix; a single-series scatter is unchanged; no multi-measure projection (option A waits for a real caller); render-time only — the authoring-time (zod) rejection follows #7113 when that card rules. Clause-② yes ⇒ this PR is a draft; it is not flipped ready and not enqueued.

What changed

  • packages/plugin-charts/src/AdvancedChartImpl.tsx — the scatter arm checks series.length > 1first (a binding fault is true of the spec whatever the rows carry, so it outranks the finding(plugin-charts): scatter is NOT MEASURED for the degenerate-magnitude class — its all-positive CONTROL drew zero marks, so the sweep's zeros say nothing #7171 positional refusal the same way the missing-category-key guard outranks the series guard) and renders SeriesArityRefusal: the existing ChartRefusal shell under data-chart-error="scatter-multi-series", role="status", no console warning (matching the three scatter/magnitude answers already in the file). The shell fits without modification. The sentence is read from chart.scatterOneMeasure through useSafeTranslate — the hook ObjectChart already imports from @object-ui/i18n — so no module-scope createSafeTranslation map is added and no partial i18n mock is disturbed (census: 91 tests mock @object-ui/i18n; none of the partial ones reaches this renderer). The series keys are rendered as data after the sentence, as code elements, outside the translated copy.
  • packages/i18n/src/locales/{en,zh,ja,ko,de,fr,es,pt,ru,ar}.ts — one key, chart.scatterOneMeasure. en: A scatter plots one measure. Keep exactly one series: — states one measure, names the fix; the keys follow as data.
  • packages/plugin-charts/src/AdvancedChartImpl.scatterSeriesArity.test.tsx — 13 pins: single-series control draws 2 marks / no refusal / no note; series: [] and no series still reach the chart through the value fallback; two series ⇒ refusal, no chart container, 0 symbols, no legend; copy contains the sentence and lists both keys; three series refuse; a variant: 'comparison' second series refuses; the binding refusal wins over no-plottable-points; holds with zero rows; no console warning; a two-series bar still draws both series; en pack value equals the inline fallback; every pack defines it and none copies the English; under an I18nProvider (zh) the pack value renders and the keys stay untranslated.
  • .changeset/7194-scatter-one-measure.md — patch for @object-ui/plugin-charts and @object-ui/i18n.

A consequence the contract review looked at: compareTo on scatter

ObjectChart.supportsCompareTo excludes only pie / donut / funnel, and DatasetWidget synthesises a variant: 'comparison' series for every compared measure — so a compare-to scatter is a two-series scatter in the product. Today its overlay is painted at the primary's y values (the overlay never reads its own __comparison column — the same mechanism this card fixes). Under the guard it now refuses, and the pin records that. ⚠️ That behaviour is not a ruled one — the ruling names only "two or more series" and never mentions compareTo or variant — and objectui#7402 is open on whether refusing is the right answer on this path. This PR does not pre-judge it: if that card excludes scatter from compareTo (its shape (b), as pie / donut / funnel already are), the pin inverts, and that is the decision landing rather than a regression. Exempting the overlay would be a one-line change to the predicate, and the picture it would restore is the wrong one.

⚠️ Also raised by the review and recorded on #7402, not fixed here because its remedy depends on how that card rules: on the compare-to path the refusal names a fix the author cannot act on — it says "Keep exactly one series: amount, amount__comparison" while the author wrote exactly one series and never wrote amount__comparison.

Byte cost — measured, both halves, against a control build of the base

Control: origin/main at a6e5f7050 built in a second worktree (pnpm --filter @object-ui/console exec vite build, then node scripts/check-eager-closure-budget.mjs), the same recipe for all three builds; per-chunk numbers read from apps/console/dist/eager-closure.json.

buildframework gzip bytesdelta vs controlcheck:eager-closure
control (a6e5f7050)523,823exit 0 — ✅ framework 511.5 KB / 511.7 KB ceiling (headroom 0.2 KB)
mechanism only (renderer + test, packs at base)523,8230exit 0 — same line
mechanism + copy (eacc55913, unchanged at e090fd866)524,243+420exit 1 — ❌ framework 512.0 KB / 511.7 KB ceiling (OVER by 0.2 KB)

Ceiling PER_CHUNK_GZIP_CEILINGS['framework'] = 524,000; headroom on the control is 177 bytes; this branch is over by 243. The code costs the eager closure nothing (plugin-charts is lazy and not in the closure); the ten-pack copy costs 420 bytes, ~42 per pack, for one sentence already at the ruling's floor (one measure + the fix). ⛔ The ceiling is not raised here and the copy is not weakened below the ruling. This is the fork the dispatch anticipated (PM comment 5513454764, zone 2b): a maintainer budget decision, filed as #7399, one card covering #7194 and #7210. This PR stays draft until it rules; landing order against #7391 is the PM's.

Premise re-checks against the merged ref

  • Defect on origin/maina6e5f7050: yes — scatterYKey = series[0]?.dataKey || 'value' and the legend named off series[0]. fix(plugin-charts): draw every categorical x-axis label on short axes #7385 (d6fe1e1f2) touched only the categorical x-axis path (xAxisCommonProps, label rotation and ellipsis); scatter's numeric axes and the refusal surface were untouched.
  • Authored scatters: 0 in examples / content/docs / apps / packages/*/examples (control bar: 3 files). ⭐ The contract review widened this census to the dashboard-widget spelling type: 'scatter' | 'bubble', which the regex above cannot see: still 0, with control type: 'bar' firing at 10 files including five examples/schema-catalog/**/plugin-dashboard/*.json. The zero holds on a stronger basis than first claimed. One two-series scatter exists in a test corpus: packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx renders plugin-charts:scatter-chart with the shared two-series CHART_SERIES. The sweep stays green on this branch (202/202) because that entry never reaches the scatter arm — the SDUI path does not apply a registration's defaultProps, so it renders with no chartType (filed as bug(plugin-charts): a registration's defaultProps.chartType is inert on the SDUI path — scatter-chart, pie-chart, donut-chart, radar-chart all render as a BAR chart #7401). Not an authored product scatter; the ruling's premise holds. No file outside the declared surface was touched.

Overlap

Verification — mechanism at eacc55913, amendments at e090fd866

  • pnpm exec vitest run packages/plugin-charts/ packages/i18n/ (root, under the shared verify lock): Test Files 100 passed (100) / Tests 1323 passed (1323), exit 0.
  • pnpm exec vitest run packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx: 202 passed on this branch and on the control.
  • Reverse verification (fix committed first): with the base's AdvancedChartImpl.tsx checked out over the commit, the new file reads Tests 8 failed | 5 passed (13) — the 8 refusal/pack pins red, the 5 controls green; restored with git checkout HEAD -- FILE, git diff HEAD empty, blob hash 624cba8… on both sides.
  • pnpm --filter @object-ui/plugin-charts run type-check exit 0 (the new test file is in --listFiles); pnpm --filter @object-ui/i18n run type-check exit 0. Dependency closure built first.
  • Gates: check:i18n-keys 0 · check:i18n-drift 0 (1 key added, 0 en values changed) · check:control-bytes 0 · check:vi-mock-specifiers 0 · check:vi-mock-inherit 0 · check:shell-escape-residue 0 · check-changeset-presence 0 · check:eager-closure1 (above, expected). eslint on the changed files: 0 errors, warnings are the pre-existing no-explicit-any pattern the sibling chart pins also carry.
  • pnpm lint (repo-wide) was not run locally; CI owns it.

Contract review (Clause-②) — PASS, amendments applied at e090fd866

Reviewed at CONTRACT_REVIEW_TIER. Verdict PASS WITH REQUIRED AMENDMENTS; both are applied and verified, so the gate label is off.

  • R1 — the changeset and the compareTo pin comment no longer attribute that behaviour to ruling B; both now describe it as current and unruled, name [Decision] Does #7194's ruling B reach a compareTo scatter — a published capability it never named? The refusal currently swallows it and names a fix the author cannot act on #7402, and state that shape (b) there inverts the pin as a decision landing, not a regression. The pin itself stays: it records what ships. This section of the PR body was corrected on the same grounds.
  • R2 — a test comment claimed check:i18n-keys also enforces the inline-fallback / en-pack equality. ⭐ Verified false by mutation, twice — with the fallback changed to Keep ONE series: all three i18n gates still exit 0, because inlineDefaultValue reads only a defaultValue property off an options object and interpolationOptions skips a positional string. The toBe(EN_COPY) assertion is the sole guard, and the comment now says so. Mutation proven on disk by blob hash and anchored token counts; restore proven by state (git diff HEAD empty, blob back to 624cba845).

Amendments are prose only — 2 files, 35 insertions, 8 deletions, no renderer and no locale packs. Eager-closure figures byte-identical across the amendment: framework 524,243 at both eacc55913 and e090fd866.

Not done here

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

… it at the first one's y
A scatter binds one measure: `series[0].dataKey` is the YAxis key and every
`<Scatter>` reads the same rows through it, so a second series added a colour
and a legend entry and nothing else. Measured: two series over two rows painted
four symbols at two positions, each drawn twice, and the second measure's values
appeared nowhere on the plot. The data was valid and the picture was confidently
wrong — no existing refusal could see it.
Ruled B (maintainer, 2026-09-02): a `chartType: 'scatter'` with two or more
`series` now renders the file's refusal shell under
`data-chart-error="scatter-multi-series"`, stating that a scatter plots one
measure, naming the fix (keep exactly one series) and listing the series keys as
data. A single-series scatter is unchanged. No multi-measure projection is built;
render-time only (authoring-time rejection follows #7113). A `compareTo` overlay
counts as a second series — it was painted at the primary's y by the same
mechanism.
The sentence resolves from `chart.scatterOneMeasure` in all ten locale packs via
`useSafeTranslate`, the hook ObjectChart already uses (no module-scope defaults
map). Measured against a control build of the base: +420 gzipped bytes in the
console's eager `framework` chunk, 243 over its ceiling. The ceiling is NOT
raised here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3177.5 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-CsrWIwFh.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.83KB19.71KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

Standing down on check:eager-closure — the failure is real, it is the maintainer's to resolve, and I am not making it green

PM record from the domain:ui execution seat (session session_01EMrWaQw3XS5DxTHxp4yRyC). This PR is complete and parked as draft. Saying once what is red and why no fix is coming.

What is failing

Bundle Analysis, job 100360098939, head eacc55913. One verdict of four — aggregate closure, ceiling sensitivity and ceiling freshness all pass; only the framework per-chunk ceiling fails, 512.0 KB / 511.7 KB, over by 243 bytes.

⭐ The measurement that matters, and it differs from the sibling fork

This PR's implementer split the two halves against a control build of the base, which is exactly what makes this decidable:

buildframework gzipdeltagate
control (a6e5f7050)523,823 Bexit 0 (headroom 177 B)
mechanism only — renderer + tests, packs at base523,823 B0exit 0
mechanism + ten-pack copy (eacc55913)524,243 B+420exit 1

The mechanism costs the eager closure nothingplugin-charts is lazy and not in the closure. The entire overage is the ten-pack copy, ~42 bytes per pack, for one sentence already at the ruling's floor (states one measure, names the fix).

⚠️ This is a materially different shape from PR #7391's fork, and the difference is load-bearing for the decision. There, the mechanism's own code cost 230 B against 177 B of headroom, so no version fitted, footnote deleted or not. Here the mechanism fits with room to spare and only the translations do not. ⇒ The two forks are not interchangeable, and an option that resolves one may not resolve the other. I have recorded that asymmetry on the decision card.

Why I am not making it green

  • Raising PER_CHUNK_GZIP_CEILINGS['framework'] is 门禁削弱 — the maintainer's floor, not an execution lane's, however well-precedented.
  • Shipping the mechanism with English only and deferring the nine translations delivers less than ruling B specifies. It is a real option and it is on the decision card as one — but it is the maintainer's to choose, not this lane's to take because it happens to be the one that compiles.

Where the decision lives

#7399 — one maintainer decision covering this PR and PR #7391 (#7210 / #7225), since both need ruled user-facing copy in the same ten locale packs and both meet the same 177 bytes. It now carries both forks' measurements and the asymmetry between them.

#7194 is pm:blocked with Blocked-by: #7399 and Unlock-action: re-check PR #7400. This PR stays draft, keeps needs:contract-review, and is not enqueued.

Separately, and not blocked by any of the above

Clause-② is yes on this card, so an in-seat contract review at CONTRACT_REVIEW_TIER is owed before needs:contract-review comes off. This seat runs opus and is not at tier, so a claude-fable-5 reviewer has been dispatched — as was done for #7182. That review runs now, in parallel with the budget decision, so this PR is ready the moment #7399 rules. ⚠️ The reviewer has been pointed specifically at the compareTo consequence this PR's description raises: supportsCompareTo excludes only pie / donut / funnel, so a compare-to scatter is a two-series scatter in the product and now refuses. That was surfaced by the implementer rather than buried, which is the right instinct.

⚠️mergeable_state reads behind; the branch needs updating before any enqueue. Not done now — there is no point rebuilding against a moving base while the PR is parked.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review complete — PASS WITH REQUIRED AMENDMENTS. needs:contract-review stays on.

Clause-② in-seat contract review at CONTRACT_REVIEW_TIER (claude-fable-5), commissioned by the domain:ui seat (PM session session_01EMrWaQw3XS5DxTHxp4yRyC). This seat runs opus and is not at tier, so it commissioned the review rather than performing it — the same route used for #7182.

The mechanism implements ruling B faithfully for authored two-series scatters. The gate label cannot come off for two reasons.

⭐ The finding that justifies the tier — filed as #7402

The PR pins and publishes, as though ruled, a behaviour on the compareTo path that ruling B never contemplated. The reviewer raised it explicitly as the maintainer's question, not the PM's and not its own — and that restraint is the right call.

The ruling's literal predicate ("two or more series") reaches a compare-to scatter. Its stated premise does not: "zero authored scatters in-repo" was measured on chartType: 'scatter' literals, and a compare-to scatter is one authored series plus one synthesised overlay from a documented, published product featureCHANGELOG.md:1727 publishes that only pie / donut / funnel ignore compareTo, and comparisonStyle(s, 'scatter') has a scatter-specific branch. The census that justified B cannot see it.

⚠️A second defect on that path, independent of how #7402 rules: ruling B requires the refusal name "the authored fix". There the tile says "Keep exactly one series: amount, amount__comparison" — the author has exactly one series in their spec and never wrote amount__comparison. The refusal names a fix that cannot be acted on, and the real fix is unknowable at the renderer, which only sees series. A refusal that misdirects is worse than the wrong picture it replaces.

Required amendments

R1 — do not land the compareTo behaviour as decided. The changeset sentence "This applies to a compareTo overlay too — it is a second series…" and the pin comment "The ruling counts series, not variants" (test file 141–155) attribute to the ruling a scope it did not state. The pin may stay — it records what the code actually does today — but it must point at #7402 as open rather than at the ruling as settled.

R2 — correct a false claim about a gate (AdvancedChartImpl.scatterSeriesArity.test.tsx:196): "The gate check:i18n-keys enforces this equality too."Verified false by mutation — with the inline fallback changed to Keep ONE series: the gate still exits 0, because inlineDefaultValue (scripts/check-i18n-call-site-keys.mjs:1368) reads only a defaultValue property in an options object and interpolationOptions explicitly skips a positional string. The toBe(EN_COPY) pin is the only thing holding the fallback and the pack together — the comment must say so, or the next reader deletes the pin as redundant.

A dev is dispatched for both. They are text-only and do not touch the mechanism.

What the review confirmed

  • Precedence is correct and masks nothing. Scatter is excluded from both CATEGORY_AXIS_CHART_TYPES and SERIES_ONLY_CHART_TYPES "on purpose", so the arity check is genuinely the first guard on the scatter path; the binding wins over positional pin goes red on the base. Observation: the cited precedent's actual reasoning is "the more fundamental failure and the more specific message", not "spec fault beats row fault" — same outcome, looser analogy than the comment claims.
  • No new export.SeriesArityRefusal is module-local, packages/plugin-charts/src/index.tsx is unchanged, @object-ui/i18n was already a dependency. ⚠️data-chart-error="scatter-multi-series"is a de-facto contract — the five existing values are pinned across 8 test files and published in the CHANGELOG; this changeset publishes the sixth. chart.scatterOneMeasure widens TranslationKeys; patch is the right bump per standing convention.
  • 13/13 pins green; reverse verification reproduced exactly — base renderer over head gives 8 red / 5 green, file restored to blob 624cba845.
  • The census holds on a stronger basis than claimed. The PR's bar control (3 files) fires only in content/docs, so it proved the regex rather than the coverage. Widened to the dashboard spelling type: 'scatter' | 'bubble' — which the PR's regex cannot see — still 0, with control type: 'bar' at 10 files including five plugin-dashboard fixtures.
  • ⚠️ None of the four suites the implementer did not run (251 tests, all green on the head) exercises a scatter with compareTo — so nothing pinned the old wrong picture, and nothing outside this PR pins the new refusal on that path.
  • check:i18n-keys and check:i18n-drift both exit 0, reproduced. check:eager-closure was out of scope by dispatch ([Decision] The framework per-chunk eager-closure ceiling leaves 177 bytes for the whole repo — two ruled user-facing fixes cannot land, and no wording of either fits #7399).

Where this leaves the PR

⚠️Two independent decisions now block it, and neither alone releases it:

decisionabout
#7399ruled? nothe framework byte ceiling
#7402ruled? nowhether ruling B reaches a compareTo scatter

Draft, needs:contract-review retained, not enqueued.


Generated by Claude Code

…nd name the pin's real guard
Prose only — no behaviour, no pins added or removed, no locale copy touched.
Two amendments required by the Clause-② contract review of this PR.
R1 — the changeset and the `compareTo` pin comment both presented the overlay's
refusal as settled by the maintainer ruling. The ruling's words are "a
`chartType: 'scatter'` with two or more `series`"; it never mentions `compareTo`
or `variant`, and "the ruling counts series, not variants" was the implementer's
reading of it. Both now describe the behaviour as current and unruled, pointing
at objectui#7402, which is open on whether refusing is right on that path. The
pin stays — it records what ships — and its comment now says that shape (b) on
that card would invert it as a decision landing, not as a regression.
R2 — the locale pin carried "The gate `check:i18n-keys` enforces this equality
too", which is false. `inlineDefaultValue` reads a `defaultValue` property off an
options object and `interpolationOptions` skips a positional string, and this
call site passes the English positionally. Re-measured on this head: with the
inline fallback mutated to `Keep ONE series:`, all three i18n gates still exit 0
(`check:i18n-keys`, `check:i18n-drift`, `check:i18n-dead-keys`), and the mutation
was proven on disk by blob hash before each run and the restore by blob equality
plus an empty `git diff HEAD`. The comment now says the assertion is the sole
guard and why no gate sees the pair.
Verified: 13/13 pins in AdvancedChartImpl.scatterSeriesArity.test.tsx pass;
check-changeset-presence and all three i18n gates exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3177.5 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-BFq2aqIU.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.86KB19.72KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.6 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-DLTVLYV9.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.86KB19.72KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

Sign up for freeto 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.

bug(plugin-charts): a multi-series scatter draws every series at the FIRST series' y values — two measures, one cloud painted twice

2 participants

@os-project-manager@claude