Skip to content

fix(core): execute the declared $ operators in ValueDataSource, refuse the rest - #8512

Merged
os-justin merged 2 commits into
mainfrom
claude/issue-8447-matchesfilter-unknown-operators
Sep 8, 2026
Merged

os-justin merged 2 commits into
mainfrom
claude/issue-8447-matchesfilter-unknown-operators

Conversation

@os-justin

@os-justin os-justin commented Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #8447

The defect, in its strongest form

find() picks a matcher on nothing but the shape of $filter — two arms of one if:

$filter shape matcher what it does with input it cannot execute
array matchesASTFilter refuses — excludes the row and console.warns once per distinct refusal
object matchesFilter default: break — no constraint, so every row passed, silently

Same file, opposite defaults, and nothing tells an author which dialect their filter took. $eq is the sharpest case: { age: { $eq: 26 } } selected every row while the plain { age: 26 } on the sibling equality arm was correct all along.

The census that decided refuse-vs-implement

Re-derived on origin/main with 'packages/*/src/*' pathspecs and $gt as a lit control (the 'packages/*/src' form is a whole-path glob and returns a silent zero for every operator). Producers of an object-shaped $-dialect criteria in this repo:

spelling emitted by verdict
$eq datasetFilterCondition.ts, ObjectFieldInspector.tsx implement
$nin FilterConditionField.tsx (x2), RecordPickerDialog.tsx, datasetFilterCondition.ts, ObjectFieldInspector.tsx implement
$notContains FilterConditionField.tsx implement
$startsWith / $endsWith FilterConditionField.tsx implement
$null FilterConditionField.tsx (both directions) implement
$exists FilterConditionField.tsx (exists / notExists, kept reachable on purpose by objectui#4736), datasetFilterCondition.ts (isEmpty / isNotEmpty) implement — ruling flipped in contract review, see below
$between no $-dialect producer; accepted by convertOperatorToAST and executed by the AST twin in this same file implement — see note
$like / $ilike nothing refuse — the spec stages them out of FILTER_OPERATORS on purpose
$regex / $options nothing (filter-converter.ts already throws on $regex) refuse with the spec's retirement prescription

$between is the one place the strict census reading ("a spelling nothing emits, refuse-and-warn") was widened, deliberately and declared: matchesComparisonNode implements between 130 lines above in the same file, so refusing it in the object dialect would re-create this card's own asymmetry one level down.

What changed

matchesFilter now carries one arm per member of the spec's FILTER_OPERATORS — all sixteen, a one-to-one pairing with the AST arms that already existed:

$eq -> =    $ne -> !=    $gt -> >    $gte -> >=    $lt -> <    $lte -> <=
$in -> in   $nin -> nin  $between -> between
$contains -> contains    $icontains -> icontains  $notContains -> not_contains
$startsWith -> starts_with    $endsWith -> ends_with
$null -> is_null / is_not_null      $exists -> is_not_null / is_null

$exists is the exact inverse of $null, and that is the platform's own reading rather than one invented here: convertFiltersToAST lowers $exists: true to is_not_null three lines below where it lowers $null: true to is_null.

The closed vocabulary is worth more than any one arm

Because nothing the spec declares is refused by name, REFUSED_OPERATORS in the companion test is empty, so the parity guard can assert the case table equals FILTER_OPERATORS exactly. A future spec release that adds a $ operator now turns this file red instead of letting the new spelling reach the refusal arm unannounced. An operator parked on a refused list would have been a hole that guard could not see into.

What is still refused — everything outside the declared vocabulary

Through the same refuseFilterNode the AST arm uses — excluded, and logged once per distinct refusal per find() — with a message that names the spelling that does work:

  • $like / $ilike, declared by StringOperatorSchema but deliberately staged out of FILTER_OPERATORS (objectstack#7536); this matcher has no pattern engine, and the spec's own note says naming them before an arm exists turns a loud refusal into a dropped predicate;
  • $regex / $options print RETIRED_FILTER_OPERATORS' prescription verbatim, the way the five driver-side refusal sites do, so six faces say one thing;
  • lowercase aliases ($startswith, $notcontains, $notin) and the off-spec $ncontains are refused rather than aliased — growing alias arms in the renderer is exactly the second de-facto contract AGENTS.md #0.1 forbids;
  • a nested relation constraint ({ profile: { verified: true } }) is refused by the key it could not read; this matcher does not descend into relations.

Both arms of find() now drain one refusal collector. That is what makes the two dialects symmetrical, rather than merely both non-empty.

Combinators — stated as their own case, not swept in

  • $and / $or carry an array, so they fell to the simple-equality branch and were already excluding every row, silently. The rows do not move here; only the silence does.
  • $not carries an object (FilterConditionSchema, not an array), so it entered the operator branch, its inner field names were read as operator names, and each hit default: break. It matched every row and now matches none.

None of the three is implemented: executing a group in this dialect has its own semantics to settle (the empty-group identities, objectstack#5322, and $not's NULL-safe rule, objectstack#5146). The refusal names the AST array $filter as the door that works today, and there is a test proving that door is open.

Pin discipline — both ablations were RUN, and RE-RUN after the flip

Restored by state each time (git diff HEAD empty and git hash-object PATH equal to git rev-parse HEAD:PATH), from a committed implementation, with the mutation proven on disk by a marker count before and after plus a blob-hash inequality.

ablation red green of
the bug — ValueDataSource.ts restored to its pre-card bytes 38 24 62
the caricature — matchesFilter returns false unconditionally (an empty result set, strictly worse than the bug) 54 8 62

The survivor list was re-derived, not carried across the $exists flip. Exactly one case moved, and it is the one the review predicted: `$exists false` selects neither row was RED under the caricature before the flip and is GREEN after it — it discriminated only because it asserted the refusal was logged, and executing the operator removed the log. Nothing else moved; the red count is 54 in both runs.

The eight survivors are named rather than counted: the two {} cases and the AST-group case never reach matchesFilter at all; the FILTER_OPERATORS parity guard reads no rows; and the remaining four are the card's own $nin / $startsWith / $null / $exists examples — the quotable ones, and none of them can tell this fix from an empty result set. They are kept in the test file's section 5 and labelled a scope declaration, not proof. Section 5's last case is the exception that keeps the block from being decorative: it asserts the complement of all four, which is four non-empty answers.

The weight is carried by the non-regression axis: every executed operator asserts a non-empty proper subset of the three rows, so the full set (the bug) and the empty set (the caricature) are both red; the plain-equality arm and a genuinely matching row have their own controls; every operator is additionally asserted to answer the same rows as its AST twin; and $exists / $null are pinned as one predicate read from opposite ends, including a not.toEqual so an arm with the boolean inverted cannot pass.

Verification

Every row below was run on this branch's final commit, 09374d601.

run result
pnpm exec vitest run on the new file ValueDataSource.dollarFilterVocabulary.test.ts 62 passed
pnpm exec vitest run packages/core/ 126 files, 2694 passed
pnpm exec vitest run over data-objectstack + fields + components tests + plugin-list + app-shell metadata-admin inspectors — every package or directory in the repo that mentions $exists 473 files, 6142 passed, 1 skipped
pnpm --filter @object-ui/core run type-check (both programs: tsc --noEmit and tsc -p tsconfig.test.json) exit 0
pnpm --filter @object-ui/core run lint (eslint ., the unit turbo run lint runs) exit 0 — 0 errors, 526 pre-existing warnings
node scripts/check-changeset-presence.mjs 3 source files of 1 released package, 1 changeset declared
check:control-bytes · check:spec-symbols · check:doc-fences · check:vi-mock-specifiers · check:vi-mock-inherit · check:unreferenced-sources all exit 0
check:readme-exports not measured — it needs a full pnpm build first ("type entry ./dist/index.d.ts is not on disk") and reports 532 unjudged self-imports across every package, none of them this diff's. Declared to CI.

On the first commit of this branch the wider consumer set was also green — core + fields + data-objectstack + types + plugin-designer + plugin-map + plugin-list + plugin-gantt + examples/schema-catalog (669 files, 12322 passed) and react + app-shell + core/adapters (731 files, 7393 passed, 1 skipped). The second commit's only behavioural delta is the $exists arm, and no file outside packages/core both constructs a value data source and mentions $exists (measured: the two matches are a CHANGELOG and a comment in filter-entry-translation.test.ts, which constructs none). The whole-repo pnpm test (4 shards) and turbo run lint are CI's runs.

Lint narrowing is a measurement, not an omission: the population is packages/core's own eslint . (the exact unit CI's turbo run lint runs for the only package this diff touches), the count is eslint's own (0 errors / 526 warnings), and type-aware linting is not enabled — eslint.config.js uses tseslint.configs.recommended with no parserOptions.project or projectService — so this diff cannot move the verdict of any file it does not touch.

Contract review — two corrections against the dispatch brief, both accepted

Recorded because the reasoning is load-bearing for the census, not to relitigate.

  1. $not does not fall to the equality arm. The brief said $and / $or / $not all carry an array and therefore all fail closed. FilterConditionSchema declares $not: FilterConditionSchema — an object — so $not reached default: break and matched every row. It was in this card's defect class after all. Fixed here as a refusal, with its own case. Accepted.
  2. $exists was refused on a falsified ground, and the ruling is now flipped. The brief refused $exists because filter-converter.test.ts:31 pins convertOperatorToAST('$exists') to null. That map contains neither $null nor $exists; both are special-cased before it is consulted (:283 and :287), so the pin distinguished nothing between the operator to refuse and the operator to implement. The first commit of this branch shipped the ruling as written and flagged the flip; the second commit implements $exists, per the review.

Out of scope — found here, filed by the PM

Both dedupe channels were down for this seat (search_issues: API rate limit already exceeded; repo-scoped REST: 403), so these were handed over rather than filed blind:

  1. The object dialect cannot express a group. filterGroupToMongo in FilterConditionField.tsx emits { $or: [...] } / { $and: [...] }, so a value-provider list given that criteria returns zero rows. Pre-existing; this PR makes it explicit instead of silent.
  2. An array comparand under the simple-equality arm can never match. { tags: ['a','b'] } compares by reference, so it excludes every row even when the stored array is deep-equal. Silent and fail-closed; untouched here because it is the equality branch, not the default arm.
  3. Neither dialect resolves a { $field: path } comparand. Declared by the spec and produced by @objectstack/formula; both matchers compare the reference object itself. Symmetric across the two arms, so not this card's asymmetry — but after this PR the $ side excludes rather than passes, which makes it visible.

Drafted by the domain:ui dev seat, session https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S. (Written into the prose deliberately: a PATCH downgrades a session-URL attribution footer to the bare form, so a PR body that is edited after creation loses the reference unless it is carried as a code span.)

…use the rest

`matchesFilter` ended its operator switch with `default: break`, which adds no
constraint — so an unrecognised operator matched EVERY row, silently. The
asymmetry sat inside one `if` in `find()`: an array `$filter` goes to
`matchesASTFilter`, which refuses an unknown node, excludes the row and logs it;
an object `$filter` went to `matchesFilter`, which admitted it.

The object dialect now carries one arm per member of the spec's
`FILTER_OPERATORS`, each answering the same question its AST twin answers —
`$eq`, `$nin`, `$between`, `$notContains`, `$startsWith`, `$endsWith` and
`$null` join the eight already implemented. Everything else is refused through
the same `refuseFilterNode` the AST arm uses: excluded, and logged once per
distinct refusal per `find()`, with a prescription naming the spelling that
works. `$exists` prescribes `$null`; `$regex` / `$options` print the spec's own
`RETIRED_FILTER_OPERATORS` guidance verbatim; `$like` / `$ilike` say why they
are staged out; `$and` / `$or` / `$not` say that a group belongs in the AST
array dialect.

Both arms of `find()` now drain ONE refusal collector, which is what makes the
two dialects symmetrical rather than merely both non-empty.

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

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3474.2 KB 3512.7 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-DCaOWppp.js —
Status FAIL —

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

Which half objected:

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

📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
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) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
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.41KB 1.23KB
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.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
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) 26.08KB 7.56KB
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) 498.87KB 114.10KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.12KB 3.28KB
data-objectstack (index.js) 191.36KB 53.16KB
fields (index.js) 243.24KB 61.42KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
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) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
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) 5.12KB 1.74KB
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) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.52KB 46.34KB
plugin-dashboard (index.js) 131.48KB 34.45KB
plugin-designer (index.js) 213.21KB 43.63KB
plugin-detail (index.js) 248.73KB 63.97KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.37KB 56.63KB
plugin-kanban (index.js) 55.40KB 15.71KB
plugin-list (index.js) 112.74KB 27.70KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.54KB 20.84KB
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.66KB 3.50KB
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) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
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) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
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 (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

…oses

Contract review on objectui#8447 flipped the one ruling this PR carried
against its own census. `$exists` is emitted by two producers in this repo
(`FilterConditionField`'s `exists` / `notExists`, kept reachable on purpose by
objectui#4736, and `datasetFilterCondition`'s `isEmpty` / `isNotEmpty`) and its
semantics are settled: `convertFiltersToAST` lowers `$exists: true` to
`is_not_null` and `$exists: false` to `is_null`, three lines from where it
lowers `$null` the other way round. Refusing it would have turned "every row"
into "no rows" on a filter that looks like it works.

The arm is the smaller half. With nothing the spec declares refused by name,
`REFUSED_OPERATORS` in the companion test is EMPTY, so the parity guard asserts
the case table equals `FILTER_OPERATORS` exactly — a spec release that adds a
`$` operator now reddens this file instead of letting the new spelling reach
the refusal arm unannounced.

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

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3474.1 KB 3512.7 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-BKnG0H0Q.js —
Status FAIL —

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

Which half objected:

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

📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
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) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
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.41KB 1.23KB
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.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
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) 26.08KB 7.56KB
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) 498.87KB 114.10KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.12KB 3.28KB
data-objectstack (index.js) 191.36KB 53.16KB
fields (index.js) 243.24KB 61.42KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
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) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
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) 5.12KB 1.74KB
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) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.52KB 46.34KB
plugin-dashboard (index.js) 131.48KB 34.45KB
plugin-designer (index.js) 213.21KB 43.63KB
plugin-detail (index.js) 248.73KB 63.97KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.37KB 56.63KB
plugin-kanban (index.js) 55.40KB 15.71KB
plugin-list (index.js) 112.74KB 27.70KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.54KB 20.84KB
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.66KB 3.50KB
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) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
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) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
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 (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

@os-justin
os-justin marked this pull request as ready for review September 8, 2026 05:15
@os-justin
os-justin enabled auto-merge September 8, 2026 05:15
@os-justin
os-justin added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit f76f436 Sep 8, 2026
33 of 34 checks passed
@os-justin
os-justin deleted the claude/issue-8447-matchesfilter-unknown-operators branch September 8, 2026 05:44
os-justin pushed a commit that referenced this pull request Sep 8, 2026
…ertFiltersToAST and pin it

The README's "Supported Filter Operators" table was a hand-written mirror of
convertOperatorToAST's operatorMap and had drifted from the code three ways:
`$nin` / `$notin` documented as lowering to `notin` (the map says `nin`, and
the worked example's output was a node the server refuses); `$regex`
documented as lowering to `contains` (it has been refused with a
FilterOperatorError since PR #8512); only the non-spec `$startswith` spelling
listed. Re-deriving every row found four supported operators with no row at
all — `$notContains`, `$endsWith`, `$null`, `$exists`, the ones the code's own
"Supported operators" message enumerates — and no mention of `$and` / `$or` or
the `$not` refusal.

Every row is corrected against the file as it stands after PRs #8512, #8529
and #8551, the missing rows are added, and two small tables document the
combinators and the refusals. A new pin,
src/readme-filter-operator-table.test.ts, reconciles the tables against the
code on every run: each worked example is executed through
convertFiltersToAST and must produce exactly its documented output (or throw
the INVALID_FILTER / 400 envelope the refused table promises), each spelling
must lower to the operator its row names, and the supported table must carry
a row for every key of the operator map (read from source, aliases included)
and for every operator the unknown-operator error calls supported.

The package tsconfig names `types: ["node"]` so the pin's node:fs / node:url
reads type-check in the program that already compiles this package's tests
(measured: TypeScript 6.0.3 does not see the root @types/node from here
without it).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
os-justin pushed a commit that referenced this pull request Sep 8, 2026
…g raise could not (objectui#8542)

`fa9e76ccd` raised `PER_CHUNK_GZIP_CEILINGS.framework` to 100,000 and recorded,
honestly, that it could not say what the bytes bought: "the commits in that
window have never been bisected — this checker is a two-build predicate over
them and no one has run it." That is the one sentence this change replaces.

The bisect has now been run — one `apps/console` build per point, each from the
repo ROOT, `framework` read out of the `apps/console/dist/eager-closure.json`
the build itself writes:

  | build       | landed by | `framework` gzip | moved by                 |
  | `40a7c538a` | #8503     | 70,999           | last GREEN, 1 byte under |
  | `512c84b16` | #8519     | 70,999           |                        0 |
  | `f76f43628` | #8512     | 71,261           |                     +262 |
  | `e76634cc8` | #8529     | 72,245           |                     +984 |
  | `e411c3e58` | #8562     | 72,248           |                       +3 |

Two commits own the overage, not one, and the LARGER one is outside the window
every card bounded its reasoning to: `e76634cc8` landed after the red had
already started and carries 984 of the 1,246 bytes the pair added. A repair
scoped to that window would have left `main` red. `512c84b16` emitted a
byte-identical chunk to `40a7c538a` — the same `framework-nDOJv2Ij.js` content
hash — so the three commits between them moved this chunk by zero, and
`270f2825b` touches no file under `packages/(core|react|types)` at all.

What the bytes buy is therefore sayable, which is what the gate's own failure
message asks of a raise: both contributors are silent-wrong-answer fixes on one
file's filter path. `f76f43628` stopped an unrecognised `$` operator adding no
constraint and matching every row; `e76634cc8` stopped an array comparand and a
`{ $field }` reference being compared by reference, which selected every row
under `$ne` and no rows under `$eq` without saying so.

No constant moves. The prose that records what the raise COSTS is kept intact,
including that the attribution arrived after the ceiling did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 9, 2026
…ertFiltersToAST and pin it (objectstack-ai#8565)

The README's "Supported Filter Operators" table was a hand-written mirror of
convertOperatorToAST's operatorMap and had drifted from the code three ways:
`$nin` / `$notin` documented as lowering to `notin` (the map says `nin`, and
the worked example's output was a node the server refuses); `$regex`
documented as lowering to `contains` (it has been refused with a
FilterOperatorError since PR objectstack-ai#8512); only the non-spec `$startswith` spelling
listed. Re-deriving every row found four supported operators with no row at
all — `$notContains`, `$endsWith`, `$null`, `$exists`, the ones the code's own
"Supported operators" message enumerates — and no mention of `$and` / `$or` or
the `$not` refusal.

Every row is corrected against the file as it stands after PRs objectstack-ai#8512, objectstack-ai#8529
and objectstack-ai#8551, the missing rows are added, and two small tables document the
combinators and the refusals. A new pin,
src/readme-filter-operator-table.test.ts, reconciles the tables against the
code on every run: each worked example is executed through
convertFiltersToAST and must produce exactly its documented output (or throw
the INVALID_FILTER / 400 envelope the refused table promises), each spelling
must lower to the operator its row names, and the supported table must carry
a row for every key of the operator map (read from source, aliases included)
and for every operator the unknown-operator error calls supported.

The package tsconfig names `types: ["node"]` so the pin's node:fs / node:url
reads type-check in the program that already compiles this package's tests
(measured: TypeScript 6.0.3 does not see the root @types/node from here
without it).


Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 9, 2026
…g raise could not (objectui#8542) (objectstack-ai#8579)

`fa9e76ccd` raised `PER_CHUNK_GZIP_CEILINGS.framework` to 100,000 and recorded,
honestly, that it could not say what the bytes bought: "the commits in that
window have never been bisected — this checker is a two-build predicate over
them and no one has run it." That is the one sentence this change replaces.

The bisect has now been run — one `apps/console` build per point, each from the
repo ROOT, `framework` read out of the `apps/console/dist/eager-closure.json`
the build itself writes:

  | build       | landed by | `framework` gzip | moved by                 |
  | `40a7c538a` | objectstack-ai#8503     | 70,999           | last GREEN, 1 byte under |
  | `512c84b16` | objectstack-ai#8519     | 70,999           |                        0 |
  | `f76f43628` | objectstack-ai#8512     | 71,261           |                     +262 |
  | `e76634cc8` | objectstack-ai#8529     | 72,245           |                     +984 |
  | `e411c3e58` | objectstack-ai#8562     | 72,248           |                       +3 |

Two commits own the overage, not one, and the LARGER one is outside the window
every card bounded its reasoning to: `e76634cc8` landed after the red had
already started and carries 984 of the 1,246 bytes the pair added. A repair
scoped to that window would have left `main` red. `512c84b16` emitted a
byte-identical chunk to `40a7c538a` — the same `framework-nDOJv2Ij.js` content
hash — so the three commits between them moved this chunk by zero, and
`270f2825b` touches no file under `packages/(core|react|types)` at all.

What the bytes buy is therefore sayable, which is what the gate's own failure
message asks of a raise: both contributors are silent-wrong-answer fixes on one
file's filter path. `f76f43628` stopped an unrecognised `$` operator adding no
constraint and matching every row; `e76634cc8` stopped an array comparand and a
`{ $field }` reference being compared by reference, which selected every row
under `$ne` and no rows under `$eq` without saying so.

No constant moves. The prose that records what the raise COSTS is kept intact,
including that the attribution arrived after the ceiling did.


Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation package: core tests

Projects

None yet

2 participants