Skip to content

fix(react,fields,charts,dashboard,report,list,core): surface filter refusals at render/click-time merges instead of throwing (objectui#10789) - #10830

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-10789-render-time-filter-mergers
Sep 27, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-10789-render-time-filter-mergers

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #10789
Clause-②: yes

Seat note (domain:ui seat #1, line rewritten by the seat): yes, not the claim’s no. filterRefusal is a new optional member on UseElementDataSourceResult and UseElementDataSourceSchemaResult, both of which are reachable from @object-ui/react’s . entry through export *. That is public-face growth, and the claim amendment on objectui#10789 records it. The acceptance set only narrows.

The render-time and click-time filter merges called the THROWING converter form, so a malformed authored filter threw a FilterOperatorError out of render (into the nearest error boundary) or out of a click handler, instead of surfacing the refusal the way the sibling sites already do. Every such site now keeps the refusal as a value and surfaces it on the channel its nearest sibling uses. A malformed filter stays refused: nothing refused becomes accepted, and no site falls back to "no filter".

Dispatched by the domain:ui seat #1 PM; implemented in session https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk.

Census (execution note 1)

Every caller of the throwing forms (convertFiltersToAST, toFilterNode, mergeFilterNodes) and of the core utilities that wrap them (composeElementDataSource, composeDrillFilter, buildDatasetDrillFilter, buildEffectiveFilter), read on origin/main at a14fb23b3. Cited by symbol, never by line (AGENTS.md #11).

# Site (symbol) Package Reaches the converter via Class on main This PR
R1 useElementDataSourceSchema — component filter AND composed binding react mergeFilterNodes render, uncaught each source through toFilterNodeSafely; refusal → status: 'missing' + filterRefusal; ElementDataSourceGate draws the malformed-filter notice
R2 useElementDataSource → composeElementDataSource — view filter AND binding filter react / core mergeFilterNodes render, uncaught (consumers: ElementDataSourceGate, element:record_picker) FilterOperatorError caught at the hook → missing + filterRefusal; gate → notice; record picker → its own configuration-error panel
R3 PeoplePicker recentFilter fields mergeFilterNodes render, uncaught — on every render with a declared filter, not only with recents toFilterNodeSafely(baseFilter); recents query disabled; refusal in the candidate area's error slot
R4 RecordPickerDialog mergedFilter (rule-array arm) fields mergeFilterNodes render, uncaught each side through toFilterNodeSafely; query disabled; refusal in the dialog's error alert
R5 ObjectChart drillFilter plugin-charts composeDrillFilter render after a click, uncaught caught; no drawer, no navigate; console.warn naming the operator
R6 ObjectPivotTable renderDrillDrawer plugin-dashboard composeDrillFilter render after a click, uncaught composition memo above the early returns; caught; no drawer, no navigate; console.warn
R7 DatasetWidget openDrill plugin-dashboard buildDatasetDrillFilter → composeDrillFilter click handler, uncaught caught; no drawer; console.warn
R8 DatasetReportRenderer row drill and matrix drillCell plugin-report buildDatasetDrillFilter → composeDrillFilter click handler, uncaught caught; onDrill NOT emitted (never with objectFilter absent); console.warn
R9 ListView handleExport, server leg — buildEffectiveFilter ran BEFORE the export try plugin-list buildEffectiveFilter → mergeFilterNodes click handler, uncaught (found by this census) moved inside the export try; refusal named in the export menu
S ObjectGrid schema.filter; RelatedList list filter; LineItemsPanel filter; ValueDataSource.find array $filter grid / detail / form / core toFilterNodeSafely safe untouched
L ListView data fetch; ListView self-query filter (own catch); ObjectView finalFilter; ObjectGrid defaultFilters leg; LookupField recents; related-count-store fetchCount; data-objectstack translateFilterToAST (find, exportDownload) and the SDK find leg; ViewDataProvider.resolveElementDataSource (async, no production caller) various various inside a load try / async untouched
N RelatedList / LineItemsPanel mergeFilterNodes(parentScope, listFilterNode) detail / form mergeFilterNodes cannot refuse: both operands already lowered or producer-built untouched
X DrillDownDrawer report-drill mergedFilter plugin-dashboard none (literal $and) no converter call untouched
C lowerLogicalGroup $or early return (execution note 4) core — refusal depends on member order every member is read; the TRUE disjunct absorbs after the loop

Design choice (brief, zone 2 item 2)

  • Where a site lowers ONE source it owns (R1, R3, R4): toFilterNodeSafely per source, then mergeFilterNodes over the lowered nodes — the idiom RelatedList and LineItemsPanel already use. Re-lowering a lowered node is the identity (toFilterNode returns an AST array or undefined), so a well-formed filter reaches the wire byte-for-byte as before.
  • Where the lowering sits inside a core utility (R2, R5–R9): the utilities keep throwing, and their async callers are unchanged. Since patch round 1, composeDrillFilter throws one refusal type: the spec parseFilterAST's INVALID_FILTER is re-raised as a FilterOperatorError with the same envelope, and any other error passes through untouched. The render/click caller catches ONLY FilterOperatorError — the rule toFilterNodeSafely states; anything else is rethrown.
  • No new export. @object-ui/core gains nothing; @object-ui/react gains one optional field, filterRefusal, on the two binding-hook result types (see Acceptance notes).
  • The notice per site (zone 2 item 4), matched to the nearest sibling:
    • gate (R1/R2) — the malformed-filter notice the wrapped blocks draw (view.malformedFilter, amber role="alert", the refused operator in a separately addressable headline, the refusal's message beneath), with the gate's testId stem. Since patch round 1 its provider-less default is a literal inline defaultValue, held byte-identical to the pack by check:i18n-keys; the gate imports no en pack;
    • record picker (R2) — its own configuration-error panel, reached through missing;
    • pickers (R3/R4) — the error slot a refusal from dataSource.find already lands in; Retry hidden for a refusal (nothing was read);
    • drills (R5–R8) — no rendered notice exists in any drill surface, so none was invented: the refusal is logged (console.warn), the channel ReportView's drill failure and ValueDataSource's refused filter already use. Since patch round 1 this covers both refusal steps: the converter's, and the spec parseFilterAST's INVALID_FILTER, which composeDrillFilter re-raises as a FilterOperatorError (any other error passes through untouched), so the four sites catch it with the catch they already had;
    • export (R9) — the export menu's existing alert.

Evidence

Seat note (domain:ui seat #1), patch round 1. The head is 6f4bee846. It answers contract review 5858758764 (the pack-object importer gate and the drill seam) and follows dev report 5859112009. Red leg 2 checked out drill-down.ts at 664305cb3 and kept the new pins: Tests 9 failed | 13 passed (22), the 9 reds being exactly the spec-refusal assertions. The readings below are round 1's, at 664305cb3.

Nine new pin files: eight at round 1, one malformed filter per site plus a well-formed CONTROL per file; patch round 1 added the core drill-seam pin drill-filter-spec-refusal-10789.test.ts, and a spec-refusal case in each drill-site pin. Assertions are the notice's content (the refused operator) or the refusal envelope (code INVALID_FILTER, httpStatus 400), plus the absence of any unfiltered read — never a bare toThrow.

Red leg (one-shot, not left in the tree). The implementation was committed first (9f1d0712e); the ten source files checked out at the base a14fb23b3 (on-disk marker count objectui#10789 = 0 in each), new pins run, then restored with git checkout HEAD -- and proven by git diff HEAD empty plus a hash match of all ten blobs against HEAD:

Test Files  8 failed (8)
     Tests  18 failed | 12 passed (30)
RESTORED: git diff HEAD empty, 10 blobs match HEAD

The 18 reds are the 18 site assertions (each fails with the FilterOperatorError the site threw, or with the notice absent); the 12 greens are the CONTROLs plus the member order main already refused — they pass both ways by design.

Local verification — at 664305cb3 (this branch after merging origin/main ff94a12d2)

Heavy runs went through the shared verify lock; each line quotes the lock's own verdict.

  • Pins + core + react — pnpm exec vitest run --maxWorkers=2 packages/core/ packages/react/ plus the six other pin files → Test Files 285 passed (285), Tests 4931 passed (4931); VERDICT batch-last-exit 0 (the batch's last part is the vitest run).
  • fields + plugin-charts + plugin-report (whole packages) → Test Files 321 passed | 1 skipped (322), Tests 4722 passed | 7 skipped (4729); VERDICT command-exit 0.
  • plugin-list + plugin-dashboard (whole packages) + gate-consumer sweep (every *elementDataSource* suite of calendar, detail, form ×4, gantt, grid, kanban, map, timeline; the console element-data-source-input-injection probe; three element:record_picker suites) → Test Files 258 passed (258), Tests 2565 passed (2565); VERDICT command-exit 0. packages/components changed no source, so only its record-picker suites ran.
  • type-check (the hyphenated script, echoed for all eight packages, every one a cache miss): turbo run type-check over core, react, fields, components, plugin-charts, plugin-dashboard, plugin-report, plugin-list with the ^build closure → Tasks: 22 successful, 22 total; VERDICT command-exit 0.
  • Changeset gates — check-changeset-presence ✅ (one changeset, .changeset/10789-render-time-filter-refusals.md, patch for the seven packages whose shipped source changed); check-changeset-no-major ✅.
  • Light gates, all exit 0 — check:phantom-deps, check:self-import, check:element-data-source-declaration, check:esm-specifiers (specifier leg), check:i18n-keys, check:new-line-citations (0 new), check:test-path-roots, check-control-bytes, check-vi-mock-specifiers, check-vi-mock-inherit, check-vi-mock-override-shape.
  • Lint, a declared narrowing — eslint --format json over the 18 files this branch changes (git diff --name-only HEAD^2 HEAD): 18 files, 0 errors; per source file and per rule, the warning count equals the base version of the same file (every delta zero; the new pin files carry warn-level no-explicit-any only). Population: eslint.config.js applies to **/*.{ts,tsx}. Invariance: no type-aware linting is configured (eslint.config.js has no parserOptions / projectService), so this diff cannot move a verdict on an untouched file. Repo-wide pnpm lint is CI's.
  • NOT MEASURED locally, CI's: repo-wide pnpm test and pnpm lint, the check:esm-specifiers load leg, the remaining check:* families.

Acceptance notes

  • Public face. UseElementDataSourceResult and UseElementDataSourceSchemaResult gain an optional filterRefusal, and the missing status now also covers "a filter the binding combines is refused". Chosen over a new status value because every existing consumer already withholds its query on missing — a consumer that has not learned the new field still cannot widen. The PM's Clause-②: no rests on "no public face grows"; this additive optional field is flagged for that reading.
  • lowerLogicalGroup (execution note 4) narrows, never widens: { $or: [{}, { a: {} }] } is now refused like { $or: [{ a: {} }, {}] } already was; a TRUE disjunct beside members that lower still absorbs (both orders pinned).
  • Drill sites draw no on-screen notice. No drill surface has a rendered malformed-filter state to match, so the refusal is logged and the drill does not open. An on-screen drill notice would be new UI — left to a decision, not invented here.
  • element:record_picker shows its configuration-error panel (headline "data source could not be resolved") carrying the refusal's message, rather than the malformed-filter sentence: that panel is its own sibling state, and the gate's notice component is not exported.
  • Observed, not in scope: LookupField's recents effect catches every error into an empty rail (the main popover query still surfaces a refusal); DrillDownDrawer's report drill composes with a literal $and rather than the single sink. None measured through a public door; noted, not filed.
  • RecordPickerDialog.tsx is shared with this seat's in-flight objectui#10594 (withoutDeniedFields); this diff touches only the mergedFilter region and the error block's Retry.

Generated by Claude Code

…ed-filter notice where render/click-time merges refused (objectui#10789)

Render-time and click-time filter merges called the throwing converter form,
so a malformed authored filter threw a FilterOperatorError out of render (or
out of a click handler) instead of surfacing the refusal the way the sibling
sites do.

- ElementDataSourceGate / useElementDataSource: both merges (component filter
  with the composed binding; view filter with the binding's own) now report the
  refusal as `missing` + `filterRefusal`; the gate draws the malformed-filter
  notice the wrapped blocks draw (`view.malformedFilter`).
- PeoplePicker recents and RecordPickerDialog mergedFilter lower through
  toFilterNodeSafely; the refusal lands in the picker's existing error state
  and no query runs without the filter.
- ObjectChart, ObjectPivotTable, DatasetWidget and DatasetReportRenderer drills
  log the refusal and open/emit no drill (never a filter-less one).
- ListView server export builds its filter inside its try, so the refusal is
  named in the export menu.
- lowerLogicalGroup reads every $or member before the TRUE disjunct absorbs it,
  so a refused member is refused in either member order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
…hed packages

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
…ctui#10789 drill pins

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
Brings in objectui#10594 (the brief's merge condition), objectui#10788 (the
converter's empty-operator-map tail) and objectui#10778 (the pivot's
invalidation reader). One conflict, the pivot's import lines, resolved as the
union: `useMemo` for this branch's drill composition, `useDataInvalidation` for
objectui#10778.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

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

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

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

.changeset/7070-no-invented-gantt-date-fields.md

  • names plugin-list/src/ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    • app-shell/src/views/ObjectView.tsx — the console object page. The inline branch becomes ganttViewOptions, the sibling of calendarViewOptions and timelineViewOptions: the declared block spread whole, title floored at 'name', no date field invented. - plugin-list/src/ListView.tsx — the render branch AND the capability gate. - plugin-view/src/ObjectView.tsx — generateViewSchema, the authored object-view element route, which bypasses ListView entirely.

.changeset/7125-dashboard-empty-state-keys-retired.md

  • names DatasetWidget.tsx → packages/plugin-dashboard/src/DatasetWidget.tsx — edited by this change

    Not touched: table.noRows ('No rows to display') and engine.form.noRows (packages/app-shell/src/views/metadata-admin/i18n.ts, read at widgets.tsx) — two different, same-named keys in different namespaces. Nor the comments in WidgetEmptyState.tsx, DatasetWidget.tsx, ObjectDataTable.tsx and PivotTable.tsx that record WHY three widgets with three strings became one shared empty state; the packs' own comment keeps that rationale and now names the retirement instead of a row that is gone.

.changeset/7499-gantt-non-axis-floors-omitted.md

  • names plugin-list/src/ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    • plugin-list/src/ListView.tsx — the object-gantt render branch. - plugin-view/src/ObjectView.tsx — generateViewSchema, the authored object-view element route, which bypasses ListView entirely.

.changeset/7773-kanban-adapter-groupfield-write.md

  • names ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    Who is NOT affected — the boundary is node-local. Every VIEW-LEVEL groupField read is untouched and still live: it is a legacy alias of the spec's groupByField on the kanban view config, mapped by normalize-list-view.ts, and both adapters still resolve lanes through it (ObjectView.tsx's kanbanCfg.groupField ||, ListView.tsx's groupByField || groupField). Authoring options.kanban.groupField on a list-view or object-view keeps working exactly as documented in packages/plugin-list/README.md. groupField is dead only on the generated object-kanban NODE.

.changeset/7780-object-kanban-record-source.md

.changeset/8650-chart-foreign-dialect-retired.md

  • names ObjectChart.tsx → packages/plugin-charts/src/ObjectChart.tsx — edited by this change

    Also deletes six (schema as any) casts that the published declarations had already made unnecessary — colors, categoryColors and categoryOrder on ChartRenderer, and colors, compareTo and series on ObjectChart (the objectui#8327 bucket-(b) class: declared, then read through a needless cast). No behaviour changes with them; ObjectChart.tsx now has no (schema as any) read left at all.

.changeset/8885-object-chart-drilldown-title-compareto.md

  • names ObjectChart.tsx → packages/plugin-charts/src/ObjectChart.tsx — edited by this change

    ObjectChart.tsx reads all three off schema, and until now neither published copy declared any of them: not the TS interface (packages/types/src/objectql.ts) and not the zod mirror (packages/types/src/zod/objectql.zod.ts). They rode BaseSchema's index signature / .passthrough() and arrived unvalidated. drillDown was the sharpest case — this component's registry inputs advertise it to the designer palette, and @objectstack/spec publishes ChartDrillDownSchema for exactly this carrier, so an author was offered a key that neither published shape mentioned.

.changeset/8990-object-kanban-groupby-optional.md

  • names packages/plugin-list/src/ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    • packages/plugin-list/src/ListView.tsx generates the node as groupBy: laneField. objectDef loads asynchronously, so laneField is undefined on every load until it lands, and stays undefined whenever the object offers no stageField hint and none of status / stage / state / phase. The renderer serves that node; both published faces refused it. - content/docs/utilities/data-objectstack.mdx documents an object-kanban node that is exactly { type, dataSource }, with no groupBy. ⚠️ This one is weaker and is cited for what it is: that fragment is still refused after this change, at RECORD_SOURCE_REQUIRED, because dataSource is not a rung of the record-source ladder. It shows a lane-less board is a documented authoring; it is not a document this change admits.

.changeset/humanize-label-single-home-5444.md

  • names ObjectChart.tsx → packages/plugin-charts/src/ObjectChart.tsx — edited by this change

    humanizeLabel turns a stored value into a display string when nothing else resolves it — an option with no declared label, an object name, a chart axis member. It existed twice, byte for byte: once in @object-ui/fields (read by plugin-grid, plugin-gantt, plugin-detail and by that package's own renderers) and once as a deliberate local copy in plugin-charts' ObjectChart.tsx, whose comment said it was there "to avoid a dependency on @object-ui/fields".

.changeset/list-user-actions-collision-5398.md

  • names ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    The harvest now reads the object block only. Both userActions read sites in ListView.tsx carry a comment naming the collision, and __tests__/ListView.userActionsCollision.test.tsx pins each clause of it: the two shapes, a producer that manufactures the view one, the harvest's blindness to it, and the projection that must keep the object's operand with a toolbar block — or an empty block — present on the view.

.changeset/listview-comment-pair-4559.md

  • names ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    Two comment corrections in ListView.tsx (objectui#4559, objectui#4966). No runtime behaviour changes and the emitted bundle is byte-identical; the published .d.ts does change, which is why this is a patch rather than an empty frontmatter.

.changeset/objectchart-schema-anchor-7946.md

  • names ObjectChart.tsx → packages/plugin-charts/src/ObjectChart.tsx — edited by this change

    • ObjectChartProps.schema is ObjectChartSchema; the published .d.ts no longer says props: any. ObjectChartProps is exported. - The four keys are declared on BOTH copies, with value types taken from their READ sites (ChartRendererProps for xAxisKey / series, ObjectChart.tsx for filter) rather than copied from any producer's literal — except where @objectstack/spec already owns the shape, which is aggregate: that one is declared BY REFERENCE as ChartAggregate / ChartAggregateSchema, so the authoring door here and at the react-page publish gate are one shape and cannot drift into two dialects. - colors converges: the zod mirror has declared it since objectui#3913 and the TS interface did not, a drift no ratchet could see because a mirror-only key is in neither of the parity guard's two difference ledgers.

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

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

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

Compared the checked-out tree with 4d0af04cb (merge-base with origin/main): 20 file(s) changed outside .changeset/, read against 1622 pending declaration(s) that publish a body (2211 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3079.1 KB 3104.5 KB
Main entry chunk (gzip) 148.3 KB 350 KB
Entry file index-Cx_g2576.js —
Status PASS —

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


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.57KB 6.15KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.17KB 10.58KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 556.80KB 133.32KB
core (index.js) 9.92KB 3.93KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 225.01KB 62.50KB
fields (index.js) 261.26KB 66.37KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.67KB 14.70KB
plugin-charts (index.js) 83.99KB 22.86KB
plugin-chatbot (index.js) 197.67KB 46.90KB
plugin-dashboard (index.js) 135.30KB 35.96KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 233.12KB 61.68KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 158.35KB 40.47KB
plugin-gantt (index.js) 169.83KB 41.99KB
plugin-grid (index.js) 218.66KB 59.90KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 114.78KB 28.44KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.90KB 12.09KB
plugin-timeline (index.js) 30.91KB 9.05KB
plugin-tree (index.js) 10.64KB 3.75KB
plugin-view (index.js) 87.89KB 22.03KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 119.16KB 39.05KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 7.50KB 3.05KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.16KB 2.71KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

…ter in check-i18n-dead-keys (objectui#10789)

The malformed-filter panel objectui#10789 added to ElementDataSourceGate reads
the `en` pack at render for a `t()` defaultValue, which makes the file a
pack-object importer the header's enumeration must read by hand. Its reading:
a class-4 read (two-segment key) that no leg sees and that the co-located `t()`
literal keeps visible BY COINCIDENCE. The key itself is not at risk: the sibling
malformed-filter states spell it as a literal `t()` argument too. The bullet is
written first; the path joins ANALYSED_PACK_OBJECT_IMPORTERS after it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 664305cb327db56f1ed799d103dfc6bc993a0a00

Reviewed against the card objectui#10789 (body, triage, claim, dev report, claim amendment), the PR body and diff (19 files, merge-base ff94a12d2), the check-runs on the head, and the objectui tree at the head and at origin/main. Read-only on the shared checkouts; probes ran in a scratch worktree of the head with an offline install, removed afterwards. MEASURED means a probe or a CI reading; READ means source reading.

① Derived judgments

Acceptance set — per site. Every touched site was judged on three questions: can a refused filter now reach an UNFILTERED query, drill, export or picker list; is anything beyond FilterOperatorError newly caught; does the notice reach the user or host.

  • R1 useElementDataSourceSchema (packages/react/src/element-data-source/ElementDataSourceGate.tsx, the mapped memo): each source lowered with toFilterNodeSafely, the refusal returned as a value, the result memo forced to status: 'missing' with filterRefusal. The only consumer is ElementDataSourceGate, which returns the malformed-filter panel before its missing branch and never calls children — the block cannot mount, so it cannot query. READ right. MEASURED: the pin's two merge cases fail with the base gate and hook sources (the $regex refusal thrown out of render or out of renderHook) and pass at the head; the CONTROL passes both ways.
  • R2 useElementDataSource (packages/react/src/hooks/useElementDataSource.ts): the composeElementDataSource call is wrapped, ONLY FilterOperatorError is caught, the answer is missing plus filterRefusal. The ready branch composes without a view, and composeElementDataSource merges only when both a view filter and a binding filter exist, so nothing throws there. Consumers: the gate (above) and element:record_picker (packages/components/src/renderers/basic/record-picker.tsx), whose object is undefined while missing, parking the fetch effect, and whose render returns the configuration-error panel carrying dataBinding.error. No unfiltered read. READ right.
  • R3 PeoplePicker recentFilter: baseFilter lowered safely; the recents query is enabled only when ok; the refusal lands in the candidate area's error slot with Retry hidden. The main query still carries the raw baseFilter to the adapter, which refuses it on its own leg — not "no filter". READ right. mergeFilterNodes re-lowers a lowered node as the identity (toFilterNode passes an AST array through), so a well-formed filter reaches the wire unchanged.
  • R4 RecordPickerDialog mergedFilter (rule-array arm): both sides lowered safely, the query disabled on refusal, error set to the refusal message, Retry hidden. The record-form-only arm is unchanged and its refusal stays the adapter's, the sibling channel. READ right.
  • R5 ObjectChart drillComposition: only FilterOperatorError caught; navigateOnly and the drawer both gated on !drillRefused, so openRecordList is never handed a filter-less list; the warning fires from an effect keyed on the message. READ right for the converter's refusals; see the census finding below for what still escapes.
  • R6 ObjectPivotTable: composition moved above the early returns, keyed on schema.drillDown (the same value drillDown aliases at line 368), renderDrillDrawer returns null unless ok, and the navigate target lives inside DrillDownDrawer, which is then never rendered. READ right.
  • R7 DatasetWidget openDrill and R8 DatasetReportRenderer row and matrix drills: catch FilterOperatorError only, rethrow everything else, no setDrill and no onDrill — never objectFilter: undefined, which the host reads as "older server" and rebuilds from the clicked group alone. READ right.
  • R9 ListView server export: buildEffectiveFilter moved inside the export try. That catch catches every error, but it is the export's pre-existing channel, logs with console.error and shows the message in the export menu (exportError, line 4746) — surfaced, not swallowed. READ right.
  • Nothing newly catches more than FilterOperatorError except the pre-existing export catch above.

lowerLogicalGroup (packages/core/src/utils/filter-converter.ts). A TRUE $or disjunct now sets a flag and the loop keeps reading; absorption is decided after the loop. Pure narrowing: an $or whose every member lowers or is TRUE answers exactly as before (both CONTROL orders undefined); only an $or carrying a refused member AFTER a TRUE one changes, from TRUE (every row) to refused — the same member alone was already refused, so nothing valid is newly refused. MEASURED: with the base converter, "TRUE disjunct first", "TRUE-identity group first" and "non-object member after TRUE" fail (returned undefined, nothing thrown); "TRUE disjunct last" and both CONTROLs pass both ways, which matches the changeset's "already was".

Public face. CONFIRMED: UseElementDataSourceResult and UseElementDataSourceSchemaResult (generic over S) each gain optional filterRefusal; packages/react/src/index.ts re-exports ./hooks/index.js (which re-exports ./useElementDataSource.js) and ./element-data-source/ElementDataSourceGate.js with export *, and packages/react/package.json has the single . entry. The widened missing ("or a filter it combines is refused") cannot mislead a consumer into an unsafe read: composed is withheld on missing exactly as before, both in-repo consumers withhold their query on it, and a consumer that already ignored missing was already running without the view's filter before this PR. The one cosmetic cost: the record picker's panel headline still says the data source "could not be resolved" while its body names the refused operator — disclosed by the dev.

Census. Every non-test caller of mergeFilterNodes, convertFiltersToAST, toFilterNode, composeDrillFilter, buildDatasetDrillFilter, buildEffectiveFilter and composeElementDataSource at the head was classed: R1–R9 as above; RelatedList, LineItemsPanel, ObjectGrid schema.filter and ValueDataSource safe; LookupField recents inside an async effect try, ObjectGrid defaultFilters inside loadSchemaAndData's try, ListView fetch and the self-query leg (own catch), ObjectView finalFilter inside its load try (line 1138), related-count-store inside an async promise, data-objectstack adapter legs, ViewDataProvider async — none render- or click-uncaught. DrillDownDrawer composes with a literal $and and calls no converter. The PR's table is accurate.

Census finding — the drill seam still throws for an authored filter (MEASURED). composeDrillFilter (packages/core/src/utils/drill-down.ts:254) ends in the spec's parseFilterAST. An ARRAY-dialect widget filter holds no rule objects, so toFilterNode passes it through untouched and the converter never judges it; the spec's comparand-shape gate then refuses it with a plain Error carrying code: 'INVALID_FILTER', httpStatus 400 — not a FilterOperatorError. Probe at the head: composeDrillFilter([['stage', 'in', 'won']], { stage: 'won' }) throws that error; an inline-data ObjectChart (data rows, filter: [['stage', 'in', 'won']], drillDown.enabled) is thrown into the error boundary on a segment click for both drawer and navigate — chart unmounted, no warning, nothing drawn. The same seam serves R6–R8, whose catches rethrow anything that is not a FilterOperatorError. ObjectChartSchema.filter admits the array dialect by its own docblock, so this is reachable through a public door. Judgment: the card defines its class by the CONVERTER's refusals, so the dev's "separate class, noted not filed" is a defensible reading of the fix's scope — but the symptom is the card's symptom at four of the card's sites, and the changeset's first sentence claims it closed (see ②). Either widen the four drill catches to the spec envelope as well (an error whose code is INVALID_FILTER, same no-drill behaviour, same log line) or narrow the sentence and file the residual.

Pins. All eight files assert notice content (the refused operator in the headline or log line) or the envelope (code, httpStatus, operator or field) plus the absence of an unfiltered read (find not called, no drawer props, openRecordList not called, onDrill not called, exportDownload not called), each with a well-formed CONTROL. By reading, each site assertion goes red on base: the render sites throw out of render or renderHook, the handler sites log no warning and the alert never appears. MEASURED for two files (core, gate): base sources 7 failed and 4 passed, head 11 passed. The dashboard pin carries one bare not.toThrow(), but beside content assertions, so it is not the discriminating half. CI's eight shards ran every pin on the head green; only the gate below is red.

② Semver level

  • Level. One changeset, patch for core, react, fields, plugin-charts, plugin-dashboard, plugin-report, plugin-list. packages/components changed only a test file, so its omission is right. The repo's written policy (AGENTS.md §版本号策略, scripts/check-changeset-no-major.mjs, scripts/check-changeset-presence.mjs) forbids major, sends objectui's own breaking changes to minor, and says nothing about additive optional members. Pure semver would call an additive public member on @object-ui/react a minor; this repo's own practice is mixed and leans patch: packages/react/CHANGELOG.md ships "DiscussionContext gains an optional mentionSuggestions" under Patch Changes and "InlineEditProvider takes a new optional approvalPending prop" under Minor Changes; pending .changeset/10500-search-labels-read-gate.md adds an optional useRecordSearch option at react patch, and .changeset/10354-grid-fixed-currency-cell-footer.md adds an optional return member at patch. The fixed group makes the published version identical either way. Judged CONSISTENT with the repo's policy and practice; not a defect. Changeset Bump Policy and Changeset Declaration are green on the head.
  • Prose, sentence by sentence. The per-site bullets are TRUE: the gate's notice uses view.malformedFilter (packages/i18n/src/locales/en.ts:1080, the key RelatedList, LineItemsPanel and ObjectGrid draw), error carries the message, the record picker shows its panel with the refusal, the pickers run no query without the filter, the drills open or emit nothing and log console.warn naming the operator, the export names the refusal in its menu, and the $or sentence is measured in both orders. FALSE: the opening sentence, "A malformed authored filter no longer throws out of render or out of a click handler at the sites that merge filters there" — measured above, an authored array-dialect widget filter still throws out of render at R5 (and by the same seam at R6–R8). The drill bullet is careful to say "a widget or report filter this layer refuses"; the opening sentence is not, and it ships verbatim to the CHANGELOG. Fix: qualify it ("a filter the converter refuses") or close the seam and keep it.

③ Boundary flags

  • Surface grew to plugin-charts, plugin-dashboard, plugin-report, plugin-list. Under the claim's "plus any further render/click-uncaught site the census finds" clause; each of the four is a genuine member of the class (verified by reading), and R9 was a real find. Accepted.
  • composeDrillFilter reclassified from "a click handler" to a core utility with two render-after-click callers and three click-handler callers, DrillDownDrawer classed out (literal $and). Verified; the reclassification is correct. The utility keeps throwing and its async caller is untouched.
  • Drill sites log via console.warn and open nothing. ReportView.tsx:161 warns on drill failure and ValueDataSource.ts:1175 warns per refusal, so the channel claim is true, and the seat's decision A is inside the card's line ("the channel its nearest sibling already uses"). Flagged, not a defect: the user sees no on-screen change on click, only the host console does — a maintainer may still want B as a new card.
  • filterRefusal public-face question. Resolved by the claim amendment (Clause-②: yes); confirmed reachable through the package entry; the level judged above.
  • PeoplePicker threw on every render. True on base: the useMemo merged baseFilter whenever it was set, regardless of recentIds.length.
  • Merge of origin/main. The merge commit's parents are f2bc82a45 and ff94a12d2; the diff from ff94a12d2 to the head is exactly the PR's 19 files, and the diff from f2bc82a45 to the head is exactly main's 78 files — for ObjectPivotTable.tsx and RecordPickerDialog.tsx the two diffs match line for line. withoutDeniedFields from objectui#10594 is present at line 451 on both sides. Nothing lost on either side.
  • PR body. Site table, census rows, design choice, the "NOT MEASURED locally: repo-wide pnpm test" declaration and the acceptance notes are true as read and as measured. The dev's claimed red-leg and green numbers are consistent with the two files re-measured here.
  • CI on the head — RED, caused by the PR (MEASURED). Test (shard 2/8) and the Test rollup fail; every other check is green or skipped (Type Check, Lint, Changeset Bump Policy, Changeset Declaration, dist pins, Build and E2E all success). The failing test is scripts/__tests__/check-i18n-dead-keys.test.ts:1032, "EVERY non-test importer in the live population is analysed in the header": received ["packages/react/src/element-data-source/ElementDataSourceGate.tsx"], expected []. ElementDataSourceGate.tsx:114 now imports en from @object-ui/i18n for the panel's defaultValue, which makes it a pack-object importer; ANALYSED_PACK_OBJECT_IMPORTERS (scripts/check-i18n-dead-keys.mjs:740) lists five files and not this one. Main is green on shard 2/8 at both ff94a12d2 and fda49e557, and on main the react package's only such importer is utils/nonGridRowCeiling.tsx. Fix: read the file, write its bullet in the script's header section (what the en read means for the property-chain leg), add the path to ANALYSED_PACK_OBJECT_IMPORTERS — or drop the en import and render view.malformedFilter the way the sibling blocks do, without a pack-object defaultValue.

Defects, in fix order.

  1. packages/react/src/element-data-source/ElementDataSourceGate.tsx:114 — the en import fails the pack-object importer gate on shard 2/8; classify it in scripts/check-i18n-dead-keys.mjs (bullet plus roster entry) or remove the import.
  2. .changeset/10789-render-time-filter-refusals.md opening sentence — false for an authored array-dialect widget filter at the four drill sites (packages/core/src/utils/drill-down.ts:254 raises the spec's INVALID_FILTER, which ObjectChart.tsx, ObjectPivotTable.tsx, DatasetWidget.tsx and DatasetReportRenderer.tsx rethrow); qualify the sentence, or catch the spec envelope at the four sites and pin it.

Implemented-by: claude/issue-10789-render-time-filter-mergers
Reviewed-by: session_01DuWo5bdP9SdVebamn99GGk

VERDICT: FAIL


Generated by Claude Code

…ct importer in check-i18n-dead-keys (objectui#10789)"

This reverts commit cab1c7d; it was never pushed. The patch round takes the
reviewer's option (b) instead: the gate's malformed-filter panel stops reading
the `en` pack, so the file leaves the pack-object importer population and
needs no bullet. An entry for a file outside the population would fail the
gate's other direction, "EVERY analysed importer still exists".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
…p the gate's pack-object default (objectui#10789)

Contract review 5858758764 on the first head found two defects; this round
fixes both.

1. composeDrillFilter ends in the spec's parseFilterAST, which refuses comparand
   shapes the converter passes through, e.g. an array-dialect widget filter
   [['stage', 'in', 'won']] or { stage: { $in: 'won' } }. It threw a plain Error
   with code INVALID_FILTER / status 400. The four drill sites catch
   FilterOperatorError only, so the chart and pivot were still thrown into the
   error boundary on a click. The seam now re-raises that refusal as a
   FilterOperatorError with its message, so all four sites treat it exactly like
   the converter's refusals: no drill opened or emitted, and a console.warn
   naming the operator. Only INVALID_FILTER is re-raised; any other error
   passes through.
2. ElementDataSourceGate's malformed-filter panel read the `en` pack object for
   its defaultValue, which made the file an unclassified pack-object importer
   (check-i18n-dead-keys). It now carries the sentence as a literal inline
   default, as the sibling blocks carry theirs, held byte-identical to the pack
   by check:i18n-keys. The gate pin now proves the text reaches the screen with
   and without an I18nProvider.

The changeset's opening sentence is rewritten to hold at every site.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3079.1 KB 3104.5 KB
Main entry chunk (gzip) 148.4 KB 350 KB
Entry file index-Dy6OliLH.js —
Status PASS —

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


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.57KB 6.15KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.17KB 10.58KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 556.80KB 133.32KB
core (index.js) 9.92KB 3.93KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 225.01KB 62.50KB
fields (index.js) 261.26KB 66.37KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.67KB 14.70KB
plugin-charts (index.js) 83.99KB 22.86KB
plugin-chatbot (index.js) 197.67KB 46.90KB
plugin-dashboard (index.js) 135.30KB 35.96KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 233.12KB 61.68KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 158.35KB 40.47KB
plugin-gantt (index.js) 169.83KB 41.99KB
plugin-grid (index.js) 218.66KB 59.90KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 114.78KB 28.44KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.90KB 12.09KB
plugin-timeline (index.js) 30.91KB 9.05KB
plugin-tree (index.js) 10.64KB 3.75KB
plugin-view (index.js) 87.89KB 22.03KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 119.16KB 39.05KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 7.50KB 3.05KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.16KB 2.71KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

…er isolate:false (objectui#10789)

The `unit` project shares one module graph per worker, so a module-scope
vi.mock of the spec only applied when no earlier file had loaded the seam. The
pin now swaps the spec's lowering in a fresh module graph (vi.resetModules +
vi.doMock + dynamic import) and undoes it in `finally`, so nothing leaks into
later files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3079.1 KB 3104.5 KB
Main entry chunk (gzip) 148.4 KB 350 KB
Entry file index-D7PYW2SR.js —
Status PASS —

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


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.57KB 6.15KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.17KB 10.58KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 556.80KB 133.32KB
core (index.js) 9.92KB 3.93KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 225.01KB 62.50KB
fields (index.js) 261.26KB 66.37KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.67KB 14.70KB
plugin-charts (index.js) 83.99KB 22.86KB
plugin-chatbot (index.js) 197.67KB 46.90KB
plugin-dashboard (index.js) 135.30KB 35.96KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 233.12KB 61.68KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 158.35KB 40.47KB
plugin-gantt (index.js) 169.83KB 41.99KB
plugin-grid (index.js) 218.66KB 59.90KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 114.78KB 28.44KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.90KB 12.09KB
plugin-timeline (index.js) 30.91KB 9.05KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 87.89KB 22.03KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 119.16KB 39.05KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 7.50KB 3.05KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.16KB 2.71KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 6f4bee84658bf84753acd58b78476e4ae994b442

Delta review after the FAIL record (comment 5858758764, head 664305cb3). Four commits since: cab1c7d1f (classify the gate as a pack-object importer), 3dbcabab9 (its exact revert), c5cee6046 (close the drill seam's second refusal shape; drop the gate's pack-object default), 6f4bee846 (the drill-seam pin made order-independent). The delta is 8 files; its only non-test sources are packages/core/src/utils/drill-down.ts and packages/react/src/element-data-source/ElementDataSourceGate.tsx. The nine sites' sources, the binding hook, the pickers, ListView and the converter are byte-identical to the head already judged, so every round-1 PASS item stands. CI on this head: 40 success, 3 skipped; Test (shard 2/8) and the Test rollup green. Read-only on the shared checkouts; probes ran in a scratch worktree of this head with an offline install, removed afterwards. MEASURED means a probe or a CI reading; READ means source reading.

① Derived judgments

Defect 1 (the en pack import) — closed honestly. ElementDataSourceGate.tsx:114 now imports only useObjectTranslation; the react package's sole pack-object importer at the head is utils/nonGridRowCeiling.tsx, exactly as on main. The net diff to scripts/ against the merge-base is zero, i.e. 3dbcabab9 reverted cab1c7d1f whole, and check-i18n-dead-keys is green on the head (shard 2/8 success). The panel's defaultValue is now a literal; MEASURED byte-identical to view.malformedFilter in packages/i18n/src/locales/en.ts:1080, and scripts/check-i18n-call-site-keys.mjs (the script behind check:i18n-keys) does compare an inline defaultValue literal against the en value, so the dev's comment that the gate holds it is true. The notice reaches the screen as the sentence, not the key: the pin asserts the headline text ("filter is malformed", "the $regex condition") and asserts the raw key is absent, both without a provider and under I18nProvider; the sibling blocks carry the same sentence through their module-scope safe-translation tables, which this re-exported module cannot host. READ and MEASURED (pin green at the head).

Defect 2 (the spec's INVALID_FILTER at the drill seam) — closed. composeDrillFilter keeps mergeFilterNodes outside the try and wraps only parseFilterAST: an error whose code is INVALID_FILTER is re-raised as FilterOperatorError(message); anything else is rethrown untouched. No new symbol: drill-down.ts's export list is unchanged and core's index.ts re-exports it with export * as before, so @object-ui/core's public face does not grow. The four drill sites are unchanged; their instanceof FilterOperatorError catches now cover both refusers with the same no-drill, no-onDrill, never-filter-less behaviour and the same console.warn line. MEASURED at the head: composeDrillFilter([['stage', 'in', 'won']], …) and composeDrillFilter({ stage: { $in: 'won' } }, …) both throw a FilterOperatorError with code INVALID_FILTER, httpStatus 400 and the spec's sentence naming $in; the object spelling is PASSED by the converter (['stage', 'in', 'won']) and refused only by the spec, so "either dialect" is a fact. My round-1 ObjectChart probe re-run on this head: no error boundary, chart still mounted, no drawer, openRecordList not called, exactly one warning naming $in — for both targets and both dialects. Observation, not a defect: the re-raised error carries no operator or field subject, so filterRefusalSubject answers undefined for it; no drill site draws a headline from it and the logged message names the operator.

Pins for the new shape. New packages/core/src/utils/__tests__/drill-filter-spec-refusal-10789.test.ts: §1 both dialects on the envelope plus the message, §2 a non-refusal error passes through by identity, §3 CONTROL. The chart, pivot, dataset-widget and report pins each gain the spec case beside the converter case, keyed on the operator the warning must name. MEASURED red leg: with the round-1 drill-down.ts swapped into the worktree, 9 tests fail (core §1 two, chart two, pivot two, dataset widget one, report two) and 13 pass (the converter cases, §2, the CONTROLs); restored, and at the head all 34 tests across the seven touched pin files pass.

The isolate: false fix. The unit project (vitest.config.mts: isolate: false, includes packages/**/*.test.ts) holds the core pin. c5cee6046 used a module-scope vi.mock with a mutable override, which under a shared module graph depends on whether an earlier file in the worker already loaded the seam and leaks into later files; 6f4bee846 replaces it with vi.resetModules plus vi.doMock plus a dynamic import inside the test body, undone in finally. §2 remains discriminating for what it guards: a broad catch that re-raised every error as a refusal fails it. It does not go red on the pre-fix base, which had no catch at all, and need not — §1 is the red leg. CI's eight shards are green on this head.

② Semver level

  • Level. Frontmatter unchanged, patch for the same seven packages. Core adds no export; composeDrillFilter's thrown class for one input class narrows from a plain Error to FilterOperatorError with the same envelope, a bug fix on a published function rather than public-face growth. patch holds under the practice measured in round 1 (additive optional members and behaviour fixes shipping at patch in this repo's own CHANGELOG and pending changesets). Changeset Bump Policy and Changeset Declaration green.
  • Prose, sentence by sentence. The opening sentence is rewritten and now TRUE: "A filter refused as malformed (INVALID_FILTER) no longer throws out of render or out of a click handler at the sites that merge filters there. That holds whether this layer's filter converter refused it or, at the drill seam, the spec's own parseFilterAST." — both refusers measured at the drill seam; R1–R4 and R9 call only the converter at render or click time. The gate bullet is reworded and true. The drill bullet is rewritten and true: "refused at the drill seam", the two example spellings (measured), "composeDrillFilter now throws one refusal type: the spec's INVALID_FILTER error is re-raised as a FilterOperatorError with its message and the same INVALID_FILTER / 400 envelope, and any other error passes through unchanged" (§2 measured). The pickers, export and $or bullets are unchanged and were judged true in round 1.

③ Boundary flags

  • Pending changesets (the 12 the changeset-claim-re-read comment names). Each read at the head. The files this PR touches among those they name are ListView.tsx, DatasetWidget.tsx and ObjectChart.tsx. This PR makes no sentence in them false: 7070 and 7499 speak of the gantt render branch and capability gate (untouched); 7125 says the empty-state consolidation comments in DatasetWidget.tsx are left in place (still present); 7773, 7780 and 8990 speak of the kanban node ListView generates (untouched; 7773's line citations are its own base measurement, stated as such); 8650 says "ObjectChart.tsx now has no (schema as any) read left" — the count is 1 at both the merge-base and this head, so whatever that sentence's standing, this PR does not move it; 8885 is types-only and ObjectChart still reads drillDown, title and compareTo; humanize-label-5444 says ObjectChart imports humanizeLabel from core, which it does; 5398's two userActions read sites are untouched; 4559 describes its own comment-only diff; 7946 says "what blocks the narrowing is this component's own drill-down spread, which mis-composes the array arm into index keys" — already superseded on main by objectui#8944's sink before this PR, so stale independently of it; this PR only makes the array arm's refusal handled rather than thrown.
  • PR body — stale, three sentences now false at this head. The body carries no patch-round note. Its design-choice line "the utilities keep throwing (their contract and their async callers are unchanged)" is false for composeDrillFilter, whose contract now re-raises the spec's refusal; "Eight new pin files" is nine; and the "Observed, not in scope" bullet — "composeDrillFilter's final parseFilterAST can raise the spec's own INVALID_FILTER … which is not a FilterOperatorError and is not caught by these sites … None measured through a public door; noted, not filed" — is now re-raised, caught, measured and pinned. Fix: patch the body (a seat-side edit; the dev writes a body once). This is not release prose and not the diff, so it is not a FAIL ground; it is named so it is corrected before merge. The patch-round os-dev-report is not on the card yet.
  • Commit narrative as the PR states it, verified: the classification commit and its revert net to zero; the closure commit carries exactly the two closures; the last commit touches only the core pin.
  • Round-1 flags (surface growth, composeDrillFilter reclassification, console.warn channel, filterRefusal public face, PeoplePicker every-render throw, the origin/main merge) are unchanged by the delta and stand as judged.

Defects. None in the diff, the changeset or CI. One correction owed outside the diff: the PR body's three stale sentences named above.

Implemented-by: claude/issue-10789-render-time-filter-mergers
Reviewed-by: session_01DuWo5bdP9SdVebamn99GGk

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 27, 2026 19:35
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit 256b4c9 Sep 27, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-10789-render-time-filter-mergers branch September 27, 2026 19:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment