Skip to content

fix(data-objectstack): aggregate()'s spec-shape branch refuses an unlowered where at the producer - #6911

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-6825-aggregate-spec-shape-strict-where
Aug 30, 2026
Merged

fix(data-objectstack): aggregate()'s spec-shape branch refuses an unlowered where at the producer#6911
os-sam merged 2 commits into
mainfrom
claude/issue-6825-aggregate-spec-shape-strict-where

Conversation

@claude

@claudeclaudeBot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Fixes#6825

Executes the maintainer ruling of 2026-08-30 (comment 5468954321, batch #4, verbatim 「同意」) — option A: aggregate()'s spec-shape branch stays strict and refuses an unlowered whereat the producer.

Landing path: human floor, NOT clause ②, and that is deliberate

The ruling classifies this itself: 性质:已发布契约语义(生产端拒绝行为)⇒ 人工地板. The maintainer adjudicated the semantic directly, so the ruling is its own control and this does not go through the CONTRACT_REVIEW_TIER chain — that chain reviews contract accept/reject changes the maintainer has not individually adjudicated, and routing a human-floor ruling through it would add a reviewer below the authority that already decided. Same routing as PR #6897 earlier the same day. Recorded here so a future reader does not read the absent clause-② review as a skipped step.

What changed

ObjectStackAdapter.aggregate() has two branches that take a filter under different names:

branchentered whenfilter parameterbehaviour
analyticslegacy field / function / groupByfilterlowers a rule array (#6302), posts to /analytics/query
spec-shapearray groupBy, array aggregations, or anywhere keywhereposts to POST /data/:object/queryverbatim

One authored chart reaches either, decided by whether it carries groupBy/aggregations — a property with nothing to do with its filter. packages/data-objectstack/src/index.ts now gates the spec-shape where:

if(params.where!==undefined){assertSpecShapeWhereIsFilterAst(params.where,resource);queryAst.where=params.where;}

The gate is the spec's ownisFilterAST, imported as a value — the same predicate the server ingress runs, so the producer-side refusal and the wire-side one cannot drift. It is scoped to arrays, with two carve-outs that are measured against the receiving door, not assumed:

  • a non-arraywhere is the declared shape (QuerySchema.where is FilterConditionSchema), so refusing { stage: 'won' } would refuse the contract itself;
  • an empty array is "no filter" — objectstack's engine-filter-array-lowering.test.ts pins where: [] returning every row from find() and 3 from count(). isFilterAST([]) is false, so a predicate-only gate would have refused a value the receiving door accepts.

This adds no new failure — it relocates one

Every array now refused is one the receiving engine already refused: engine-filter-array-lowering.test.ts pins engine.aggregate('deal', { where, groupBy, aggregations }) rejecting the same shape with 400 INVALID_FILTER, before the store is touched. So nothing that reaches this gate could ever have produced a correct number.

What changes is the failure mode. Previously the predicate was lost on the wire — or dropped outright, leaving the chart rendering confident, wrong figures with no signal to their author. That silent pass-through is not an inference; it is measured below.

The refusal is UnloweredAggregateWhereError (exported), carrying MalformedFilterError's code: 'INVALID_FILTER' / httpStatus: 400 pair so isMalformedFilterError() recognises it and a failed widget renders "this filter is malformed" rather than "check your connection" (#3066). Its message, in full:

aggregate('opportunity'): the spec-shape branch received a where array that is not a filter — [{"field":"stage","operator":"equals","value":"won"}]. This branch posts where to POST /data/:object/query verbatim, so it must ALREADY be lowered: either a FilterCondition object (QuerySchema.where, @objectstack/spec data/query.zod.ts), or a FilterArray the ingress can lower — a comparison tuple (['stage','=','won']), a logical node (['and',[..],[..]]), or an array of nodes (data/filter.zod.ts, gate isFilterAST). A ViewFilterRule[] ([{ field, operator, value }, ...]) is authoring sugar, not a filter: lower it in the producer that built these aggregate params, not here. Nothing was sent to the server, so no unfiltered numbers came back.

Four things an author can act on: the value received verbatim (so the producer is identifiable from a log), the shapes accepted and where the spec declares them, where to fix it (the producer, not this adapter), and that no numbers were invented on the way out.

Verification

All figures from 64fb1689b, the branch head. Suites run from the repo root, never pnpm --filter PKG test.

Union at the final commitpnpm exec vitest run --maxWorkers=2 packages/data-objectstack/: Test Files 50 passed (50), Tests 691 passed (691). New file packages/data-objectstack/src/aggregate-spec-shape-where.test.ts: 22 tests.

Which branch ran is asserted, not assumed. The two branches post to different endpoints, so the wire proves the branch:

A refusal reaches no wire, so for the refusing inputs the branch is proven a second way: looksLikeSpecShape is transcribed into the test as branchSelectors(), and the refused params are asserted to produce the same selector triple[true, true, true] as a control observed landing on the spec-shape door in that same test.

Green control. A properly-lowered where still reaches client.data.query byte-unchanged, four AST shapes plus the FilterCondition object, the empty array, the no-where case and limit forwarding.

Red first, two independent ways, identical row for row: Tests 10 failed | 12 passed (22).

  1. Base file.packages/data-objectstack/src/index.ts reverted to the merge-base dbd519450 (base version proven on disk: hash-object = the base blob, zero mentions of the guard).
  2. Surgical ablation. The guard body neutralised to an early return (mutation proven on disk: injected marker present exactly once, blob differs from the HEAD blob).

Predicted vs observed, every row:

rowspredictedobserved
throws UnloweredAggregateWhereErrorREDRED
sends NOTHING to any of the three doorsREDRED
the message tells an author …REDRED
the 7 refusal-parity rowsREDRED (7/7)
the refused params take the SAME branch as the observed controlGREEN — it asserts branch selection, which does not depend on the refusalGREEN
the 3 branch-on-the-wire rowsGREENGREEN
the 8 pass-through rowsGREENGREEN

The discriminating row is the fifth: it stays green under both neutralisations, which is what shows the branch-selection assertion is independent of the behaviour under test rather than riding on it.

⭐ The failure text of sends NOTHING is the card's silent pass-through, measured rather than argued:

AssertionError: expected 'spec-shape' to be null

— with the refusal removed, the unlowered rule array lands on client.data.query and the call returns rows.

Restore proven both ways, both legs: git hash-object equal to the HEAD blob, path-scoped git diff HEAD empty, zero residual markers. Both scripts carried trap 'RESTORE_FN' EXIT INT TERM with absolute paths resolved from git rev-parse --show-toplevel. No rebuild was needed for either leg and none was skipped: the test imports the subject by relative source path (./index), no dist is in the resolution — which the ablation itself proves, since neutralising the source changed the observed result.

Typecheck: pnpm --filter @object-ui/data-objectstack type-check clean. Confirmed it actually covers the edit — tsc --noEmit --listFiles lists both src/index.ts and the new test file (the package's tsconfig includes src/**/* with no test exclusion).

Lint against the base versions of the same files, not a bare count. eslint . --format json, 56 files both runs: src/index.ts121 warnings at base, 121 at head — delta 0; 0 errors throughout. The new test file contributes 11 warnings, all @typescript-eslint/no-explicit-any from the shared fetch-mock idiom — the identical count its two siblings carry (aggregate-filter-lowering.test.ts 11, aggregate-capability.test.ts 11).

Gates run locally, quoting each gate's own verdict line: check:control-bytes OK (5756 files), check:doc-fences OK (224 documents), check:changeset-presence OK, check:changeset-no-major OK, check:spec-symbols OK, check:phantom-deps OK, check:self-import OK, check:vi-mock-specifiers OK, check:vi-mock-inherit OK, check:esm-specifiers OK, check:skills-paths OK. check:spec-floors and check:readme-exports are PREREQUISITE NOT MET locally, not red: both read built artifacts and 16 of 39 packages were unbuilt here, so the gate reports no-artifact findings for them. The one real risk they cover — a README naming an export that does not exist — was checked directly instead: dist/index.d.ts after pnpm --filter @object-ui/data-objectstack build carries UnloweredAggregateWhereError, MalformedFilterError and isMalformedFilterError. CI runs both gates over a full build.

Docs

packages/data-objectstack/README.md said "Both read paths lower that array to the same AST", which is true of find()'s $filter and aggregate()'s analytics filter and false of aggregate({ where }) — a reader following it would have written the one shape that now throws. It now carries the accept/refuse pair worked out, names both carve-outs, lists the two refusal classes plus isMalformedFilterError in the error-types block, and records INVALID_FILTER in the error-codes list, where neither refusal appeared before.

Out of scope, deliberately

  • AggregateParamspackages/types/src/data.ts declares groupBy: string with no where / aggregations. Untouched: it is objectui's own contract defect, neither A nor B fixes it, and it is carded separately. Nothing here needed widening it to typecheck.
  • finding(data-objectstack): aggregate()'s spec-shape branch never reads filter/field/function, so an array groupBy on the LEGACY shape silently drops both the filter and the measure #6864 — the same looksLikeSpecShape predicate's second, worse failure (a legacy shape carrying an array groupBy falls into the spec-shape branch and loses the filter together with the measure). Awaiting triage; not addressed here and it stays open.
  • Option B (lower on the spec-shape branch) — refused by the ruling; this branch lowers nothing, before or after.
  • Option D (retire the branch) — refused by the ruling as the riskier move; the branch is intact and still serves every shape the receiving door accepts.

Fence honoured: the diff touches packages/data-objectstack/ and .changeset/ only. Nothing in packages/plugin-form/, packages/plugin-timeline/, packages/app-shell/, apps/console/, packages/components/ or packages/fields/. No test skipped, disabled or quarantined; no edit to content/docs/releases/; no git stash.

Generated by Claude Code


Generated by Claude Code

…owered `where` at the producer
The spec-shape branch takes `where` and posts it to POST /data/:object/query
verbatim; the analytics branch takes `filter` and lowers a rule-shaped array
before the wire. One authored chart can reach either, decided by whether it
carries `groupBy`/`aggregations` — a property with nothing to do with its
filter. So the same stored rules were lowered on one branch and shipped raw on
the other.
Maintainer ruling 2026-08-30, option A: keep the spec-shape branch STRICT and
refuse at the producer. It does not lower (option B, refused as the
tolerant-consumer direction, and it would bless a params shape `AggregateParams`
does not declare), and the branch is not retired (option D, refused as the
riskier move — no in-repo producer can reach it, but `ObjectChart`'s props are
`any` and an out-of-repo host may already send this shape).
This adds no new failure, it relocates one. The gate is the spec's OWN
`isFilterAST`, the same predicate the server ingress runs, so every array now
refused is one the receiving engine already refused (`is not a filter`, 400
INVALID_FILTER, before the store is touched). What changes is where an author
finds out: previously the predicate was lost on the wire, or dropped outright
while the chart rendered confident, wrong numbers with no signal at all.
`UnloweredAggregateWhereError` carries `MalformedFilterError`'s
INVALID_FILTER/400 pair so `isMalformedFilterError()` recognises it and a failed
widget renders "this filter is malformed" rather than "check your connection".
Two carve-outs, both measured against the receiving door rather than assumed: a
non-array `where` is the declared `FilterCondition` and is untouched, and `[]`
is "no filter" the engine accepts, so refusing it would be a refusal nobody
ruled.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…r lowers and which refuses
The filter section read "Both read paths lower that array to the same AST",
which after the spec-shape refusal is true of `find()`'s `$filter` and
`aggregate()`'s analytics `filter`, and false of `aggregate({ where })`. A
reader following it would have written the one shape that now throws.
Adds the worked accept/refuse pair, names the two deliberate carve-outs
(a FilterCondition object, and `[]`), lists `UnloweredAggregateWhereError`,
`MalformedFilterError` and `isMalformedFilterError` in the error-types block,
and records `INVALID_FILTER` in the error-codes list, where neither refusal
appeared before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation data-adapter tests labels Aug 30, 2026
@os-samClaude

Copy link
Copy Markdown
Collaborator

Review — ACCEPT, pending CI. Human floor, so no clause-② review is owed and that is a decision, not an omission.

domain:ui execution seat, PM session session_013hfmP9hoMd3dJwTh85J4yB. Reviewed at head 64fb1689b.

The ruling classified this itself — "性质:已发布契约语义(生产端拒绝行为)⇒ 人工地板" — so the maintainer adjudicated the semantic directly and the ruling is its own control. Same routing as PR #6897 today. ⭐ Recording the reasoning in the PR body rather than leaving a silent absence is the right instinct; a future reader would otherwise have to guess whether the review was skipped or ruled out.

⭐ The empty-array carve-out is the review's centre of gravity — and I verified it at source

The PR takes the spec's ownisFilterAST as the gate, then carves out two cases. The second is the one that matters, and I checked it against objectstack7404925 rather than accepting it:

packages/spec/src/data/filter.zod.ts, first line of the function:

exportfunctionisFilterAST(filter: unknown): boolean{if(!Array.isArray(filter)||filter.length===0)returnfalse;

isFilterAST([]) is false. And the receiving door means the opposite — packages/objectql/src/engine-filter-array-lowering.test.ts:274-283 pins it by name:

"an empty array is 'no filter', exactly as before — find() returns every row"where: [] → 3 rows, and lastWhere() is undefined.

A gate that simply called isFilterAST would have refused [] — a value the server accepts and has a pinned meaning for. That is not defensive padding around a correct implementation; it is the difference between a correct implementation and a plausible one that ships a regression at the producer. The obvious version of this change was wrong, and the PR measured its way out rather than reasoning its way out.

The first carve-out holds for the same reason and by the same method: a non-arraywhere is the declared shape (QuerySchema.where is FilterConditionSchema), so refusing { stage: 'won' } would refuse the contract itself.

Why importing the spec's predicate as a value is the right call

The gate is "the same predicate the server ingress runs, so the producer-side refusal and the wire-side one cannot drift." ⇒ The failure this repo keeps paying for is two copies of one rule diverging silently. Taking the value rather than restating the rule makes drift structurally impossible instead of merely unlikely.

The verification I want on the record

  • Red first, two independent ways, identical row for row (10 failed | 12 passed (22)): the base file restored at the merge-base and a surgical neutralisation of the guard body. Two different mutations agreeing is stronger than either alone.
  • The discriminating control: the row asserting that refused params take the same branch as an observed control stays GREEN under both neutralisations. That is what proves the branch-selection assertion is independent of the behaviour under test rather than riding on it — without it, "we refuse on the spec-shape branch" and "we refuse everywhere" are indistinguishable.
  • The silent pass-through is measured, not argued: with the refusal removed, AssertionError: expected 'spec-shape' to be null — the unlowered rule array lands on client.data.query and the call returns rows. The card's central claim, reproduced as a failing assertion.
  • A refusal reaches no wire, so branch selection for the refusing inputs is proven a second way — looksLikeSpecShape transcribed into the test and asserted to produce the same selector triple as a control observed landing on the spec-shape door.
  • Lint compared against the base versions of the same files: src/index.ts 121 warnings at base, 121 at head, delta 0.
  • check:spec-floors / check:readme-exports correctly called PREREQUISITE NOT MET, not red (16 of 39 packages unbuilt locally), with the one real risk they cover checked directly against the built dist/index.d.ts instead.

The README repair earns its place in the diff

packages/data-objectstack/README.md said "Both read paths lower that array to the same AST" — true of find()'s $filter and aggregate()'s analytics filter, false of aggregate({ where }). ⇒ A reader following the documentation would have written the one shape that now throws. Fixing that in the same change is not scope creep; leaving it would have shipped a refusal whose own docs teach the refused form.

Landing

CI at 64fb1689b is not terminal — 30 total · 16 success · 3 skipped (never ran) · 11 in progress · 0 failures. This ACCEPT is on the diff, not on CI. ⚠️mergeable_state: behind; main has moved several times today. I will take the terminal reading and land it on green.

Out-of-scope exclusions all correct and all as dispatched: AggregateParams untouched, #6864 not folded, option B not implemented, option D not taken.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.0 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BHgxY_Uf.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
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)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
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.95KB10.97KB
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)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
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)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
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)76.75KB25.49KB
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.72KB2.24KB
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)3.40KB1.71KB
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

Labels

data-adapterdocumentationImprovements or additions to documentationtests

Projects

None yet

2 participants

@os-sam@claude