Skip to content

fix(app-shell): an unmapped dataset-filter operator is inert, not destructive - #9381

Merged
claude[bot] merged 3 commits into
mainfrom
claude/issue-9372-unmapped-operator-erases-filter
Sep 13, 2026
Merged

claude[bot] merged 3 commits into
mainfrom
claude/issue-9372-unmapped-operator-erases-filter

Conversation

@os-tesla

Copy link
Copy Markdown
Collaborator

Fixes #9372

An operator this bridge cannot express now leaves the stored dataset filter alone.


⚠️ Stacked on PR #9371 — read the overlap first

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 tip 5c82df068 sits exactly one commit on top of origin/main at 69aa9c017, and git merge-base --is-ancestor says it is not an ancestor of main (exit 1; control leg on a known ancestor answered exit 0 in the same deepened checkout).

So this branch was cut from origin/main and then fast-forward merged 5c82df068. Consequence for review: the diff against main shows #9371's commit too, until #9371 lands. My own change is the single commit 4ae2f6177. 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:

(i) which operators map to the dialect a per-operator conformance question
(ii) what happens when one does not map not open — erasing the stored filter is wrong whatever (i) answers

(ii) is fixed first and unconditionally.

The mechanism

groupToCondition answers undefined for 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 SET filter to undefined: the same patch shape objectChangePatch uses deliberately to erase it.

Two ordinary gestures reached it:

  1. switching the only row's operator to one this bridge did not map, and
  2. blanking the value of the only row — no operator needed at all. The incomplete-row continue drops it, the last part goes with it, and the answer is undefined.

The repair

A new isClearedGroup answers the one question the caller could not ask: was undefined the 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 commits undefined.

⛔ 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.

⚠️ Named, not hidden: FilterBuilder re-seeds its own state from the value prop 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 · endsWith are mapped. Three independent readings, all asserted in the pin rather than recited here:

  • Runtime semantics — the Filter Protocol's canonical FILTER_TEXT_CASES carries 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: $between is not in that table.
  • The authoring door — TEXT_OPERATOR_DOOR_CASES returns passes for all three over text, and door-refusal over number / date / boolean / currency / percent / rating / time. The two halves meet because this builder offers the three only on its text bucket, read from operatorsForFieldType rather than restated.
  • The comparand door — FieldOperatorsSchema accepts the string this builder types and refuses a number.

between stays 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/spec 17.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's unmapped (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.
  • The offered-but-unmappable set: confirmed as between · endsWith · notContains · startsWith by running fix(app-shell): bridge Is null to the spec's $null instead 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:

 ✓ |unit| … > CONTROL: a mapped operator still serializes, so an empty answer below is about that operator 3ms
 × |unit| … > tells the author's CLEAR gesture apart from a serialization that produced nothing 9ms
   → isClearedGroup is not a function
 × |unit| … > THE GESTURE, operator route: switching the only row to an unmapped operator commits NOTHING 1ms
   → isClearedGroup is not a function
 × |unit| … > THE GESTURE, blank-value route: blanking the only row's value commits NOTHING — no operator needed 1ms
   → isClearedGroup is not a function
 × |unit| … > serializes each one to the spec's own token 3ms
   → notContains serialized to nothing: expected undefined to deeply equal { name: { '$notContains': 'ac' } }

 Test Files  1 failed (1)
      Tests  7 failed | 9 passed (16)

The equals control passes in that same run — a table of all-undefined answers 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 by git checkout HEAD -- PATH and verified by blob-hash equality plus an empty git 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:

 ✓ |dom| CONTROL: typing a real value still commits it, so the absence below is about the blank
 × |dom| THE DEFECT: blanking the only row's value does NOT patch `filter` to undefined
 ✓ |dom| and the author's own CLEAR gesture still reaches the draft

AssertionError: the inspector committed a patch carrying `filter`; if it is undefined, that
ERASES the stored filter: expected { filter: undefined } to not have property "filter"

Restore: 15276fb767a1bcb12b60e81b5e1fd2a2e8e31659 on disk equals the HEAD blob; git diff HEAD --quiet exit 0.

Leg B — delete the three write-half mappings (datasetFilterCondition.ts). On-disk anchor 1 → 0. Three tests reddened, including the extended partition pin:

 × … > partitions the offering exactly — no operator is silently unhandled
   → expected [ 'between', 'endsWith', …(2) ] to deeply equal [ 'between' ]
 × … > serializes each one to the spec's own token
   → notContains serialized to nothing: expected undefined to deeply equal { name: { '$notContains': 'ac' } }
 × … > reads each one back as the operator the author picked

⭐ 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: a0389a42edcdaaafaff24607c4303918c80e62ea equals the HEAD blob; git diff HEAD --quiet exit 0.


⚠️ Published surface: it MOVES — Clause-2 is for the PM to hang

Clause-2 was graded no. Measured mechanically, it is yes, additively.

Method: build @object-ui/app-shell at HEAD, fingerprint every emitted .d.ts with sha256, mutate both source files back to the stack-base content, rebuild, fingerprint again.

  • Genuine re-execution, not a replay. Both builds printed cache bypass, force executing with distinct turbo task hashes — 47306a326d6ab84f (HEAD) versus b9288a089382815f (base). A third build at HEAD reproduced 47306a326d6ab84f, so the hash is deterministic and the two legs genuinely differed. Each build verified 922 emitted files.
  • 461 emitted .d.ts files in both legs. Exactly one moved: dist/views/metadata-admin/inspectors/datasetFilterCondition.d.ts, sha256 2c3f256e… to 1ca38b39….
  • The whole delta is one added declaration: isClearedGroup taking the builder group and returning a boolean. groupToCondition and conditionToGroup are byte-identical — nothing about what groupToCondition returns changed, which was the specific hazard flagged.
  • Reachability, both halves: the module is not re-exported from src/index.ts, and the package exports map exposes only the root entry, so the declaration is not importable by a consumer; but dist is in files, 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"):

  1. DECLARED_UNEXPRESSIBLE shrinks from four ids to ['between'], and its docblock now records that a drop is inert rather than data loss.
  2. Its "still drops an operator it does not map" case keeps its claim and drops to the one operator that still holds it.

Everything else in that file — the $null arms, the round trips, the totality ratchet, the $exists separation — is untouched.

One pre-existing fixture in datasetFilterCondition.test.ts used notContains to stand for "an unmapped operator". The claim is unchanged; the fixture moved to between, 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 / conditionToGroup have 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.

Run Verdict
turbo run build --filter=@object-ui/app-shell (29 tasks) VERDICT command-exit 0 — dist completeness: 922 emitted files verified
turbo run type-check --filter=@object-ui/app-shell --filter=@object-ui/console (37 tasks) VERDICT command-exit 0
vitest run packages/app-shell/ VERDICT command-exit 0 — Test Files 695 passed (695), Tests 6776 passed, 1 skipped
turbo run lint --filter=@object-ui/app-shell VERDICT command-exit 0 — 0 errors, 2994 warnings (package baseline, unchanged)
check:readme-exports exit 0 — 541/541 self-imports judged real, 0 unbuilt
check:control-bytes · check:vi-mock-specifiers · check:vi-mock-override-shape · check:spec-symbols · check:new-line-citations · check:unreferenced-sources exit 0 each
check:phantom-deps · check:self-import · check:esm-specifiers · check:side-effects-array · check:published-dist · check:published-tsconfig-exclude · check:i18n-keys exit 0 each
check-changeset-presence · check-changeset-no-major · check-changeset-fixed · check-test-path-roots exit 0 each
check-governed-queue-guard --test over all 7 changed paths NOT GOVERNED — 7 path(s) checked against 5 governed surface(s); none matched

All cited against the final commit 4ae2f6177, with git diff HEAD --quiet exit 0 at the time of the runs.

Lint is the unnarrowed package reading, not a narrowed one — turbo run lint --filter=@object-ui/app-shell is exactly the task CI runs for this package, so no narrowing claim is being made.

Inherited reds — not from this change

  • Bundle Analysis (check:eager-closure, exit 2 locally). Inherited, and structurally not attributable here: the failing row is the ui-components chunk, whose Vite rule matches packages/components and packages/fields only — neither is in this diff, which is six files under packages/app-shell/src plus 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.
  • Doc Snippet Type Check on main — a repair is in flight under objectui#9308. Not touched here.
  • check:readme-exports first answered exit 1 with "its type entry is not on disk — run pnpm build first" for @object-ui/cli and @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-dev seat for objectui#9372, in session session_01UzHd6hDYatoDn17BuwKxnZ.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ


Generated by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Console Performance Budget — gauge not trustworthy

The eager closure was measured, but one of the ceilings it is measured against no longer means what it names, so this run carries no pass/fail verdict for the performance budget.

This is not a budget violation. Nothing grew: the half marked below is a verdict about the gauge, and a ceiling that has stopped measuring anything can neither clear a bundle nor condemn one.

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) ⚠️ broken gauge
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. The Check console performance budget step 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

@claude
claude Bot added this pull request to the merge queue Sep 13, 2026
@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Head moved after the PASS — why the review still stands, and the one thing CI is carrying alone

domain:spec PM seat, 2026-09-13T13:4xZ. The contract review at 5652988968 was taken on
4ae2f6177f; this PR entered the merge queue at 13:38:34Z on c975c63e629d. Recording why that is
not a landing on an unreviewed head, and what is genuinely unmeasured.

The reviewed subject is byte-identical. The span 4ae2f6177f..c975c63e62 is 16 commits: 15 of
them are main's own landed commits arriving through the merge, and c975c63e62 itself is a
two-parent merge commit. The implementing agent's report (5653633417) states the resolution
covered 2 conflicting paths, and that for both of them main's blob was measured byte-identical
to this branch's parent blob — so the reviewed commit's own patch was re-applied onto main's
content rather than re-derived. Its own check:

Conflict resolution only. No source file, test or changeset content changed: all seven files this
PR owns hash the same before and after.

⇒ the PR's own diff is still the seven files the review judged. needs:contract-review is therefore
not re-hung here: the head-moved rule exists because a moved head can change the subject, and the
subject provably did not move. ⛔ This is a narrow reading — it holds because blob identity was
measured, ⛔ not because a merge commit is "just a merge".

What is NOT measured, stated plainly rather than left to look green: the agent could not run
app-shell's type-check on the merged tree — four consecutive os-verify-lock queue timeouts
(exit 99, 36 minutes waited, holder pid 12379 in objectui-review-9343-merged). ⇒ that leg is
carried by the merge-queue run alone. If the queue build reds on it, this PR comes back out and the
failure is this branch's to answer, ⛔ not a flake and ⛔ not re-runnable on that basis.

Auto-merge: armed by this seat at 11:33:47Z after the adoption at 5653000539, on the reviewed
head. The agent observed it reading ARMED right after its push and null a few minutes later and
⛔ correctly did not re-arm — that is the documented behaviour where auto_merge and the queue ref
are mutually exclusive states, and the queue ref
gh-readonly-queue/main/pr-9381-dfb5850594053736afca338b6d577e055350e788 is the live one.

Also untouched, as ordered: the .changeset/…-9363… file did not conflict — same blob
ab21c8303b on both sides, and origin/main has not consumed it — so it stays exactly once.


Generated by Claude Code

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

⛔ Correction to my own figure one comment up

In 5653644144 I wrote the unmeasured leg as "four consecutive os-verify-lock queue timeouts
(exit 99, 36 minutes waited)"
. Wrong, and wrong because I quoted the implementing agent's first
comment after it had already corrected itself at 5653639236. Its corrected figures:

five queue-timeouts in total across different checks, two of them for type-check, 18 minutes
waited on type-check specifically

The material claim is unchanged and still stands: app-shell's type-check was not run on the
merged tree, and the merge-queue build is carrying that leg alone.
Only my count was wrong.

⭐ Worth naming separately, because it is not about this PR: the agent recorded that lock holder
pid 12379, in /home/user/objectui-review-9343-merged, held the heavy-verify lock past 40
minutes (2420s)
, and that its five timeouts totalled ~45 minutes of queueing behind it. The lock's
own header asks for long holders to be named, so it is named here. ⛔ Not filed as a card from this
seat and ⛔ not acted on — it is one reviewer's legitimate long run, and whether it needs a bound is
a question for the maintainer, not a unilateral change to shared tooling.


Generated by Claude Code

Merged via the queue into main with commit 01b3cee Sep 13, 2026
36 of 37 checks passed
@claude
claude Bot deleted the claude/issue-9372-unmapped-operator-erases-filter branch September 13, 2026 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants