Skip to content

fix(app-shell): the dataset-filter read half opens a stored condition only when the builder can hold it (objectui#10257) - #10455

Merged
os-litant merged 3 commits into
mainfrom
claude/issue-10257-dataset-filter-read-half
Sep 25, 2026
Merged

os-litant merged 3 commits into
mainfrom
claude/issue-10257-dataset-filter-read-half

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #10257

Clause-②: no

What was wrong

conditionToGroup (the read half of the Studio dataset-filter bridge) opened two kinds of stored condition as editable rows that the builder cannot hold. DatasetFilterField.commitFilterGroup saves on every change, so an edit to a SIBLING row rewrote or dropped them:

  1. Incomplete stored values (the card): { $eq: '' } and { $in: [] }, plus the rest of the class ($ne: '', $nin: [], a null comparand, and the implicit { field: '' } / { field: null } / { field: [] }). The read half returned representable: true, and groupToCondition drops these as unfinished rows. I re-measured this on the branch base 747f3347d: conditionToGroup({ $and: [{ stage: { $eq: 'won' } }, { name: { $eq: '' } }] }) is representable, and editing stage commits { stage: { $eq: 'lost' } }.
  2. Operators the column's bucket does not offer (seat 1's addendum, comment 5817965568): $in on a date column, $gt on a text column, $in on a number column. Generalised to every token, this also covers $exists / $null on a boolean column. The panel drew a blank operator trigger, and reconcileOperatorForField gives equals.

The direction: the read half refuses, for both classes

This applies objectui#10062's read-half $between guard (PR #10253) to every token. objectui#9363 and objectui#9372 made the write half CARRY a shape only where a finished builder row means exactly that spec predicate. A shape the write half could not carry faithfully stayed out of the builder: it was dropped inertly, or its stored form read back as non-representable. Neither class here can be carried by the write half:

  • equals '' is also the row the builder SEEDS on "Add condition". A write half that kept { $eq: '' } would emit a filter for every unfinished row. That is the silently-wrong filter its incomplete-row drop exists to prevent.
  • The bucket gap happens in the panel, before anything reaches the bridge, so the write half has nothing to keep.

So one shape gets one answer from both halves: the row is refused, and the Source tab takes the filter.

What changed

  • There is a new builderHolds(row, fields) in datasetFilterCondition.ts. conditionToGroup calls it on every read-back row, from every arm: $exists, $null, the $-operator arm and implicit equality. Any false gives representable: false. It checks two things:
    1. Would the write half keep the row? It asks groupToCondition itself, not a copy of its rules, so the halves cannot drift apart. This refuses an incomplete value. It also refuses a stored condition keyed by an empty field name, which the write half drops for the same reason.
    2. Does the column's bucket offer the row's operator? Checked only when a field list is given, through operatorsForFieldType(type). That is the same bucket function, called the same way (no opt-ins), that the builder uses for each row and that readBackOperator already reads. No second operator table. A column listed without a type, or not listed at all, gets the text bucket the builder draws for it, as the $between rule already did.
  • The pair-only $between block is removed because builderHolds covers it. The four objectui#10062 $between pins pass unchanged, and they now go red when builderHolds is ablated.
  • groupToCondition (the write half) is byte-identical. The one-time probe below compares its source text.
  • In datasetFilterCondition.dateRoundTrip-9382.test.tsx, one leg is inverted. It expected { mystery: { $gt: 1 } } on an unlisted column to read back as greaterThan. The builder draws the text bucket for an unlisted column, which lacks greaterThan, so that read now refuses. A new control shows that an unlisted $eq still reads back as equals.

PM mechanism assumptions, measured

  • A1 holds. The generalisation reuses operatorsForFieldType with no extras. FilterBuilder.getOperatorsForField calls that function, and the $between rule and readBackOperator already read it.
  • A2 holds. I ran a one-time probe comparing the old bridge (747f3347d) with the new one over 7980 reads.
    • Inputs: 210 stored shapes, each alone and beside a finished row. Each was read with no field list, with the column unlisted, with an empty list, and listed under each of 16 column-type values (including no type and an unknown type).
    • 3418 reads give byte-identical answers. The other 4562 all went from representable to non-representable. Each one is explained: 714 incomplete values, 3080 operators off the column's bucket, 768 both. 0 are unexplained.
    • The write half's source text is identical.
    • The new test adds a permanent byte-identity leg. Every row the dropdown can build, filled in, reopens as itself and writes back byte-identical, across all 14 inspector bucket types.
  • A3 is partly falsified. "Field-less reads unchanged" holds: a read with NO field list skips the bucket check, as the addendum says. The addendum's "field-less" matches the $between comment's "Only a read with NO field list skips this".
    • A condition whose field is NOT IN the list does change. FilterBuilder draws such a row with operatorsForFieldType(undefined), the text bucket, and the $between precedent already refuses it. The pin: "a column listed WITHOUT a type, or not listed at all ... not representable". One rule for every token therefore refuses an unlisted $in / $gt too.
    • A token the text bucket offers ($eq, $contains, and so on) on an unlisted column is unchanged. I followed the ruling's "generalise the $between rule to every token" over the assumption's reading.

