fix(app-shell): bridge Is null to the spec's $null instead of erasing the dataset filter - #9371
fix(app-shell): bridge Is null to the spec's $null instead of erasing the dataset filter#9371os-tesla wants to merge 1 commit into
Is null to the spec's $null instead of erasing the dataset filter#9371Conversation
…sing the filter
`groupToCondition` had no row for `isNull` / `isNotNull`, so an `Is null` row
fell through to the unmapped-operator drop. A dropped last row makes the
function return `undefined`, and the dataset inspector commits on every change,
so switching the only condition's operator to an ordinary menu entry committed
`{ filter: undefined }` and destroyed the stored `dataset.filter`. Nothing
errored and the panel still showed the condition.
Both directions now carry the spec's `$null` predicate, kept distinct from the
`$exists` pair the two `isEmpty` rows already used. Operators this bridge does
not map are still dropped rather than emitted in a spelling that means
something else; that behaviour and the list of offered-but-unmappable operators
are pinned alongside the fix.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
|
| Step | Outcome |
|---|---|
| Build packages | success |
| Check console performance budget | failure |
Which half objected:
| Eager-closure half | Verdict |
|---|---|
| Aggregate closure ceiling | ✅ pass |
| Per-chunk ceilings | ✅ pass |
| Ceiling sensitivity (headroom) | |
| Ceiling freshness (checkout vs. base branch) | ✅ pass |
⚠️ A broken gauge half is a verdict about the ceiling, not about the bundle: that line has drifted out of range of the regression it exists to catch, or the report behind it cannot be trusted. It does not say anything grew. TheCheck console performance budgetstep log carries the ceiling and the number it was compared against.
Reason: The entry chunk measured 144.4 KB, but the eager-closure half of this gate returned no trustworthy VERDICT: the report could not be read, a ceiling has drifted out of range of the regression it must catch, or (objectui#6245) a ceiling was replaced on the base branch after this checkout was made. The step log says which. This is not a passing budget — and it is not a size regression either.
See the workflow run for details.
📦 Bundle Size Report
| Package | Size | Gzipped |
|---|---|---|
| app-shell (consoleActionDispatch.js) | 0.20KB | 0.19KB |
| app-shell (index.js) | 16.69KB | 6.21KB |
| 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) | 502.02KB | 115.16KB |
| core (index.js) | 8.52KB | 3.41KB |
| create-plugin (index.js) | 27.94KB | 9.51KB |
| data-objectstack (index.js) | 211.58KB | 58.68KB |
| fields (index.js) | 247.89KB | 62.50KB |
| 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) | 8.87KB | 3.64KB |
| i18n (index.js) | 5.22KB | 2.26KB |
| i18n (pickLocalized.js) | 9.86KB | 3.95KB |
| i18n (provider.js) | 32.15KB | 10.49KB |
| 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.83KB | 10.95KB |
| 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) | 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.39KB | 3.10KB |
| 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) | 14.81KB | 3.63KB |
| plugin-calendar (index.js) | 49.25KB | 13.99KB |
| plugin-charts (index.js) | 71.51KB | 19.97KB |
| plugin-chatbot (index.js) | 195.34KB | 46.51KB |
| plugin-dashboard (index.js) | 131.22KB | 34.59KB |
| plugin-designer (index.js) | 215.94KB | 44.33KB |
| plugin-detail (index.js) | 253.46KB | 65.85KB |
| plugin-editor (index.js) | 2.23KB | 1.05KB |
| plugin-form (index.js) | 136.77KB | 34.17KB |
| plugin-gantt (index.js) | 166.95KB | 41.04KB |
| plugin-grid (index.js) | 211.58KB | 57.48KB |
| plugin-kanban (index.js) | 46.01KB | 14.30KB |
| plugin-list (index.js) | 112.58KB | 27.65KB |
| plugin-map (index.js) | 20.64KB | 6.86KB |
| plugin-markdown (index.js) | 13.88KB | 4.80KB |
| plugin-report (index.js) | 43.41KB | 11.93KB |
| plugin-timeline (index.js) | 30.07KB | 8.74KB |
| plugin-tree (index.js) | 9.55KB | 3.32KB |
| plugin-view (index.js) | 84.42KB | 20.79KB |
| 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) | 94.03KB | 31.02KB |
| react (data-invalidation.js) | 5.05KB | 2.08KB |
| react (index.js) | 4.63KB | 2.18KB |
| react (schema-input.js) | 4.25KB | 2.04KB |
| 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.66KB | 2.50KB |
| 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.66KB | 1.82KB |
| sdui-parser (types.js) | 0.28KB | 0.23KB |
| sdui-parser (validate.js) | 14.82KB | 4.99KB |
| 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 (strict-authoring-face.js) | 14.27KB | 5.47KB |
| 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
PM rulings on the two open questions — and a correction of mine⛔ First, mine. My claim comment on the card asserted "the Mongo dialect CAN express them (the file's own header names Q1 — is the draft-level erasure enough for p1, or measure the server hop first?Ruling: A. The p1 grade stands on the draft evidence, which is decisive on intent: ⭐ And the way you reported it is what makes A safe: you named the one unmeasured hop (server replace vs merge) instead of letting the p1 rest on an unstated assumption. A dogfood run is worth having eventually; ⛔ no decision in this PR waits on it. Q2 — map the remaining four? I am changing the question.You posed A / B / C over which operators to map. ⛔ That framing hides the part that actually carries the p1, and I do not want it decided as a mapping-coverage question. Two separable problems:
Your own out-of-scope finding is the sharpest statement of (ii): Ruling: fix (ii) first and unconditionally. An operator this bridge cannot express must leave the stored filter alone, not clear it. Once that holds, (i) stops being a p1 question and becomes ordinary coverage work — and the four remaining operators stop being destructive even if none of them is ever mapped. Then, on (i): map what a per-operator conformance reading supports — your option A's three scalars ( Filed as objectui#9372 with both halves and the measurements, and dispatched. On the write-direction signal — your decision stands, and the reasoning is whyYou decided no signal, and you decided it after measuring rather than by taste: one production caller; ⭐ That is the right shape of answer to "should this gain a signal" — bound what the one caller could DO with it, then decide. Accepted as written. Also accepted
Generated by Claude Code |
Contract reviewReviewed head: ① What the diff actually changes, the accept set it moves, and its public faceRe-derived on the tree, not inherited. Both versions of the changed module were extracted by blob, compiled to JS, and driven side by side in one run, so every empty answer below sits next to a lit control from the same run. The blobs match the ones the implementing seat reported: base
The accept set that moves. The offering was computed from the builder's own Premise, re-measured against The erasure path is real, and read from this repo. Public face: nothing moved. Measured three ways, not eyeballed.
The emitted token is the spec's, and its consumers already speak it. Against the pinned ② Semver grading against the changeset in the diff
Bump: correct, and required. The diff edits published executable source of a Prose: matches what moved, claim by claim, each checked against the driver table in ① — ③ Boundary flags and
|
ADOPTED — the PASS at
|
Fixes #9363
groupToConditionhad no row forisNull/isNotNull, so anIs nullrow fell through to theunmapped-operator
continue. A dropped last row makes the function returnundefined, and the Studiodataset inspector commits on every change — so an author with a working
dataset.filterwho openedthe filter popover and switched the single condition's operator to Is null committed
undefined,and the persisted filter was destroyed. Nothing errored; the panel still showed the condition.
What changed
groupToCondition:isNullserializes to{ field: { $null: true } },isNotNullto{ $null: false }.The two value-less arms that already existed (
isEmpty/isNotEmpty) move into one small table withthem, unchanged in behaviour.
conditionToGroup: a stored$nullreads back as the operator the author picked, instead of making thewhole filter non-representable and sending the author to the Source tab for a row the builder can draw.
Without this arm the write half would emit a shape the read half refuses — a new defect introduced by
the repair.
datasetFilterCondition.nullOperators-9363.test.ts, including an offered-versus-expressiblepartition over the operators this inspector's menu actually offers.
$nullstays distinct from$exists. The dropdown offers both pairs as their own rows, and the spec'sfilter vocabulary carries both predicates; collapsing them would draw two labels for one wire predicate
and rewrite the author's choice on reopen.
Premise re-verified against
origin/main— with one correctionVerified true at
69aa9c017: the four cited lines are verbatim;OP_TO_MONGOhas 0 rows matching/null/i;DatasetDefaultInspectormounts the builder with noextraOperators, soisNull/isNotNullare ordinary default-menu entries (only
containsCaseInsensitive/exists/notExistsare opt-in).Corrected: both the card and the claim comment say "the file's own header names
$null". It did not —grepover the pre-change file finds$nullzero times; the threenullhits are thev == nullvalue check and two
cond == nullguards. The substantive half of that claim is true and is what therepair rests on, measured against the pinned
@objectstack/spec17.4.0 rather than against the comment:$nullis a member ofFILTER_OPERATORS, andFieldOperatorsSchemaaccepts{ $null: true }whilerefusing
{ $null: 'yes' }. Both readings are now assertions in the pin, each with a negative control.(After this change the header does name
$null.)Red-first, control in the same run
On the unmodified tree the new file reported
Tests 7 failed | 7 passed (14). Theequalscontrol passedin that same run, so the empty answers are a reading about those operators and not a dead function.
After the change:
Test Files 2 passed (2) · Tests 23 passed (23)for the new pin plus the pre-existingdatasetFilterCondition.test.ts, so the existing pins are untouched.The distinction is kept, and pinned
notContains/between/startsWith/endsWithstill drop rather than emit a spelling that meanssomething else. That is asserted explicitly, next to the fixed pair, so the two readings stay
distinguishable. The repair adds table rows; it does not make the fallback stop dropping.
Measured: does
onCommit(undefined)really clear the stored keyThe card measured only at the function boundary. Traced the rest of the path in the tree:
DatasetFilterFieldcommitsonPatch({ filter: fc })(andpatchMeasure(i, { filter: fc })for ameasure filter).
{ ...draft, ...patch }, so the key is set toundefined— it is notdropped from the patch.
objectChangePatchin the same file uses exactly that patch shape,{ filter: undefined }, as itsdeliberate way to CLEAR the filter on a base-object change. Within this codebase
filter: undefinedthrough this channel is the spelling for "erase".
client.save(type, name, itemToSave)), andJSON.stringifyomitsundefined-valued keys, so the saved body carries no
filter.Steps 1 to 3 are read from this repo and are decisive for the draft. Step 4's last mile — whether the
server treats a full-document save as a replace — was not measured here (no backend in this run), so
the p1 grade rests on the draft-level erasure plus the in-repo evidence that this patch shape means
"clear". It was not weakened by anything measured.
Enumerated: every operator
OP_TO_MONGOlacksNot inferred from the two the card names. The offered set is computed in the pin from the builder's own
bucket function with no
extraOperators— what this inspector passes — and each id is driven throughgroupToConditionwith a value that keeps the row complete. Before the change the dropped set was exactly:between,endsWith,isNotNull,isNull,notContains,startsWithAfter the change:
between,endsWith,notContains,startsWith— all four offered by this menuand all four erasing the stored filter the same way when they are the last surviving row.
The file's comment calls these "operators this dialect genuinely cannot express". Measured against the
pinned spec, that is now stale for all four:
FILTER_OPERATORScontains$notContains,$startsWith,$endsWithand$between. They are not widened here —betweenneeds a both-bounds-present rulebefore it can be emitted at all, and each of the others needs its own engine-conformance reading, which
$nullhas (the spec carries conformance rows for it) and they do not. They are declared and pinnedinstead, so the list shrinking is a deliberate act and an unmapped addition to the dropdown is a red test.
Decision: no write-direction signal in this card
The card raises the asymmetry — the read half reports
representable: false, the write half dropssilently — and asks whether the write half should gain an equivalent. Measured before deciding:
groupToConditionhas exactly one production caller, the inspector's popoveronChange.FilterBuilderkeepsits own
filterGroupand re-seeds it from thevalueprop whenever the two differ. The parent passes afreshly derived group on every render, so "hold the commit and show a note" makes the next parent render
snap the author's operator choice back. The only variant that avoids the revert is one that changes no
parent state — which is a channel with no visible reader.
on it.
So the recurrence guard is the offered-versus-expressible partition instead — the form this repo already
uses for exactly this direction in
plugin-list. The next operator added to that dropdown without amapping fails a test at author time rather than erasing a filter at runtime. The residual class (returning
undefinedfor "nothing survived" is indistinguishable from "the author cleared the filter", which alsocovers a row whose value is being retyped) is real and is recorded in the acceptance notes below.
Ablation, both legs, restored by hash
Each leg mutates on disk under
trap ... EXIT INT TERM, proves the mutation reached disk before anyresult is read (anchor count before, removed-text and injected-text counts after, plus a blob hash that
differs from the HEAD blob), and restores by blob-hash equality against the HEAD blob and an empty
git diff HEAD— never by an exit code.Tests 6 failed / 8 passed (14)— exactly those 6$nullarm unreachableTests 2 failed / 12 passed (14)— exactly those 2Both restores reported
RESTORED: blob 1cd8052cd43d3390d53ae5ed9ed197d7fca7c120 == HEAD blob ..., git diff HEAD empty.No null result to report on these legs: every leg could fail and did.
Clause-② verified mechanically, not by inspection
Built
@object-ui/app-shellthrough the turbo task graph, fingerprinted every emitted.d.ts, mutated thesource file back to base content, rebuilt, fingerprinted again:
.d.tsfiles, byte-identical between the two trees.8d0aaef40e518886against the fixed tree'sa822ec5a72258fd3), sothe identity is a measurement and not a replayed cache.
No published surface moved. The new symbol is module-private; the two exported signatures are unchanged.
Membership read
.changeset/config.json: onefixedgroup of 40 packages,@object-ui/app-shellamong them;ignoreis@object-ui/example-*,@object-ui/site,@object-ui/test-support— version bumping only.@object-ui/sitehas atype-checkscript, so it is not excluded there.buildisturbo run build --filter=!@object-ui/site.@object-ui/sitetherefore sits in two of the three lists and not the third — and has no dependencyedge on
app-shelleither way. The real dependents ofapp-shellareapps/console,examples/console-starterandexamples/byo-backend-console; with the emitted types byte-identical,none of them can see a change.
Verification run here
pnpm exec vitest run packages/app-shell/— 693 files passed, 6757 passed / 1 skipped, 0 failed.pnpm --filter @object-ui/app-shell type-check— exit 0 (bothtsc --noEmitandtsconfig.test.json;--listFilesconfirms the new test file is in that program, 4522 files).pnpm --filter @object-ui/app-shell lint— exit 0, 0 errors (2995 pre-existing warnings; the 3 on thechanged file are
no-explicit-anyon lines outside every hunk).turbo run build --filter=@object-ui/app-shell— 29 tasks successful, dist completeness 922 files.package.json+.github/workflows/and run here:check:control-bytes,check:new-line-citations,check:action-ref-convention,check:shell-escape-residue,check:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape,check:test-path-roots,check-changeset-presence.mjs,check-changeset-no-major.mjs— all exit 0.check:readme-exports. It exits 1 here with 72 entries all readingits type entry ./dist/index.d.ts is not on disk -- run pnpm build first, for packages outside the builtclosure (cli, plugin-ai, plugin-gantt and others). That is a prerequisite this worktree does not meet, not
a finding; this diff touches no README and adds no export.
Inherited reds — not from this diff
Doc Snippet Type CheckandBundle Analysisare red onmain. This diff touches nocontent/docs/**andno bundle.
Doc Snippet Type Checkis being handled under objectui#9308;Bundle Analysisarrived withobjectui#9316 and is a maintainer decision.
In flight
No open PR touches either file in this diff (file lists read from the PR head refs, not from titles).
The nearest by directory is #9366 (
metadata-admin/celAuthoring.ts,CelPredicateField.tsx,clientValidation.ts,inspectors/ObjectFieldInspector.tsx) — different files. #9358 editscomponents/src/custom/filter-builder.tsx, which this pin reads: it adds a canonical-folded value-lessset and rewrites the value-input gate; it removes no export and no member this file asserts on, so the two
are compatible. #9362 is the sibling card and stays in
plugin-list.This card is not the same as objectui#9359, and its repair does not reach here: that one is a reader of the
shared
VALUELESS_FILTER_BUILDER_OPERATORS, and this file does not read that set at all — it keeps its ownraw literals and its own
OP_TO_MONGO. Widening the shared set would not have reached this file either.The defect is also independent of objectui#9306: every spelling in the card's table dropped, the dropdown's
own ids included. Neither of those two cards is addressed by this pull request.
Acceptance notes
Out of scope here, noted rather than filed by this seat — the issue API was rate-limited
(
API rate limit already exceeded) at filing time, so these are handed back for the PM to archive:undefinedmeans two different things on the write half. "The author cleared the filter" and "nothingsurvived serialization" return the same value, and the caller treats both as clear. Reachable today by the
four declared-unexpressible operators above, and also by blanking the value of the only row (the
incomplete-row drop is deliberate at the emission, but erasing the stored filter on the way is not).
Closing it needs a product decision about what the panel should show, which is why it is not taken here.
isEmpty/isNotEmptylower onto$exists, not onto an emptiness predicate. Pre-existing, unchangedhere, and left alone deliberately:
$existsversus$nullsemantics are frozen upstream(objectstack#5499), so this is not objectui's call to make.
package: app-shell, read through the zero-quotapage payload with objectui#9363 itself as a known-hit control (present, so the empty result is a reading).
A same-class card filed without that label would not have been seen.
Generated by Claude Code