fix(app-shell): an unmapped dataset-filter operator is inert, not destructive - #9381
Conversation
…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
…tructive
`groupToCondition` answers `undefined` both when the author CLEARED the filter
and when nothing survived serialization, and the dataset inspector — which
commits on every change — treated the two the same. The host applies patches as
`{ ...draft, ...patch }`, so that commit SET `dataset.filter` to `undefined`,
which is exactly the shape `objectChangePatch` uses deliberately to erase it.
Two ordinary gestures reached it: switching the only condition's operator to one
this bridge did not map, and — needing no operator at all — blanking the value of
the only row. The stored filter was destroyed silently in both.
The two meanings are now distinguished by `isClearedGroup`: a group that still
holds rows commits nothing and leaves the stored filter alone; a group with no
rows is the author's own clear gesture and still commits `undefined`. Deliberately
not "emit something anyway" — a filter in a spelling that means something else is
worse than a dropped one, so the unmapped arm still drops.
`notContains`, `startsWith` and `endsWith` are no longer unmapped: each bridges to
the spec's own token, backed by the Filter Protocol's canonical text-operator
cases and its declared-type door, which passes them over `text` and refuses them
over `number` / `date` — the only bucket this builder offers them on. `between`
stays out until it has a both-bounds-present rule, and is now inert.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
Contract reviewReviewed head: ① Derived judgments1.1 — The stack, with controls. The checkout is shallow, so the reading was deepened first and the graft boundary located before any ancestry answer was trusted:
⇒ the stacked-on-#9371 claim holds, and the diff against 1.2 — What the diff changes, measured at runtime rather than read. I extracted the bridge module from the head blob and drove it standalone under Node (type-stripped; the module has no runtime imports), transcribing the call site's rule verbatim from The zero is not void: the same matrix with the guard removed names Round trips green in both directions for the three newly mapped operators ( 1.3 — The accept set it moves. Independently re-derived, not taken from the pin: the offered union minus 1.4 — Its public face. 1.5 — The reachability re-derivation (the load-bearing one). Three independent instruments, each with a control that fires. It holds. (a) Re-export closure of the root barrel, at the head. Enumeration and content both read from the same ref. Walking
(b) Whole-tree symbol grep at the same ref. (c) Node's own exports gate, from a real consumer. The first attempt ran from the repo root and its POSITIVE CONTROL failed ( The ⇒ the maintainer's premise — not re-exported from ② Semver grading against the changesets in the diff2.1 — What is in the diff. Two changeset files, both 2.2 — The stacked changeset is safe. 2.3 — Grading. On API grounds 2.4 — The prose gap handed over from #9371's review: CLOSED. #9371's changeset says unmapped operators are "still dropped … deliberate" without saying the drop still erased for those four. This one states it plainly, adjacent in the same CHANGELOG section:
That is the sentence #9371's entry lacked, it names the four, and it names the operator-free route as well. Nothing further is owed here. ③ Boundary flags and open questions3.1 — The one branch where the new predicate can still commit a clear, and why it is not reachable from this caller. 3.2 — 3.3 — The visible trade, already named in the code. 3.4 — CI accounting on this head, read live at 11:21Z. 21 check runs: all 3.5 — My own governed-surface reading. 3.6 — Merge mechanics. 3.7 — What I did NOT re-run. The PASS Generated by Claude Code |
ADOPTED — the PASS at
|
This branch was stacked on the objectui#9363 fix: its parent IS that PR's head commit (5c82df0), carried verbatim. That PR landed on `main` as a SQUASH (964d953), so git saw two unrelated commits touching the same regions and refused the merge. Two paths conflicted; both were re-derived, neither was side-picked. For each one, `main`'s blob was measured byte-identical to this branch's parent blob (theirs == 4ae2f61^1 for both paths), so the reviewed commit's own patch applies to `main`'s content cleanly and by definition reproduces this branch's blob. That derivation was run and its output hashed against the branch bytes before either file was written: datasetFilterCondition.ts -> a0389a4 datasetFilterCondition.nullOperators-9363.ts -> 4c8f873 Conflict content in both files is documentation-of-record that objectui#9372 deliberately rewrote: the operator partition (`between` is now the whole residual) and the prose that used to call an unmapped drop destructive. Taking `main`'s side would have reinstated both as false statements and removed `isClearedGroup` outright. `.changeset/9363-dataset-filter-isnull-erases.md` did not conflict: the same blob is on both sides and `main` has not consumed it yet, so it stays exactly once, untouched. No reviewed file changed: all seven paths this branch owns hash the same before and after this merge. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt
|
| 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.3 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.34KB | 19.90KB |
| 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.66KB | 57.50KB |
| plugin-kanban (index.js) | 46.00KB | 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) | 96.00KB | 31.71KB |
| 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.04KB | 5.36KB |
| 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-dev-report Conflict resolution only. No source file, test or changeset content changed: all seven files this PR owns hash the same before and after, and the PR still reports changed_files 7. {
"task": "resolve merge conflict on PR 9381 (card objectui#9372)",
"status": "done",
"branch": "claude/issue-9372-unmapped-operator-erases-filter",
"old_head": "4ae2f6177f0f7259a6c733bd0bbc8272dba384a8",
"new_head": "c975c63e629d89192ffd36507b81e138c5d39708",
"method": "merge commit of origin/main a272a4ffec — no rebase, no amend, no force-push (push was a fast-forward 4ae2f6177f..c975c63e62)",
"cause_confirmed": "branch parent IS 5c82df0685 (PR 9371 head) verbatim; 5c82df0685 is NOT an ancestor of origin/main (control leg: a commit 500 deep in main answers exit 0 on the same checkout) — PR 9371 landed as squash 964d9537f1",
"conflicting_paths": 2,
"resolution": "re-derived: for BOTH paths main's blob was measured byte-identical to this branch's parent blob, so the reviewed commit's own patch was applied to main's content and the output hashed against the branch bytes BEFORE either file was written — a0389a42edcdaaafaff24607c4303918c80e62ea and 4c8f8735f330262924182a51500964c322669a5d, both matching",
"changeset_9363": "did NOT conflict. Same blob ab21c8303b on both sides, and origin/main has NOT consumed it — so it stays exactly once, untouched. Deleting it would have stripped PR 9371's own changeset.",
"pins_on_merged_tree": "4 files, 42 tests, all passing; whole app-shell surface 695 files / 6782 passed / 1 skipped, vitest exit 0",
"behavioural_facts": {
"unmapped_operator_inert": "PASS — operator route commits NOTHING",
"clear_all_commits_undefined": "PASS — the author's own CLEAR gesture still reaches the draft",
"blank_value_route_holds": "PASS — blanking the only row's value does NOT patch filter to undefined",
"residual_unmapped_is_between": "PASS — DECLARED_UNEXPRESSIBLE is the one-element list, and the offering partitions exactly"
},
"not_measured": "app-shell type-check: four consecutive os-verify-lock queue-timeouts (exit 99, 36 min waited). Holder pid 12379 in objectui-review-9343-merged held the lock past 40 minutes. Declared to CI.",
"observed_not_corrected": "auto-merge read as ARMED right after the push and as null a few minutes later; not my doing, not re-armed — landing is the PM's"
}Session reference, as a code span so it survives a later edit: Generated by Claude Code |
|
os-dev-report correction Correcting one figure in my previous comment. Nothing about the resolution, the pins or the head sha changes; only the What I wrote: "app-shell type-check: four consecutive os-verify-lock queue-timeouts (exit 99, 36 min waited)". What actually happened: there were five queue-timeouts in this run, and only two of them were for type-check. The other three were for other checks, each of which I then obtained another way. Per check:
Cumulative queueing across all five: 45 minutes. The single holder throughout was pid 12379 in objectui-review-9343-merged, which passed 40 minutes of hold while I was waiting on it. That long hold is the real finding here and is worth someone's attention independently of this PR. So the corrected line is: app-shell type-check is NOT MEASURED locally — two queue-timeouts, 18 min waited, declared to CI. It is not red and it is not green. Two cheap readings I did take instead, neither a substitute for the type-check:
Generated by Claude Code |
Head moved after the PASS — why the review still stands, and the one thing CI is carrying alone
The reviewed subject is byte-identical. The span
⇒ the PR's own diff is still the seven files the review judged. What is NOT measured, stated plainly rather than left to look green: the agent could not run Auto-merge: armed by this seat at 11:33:47Z after the adoption at Also untouched, as ordered: the Generated by Claude Code |
⛔ Correction to my own figure one comment upIn
The material claim is unchanged and still stands: ⭐ Worth naming separately, because it is not about this PR: the agent recorded that lock holder Generated by Claude Code |
Fixes #9372
An operator this bridge cannot express now leaves the stored dataset filter alone.
PR #9371 (card objectui#9363,
Is null/Is not null) had not landed when this branch was cut, and it is the direct predecessor in the same file. Measured rather than assumed: its branch tip5c82df068sits exactly one commit on top oforigin/mainat69aa9c017, andgit merge-base --is-ancestorsays it is not an ancestor ofmain(exit 1; control leg on a known ancestor answered exit 0 in the same deepened checkout).So this branch was cut from
origin/mainand then fast-forward merged5c82df068. Consequence for review: the diff againstmainshows #9371's commit too, until #9371 lands. My own change is the single commit4ae2f6177. Nothing in #9371 was rewritten — its partition pin was extended, per the two edits listed under "What changed in #9371's pin" below.The ruling this implements: (ii) before (i)
The card offered three options over which operators to map. That framing hides the part carrying the p1, so it was split:
(ii) is fixed first and unconditionally.
The mechanism
groupToConditionanswersundefinedfor two different things — "the author cleared the filter" and "nothing survived serialization" — and the only caller treated both as clear. The inspector commits on every change, the host applies patches as a spread of the patch over the draft, so that commit SETfiltertoundefined: the same patch shapeobjectChangePatchuses deliberately to erase it.Two ordinary gestures reached it:
continuedrops it, the last part goes with it, and the answer isundefined.The repair
A new
isClearedGroupanswers the one question the caller could not ask: wasundefinedthe author's clear gesture? A group that still holds field-bearing rows commits nothing and the stored value is untouched; a group with no rows — Clear all, or the last row removed — is the author's own gesture and still commitsundefined.⛔ Deliberately not "emit something anyway". A filter emitted in a spelling that means something else is worse than a dropped one — that was the original comment's intent and it survives intact. The unmapped arm still drops; the drop is now inert.
FilterBuilderre-seeds its own state from thevalueprop whenever the two differ, so an unexpressible row is lost from the panel on the next render the inspector happens to do. Losing an edit the bridge could never have stored is not in the same class as destroying one it had. A signal-plus-note variant was measured and rejected by objectui#9363's seat (one production caller, and the only revert-free variant changes no parent state); this change does not revive it.(i) Which operators, and the conformance reading behind each
notContains·startsWith·endsWithare mapped. Three independent readings, all asserted in the pin rather than recited here:FILTER_TEXT_CASEScarries rows for all three; the spec's own docblock records the driver ledger as empty, all five drivers importing the table. Negative control in the same assertion:$betweenis not in that table.TEXT_OPERATOR_DOOR_CASESreturnspassesfor all three overtext, anddoor-refusalovernumber/date/boolean/currency/percent/rating/time. The two halves meet because this builder offers the three only on its text bucket, read fromoperatorsForFieldTyperather than restated.FieldOperatorsSchemaaccepts the string this builder types and refuses a number.betweenstays out, and the reason is about this bridge rather than the vocabulary. The builder pads a half-typed pair with an empty bound, the row is two entries long so the completeness check passes it, and the spec's comparand door accepts a pair with a blank bound (measured: it parses). Emitting it would emit a filter the author did not ask for. It needs a both-bounds-present rule first — and it is now unmapped inert instead of unmapped destructive.Premise, re-measured in this worktree
Against the spec actually installed here —
@objectstack/spec17.4.0:FILTER_OPERATORS=$eq $ne $gt $gte $lt $lte $in $nin $between $contains $notContains $startsWith $endsWith $icontains $null $exists. All four of the card's operators are members ⇒ the file'sunmapped (e.g. notContains/between)comment, read as "operators this dialect genuinely cannot express", was stale for all four. Confirmed, and now pinned with a negative control.between·endsWith·notContains·startsWithby running fix(app-shell): bridgeIs nullto the spec's$nullinstead of erasing the dataset filter #9371's own partition pin on the unmodified tree — green, including its totality ratchet ("the probe types cover every bucket"). Not inherited from the card.Red-first, with the control firing in the same run
The pin was written and run before the code. Verbatim,
--reporter=verbose, unmodified tree:The
equalscontrol passes in that same run — a table of all-undefinedanswers and a dead function look identical otherwise.Ablation — two legs, on-disk proofs, restore by blob hash
Both under
trap RESTORE EXIT INT TERM, both mutating on disk and proving the mutation landed before any result was read, both restored bygit checkout HEAD -- PATHand verified by blob-hash equality plus an emptygit diff HEAD. No null results: both legs reddened.Leg A — delete the call-site guard (
DatasetDefaultInspector.tsx). On-disk anchor 1 → 0 occurrences. The DOM pin reddened with the defect reproduced verbatim, while both of its controls stayed green:Restore:
15276fb767a1bcb12b60e81b5e1fd2a2e8e31659on disk equals the HEAD blob;git diff HEAD --quietexit 0.Leg B — delete the three write-half mappings (
datasetFilterCondition.ts). On-disk anchor 1 → 0. Three tests reddened, including the extended partition pin:⭐ Worth reading: every (ii) assertion stayed green through leg B. That is the split holding — inertness does not depend on which operators (i) mapped.
Restore:
a0389a42edcdaaafaff24607c4303918c80e62eaequals the HEAD blob;git diff HEAD --quietexit 0.Clause-2 was graded
no. Measured mechanically, it isyes, additively.Method: build
@object-ui/app-shellat HEAD, fingerprint every emitted.d.tswith sha256, mutate both source files back to the stack-base content, rebuild, fingerprint again.cache bypass, force executingwith distinct turbo task hashes —47306a326d6ab84f(HEAD) versusb9288a089382815f(base). A third build at HEAD reproduced47306a326d6ab84f, so the hash is deterministic and the two legs genuinely differed. Each build verified 922 emitted files..d.tsfiles in both legs. Exactly one moved:dist/views/metadata-admin/inspectors/datasetFilterCondition.d.ts, sha2562c3f256e…to1ca38b39….isClearedGrouptaking the builder group and returning a boolean.groupToConditionandconditionToGroupare byte-identical — nothing about whatgroupToConditionreturns changed, which was the specific hazard flagged.src/index.ts, and the packageexportsmap exposes only the root entry, so the declaration is not importable by a consumer; butdistis infiles, so it does ship.⛔ No carrier label hung or cleared by this seat. Over to the PM.
What changed in #9371's pin — extended, not rewritten
Two surgical edits to
datasetFilterCondition.nullOperators-9363.test.ts, both anticipated by its own comment ("Mapping one is what makes this list shrink — and this assertion go red until it is updated"):DECLARED_UNEXPRESSIBLEshrinks from four ids to['between'], and its docblock now records that a drop is inert rather than data loss.Everything else in that file — the
$nullarms, the round trips, the totality ratchet, the$existsseparation — is untouched.One pre-existing fixture in
datasetFilterCondition.test.tsusednotContainsto stand for "an unmapped operator". The claim is unchanged; the fixture moved tobetween, because keeping the old one would have pinned a branch it no longer reaches — an assertion that passes because nothing is produced. Consumer radius swept:groupToCondition/conditionToGrouphave exactly one production caller and no other package carries a fixture against them.Verification
Every line below is the tool's own verdict. Heavy runs went through the shared verify lock on slot
os-dev-9372-objectui;check:*gates ran unlocked, as the lock does not cover them.turbo run build --filter=@object-ui/app-shell(29 tasks)VERDICT command-exit 0— dist completeness: 922 emitted files verifiedturbo run type-check --filter=@object-ui/app-shell --filter=@object-ui/console(37 tasks)VERDICT command-exit 0vitest run packages/app-shell/VERDICT command-exit 0— Test Files 695 passed (695), Tests 6776 passed, 1 skippedturbo run lint --filter=@object-ui/app-shellVERDICT command-exit 0— 0 errors, 2994 warnings (package baseline, unchanged)check:readme-exportscheck:control-bytes·check:vi-mock-specifiers·check:vi-mock-override-shape·check:spec-symbols·check:new-line-citations·check:unreferenced-sourcescheck:phantom-deps·check:self-import·check:esm-specifiers·check:side-effects-array·check:published-dist·check:published-tsconfig-exclude·check:i18n-keyscheck-changeset-presence·check-changeset-no-major·check-changeset-fixed·check-test-path-rootscheck-governed-queue-guard --testover all 7 changed pathsNOT GOVERNED — 7 path(s) checked against 5 governed surface(s); none matchedAll cited against the final commit
4ae2f6177, withgit diff HEAD --quietexit 0 at the time of the runs.Lint is the unnarrowed package reading, not a narrowed one —
turbo run lint --filter=@object-ui/app-shellis exactly the task CI runs for this package, so no narrowing claim is being made.Inherited reds — not from this change
check:eager-closure, exit 2 locally). Inherited, and structurally not attributable here: the failing row is theui-componentschunk, whose Vite rule matchespackages/componentsandpackages/fieldsonly — neither is in this diff, which is six files underpackages/app-shell/srcplus one changeset. The gate says so itself: "this row's headroom is a standing debt that predates this change … there is therefore nothing here for this pull request to fix", and both edits that would green it are forbidden. It also prints that its ceiling-freshness comparison is not applicable to a local run, so this local exit code is not the CI verdict.main— a repair is in flight under objectui#9308. Not touched here.check:readme-exportsfirst answered exit 1 with "its type entry is not on disk — run pnpm build first" for@object-ui/cliand@object-ui/plugin-ai: prerequisite not met, not a red gate. Building those two turned it into the real exit 0 recorded above.In flight
Reviewed by file list and named witness, per the two-green-alone-red-together hazard. The only overlap is PR #9371, handled by the stack above. PR #9358 edits
components/custom/filter-builder.tsx, which this pin reads (operatorsForFieldType,filterValueArity,FILTER_BUILDER_OPERATORS,VALUELESS_FILTER_BUILDER_OPERATORS) — no string is asserted from it, the readings are of exported values, and nothing in this diff removes anything #9358 could be asserting. PR #9366 edits neighbours in the same directory (celAuthoring.ts,CelPredicateField.tsx,clientValidation.ts,inspectors/ObjectFieldInspector.tsx); no file overlap and no shared symbol.Not measured
Whether a full-document metadata save is a replace or a merge on the server — no backend in this run. Named rather than assumed. The draft is destroyed either way, so nothing in this repair depends on the answer.
Raised by the
os-devseat for objectui#9372, in sessionsession_01UzHd6hDYatoDn17BuwKxnZ.🤖 Generated with Claude Code
https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
Generated by Claude Code