Evidence (head 7543da0bd)

  • Red-first. I ran the new test file on the unmodified tree: Tests 14 failed | 11 passed (25). The failures were exactly the predicted legs:

    • every representable: false pin;
    • the swept invariant, which listed 924 broken entries covering both classes;
    • the three real-inspector refusals.

    The WHY, CONTROL, reachable, builder-domain and field-less legs were green.

  • Reverse proof. The fix was committed first. Then objectstack's node scripts/ablation-replace.mjs removed the single builderHolds call in conditionToGroup. The anchor hit once, went from 1 to 0, and the blob changed from 5037f5c55a4d to 927bf20ec8cb.

    • Result over the new file and the 9382 file: Tests 19 failed | 33 passed (52). The 19 are the 14 new pins, the inverted 9382 leg, and the four $between precedent pins.
    • Restore verified: the blob equals HEAD and git diff HEAD is empty.
    • No dist build was involved, because the tests import the bridge by relative path.
  • Targeted tests at 7543da0bd: the new file, the four existing datasetFilterCondition* suites and the three DatasetDefaultInspector* suites. Result: Test Files 8 passed (8), Tests 125 passed (125).

  • Build and type-check. The closure build pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build exited 0. pnpm --filter @object-ui/app-shell type-check (tsc --noEmit && tsc -p tsconfig.test.json) exited 0. --listFiles on tsconfig.test.json lists the new test, the 9382 test and the bridge, once each.

  • Lint, narrowed to the 3 touched files, as a measurement:

    1. Population: app-shell's lint is eslint . under the root eslint.config.js. There is no package-local config.
    2. --format json counts 3 files, 0 errors and 3 no-explicit-any warnings. All three warnings are on the two as any lines that were already there; the base file has the same count.
    3. The config enables no type-aware linting (no projectService or parserOptions.project), so this diff cannot change a verdict on an untouched file.
  • Gates, all exit 0 at 7543da0bd: check-changeset-presence, check-changeset-no-major, check-changeset-fixed, check-changeset-overwrite, check:changeset-claims, check:new-line-citations (0 new), check:control-bytes, check-vi-mock-override-shape, check-test-path-roots, check:phantom-deps, check:esm-specifiers, check:spec-symbols, check:unreferenced-sources, check:self-import, check-lint-coverage.

  • Source-text anchor pins: git grep finds no test or script that quotes the lines this diff changes.

REWORK round 1 (head f3de6d54f)

The contract review passed on the code. The seat's comment 5825731886 on objectui#10257 amended the claim's file surface to add .changeset/9382-dataset-filter-date-operator-round-trip.md, body only. One commit, f3de6d54f, touches that file and nothing else: git diff --name-only 7543da0bd f3de6d54f lists only it. So the source, the tests, and every code measurement above are unchanged.

  • The false sentence. It said an ambiguous $gt / $lt the column cannot settle "falls back to the unchanged fixed table". With a field list, which the inspector always passes, builderHolds now refuses that row and sends the filter to the Source tab. The paragraph now says so. It adds that, with a field list, the refusal covers all three cases it lists. An untyped or unlisted column gets the text menu, which offers neither greaterThan nor lessThan. A menu offering neither candidate cannot offer the fallback, since the fallback is one of them. Only a read with no field list still opens the row under the fixed-table operator.
  • Measured at head with a one-time probe (deleted). With no field list, $gt and $lt open as greaterThan / lessThan. With a field list they are refused when the column is untyped, when it is unlisted, and under each of text, boolean, select, status, lookup, master_detail, user and an unknown type; all eight offer neither candidate.
  • Frontmatter byte-identical. The sha256 of the frontmatter block is d5a45968c712fcbb04974d23f5e3a6a19e3a6dc646176d2d6034bab8f1d93a54 at both 7543da0bd and f3de6d54f.
  • The rest of the file, re-read against head, is left alone.
    • The "6 broke, the other 116" figures are a dated measurement of the pre-objectui#9382 tree, not a claim about head.
    • "The write half is unchanged" and "@object-ui/components is untouched" still hold with this PR applied.
  • Gates at f3de6d54f, all exit 0:
    • check-changeset-presence;
    • check:changeset-claims: 2 changesets in the self-contradiction corpus, both clean, and "No pending changeset names a file this change touches";
    • check-changeset-no-major;
    • check-changeset-overwrite: report-only, case 2, "declared at base: @object-ui/app-shell: patch" and "declares now: @object-ui/app-shell: patch";
    • check:control-bytes;
    • check:new-line-citations: 0 new.
  • Not changed, as the seat directed: the "Advanced filter (nested / OR)" note in DatasetDefaultInspector.tsx.

