Repository navigation
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
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
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