Acceptance notes

  • An empty field list (loading). DatasetDefaultInspector builds the filter columns from useDatasetFieldCatalog. Its fieldOptions is [] until the base object loads, and stays [] if the load fails.
    • Under the precedent's rule an empty list is still a list. So during that window, a filter whose operator the text bucket lacks (for example $in on a select column) shows the Source-tab note instead of the "N conditions" trigger.
    • With [] the popover draws no builder in either case: it shows "Pick a base object to add filter conditions." Nothing is written in either state.
    • $between has behaved this way since objectui#10062. The changeset says so. The seat ruled to keep it (Q1 A, comment 5825731886).
  • The Source-tab note reads "Advanced filter (nested / OR)". It now also covers incomplete values and off-bucket operators, as it already did for $between. DatasetDefaultInspector.tsx is outside this claim's file surface, so I left the wording alone.
  • The pending objectui#9382 changeset's fallback sentence is corrected in place (see REWORK round 1 above), so it stays true when both changesets publish together.
  • This is a dispatched draft and stays draft. Marking it ready and queueing it belong to the dispatching seat.

Generated by Claude Code

…der can hold

`conditionToGroup` opened two kinds of stored condition as editable rows that
the builder could not hold, and the inspector commits on every change, so an
edit to a SIBLING row rewrote or dropped them:

- an incomplete stored value (`{ $eq: '' }`, `{ $in: [] }`, and the implicit
  `{ field: '' }` spelling), which `groupToCondition` drops as an unfinished
  row; for `$in: []` the loss widened the dataset from no rows to every row;
- a token read back as an operator the column's builder bucket does not offer
  (`$in` on a date or number column, `$gt` on a text column, `$exists` /
  `$null` on a boolean column), which drew a blank operator trigger that one
  field-picker touch reconciled to `equals`.

Every read-back row now goes through one guard, `builderHolds`: the write half
itself must keep the row, and, when a field list is given, the listed column's
bucket (`operatorsForFieldType`, the function the panel uses) must offer its
operator. A row that fails either sends the whole filter to the Source tab.
This is the `$between` guard from objectui#10062, applied to every token
instead of the pair arity only. Reads with no field list skip the bucket
question, as before. The write half is unchanged.

The dateRoundTrip-9382 leg that expected an unlisted column's `$gt` to read
back as `greaterThan` is inverted: the builder draws an unlisted column with
the text bucket, which lacks `greaterThan`.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3045.1 KB 3104.5 KB
Main entry chunk (gzip) 147.9 KB 350 KB
Entry file index-iDTM6DQH.js —
Status PASS —

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


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.68KB 6.20KB
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) 27.95KB 10.04KB
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.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
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) 27.13KB 7.95KB
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) 543.20KB 129.69KB
core (index.js) 9.22KB 3.71KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.86KB 62.25KB
fields (index.js) 256.84KB 65.07KB
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.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
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) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
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) 16.01KB 3.93KB
plugin-calendar (index.js) 51.22KB 14.56KB
plugin-charts (index.js) 74.76KB 20.82KB
plugin-chatbot (index.js) 198.27KB 47.18KB
plugin-dashboard (index.js) 133.45KB 35.33KB
plugin-designer (index.js) 216.12KB 44.37KB
plugin-detail (index.js) 261.04KB 67.97KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 146.34KB 37.47KB
plugin-gantt (index.js) 168.25KB 41.46KB
plugin-grid (index.js) 215.43KB 58.90KB
plugin-kanban (index.js) 49.30KB 15.39KB
plugin-list (index.js) 114.17KB 28.21KB
plugin-map (index.js) 21.83KB 7.07KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.64KB 8.94KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 87.90KB 21.97KB
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) 114.58KB 37.60KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
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) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
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) 5.00KB 2.39KB
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) 17.15KB 6.32KB
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

…l its fallback read now meets

The pending 9382 changeset said an ambiguous `$gt` / `$lt` the column cannot
settle "falls back to the unchanged fixed table". With a field list, which the
inspector always passes, `builderHolds` now refuses such a row and the filter
goes to the Source tab. Published beside this branch's own changeset, the old
sentence would say the filter still opens in the builder. The sentence now
names the refusal. The body is the only change; the frontmatter is
byte-identical.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3044.6 KB 3104.5 KB
Main entry chunk (gzip) 147.9 KB 350 KB
Entry file index-D-6TNxGt.js —
Status PASS —

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


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.68KB 6.20KB
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) 27.95KB 10.04KB
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.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
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) 27.13KB 7.95KB
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) 543.20KB 129.69KB
core (index.js) 9.22KB 3.71KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.86KB 62.25KB
fields (index.js) 256.84KB 65.07KB
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.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
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) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
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) 16.01KB 3.93KB
plugin-calendar (index.js) 51.22KB 14.56KB
plugin-charts (index.js) 74.76KB 20.82KB
plugin-chatbot (index.js) 198.27KB 47.18KB
plugin-dashboard (index.js) 133.45KB 35.33KB
plugin-designer (index.js) 216.12KB 44.37KB
plugin-detail (index.js) 231.66KB 61.23KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 146.34KB 37.47KB
plugin-gantt (index.js) 168.25KB 41.46KB
plugin-grid (index.js) 215.43KB 58.90KB
plugin-kanban (index.js) 49.30KB 15.39KB
plugin-list (index.js) 114.37KB 28.24KB
plugin-map (index.js) 21.83KB 7.07KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.64KB 8.94KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 87.90KB 21.97KB
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) 114.58KB 37.60KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
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) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
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) 5.00KB 2.39KB
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) 17.15KB 6.32KB
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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 7543da0bd560835e823188774d519312bcf6349e

Rendered by an isolated review subagent spawned by the domain:ui#4 seat; its served tier was checked against its transcript stamps (61 of 61 model stamps at the review tier). Adopted by this seat.

① Derived judgments

  • (a) Guard correctness — holds. Read at 7543da0bd560835e823188774d519312bcf6349e. builderHolds(row, fields) asks exactly two questions: groupToCondition({ logic: 'and', conditions: [row] }) === undefined refuses, then if (!fields) return true, then operatorsForFieldType(fieldType).some(...) over the operator id, where fieldType is the listed column's type found by value === row.field. In conditionToGroup every arm now assigns let row (the $exists arm, the $null arm, the $-operator arm via readBackOperator, and implicit equality) and the single if (!builderHolds(row, fields)) return { group: empty, representable: false } sits after the whole if/else chain and before conditions.push(row); the only earlier returns are refusals (multi-key, multi-op, unmapped token), never a pushed row. So every read-back row, from every arm, for every token, passes through it.
    • "Survives the write half as-is" is literally the exported groupToCondition, the same symbol DatasetFilterField.commitFilterGroup calls; not a copy of its rules.
    • The bucket check is operatorsForFieldType(fieldType) with no second argument (extras default to an empty list): the same call readBackOperator makes, and the same function FilterBuilder.getOperatorsForField calls as operatorsForFieldType(field?.type, extraOperators), where the inspector mounts FilterBuilder with no extraOperators prop, so the default applies. The unlisted / untyped lookup (fields.find by value, then ?.type) mirrors the builder's own; operatorsForFieldType maps a falsy type to the text bucket. No second operator table exists in the bridge: builderHolds reads no list of its own, and filterValueArity is no longer imported.
    • Spot-checks by reading the bucket lists in filter-builder.tsx: selectOperators offers in, so a select column with $in and a non-empty list reads back as in (single preimage), is kept by isFilterValueComplete (non-empty list), and stays representable; pinned by the new file's "CONTROL: $in on a select column" and by the real-inspector CONTROL. numberOperators offers greaterThan (not after), so a number column with $gt settles on greaterThan, is complete, is offered, and stays representable; pinned by the untouched 9382 controls "greaterThan IS a member of the number bucket" and "a number column still round-trips greaterThan / lessThan unchanged". 0 and false are values, not blanks (isFilterValueComplete), pinned by the CONTROL leg.
    • Can a filter that round-tripped faithfully on base now be refused? By construction a refusal needs either the write half to drop the row (then a sibling edit already erased it on base) or the bucket to lack the operator (then the panel drew a blank trigger and a field-picker touch rewrote it). Neither was a faithful round trip. No bucket offers both readings of an ambiguous token (number has only greaterThan / lessThan, date-like only after / before, the rest neither), so every unsettled $gt / $lt lands on a bucket lacking the fixed-table answer, consistent with the dev's off-bucket accounting. The 7980-read probe is the dev's one-time measurement and was not re-run here (rule); my reading finds no representable, faithfully round-tripping shape that the guard refuses. Pre-existing and unchanged: implicit equality { field: 'x' } writes back in its $eq spelling (same predicate, not byte-identical); every $-token form is byte-identical, which the swept CONTROL asserts over all 14 buckets.
    • Q1 and Q2 as shipped match the seat's answers: an empty list is a list (truthy), judged against the text bucket, identical to the $between precedent; an unlisted column is judged against the text bucket the builder draws.
  • (b) Write half unchanged — byte-identical. Extracted groupToCondition from base 747f3347d5a7d129cff52bd00b564de25d4874f3 and head: 2089 bytes, identical sha1. isClearedGroup, liveRows, the readBackOperator body, OP_TO_MONGO, MONGO_TO_OP, MONGO_PREIMAGE and VALUELESS_TO_MONGO are also byte-identical. The diff's hunks are: the header doc, the import line (drops filterValueArity), two doc lines on readBackOperator, the new builderHolds with its doc, and the conditionToGroup body (the pair-only $between block removed, let row plus the guard added).
  • (c) The inverted 9382 leg — forced, reasoned, nothing deleted; the 9382 changeset sentence is FALSE as published.
    • Forced by the precedent: the removed base comment said "A column listed WITHOUT a type, or not listed at all, is not unknown to the builder — it draws the default text bucket for both, which lacks between — so both are refused"; the 10062 changeset says the same; the 9382 file's own $between pin "a column listed WITHOUT a type, or not listed at all, draws the builder's default text bucket — not representable" carries it. textOperators lacks greaterThan, so generalising that rule to every token (the ruling, and the seat's Q2 answer A) must refuse { mystery: { $gt: 1 } } with a field list that omits mystery.
    • Reasoned in the file: a six-line comment says readBackOperator still yields the fixed-table id (nothing invented), the builder draws the unlisted column with the text bucket, which lacks greaterThan, so the row would open under a blank trigger, and 10257 refuses as 10062 did for $between; the leg asserts the premise (offeredBy('text') lacks greaterThan) and adds a CONTROL that an unlisted $eq still reads back as equals.
    • No case deleted: it( count 16 before and 16 after; it( plus it.each( 19 and 19; expanded cases 27 in both (6+1, 3+1, 2, 2, 4, 6+2). One leg renamed and inverted, with a CONTROL added inside it.
    • .changeset/9382-dataset-filter-date-operator-round-trip.md: "Nothing new is accepted: when the type is unknown, the field is absent from the list, or the bucket offers neither candidate, the read falls back to the unchanged fixed table." At head that is still true of the operator id inside readBackOperator, but in all three named cases, with a field list (which the inspector always passes), builderHolds then refuses, because no bucket that leaves $gt / $lt unsettled offers greaterThan / lessThan. A reader of that entry would take it to mean such a filter still opens in the builder with the fixed-table operator; in the inspector it no longer does. Judged FALSE as published. The 10257 changeset states the interaction correctly. Minimal in-place correction, one clause appended to that sentence: "...the read falls back to the unchanged fixed table — and, since objectui#10257, a row whose column menu does not offer that fallback operator is sent to the Source tab rather than opened." That file is outside claim 5825222085's file surface, so the edit belongs to the seat; the dev flagged it, correctly, instead of breaching.
  • (d) The pins are real and can fail — consistent.
    • datasetFilterCondition.readHalfHolds-10257.test.tsx imports the real DatasetDefaultInspector and renders it with draft / onPatch; only the ./useDatasetFields catalog hooks are mocked (a six-column catalog matching FIELDS). DatasetDefaultInspector mounts the real DatasetFilterField, which calls the real conditionToGroup(value, fields) and renders either the "Advanced filter" note or the "N condition(s)" trigger. The three refusals assert the note, the absence of the trigger, and no onPatch; the CONTROL opens the popover and finds "In". So yes, the real inspector and the real DatasetFilterField are driven.
    • Case accounting: 25 cases (class 1: 8; class 2: 11 with it.each × 5; invariant: 2; inspector: 4 with it.each × 3). Red on base = 14: the two THE CARD legs, every-incomplete-shape, empty-field-name (4); the five off-bucket it.each rows plus the without-a-type / not-listed leg (6); the swept invariant (1); the three real-inspector refusals (3). The eleven WHY / CONTROL / reachable / premise / field-less / builder-domain legs are green by construction. Matches "14 failed | 11 passed (25)".
    • Reverse proof: 25 + 27 = 52. With the guard ablated the same 14 go red, plus the inverted 9382 leg, plus exactly the four $between pins in the 9382 file that only the guard now supplies (the NUMBER-column refusal; "representable exactly when that bucket offers between"; without-a-type / not-listed; the real-inspector number-column note); the date CONTROL, the field-less read, the WHY leg and the inspector date CONTROL stay green. 14 + 1 + 4 = 19. Consistent with the case lists.
  • (e) Changeset truth — every sentence of .changeset/10257-dataset-filter-read-half-holds.md is TRUE at head; no line-address citations (scan: none; the Line Citation Gate check is success). Verified by reading: the inspector mounts DatasetFilterField for both the dataset filter and each measure filter and commits on every change; isFilterValueComplete treats null, '' and [] as unfinished for scalar and list rows; dateOperators and numberOperators lack in; textOperators lacks $gt / $gte / $lt / $lte readings; booleanOperators is equals / notEquals only; the reconcile-to-equals consequence is pinned in the premise leg; $or and nested groups already refuse; "the save path itself is unchanged" (byte-identical); "A read with no field list skips the menu check, as before" (if (!fields) return true; base had no menu check outside the $between block, which skipped the same way); the 9382 interaction sentence "no menu that leaves it unsettled offers greaterThan or lessThan" holds against the bucket lists; useDatasetFieldCatalog starts at fieldOptions: [] and resets to [] on a failed load; an empty list is truthy so the text bucket applies until load, and the base $between block behaved identically. Other pending changesets re-read: 10062-dataset-filter-between-arm.md ("exactly two cases" for $between are exactly what builderHolds asks of it) stays true; 9363-dataset-filter-isnull-erases.md stays true for any column whose menu offers Is null (a boolean $null is authorable only outside the builder); 9372-unmapped-operator-inert.md stays true; 9382 as judged in (c).
  • (f) User-visible effect — a generic note, not a false statement; polish for the seat. The operative sentence "edit it in the Source tab" is true. The parenthetical "(nested / OR)" was already an under-inclusive example list before this PR: multi-operator objects, unmapped tokens, and since 10062 a half or off-bucket $between all showed the same note. An author with { name: { $eq: '' } } now sees a label that does not describe their filter, which is wording drift, not a claim about the stored bytes. A one-line reword in DatasetDefaultInspector.tsx (for example "This filter can't be edited here — edit it in the Source tab.") is outside this claim's file surface; the dev noted it and left it, correctly.

② Semver level

patch on @object-ui/app-shell is right. The rule, objectui AGENTS.md, "版本号策略(version alignment)": "minor/patch 独立演进——objectstack 没动时不必跟发;objectui 自己的改动照常用 changeset 推进" and "推论:changeset 里不要声明 major —— fixed 组任一 major 都会把全组推上去、脱离 objectstack 的节奏。objectui 自身的破坏性变更也标 minor(在正文里写清 breaking 语义即可);唯一例外是跟随 objectstack 跨 major 的那一次同步升级" and "这是约定优先于 semver 纯粹性的取舍 … objectui 的 major 不代表「它自身 API 的破坏性变更次数」". Never major: the Changeset Bump Policy check (check-changeset-no-major) is success. "Breaking" in the rule's sense is objectui's own API breakage, which earns minor. This PR moves no declared key, schema, prop or export: datasetFilterCondition.ts is not re-exported from app-shell's public entry (no index re-export; the only out-of-tree mentions are comments in packages/core/src/adapters/ValueDataSource.ts and packages/plugin-list/src/ListView.tsx); conditionToGroup's signature is unchanged; stored filter bytes are untouched. A representable-to-Source-tab change for some stored filters corrects a representable: true that was false (the row was dropped or rewritten on the next sibling edit); it is user-visible bug-fix behaviour of the same class the sibling fixes declared as patch: 9363, 9372 (self-described "Behaviour change, not just a fix"), and 10062, which introduced $between refusals on every non-date bucket. Not breaking in the rule's sense. @object-ui/app-shell is in the fixed group, so the declaration is required and present.

③ Boundary flags

  • Closing keyword: the PR body carries exactly one, Fixes #10257; neither commit message carries any. Clean.
  • Model identifier: none in the PR title, the body, or either commit message or trailer; the trailers carry an assistant name and a session URL only. Clean.
  • File surface: 4 files, matching the claim's "bridge, tests beside it, one .changeset/10257-…md": datasetFilterCondition.ts (M), datasetFilterCondition.readHalfHolds-10257.test.tsx (A), datasetFilterCondition.dateRoundTrip-9382.test.tsx (M, 11 added / 2 removed, in the same metadata-admin/inspectors/ directory as the bridge), .changeset/10257-dataset-filter-read-half-holds.md (A). The write half is untouched, consistent with the claim's conditional on groupToCondition.
  • Serial: 15 open PRs; the file lists of all 14 others were read (10441 on retry after a transient server error; 5400, the release PR, across 600 .changeset files). None touches datasetFilterCondition*, DatasetDefaultInspector*, DatasetFilterField, or any of the bridge's pending changesets. Clean.
  • mergeable_state: behind (mergeable: true); main advanced from 747f3347d5a7d129cff52bd00b564de25d4874f3 to 94436f42a54d806b7d92042faa460c9684ff183f after the branch was cut. No conflict. The PR is a draft, as the dev's report says it should stay.
  • Check-runs on 7543da0bd560835e823188774d519312bcf6349e: polled in the foreground at 60-second intervals; all completed at t+123s. 43 check-runs: 40 success, 3 skipped, 0 failure / cancelled / timed_out / action_required, 0 in_progress. Every non-success by name: dependabot (skipped), Test (coverage shard ${{ matrix.shard }}/4) (skipped), Test (coverage) (skipped). All eight test shards, Type Check, Lint, Build & E2E, Spec Main Shape Gate, Changeset Bump Policy, Changeset Declaration, Changeset Fixed Group Check, Changeset Overwrite Report, Changeset Claim Re-read, Line Citation Gate, Control Byte Scan and Inert vi.mock Specifier Check are success.
  • Follow-ups for the seat, both outside this claim's surface and both flagged by the dev: amend the 9382 changeset sentence as named in ①(c); reword the Source-tab note as named in ①(f).

Implemented-by: claude/issue-10257-dataset-filter-read-half
Reviewed-by: session_01BP8CMtACxTdLjqR6rhd33C

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f3de6d54f0dad20b11c914ce7bf971e1aeb3e3de

Rendered by an isolated review subagent spawned by the domain:ui#4 seat; its served tier was checked against its transcript stamps (67 of 67 model stamps at the review tier). Adopted by this seat. Focused re-review of REWORK round 1; the prior record at 7543da0bd560835e823188774d519312bcf6349e covers the unchanged code.

① Derived judgments

  • (a) Only the 9382 changeset moved; frontmatter byte-identical at four refs — holds. git diff --name-status from 7543da0bd to f3de6d54f, with rename detection on, lists exactly one path: M .changeset/9382-dataset-filter-date-operator-round-trip.md, +7 / -1, one hunk. The packages tree object is the same at both heads (7752b048d1c8013d622465e50eb07cf543e39387), so no code or test file moved, and the 10257 changeset blob is the same at both heads (418ea00b81df6ece8eea9fac899a3440def530a8). The frontmatter block (both fences inclusive, trailing newline; the text is ---, '@object-ui/app-shell': patch, ---) has sha256 d5a45968c712fcbb04974d23f5e3a6a19e3a6dc646176d2d6034bab8f1d93a54, computed by me, at head, at 7543da0bd, at the merge base 747f3347d5a7d129cff52bd00b564de25d4874f3, and at origin/main (0348bc9f1, 13 commits past the branch; the file there is still blob e93e2cc3d, the merge-base version, so main has not touched it). Matches the dev's figure.
  • (b) Every sentence of the 9382 changeset at head is TRUE, or is a dated measurement scoped to the past; no line-address citations. Re-read at head. readBackOperator settles a multi-preimage token only when fieldType is truthy and exactly one candidate is offered by operatorsForFieldType(fieldType), otherwise it returns MONGO_TO_OP[mop] ($gt reads as greaterThan, $lt as lessThan); MONGO_PREIMAGE is derived from OP_TO_MONGO, so $gt has the candidates greaterThan and after, $lt has lessThan and before; builderHolds returns false when groupToCondition drops the single row, true when fields is undefined, and otherwise requires operatorsForFieldType(listedType) to offer row.operator; conditionToGroup runs it after every arm and before the push. Buckets in packages/components/src/custom/filter-builder.tsx at head, with the opt-ins (containsCaseInsensitive, exists, notExists) excluded because the inspector grants none: text offers none of greaterThan, lessThan, after, before; number-like (number, currency, percent, rating) offers greaterThan and lessThan only; date-like (date, datetime, time) offers after and before only; boolean, select-like (select, status) and lookup-like (lookup, master_detail, user) offer none of the four; a falsy, unrecognised or retired type falls to the text bucket. No bucket offers both readings of either token.
    • The corrected sentences. "Since objectui#10257, a row whose column menu does not offer that fallback operator is sent to the Source tab instead of being opened": true, builderHolds false gives representable: false and DatasetFilterField renders the Source-tab note (precision only: the whole filter goes to the Source tab, as the 10257 entry says; not false). "With a field list, which the inspector always passes": DatasetFilterField declares fields as a required prop and calls conditionToGroup(value, fields); both mount sites pass fields={filterFields}, an array derived from fieldOptions that may be empty but is never absent; this is the only non-test caller of conditionToGroup. "that covers all three cases": an unknown type (untyped, or an unrecognised string) lands on the text bucket; a column absent from the list gives fields.find undefined and the text bucket; a bucket offering neither candidate is re-asked by builderHolds and lacks the fallback. "An untyped or unlisted column gets the text menu, which offers neither greaterThan nor lessThan": true. "a menu that offers neither candidate cannot offer the fallback, which is one of them": MONGO_TO_OP.$gt is greaterThan, one of the two $gt candidates; likewise lessThan for $lt. "Only a read with no field list still opens the row under the fixed-table operator": if (!fields) return true, pinned by the 9382 file's "reads back through the fixed table when no fields are supplied" leg.
    • The sentences left alone. "Nothing new is accepted: ... the read falls back to the unchanged fixed table" is still true of the operator id. "The write half is unchanged and the stored bytes are unchanged": groupToCondition extracted at the merge base and at head is 2098 bytes with an identical sha256, and this PR rewrites no stored bytes. "@object-ui/components is untouched": the PR's diff against the merge base has zero paths under packages/components. The narrative sentences (blank trigger, reconcile to equals, one spec token and no $after, conditionToGroup takes the same fields list the inspector hands the builder) hold against the source above. "6 broke ... and the other 116 round-tripped exactly" is in the past tense, sits before the paragraph that says "now", and the 9382 test header attributes those figures to "the pre-fix tree": a dated measurement, not a claim about head. Observation for the record, outside this PR's claim: the buckets are identical at the pre-fix tree (e05553c46) and at head, and over the 14 probe types they give 119 pairs, 116 with between then unmapped and skipped, so a 122-pair denominator cannot be reconstructed from them; this PR neither restates nor depends on that figure.
    • Line-address citations: a regex scan for path-colon-number, L-number and "line N" forms finds none in the file; the Line Citation Gate check on the head is success.
  • (c) The 10257 changeset is still TRUE at head. Its blob and the code it describes are unchanged from the previously reviewed head, so the prior record's sentence-by-sentence verification carries over. Its 9382 sentence, re-judged against the amended entry: "The objectui#9382 entry says that a $gt / $lt the column's menu cannot settle (...) reads back through the fixed table" remains an accurate report, because the amendment appends after the "falls back to the unchanged fixed table" clause and leaves it in place; "It still settles on that operator" holds (readBackOperator unchanged); "no menu that leaves it unsettled offers greaterThan or lessThan" holds, since the menus that leave it unsettled are exactly those offering neither candidate and none offers both. The two entries now say the same thing about the same rows; no contradiction between them.
  • (d) The proposed PR body: every factual sentence TRUE at head. Closing keyword: the scan finds exactly one, Fixes #10257; PR #10253 and the objectui#NNNN cross-references carry no keyword. Clause-② no is right: @object-ui/app-shell's package exports are . (the dist index.js and index.d.ts) and ./styles.css; src/index.ts re-exports nothing from views/metadata-admin/inspectors/; the bridge's export list is the same seven names at the merge base and at head, the diff touches no export line, conditionToGroup's signature is unchanged, and builderHolds is a non-exported module-level function, so no exported symbol or type is added or widened. REWORK section: the seat's comment 5825731886 is on issue 10257, amends claim 5825222085's file surface to that changeset "body only, frontmatter byte-identical", answers Q1 A and Q2 A, and records the Source-tab note as not changed; "one commit ... touches that file and nothing else" is verified in (a); "the source, the tests, and every code measurement above are unchanged" is verified by the tree hash; the probe's claims (field-less $gt / $lt open as greaterThan / lessThan; refusal when untyped, unlisted, and under text, boolean, select, status, lookup, master_detail, user and an unknown type, all eight offering neither candidate) are each derivable from the source and buckets above; the probe itself is deleted and was not re-run. Gate list: check:changeset-claims, check:control-bytes and check:new-line-citations are root package.json scripts and the three check-changeset-* scripts exist under scripts/; "No pending changeset names a file this change touches", "declared at base", "declares now" and "Report-only" are strings those gates print, and "case 2" is the overwrite gate's own label for "CORRECTING a declaration on purpose ... prose that no longer matches the change. Legitimate"; the gates were not re-run here (rule), and the matching checks on the head (Changeset Declaration, Changeset Claim Re-read, Changeset Bump Policy, Changeset Overwrite Report, Control Byte Scan, Line Citation Gate) are all success. Acceptance notes: useDatasetFieldCatalog initialises fieldOptions: [] and resets it to [] in its catch; an empty array is truthy, so the text bucket applies; the popover's fields.length === 0 branch renders "Pick a base object to add filter conditions." and mounts no FilterBuilder, so nothing is written in either state; the note text "Advanced filter (nested / OR)" is verbatim in DatasetDefaultInspector.tsx, whose blob is identical at the merge base and at head; "The seat ruled to keep it (Q1 A)" matches the comment; the PR is draft: true. Line-address citations: none (same regex scan). Model or version identifiers: scanned for vendor model-id prefixes, family names, dotted version numbers and bracketed context-window suffixes; none in the proposed body. The short shas it cites (747f3347d, 7543da0bd, f3de6d54f) all resolve.
  • (e) Code unchanged — confirmed. The packages tree object is identical at 7543da0bd and f3de6d54f, git diff --stat between them under packages/ is empty, and rename detection reports only the changeset. The earlier PASS items carry over.

② Semver level

patch on @object-ui/app-shell in both changesets, never major — holds. The 9382 frontmatter is unchanged (sha256 d5a45968c712fcbb04974d23f5e3a6a19e3a6dc646176d2d6034bab8f1d93a54 at head, prior head, merge base and origin/main), and the 10257 changeset's frontmatter is the same '@object-ui/app-shell': patch. The Changeset Bump Policy and Changeset Fixed Group Check checks on the head are success. The prior record's reasoning stands: the new commit is prose only, changes no bump level, and the PR moves no exported symbol, type, prop, schema key or stored byte.

③ Boundary flags

  • Closing keyword: the body currently on GitHub carries exactly one, Fixes #10257; the proposed body likewise; none of the three commit messages carries any. Clean. Note for the seat: the current body's Acceptance note still says this PR "states the interaction instead of editing that changeset", which is now stale; the proposed body replaces that bullet, so writing it closes the gap.
  • Model identifier: none in the new commit's subject or body; its trailers are the same two forms as on the two earlier commits, an assistant name in a co-author trailer and a session URL, judged clean in the prior record. Scanned for vendor model-id prefixes, family names, dotted version numbers and bracketed context-window suffixes.
  • File surface across the whole PR (merge base 747f3347d to head), exactly five paths, and REST reports changed_files: 5 over 3 commits:
    • M packages/app-shell/src/views/metadata-admin/inspectors/datasetFilterCondition.ts (the bridge)
    • M packages/app-shell/src/views/metadata-admin/inspectors/datasetFilterCondition.dateRoundTrip-9382.test.tsx
    • A packages/app-shell/src/views/metadata-admin/inspectors/datasetFilterCondition.readHalfHolds-10257.test.tsx
    • A .changeset/10257-dataset-filter-read-half-holds.md
    • M .changeset/9382-dataset-filter-date-operator-round-trip.md
  • mergeable_state: behind at the first fetch (mergeable: true; main is 13 commits ahead of the branch, none touching the PR's paths or the filter-builder buckets), clean on the refetch after the check-runs completed; the head was unmoved at f3de6d54f0dad20b11c914ce7bf971e1aeb3e3de, open, draft.
  • Check-runs on the new head: polled in the foreground at 60-second intervals; 11 were in progress at the first snapshot and all had completed by the second poll. 43 check-runs: 40 success, 3 skipped, 0 failure / cancelled / timed_out / action_required, 0 in_progress. Every non-success by name: dependabot (skipped), Test (coverage shard ${{ matrix.shard }}/4) (skipped), Test (coverage) (skipped). All eight test shards, Test, Test (dist pins), Type Check, Lint, Build & E2E, Spec Main Shape Gate, Changeset Bump Policy, Changeset Declaration, Changeset Fixed Group Check, Changeset Overwrite Report, Changeset Claim Re-read, Line Citation Gate, Control Byte Scan and Inert vi.mock Specifier Check are success. Same counts as on the prior head.

Implemented-by: claude/issue-10257-dataset-filter-read-half
Reviewed-by: session_01BP8CMtACxTdLjqR6rhd33C

VERDICT: PASS


Generated by Claude Code

@os-litant
os-litant marked this pull request as ready for review September 25, 2026 03:51
@os-litant
os-litant added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit 34bfd60 Sep 25, 2026
45 checks passed
@os-litant
os-litant deleted the claude/issue-10257-dataset-filter-read-half branch September 25, 2026 04:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants