Skip to content

fix(fields): lookup candidate queries expand the reference columns they display (objectui#10223) - #10341

Merged
os-litant merged 8 commits into
mainfrom
claude/issue-10223-lookup-candidates-expand
Sep 24, 2026
Merged

os-litant merged 8 commits into
mainfrom
claude/issue-10223-lookup-candidates-expand

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #10223

What changes

  • LookupField: the dropdown's candidate query now sends expand = buildExpandFields(referenced schema fields, previewed columns). The previewed columns are the columns the option subtitle shows (by default the leading highlightFields, display field excluded). The recently-used rail asks for the same expansion.

  • RecordPickerDialog: its query now sends expand = buildExpandFields(fieldsMeta, rendered columns minus the id column). The id column is left out because getRecordId reads it raw.

  • No displayed column ⇒ no $expand. buildExpandFields reads an EMPTY column list as "no restriction" and returns every relation the object declares. So both expansions return nothing when their column list is empty:

    • the dropdown previews nothing when its picker columns are the display field alone (a highlightFields naming only it, or every other field system-managed or hidden);
    • the picker renders nothing besides the id when columns is just the id, or displayField equals idField with no columns.

    Without the guard, the dropdown and the recents rail asked for every declared relation (created_by, owner_id, …) and rendered none of them.

  • Field-level security gates both expansions. It uses the objectui#7429 sweep's caller-side shape on buildExpandFields' output: !perms.isLoaded || perms.checkField(referenced object, f, 'read') through usePermissions(), with perms in the memo deps. Once the policy has loaded, a relation it denies is not asked for; before it loads, nothing is filtered. This covers the dropdown, its recents rail and the picker.

  • New dependency: @object-ui/fields → @object-ui/permissions (workspace:*). @object-ui/permissions depends only on @object-ui/types, so the edge adds no cycle. Against main, the pnpm-lock.yaml hunk is the new importer link alone (3 lines), generated by pnpm install --lockfile-only over an installed tree.

  • Expansion stays a display concern. Options, the titleFormat reading of a row, the committed value and the records handed to onSelectRecord / onSelectRecords are all built from the row with its relations collapsed back to ids, using core's toPredicateRecord. Only the preview cells and table cells render the expanded record. Without that collapse, a titleFormat naming the expanded field printed C-0 - [object Object] (ablation 2a below).

  • useRecordQuery is untouched. It already forwards expand as $expand.

  • Prose made false by this change, corrected:

    • the one sentence in lookupColumnDisplay.tsx's module header that said the two surfaces agree "without either query changing";
    • the header of LookupField.pickerAgreement.test.tsx, which now names what that file pins: a backend that ignores $expand. Its assertions are unchanged;
    • the pending .changeset/lookup-dropdown-cell-renderer-5492.md, which publishes into the same @object-ui/fields release notes. It said in the present tense that neither surface's request carries populate; that clause now reads as history ("at the time") and names this change. Frontmatter is untouched.
  • origin/main is merged in twice (merge commits 177e5b5f0 and a2de421be, bringing main to 4215ed76b and then 86982ace0), which carry the Spec Main Shape Gate fix 1dbb9933c. Of this PR's files, the second merge touched only pnpm-lock.yaml, and only in another importer.

Measured: fixture mirroring the card

The fixture has 50 candidates. highlightFields is code, task, version, with task a master_detail field to task. There are no lookup_columns, and no permission policy is loaded. The "before" leg ran with both source files at base 8b1f06619. The "after" leg ran at 8f70a8b74; neither the gate nor the empty-list guard adds a request.

reading dropdown (card) dropdown (control: no reference column shown) browse-all picker (card)
before: candidate queries · $expand · per-row findOne 1 · none · 50 1 · none · 0 1 · none · 10 (one page)
after 1 · ['task'] · 0 1 · none · 0 1 · ['task'] · 0

Schema reads are a constant on both legs: the task_version schema at mount, plus the task schema once (module-cached) when a task cell renders.

Mechanism assumptions

  • A1 holds. LookupCellRenderer gives an expanded object no primitive id, so useLookupName never fetches. Its object branch names the record. packages/fields/src/index.tsx is not edited.
  • A2 holds. useRecordQuery forwards expand.
  • A3, as first dispatched, was falsified. The objectui#7215 gate lives in each caller, and @object-ui/fields had no path to the policy. The seat widened the claim's surface (issue comment 5820197003), and the gate now lands here, in the family's shape.
  • Out of scope by the seat's ruling, and to be filed by the seat as one card at acceptance:
    • the dropdown / picker display columns are not FLS-filtered (RelatedList's are);
    • PeoplePicker's $expand is ungated.

Tests (HEAD 9796fdbb4)

  • LookupField.candidateExpand-10223.test.tsx, 13 tests:
    • the card count, a user column, the control and the picker count;
    • two empty-list pins:
      • highlightFields ['name'] over an object declaring created_by (user), owner_id (lookup), task (master_detail) and owner (user): neither the dropdown request nor the recents-rail request carries a $expand key;
      • a picker whose only column is the id: no $expand key;
    • dropdown and picker invariance: labels, a titleFormat naming the expanded field, subtitles, and the callback payloads are identical whether or not the backend honours $expand;
    • five FLS pins against the real PermissionProvider:
      • a denied task_version.task is left out while the readable owner stays, on the dropdown, the recents rail and the picker;
      • a readable task is kept;
      • no provider filters nothing.
  • Full @object-ui/fields suite, run in two locked halves:
    • widgets/: 75 files passed, 696 tests passed;
    • the rest: 106 files passed plus 1 skipped, 2364 tests passed plus 7 skipped.
  • 88 consumer test files: every test outside packages/fields that names the picker or lookup trigger, plus every test that mocks @object-ui/permissions. 88 files passed, 1026 tests passed.
  • pnpm --filter @object-ui/fields run type-check exits 0. It ran after pnpm --filter '@object-ui/fields^...' run build, a closure that includes @object-ui/permissions. The new test file is in the test program.
  • Lint on the touched sources: 0 errors. Warning counts equal their base: LookupField.tsx 59, RecordPickerDialog.tsx 35.

Ablations

Each ablation ran with the fix committed. The restore was git checkout HEAD, proved by blob hash and an empty git diff HEAD.

  1. Both sources at base. The count tests go red: expected undefined to deeply equal [ 'task' ].
  2. 2a: dropdown options built from the expanded row. The dropdown invariance test goes red, with labels reading C-0 - [object Object].
  3. 2b / 2c: the picker's payload / title template read the expanded row. The picker invariance test goes red, with titles reading C-0 - [object Object] in 2c.
  4. 3a: LookupField's FLS filter removed. The dropdown and recents-rail deny pins go red.
  5. 3b: RecordPickerDialog's FLS filter removed. The picker deny pin goes red.
  6. 4: both sources at the pre-guard b7ee692eb. Both empty-list pins go red (expected true to be false on the $expand key); 2 failed, 11 passed.

Gates (exit 0 at 9796fdbb4)

  • check-changeset-presence
  • check-changeset-no-major
  • check:new-line-citations: 0 new
  • check:phantom-deps
  • check:unused-deps
  • check:control-bytes
  • check:changeset-claims
  • check:pending-changeset-literals
  • check:lockfile-integrity: clean
  • check:lockfile-dedupe: deduped
  • Governed surface: NOT GOVERNED.

Acceptance notes

  • user columns. buildExpandFields includes user, so a previewed user column now renders an avatar plus a name, where it used to show the unresolved raw-id marker. The avatar inside the one-line dropdown subtitle was not checked in a browser (NOT MEASURED: visual).
  • Numeric ids. toPredicateRecord returns ids as strings, so on a numeric-id backend that honours $expand, callback payloads carry the string form of an expanded column's key. The committed value is unaffected. The changeset says so.
  • Late schema or policy. If the referenced schema lands after the dropdown opens, the first query goes out without $expand and a second one with it. A permission policy landing after the dropdown opens triggers a refetch only when it removes a name from the list.
  • Recents labels. The recents rail now derives its option labels from the current schema.
  • Lockfile. Run in a fresh worktree with no installed tree, pnpm install --lockfile-only also re-resolved unrelated esbuild peer variants. Run over an installed tree, it gave the 3-line edge alone, and that is the hunk committed. check:lockfile-integrity and check:lockfile-dedupe are both clean.

Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C


Generated by Claude Code

…ey display (objectui#10223)

Opening a lookup's dropdown sent a candidate query with no `$expand`, so
every previewed lookup / master_detail column came back as a bare foreign
key and the lookup cell renderer resolved each one with its own `findOne`:
one request per candidate per such column on every open. The browse-all
picker had the same shape.

Both candidate queries now ask for `$expand` over the reference columns
they display, by `buildExpandFields`' rule (the inline dropdown's previewed
columns; the picker's rendered columns minus the id column). The recents
rail asks for the same expansion as the main list.

Expansion stays a display concern: options, the `titleFormat` reading of a
row and the records handed to `onSelectRecord` / `onSelectRecords` are built
from the row with its relations collapsed back to ids (core's
`toPredicateRecord`), so labels, committed values and host payloads read as
before; only the previews and table cells render the expanded record.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
…icker-agreement prose (objectui#10223)

- The candidate-expand suite now also pins that a previewed `user` column
  rides `buildExpandFields`' rule and names the person.
- `LookupField.pickerAgreement.test.tsx` said neither surface's query carries
  `$expand`; both now do, and that file's backend ignores the parameter, which
  is what makes it the bare-id fallback pin. Prose corrected, assertions
  unchanged.
- Changeset: patch for `@object-ui/fields`.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
… (objectui#10223)

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) 3040.0 KB 3104.5 KB
Main entry chunk (gzip) 147.8 KB 350 KB
Entry file index-BQGR3O0f.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) 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) 541.41KB 129.42KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 222.47KB 61.83KB
fields (index.js) 254.13KB 64.27KB
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) 33.36KB 10.88KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.15KB 11.05KB
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) 14.81KB 3.63KB
plugin-calendar (index.js) 51.44KB 14.62KB
plugin-charts (index.js) 71.82KB 20.13KB
plugin-chatbot (index.js) 198.27KB 47.18KB
plugin-dashboard (index.js) 133.06KB 35.21KB
plugin-designer (index.js) 216.12KB 44.37KB
plugin-detail (index.js) 260.65KB 67.82KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 144.77KB 37.17KB
plugin-gantt (index.js) 167.99KB 41.37KB
plugin-grid (index.js) 215.46KB 58.88KB
plugin-kanban (index.js) 49.30KB 15.39KB
plugin-list (index.js) 113.90KB 28.11KB
plugin-map (index.js) 21.74KB 7.07KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.68KB 8.95KB
plugin-tree (index.js) 10.56KB 3.71KB
plugin-view (index.js) 85.18KB 21.05KB
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) 110.46KB 36.33KB
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 (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

…-expand

Brings the branch past the Spec Main Shape Gate fix (1dbb993). No overlap with this branch's files.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
…and FLS gate (objectui#10223)

@object-ui/permissions depends only on @object-ui/types, so the edge adds no cycle. The lockfile hunk is the new importer link alone, generated by 'pnpm install --lockfile-only' over an installed tree.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
…n (objectui#10223)

The dropdown's candidate query, its recents rail and RecordPickerDialog now
filter `buildExpandFields`' output through `usePermissions().checkField(
object, field, 'read')` once the policy has loaded, the shape the
objectui#7429 sweep applied at every other call site; with no policy loaded
nothing is filtered, and `perms` in the memo deps rebuilds the list when the
answer arrives.

Pins (real PermissionProvider): a denied `task_version.task` is left out of
`$expand` while the readable `owner` stays, on the dropdown, the recents rail
and the picker; a readable `task` is expanded; no provider filters nothing.

`lookupColumnDisplay.tsx`: its module header said the two surfaces agree
"without either query changing", which this change made false; that one
sentence is corrected. Changeset updated for the gate and the new dependency.

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

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 3 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/5793-spec-range-floors.md

  • names pnpm-lock.yaml → pnpm-lock.yaml — edited by this change

    Nothing a consumer installs today changes: normal resolution already picks the newest 17.x, and pnpm-lock.yaml still resolves 17.2.0 on this edge after the bump. The change is to the declared floor only, which is why it is scored patch rather than minor — the same reasoning objectui#5753 used for the other direction on this dependency.

.changeset/6361-spec-floor-17-2-0.md

  • names pnpm-lock.yaml → pnpm-lock.yaml — edited by this change

    Nothing a consumer installs today changes: normal resolution already picks the newest 17.x, and pnpm-lock.yaml still resolves 17.2.0 on both edges after the bump — only the recorded specifier: moves. No source and no behaviour changes, which is why this is scored patch, on the reasoning objectui#5793 used for the same remediation on @object-ui/plugin-detail.

.changeset/7122-objectstack-family-17-3-0.md

  • names pnpm-lock.yaml → pnpm-lock.yaml — edited by this change

    @objectstack/client, core, formula and lint each pin @objectstack/spec EXACTLY, so resolving the spec alone to 17.3.0 left the console bundling TWO copies of it. Moving the family with it in pnpm-lock.yaml collapses the duplicate; every declared range already admitted 17.3.0, so no manifest moved.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with 8c10f4f71 (merge-base with origin/main): 7 file(s) changed outside .changeset/, read against 1330 pending declaration(s) that publish a body (1903 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3040.0 KB 3104.5 KB
Main entry chunk (gzip) 147.9 KB 350 KB
Entry file index-OLznHqK1.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) 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) 541.41KB 129.42KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 222.47KB 61.83KB
fields (index.js) 254.42KB 64.39KB
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) 33.36KB 10.88KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.15KB 11.05KB
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) 14.81KB 3.63KB
plugin-calendar (index.js) 51.44KB 14.62KB
plugin-charts (index.js) 71.82KB 20.13KB
plugin-chatbot (index.js) 198.27KB 47.18KB
plugin-dashboard (index.js) 133.06KB 35.21KB
plugin-designer (index.js) 216.12KB 44.37KB
plugin-detail (index.js) 260.65KB 67.82KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 144.77KB 37.17KB
plugin-gantt (index.js) 167.99KB 41.37KB
plugin-grid (index.js) 215.46KB 58.88KB
plugin-kanban (index.js) 49.30KB 15.39KB
plugin-list (index.js) 113.90KB 28.11KB
plugin-map (index.js) 21.74KB 7.07KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.68KB 8.95KB
plugin-tree (index.js) 10.56KB 3.71KB
plugin-view (index.js) 85.18KB 21.05KB
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) 110.46KB 36.33KB
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 (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: b7ee692eb03eb5aa8bbfb5aea2ac676f065da95a

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

① Derived judgments

  • BLOCKING (a′ / e) — the dropdown over-expands when it previews no column. LookupField.tsx candidateExpand calls buildExpandFields(refObjectSchema?.fields, previewColumns). Core's buildExpandFields (packages/core/src/utils/expand-fields.ts) restricts to the column list only when columns.length is positive; an EMPTY array falls through to "every reference field of the schema". previewColumns is pickerColumns minus the display field (plus an authored descriptionField), and deriveLookupColumns returns exactly [displayField] whenever highlightFields / displayFields / an authored lookup_columns names only the display field, or every other field is system-managed, hidden or non-tabular (created_by, modified_by, owner_id are in SYSTEM_MANAGED_FIELD_NAMES and are user / lookup typed, so a plain object with name plus audit fields hits this). Then previewColumns is [] and the dropdown AND the recents rail send $expand for every declared relation, none of which they render. Read-only node probe run from the scratch directory over copies of core's expand-fields.ts and column-identity.ts: fields name text, created_by user, modified_by user, owner_id lookup, task master_detail with columns [] → ["created_by","modified_by","owner_id","task"]; with columns [{field:'code'}] → []. Base sent no $expand for that shape, so head asks the server for strictly more work per open on that class of referenced objects; the FLS gate still filters once the policy loads, but before it loads every relation is requested — the very default the objectui#7429 sweep commit describes as the defect it removed ("called buildExpandFields with no column list … asked the server to resolve every declared relation on the object by default"). Untested: the control pin uses highlightFields ['code','version'], which keeps previewColumns non-empty; no pin has highlightFields ['name'] with relations declared. It makes the changeset sentence "Both queries now ask for $expand on the reference columns they display … the dropdown's previewed columns" and the corrected lookupColumnDisplay.tsx sentence "both queries ask for $expand on the readable reference columns they display" false for that shape, and the triage's rule (按下拉实际显示的列里的 lookup / master_detail 字段加 expand) is not met there. No data is corrupted (the collapse still applies to every declared relation). Fix is one guard in candidateExpand: return [] when previewColumns.length === 0 before calling buildExpandFields, plus one pin (highlightFields ['name'], relations declared → no $expand key). The picker is safe as written: resolvedColumns always carries the display column, so its list minus the id column is non-empty unless displayField === idField (theoretical; a guard there costs one line too).
  • (a) FLS gate — correct and family-shaped. Object: LookupField passes referenceTo (the referenced object, the one find(referenceTo, …) queries), RecordPickerDialog passes objectName (its own queried object) — never the host object. perms is in both memo dependency arrays. usePermissions() returns the frozen no-provider constant with isLoaded: false (→ unfiltered) or one identity per context value (MePermissionsProvider keys its value on isLoaded), so the memo rebuilds when the answer lands, and useRecordQuery refetches through its joined expandSignature. A denied relation is never in $expand once loaded — pinned five times against the real PermissionProvider (dropdown, recents rail, picker; allow; no provider). Shape vs the objectui#7429 sweep, RelatedList.tsx L694–696: if (!perms?.isLoaded || !relatedObjectName) return expandable; then expandable.filter of each f through perms.checkField(relatedObjectName, f, 'read') — identical semantics; head drops the optional chain because usePermissions() never returns undefined. ObjectKanban L651–654 and ObjectGrid L2296–2304 read the same.
  • (b) Collapse — nothing that leaves the widget can carry an expanded object. toPredicateRecord rewrites only fields the schema declares in EXPANDABLE_FIELD_TYPES (lookup, master_detail, tree, user) AND only when the value is an object carrying id / _id (→ String(id)); everything else, including the row's own id key, passes through, and the input is returned by reference when nothing was expanded. So the committed value option.value = record[idField] is identical to base in type and value on every backend; onChange, pushRecentLookupId, pickerResolvedRecords, onSelectRecord(option), the recents options, the picker's onSelectRecords (single via selected, multi via selectedRecordsMap filled with selected in handleRowClick and read in handleConfirm), option labels (recordToOption over the collapsed row; formatRecordTitle prints String(v)) and the picker's titleFormat display cell all read the collapsed row. Only previewOf ({ ...option, ...served }) and the picker's non-display cells see the served row. Numeric-id backend: the committed value does NOT change (the id key is not a relation). The only change is inside the onSelectRecord / onSelectRecords payload: an expanded relation column arrives as String(id) where base delivered the raw number — recorded; it matches the changeset's last sentence. Note the bare-backend leg of the invariance pins uses string ids, so a numeric-id backend is not pinned.
  • (c) A1 holds. useLookupName sets isResolvable only for a string / number value, so an object never fetches; LookupCellRenderer's object branch names it through resolveLookupRecordName. UserCellRenderer: a primitive renders UnresolvedUserReference, an object renders avatar plus name (confirms the user acceptance note). packages/fields/src/index.tsx is untouched on the branch (empty diff).
  • (d) Dependency. packages/permissions/package.json dependencies = @object-ui/types only (peer: react) — no cycle. The lockfile diff is exactly +3 lines under the packages/fields: importer ('@object-ui/permissions': specifier: workspace:*, version: link:../permissions); no other hunk, no resolution moved. @object-ui/permissions sits in dependencies (not peer / dev) and is imported by both LookupField.tsx and RecordPickerDialog.tsx, so check:phantom-deps (every import declared) and check:unused-deps (every declaration consumed) are consistent by reading; neither was re-run here.
  • (e) Prose. .changeset/10223-lookup-candidates-expand.md: every sentence true against head EXCEPT the "reference columns they display / the dropdown's previewed columns" clause (blocking finding); the FLS paragraph (including "before the policy loads, nothing is filtered"), the user paragraph and the collapse paragraph with its string-id caveat are true. Corrected lookupColumnDisplay.tsx header sentence: same caveat, and "readable" holds only once the policy is loaded. Pending lookup-dropdown-cell-renderer-5492.md: its present-tense clause "neither surface's request carries populate to begin with" (and "No query changed" read as a present state) is negated by this PR; both entries publish in the same @object-ui/fields release notes. It is not amended here (outside the claim), and check:changeset-claims is report-only by its own header, so its exit 0 says nothing about it — seat's call: amend one clause at landing or rule pending changesets historical. The lockfile changesets 5793-spec-range-floors.md, 6361-spec-floor-17-2-0.md and the 7122-* set describe @objectstack/spec resolutions and a postcss dedupe; this hunk adds a workspace link only, so none is made false.
  • (f) Pins are real and able to fail (judged by reading; ablations were not re-run, per instruction). 11 tests. Count pins assert $expand equals ['task'] and zero per-row reads — base sends none (ablation 1). Invariance pins deep-equal the expanding-backend and bare-backend readings and assert picked.task === 'same_task_0' and titles[0] === 'C-0 - samepick_task_0' — removing the collapse yields an object / [object Object] (2a–2c). FLS pins assert ['owner'] under a policy denying task_version.task — removing the filter yields ['task','owner'] (3a, 3b; the recents pin isolates its query by the $in filter). The dev-reported ablation outcomes are consistent with these assertions.
  • (g) Replacement body (body10341.new.md, byte-identical to the report's pr_body_replacement). Verified true against head: both mechanism bullets, the FLS bullet, the dependency bullet (deps, 3-line hunk), the collapse bullet, "useRecordQuery is untouched … forwards expand as $expand" (L187), the two prose corrections (the pickerAgreement hunks are comment-only), the merge bullet (4215ed76b is the merge-base; the merge touched none of the 8 branch files; 1dbb9933c is in the merge and its own body names the Spec Main Shape Gate), A1 / A2 / A3, the 11-test count and pin descriptions, Fixes #10223. Dev-measured or dev-run sentences not independently verifiable here: suite totals, 87 / 1025 consumers, eslint 266 files and warning counts, the gate list, the lockfile generation method, schema-read constancy — CI at head is green. Two precision flags: "The same happens when the permission policy lands after the dropdown opens" holds only when the loaded policy removes a name (an unchanged joined signature triggers no refetch); "filed as their own card" is true only once the seat files the display-column-FLS / PeoplePicker card it promised at acceptance. The LookupField bullet describes the call literally and is true; it inherits the blocking edge only through "the columns the option subtitle shows".

② Semver level

patch for @object-ui/fields is right. objectui AGENTS.md 版本号策略: "minor/patch 独立演进——objectstack 没动时不必跟发;objectui 自己的改动照常用 changeset 推进" and "changeset 里不要声明 major … objectui 自身的破坏性变更也标 minor(在正文里写清 breaking 语义即可)". Nothing here is breaking: no export moves, no public prop changes, useRecordQuery untouched, the request gains an optional $expand the adapter already forwards, and the new edge is on a fixed-group workspace package that depends only on @object-ui/types. Precedent: the objectui#7429 sweep's .changeset/7429-expand-fls-seven-sites.md graded the same new @object-ui/permissions dependency plus FLS gate patch on seven packages. The visible side effects (the user column face; string ids for expanded columns in callback payloads) are stated in the changeset body, which is what the rule asks for. Not major (also enforced by check-changeset-no-major).

③ Boundary flags

  • Fixes #10223 is the only closing keyword; objectui#7429 / $expand carries no FLS gate at either projection site, so a lookup column the principal cannot read is still expanded and its value returned #7215 / FLS-gate $expand at the remaining five build sites — calendar, gantt and record-detail expand EVERY declared relation with no column list at all #7230 and comment 5820197003 are cited without a closing verb. Appropriate for the card, subject to the blocking guard above.
  • No model identifier or model version token in the six commit messages, their trailers, the PR title, the current body or the replacement body (grep over the model / version token family: 0 hits; trailers are a product-name co-author line and the Claude-Session URL).
  • File surface: 8 files, all inside claim 5818896276 plus amendment 5820197003 — LookupField.tsx, RecordPickerDialog.tsx, LookupField.candidateExpand-10223.test.tsx and LookupField.pickerAgreement.test.tsx (both in packages/fields/src/widgets/, i.e. "tests beside those files"; the latter's hunks are header / comment only, assertions unchanged), .changeset/10223-lookup-candidates-expand.md, packages/fields/package.json (+1), pnpm-lock.yaml (+3), lookupColumnDisplay.tsx (module docblock only). useRecordQuery.ts and index.tsx untouched (the objectui#10299 serial constraint is honoured). Nothing outside.
  • Merge commit 177e5b5f0 is pure: the branch-side diff against e836ccd29 over the 8 files is empty and both sides' stats agree (5 files, 546+ / 42−). No rebase, no force.
  • Check-runs on b7ee692eb03eb5aa8bbfb5aea2ac676f065da95a: 45 total — 42 success, 3 skipped (dependabot, Test (coverage), Test (coverage shard …/4)), 0 failure, 0 in_progress; all completed at the single read, so no polling was needed.
  • Gate families were not re-run here; the gate list in the body is dev-reported.

Implemented-by: claude/issue-10223-lookup-candidates-expand
Reviewed-by: session_01BP8CMtACxTdLjqR6rhd33C

VERDICT: FAIL


Generated by Claude Code

…-expand

main touched pnpm-lock.yaml (two workspace links in another importer); the branch's own lockfile hunk is untouched by it.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
…e queries (objectui#10223)

`buildExpandFields` reads an EMPTY column list as "no restriction" and
returns every relation the object declares. The dropdown's preview list is
empty whenever its picker columns are the display field alone (a
`highlightFields` naming only it, or every other field system-managed), so
the dropdown and the recents rail asked for every declared relation, none of
which they render. Both candidate expansions now return nothing for an empty
column list; the picker's list (its columns minus the id column) gets the
same one-line guard for the `columns: ['id']` / `displayField === idField`
shape.

Pins: `highlightFields ['name']` over an object declaring `created_by`
(user), `owner_id` (lookup), `task` (master_detail) and `owner` (user) sends
no `$expand` key on the dropdown or the recents rail; a picker whose only
column is the id sends none either.

`.changeset/lookup-dropdown-cell-renderer-5492.md` (pending, same package)
said in the present tense that neither surface's request carries populate;
this PR makes that false, so the clause now reads as history and names the
change that superseded it. Frontmatter untouched.

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) 3042.0 KB 3104.5 KB
Main entry chunk (gzip) 148.4 KB 350 KB
Entry file index-BIZdyRBX.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) 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) 541.61KB 129.47KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 222.47KB 61.83KB
fields (index.js) 254.86KB 64.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.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 34.99KB 11.45KB
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.15KB 11.05KB
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.08KB 3.95KB
plugin-calendar (index.js) 51.44KB 14.62KB
plugin-charts (index.js) 71.84KB 20.13KB
plugin-chatbot (index.js) 198.27KB 47.18KB
plugin-dashboard (index.js) 133.06KB 35.21KB
plugin-designer (index.js) 216.12KB 44.37KB
plugin-detail (index.js) 260.65KB 67.82KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 146.34KB 37.48KB
plugin-gantt (index.js) 168.31KB 41.45KB
plugin-grid (index.js) 215.46KB 58.88KB
plugin-kanban (index.js) 49.30KB 15.39KB
plugin-list (index.js) 114.13KB 28.14KB
plugin-map (index.js) 21.74KB 7.07KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.68KB 8.95KB
plugin-tree (index.js) 10.56KB 3.71KB
plugin-view (index.js) 85.79KB 21.35KB
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) 110.46KB 36.33KB
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 (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 (re-review, patch round 1)

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9796fdbb46bdfa6919b21e624ba0a994c84c4386

Rendered by an isolated review subagent spawned by the domain:ui#4 seat; its served tier was checked against its transcript stamps (75 of 75 at the review tier). Adopted by this seat. Seat note: the replacement body is posted with the two corrections this record names in (6) (the merge-commit shas; the lockfile sentence scoped to this PR's files).

① Derived judgments

  • (1) Delta is what it claims. git diff b7ee692eb..9796fdbb4 over the PR's files touches four: .changeset/lookup-dropdown-cell-renderer-5492.md (one hunk, @@ -27,11 +27,14, the populate paragraph rewritten as history), LookupField.candidateExpand-10223.test.tsx (five hunks: header bullet extended, BackendOptions.fields, makeBackend destructures fields = TASK_VERSION_FIELDS, the schema mock returns fields, and one new describe with two its), LookupField.tsx (one hunk: a seven-line comment addition plus if (previewColumns.length === 0) return []; as the first statement of candidateExpand), RecordPickerDialog.tsx (one hunk: a three-line comment plus three statements — rendered bound to resolvedColumns filtered to columns whose field is not idField, if (rendered.length === 0) return [];, and buildExpandFields(fieldsMeta, rendered) — replacing the one-line call). The other five PR files (10223-lookup-candidates-expand.md, package.json, pickerAgreement.test.tsx, lookupColumnDisplay.tsx, pnpm-lock.yaml) have an empty diff since b7ee692eb. Merge a2de421be is pure: its diff against first parent b7ee692eb is the same 151-file set as main's own 4215ed76b..86982ace0, with byte-identical patch content outside the lockfile, and the lockfile side is main's own two hunks (both inside the packages/plugin-ai importer: an @object-ui/i18n dependency link and an @object-ui/test-support devDependency link); its diff against second parent 86982ace0 is exactly the PR's eight prior files. Head vs main on pnpm-lock.yaml is still the single 3-line packages/fields importer hunk ('@object-ui/permissions': specifier: workspace:*, version: link:../permissions).
  • (2) Guards correct and complete. git grep over the two files at head finds exactly two buildExpandFields calls: LookupField.tsx candidateExpand (guarded by previewColumns.length === 0) and RecordPickerDialog.tsx expand (guarded by rendered.length === 0). No other caller; the recents rail reuses candidateExpand. What reaches the wire for []: the dropdown and the picker both hand expand to useRecordQuery, whose builder assigns params.$expand only when expand is present with a positive length (key omitted, not $expand= empty; expandSignature is '' for []), and the recents rail spreads { $expand: candidateExpand } only when candidateExpand.length is positive, else {} — so all three requests carry NO $expand key. The empty case is real at head: displayField defaults to 'name'; deriveLookupColumns with highlightFields ['name'] returns [{field:'name',…}], previewColumns seeds seen with the display field and skips it, so the list is []; a RecordPickerDialog with columns={['id']} (or no columns and displayField === idField) resolves to the id column alone, so rendered is []. Non-empty lists with no relation among them still restrict (the positive-columns.length branch → []), and an undefined schema returns [], so no remaining path hands buildExpandFields an empty list.
  • (3) FLS gate and collapse unchanged. Every +/- line of b7ee692eb..9796fdbb4 over the two sources is listed above; zero of them mention perms, checkField, isLoaded or toPredicateRecord. The complete sets of lines naming perms / checkField / toPredicateRecord / usePermissions in each file are byte-identical between b7ee692eb and head (diff of the extracted line sets is empty for both files). The FLS lines — if (!perms.isLoaded || !referenceTo) return expandable; followed by the expandable.filter of each f through perms.checkField(referenceTo, f, 'read') — (and the picker's objectName twin) appear only as context in the guard hunks.
  • (4) 5492 correction. Read whole at head. Frontmatter ('@object-ui/fields': patch) is byte-identical to main's; the file was unchanged between main and b7ee692eb, so git diff on its frontmatter is empty on every leg. Every sentence is true at head: "This change touched no query and widened no contract" and "at the time neither surface's request carried populate … the dropdown inherited exactly that" now read as history of the 5492 change; "lookupColumns entries stay bare field names — no dot paths, no populate/expand semantics" is still true (the $expand is on the query, not in the column declaration); "A later change (objectui#10223) has both requests ask for $expand on the reference columns they display, minus any the loaded permission policy denies" is true at head including the empty case because of (2); "a value that still arrives as a bare id is resolved by that same cell renderer" matches LookupCellRenderer's primitive branch; the slot-drop sentence is unchanged and still decided on the raw value. Legitimacy: scripts/check-changeset-overwrite.mjs is report-only by its header and its measured population lists "factual corrections to prose" among the 19-for-19 legitimate modifications (the seat's case 2); the .changeset/README.md rule is about filename collisions deleting a third party's declaration, which this edit is not — declaredEntries before and after are the same single name, so lost is empty. The head's Changeset Overwrite Report and Changeset Claim Re-read check-runs are success.
  • (5) 10223-lookup-candidates-expand.md. Empty diff since b7ee692eb; read whole at head. "Both queries now ask for $expand on the reference columns they display … the dropdown's previewed columns, and the picker's columns other than its id column" is now true in the empty case too (no previewed column → no $expand). "The dropdown's recently-used rail asks for the same expansion" — it reuses candidateExpand. The FLS paragraph (including "before the policy loads, nothing is filtered" and the @object-ui/permissions dependency), the user paragraph (EXPANDABLE_FIELD_TYPES holds user), and the collapse paragraph with its string-id caveat (toPredicateRecord → String(id)) are unchanged and true.
  • (6) Replacement body (body10341.new2.md). Verified true against head: both mechanism bullets, the "No displayed column ⇒ no $expand" bullet and its two sub-cases (both match the derivations in (2)), "Without the guard, the dropdown and the recents rail asked for every declared relation", the FLS bullet, the dependency bullet (3-line lockfile hunk; @object-ui/permissions depends only on @object-ui/types), the collapse bullet, "useRecordQuery is untouched. It already forwards expand as $expand" (empty diff vs main; the L187 builder), the three prose-correction sub-bullets (the 5492 description matches the hunk; frontmatter untouched), 1dbb9933c is an ancestor of both 4215ed76b and 86982ace0 and its own body names the Spec Main Shape Gate, the "before" leg sha 8b1f06619 is the parent of 8f70a8b74, "neither the gate nor the empty-list guard adds a request" (both are pure memo computations), 13 tests with the 4 + 2 + 2 + 5 breakdown, 75 test files under packages/fields/src/widgets/ and 107 (= 106 + 1) elsewhere in the package at head, the A1 / A2 / A3 paragraphs, the acceptance notes (the late-policy sentence is now precise: "triggers a refetch only when it removes a name from the list"), and ablation 4's shape (see (7)). False as literally written: "origin/main is merged in with two merge commits, 4215ed76b and 86982ace0" — those two shas are the merged main tips; the merge commits are 177e5b5f0 and a2de421be. Recommended wording for the seat to apply before posting: "origin/main is merged in twice (merge commits 177e5b5f0 and a2de421be, bringing main to 4215ed76b and then 86982ace0), which carry …". Scoped-true, flag the wording: "The second merge touched pnpm-lock.yaml in another importer only" is true of the PR's file surface (main's lockfile change is two hunks in the one packages/plugin-ai importer) but the merge brought 151 files from main; "in another importer only" should be scoped to "of the PR's files". Unverifiable here (dev-run, not false): suite totals (696 / 2364 + 7 skipped), the 88 / 1026 consumer counts, type-check exit and "the new test file is in the test program" (plausible: src/widgets/*.test.tsx is inside include: ["src"] and the test program chains off it), eslint warning counts 59 / 35, the measured table, schema-read constancy, the gate list and "NOT GOVERNED" (several are corroborated by the head's green Changeset Bump Policy, Changeset Declaration, Line Citation Gate, Control Byte Scan, Lockfile Integrity Check, Lockfile Dedupe Check, Governed Surface Queue Guard runs), and the lockfile generation method. No model identifier or model version token in the body (token-family grep: 0 hits; the trailing product-name attribution line is a product name). No angle brackets. Fixes #10223 is the only closing keyword (regex over close/fix/resolve forms: one hit, line 1).
  • (7) New pins real and able to fail (by reading; nothing run). WITH_AUDIT_RELATIONS declares four relations (task master_detail, owner user, created_by user, owner_id lookup). Dropdown pin: highlightFields ['name'] → previewColumns = []; at b7ee692eb buildExpandFields(fields, []) returns all four, no provider so isLoaded is false and nothing is filtered, useRecordQuery sets $expand (length 4, positive) and the recents spread does too → '$expand' in main[0] is true → expect(...).toBe(false) fails with expected true to be false; at head both are [] → key omitted → passes. expect(main).toHaveLength(1) / expect(recents).toHaveLength(1) hold because the schema lands before the open (mountLookup waits on getObjectSchema and settles) and the recents effect fires once on open (one id pushed; candidateExpandKey stable). previewTexts('task') is 0 because no preview column renders. Picker pin: columns={['id']} → rendered = [] → same four at b7ee692eb → fails; [] at head → passes. So ablation 4's "2 failed, 11 passed" is consistent: exactly the two new tests fail, the prior eleven are untouched by the guard. The mock's find records the raw params object, so '$expand' in is a faithful key test.

② Semver level

Unchanged from the prior record: patch on @object-ui/fields. This round removes requests (a guard) and edits prose; no export, prop or contract moved, and the 5492 changeset keeps its own patch frontmatter.

③ Boundary flags

  • Commits a2de421be457cf2b1021eefbca9c7c32b6d24a30 and 9796fdbb46bdfa6919b21e624ba0a994c84c4386: no model identifier or version token in either message (token-family grep over both bodies: 0 hits); trailers are exactly Co-authored-by: Claude with the noreply@anthropic.com address plus Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C — the product name, no tier or version. Both are SSH-signed. The merge message's "two workspace links in another importer" matches the measured lockfile hunks.
  • File surface: the fix commit touches four files (lookup-dropdown-cell-renderer-5492.md, the candidateExpand-10223 test, LookupField.tsx, RecordPickerDialog.tsx); the merge adds nothing of its own. PR vs main is nine files: the prior eight plus the declared 5492 changeset. Nothing else. useRecordQuery.ts and packages/fields/src/index.tsx remain untouched (empty diff vs main).
  • Check-runs on 9796fdbb46bdfa6919b21e624ba0a994c84c4386: 45 total — 42 success, 3 skipped (dependabot, Test (coverage), Test (coverage shard …/4)), 0 failure, 0 in_progress. All 45 were completed at the first read (last completion 20:04:16Z), so no polling was needed; combined commit status is success. Five third-party app check-suites (deploy-preview and bot apps) sit queued with zero check runs — no run belongs to them and they are not a CI signal. The 8-way Test shards, Type Check, Lint, Spec Main Shape Gate and every changeset gate are success.
  • Read-only throughout: no GitHub writes, no repository edits, no suites or gates run; one earlier-round node probe is the only local execution referenced, and none was run this round.

Implemented-by: claude/issue-10223-lookup-candidates-expand
Reviewed-by: session_01BP8CMtACxTdLjqR6rhd33C

VERDICT: PASS


Generated by Claude Code

@os-litant
os-litant marked this pull request as ready for review September 24, 2026 20:33
@os-litant
os-litant added this pull request to the merge queue Sep 24, 2026
Merged via the queue into main with commit 65f1e8d Sep 24, 2026
47 checks passed
@os-litant
os-litant deleted the claude/issue-10223-lookup-candidates-expand branch September 24, 2026 20:45
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…unwrap `rows` (objectui#7028) (objectstack-ai#10363)

Fixes objectstack-ai#7028
Clause-②: yes

## What

`aggregate()` now reads the `client.analytics.query` answer in one
spelling: `rows` on the post-unwrap `AnalyticsResult`. This is condition
3 of the objectstack#13079 ruling's landing requirements (director batch
objectstack-ai#19, option A, maintainer 2026-08-31), quoted verbatim: 「objectui
容错链同波收紧:已立 objectui#7028,带时序门(严格在收敛合并、objectui 采版之后 —— 该链今天是承重的)」. The
card's sequencing gate is met: `@objectstack/client` 17.3.0 carries the
convergence, and the workspace lockfile resolves 17.4.0.

- **The ladder had five spellings, not the card's two.** It read a bare
array, `rows`, `data` as an array, `data.data.rows` and `results`, and
answered anything else with `[]`. Only `rows` survives. Any other value
throws the new exported `AnalyticsResultShapeError` (`code:
'ANALYTICS_RESULT_SHAPE_INVALID'`, `envelope: true` for the pre-17.3.0
envelope).
- **Loud means a throw, and the throw escapes the fallback.** The row
read sits inside `aggregate()`'s `try`. Its `catch` would have
classified the error as `unknown` and answered it with
`aggregateViaFind`'s client-side numbers, so the catch now rethrows
`AnalyticsResultShapeError` before it classifies anything. I chose a
throw over a diagnostic because the file already treats contract
violations that way: `AnalyticsQueryRejectedError` refuses the fallback
for the same reason (framework#3878). A `[]` would repeat the confident
zero that objectui#5954 removed on the failure side.
- **The widened `AnalyticsResult` alias is gone**, along with the
branches it existed for.
- **The `@objectstack/client` floor moves from `^17.0.0` to `^17.3.0`**
in `packages/data-objectstack` and `apps/console`. Only the lockfile
specifier changes; the resolution stays at 17.4.0.
`QUICK_REFERENCE.md`'s Client row was rewritten by `pnpm
quick-reference:sync`.

## The premise, measured

- **This was never a server question.** The comment above the old ladder
(and the objectui#7122 changeset) described deleting the branches as "a
runtime compatibility decision about servers older than #13079". The
convergence commit `db16b94` changes only `packages/client` and the spec
migration registry, and no server code. `POST /analytics/query` answers
`deps.success(result)` in objectstack's `domains/analytics.ts` from its
first extraction (`8f124a7b7`, 2026-07-27, before 17.0.0 shipped on
2026-08-14) to `main`. Every 17.x server therefore sends the same `{
success, data }` envelope. Only the client decides whether it is
unwrapped: `unwrapResponse` strips it at 17.3.0+, while 17.2.0 ended
`return res.json()` (read in the installed 17.4.0 `dist/index.mjs` and a
packed 17.2.0 tarball). No server is dropped. The compatibility axis is
the **client** range, and that is why the floor moves.
- **Which producer each branch served** (`git log -S` on the unshallowed
history):
- `rows` and `data.data.rows` came from `70cb62b85` (2026-04-01): the
post-unwrap and envelope forms of the one route.
- A bare array, `data` as an array and `results` came from `f25e6c288`
(2026-02-25). That commit targeted a raw `GET
/api/v1/analytics/{resource}` fetch, which `d91f2e2a0` replaced with
`client.analytics.query` ten minutes later, and the three branches were
carried over without shape evidence.
- Even the retired in-kernel shim (removed by objectstack `77fadbfca`
before 17.0.0) answered `{ success, data: { rows, fields } }`. No
producer of this call site ever returned the other three shapes.
- **`analytics.meta` and `analytics.explain` have no call sites in
objectui.** `git grep` finds only `analytics.query` here, with a
positive control on the same pattern. In the installed 17.4.0 client,
`meta` and `explain` both end `return this.unwrapResponse(res)`,
declared `AnalyticsMetadataResponse["data"]` and
`AnalyticsSqlResponse["data"]`. `analytics.query` is declared as a
Promise of `AnalyticsResult` (the generic is spelled out in words
because GitHub strips angle-bracket spans).

## Surface beyond the claim, stated

The claim named only `index.ts` (the ladder), the tests beside it and
one changeset. Two extensions were needed:

1. **The rethrow line in `aggregate()`'s `catch` plus the new error
class.** Without them the throw is silently answered by the fallback,
which the pins require it must not be.
2. **The client floor, in `packages/data-objectstack/package.json`,
`apps/console/package.json`, the two `pnpm-lock.yaml` specifier lines
and `QUICK_REFERENCE.md`.** Leaving `^17.0.0` would publish a range this
change makes false.
`scripts/__tests__/quick-reference-current-release-4143.test.ts` went
red on the adapter-only bump, because that row anchors both manifests.
No open pull request holds these lines: objectstack-ai#10341 and objectstack-ai#8941 touch the
lockfile and the console manifest in disjoint hunks, and objectstack-ai#5400 is the
release pull request.

`isLegacyOverlayRow` and the rest of `index.ts` are untouched.

**Seat amendment (2026-09-24T20:14Z).** The seat accepted both
extensions and widened the claim's File surface to match (claim comment
`5820950909`, edited). Line 2 now reads `Clause-②: yes`, because the
diff adds a public export (`AnalyticsResultShapeError`); a contract
review is owed before enqueue. The changeset level moved from `patch` to
`minor` at head `e6e26e35d` (objectui precedent objectstack-ai#9061 / objectstack-ai#9175).

## Pins (site-scoped, new file)


`packages/data-objectstack/src/aggregate-rows-post-unwrap-7028.test.ts`,
7 tests:
- The unwrapped shape yields its rows. The server's `{ success, data: {
rows } }` wire envelope goes through the real client.
- An envelope at the boundary throws, and `/api/v1/data` is never
requested. This is tested twice: through the real client (a success-less
`{ data: { rows } }` body that `unwrapResponse` leaves alone), and as
the value a pre-17.3.0 client handed back.
- The other three retired spellings throw instead of degrading to `[]`.
- CONTROL: rows missing the measure still fall back to client-side
aggregation.

## Verification (final head `52f5a6c21`)

- `pnpm --filter @object-ui/data-objectstack type-check`: exit 0.
`--listFiles` confirms the new test file is in that program.
- `pnpm exec vitest run packages/data-objectstack/` from the repo root:
`Test Files 68 passed (68)`, `Tests 933 passed (933)`.
- These root suites read the touched files off disk, outside the package
graph: every `scripts/__tests__` suite naming `data-objectstack`,
`QUICK_REFERENCE`, `apps/console/package.json` or `pnpm-lock.yaml`.
Result: `Test Files 27 passed (27)`, `Tests 1137 passed (1137)`.
- `eslint .` in the package: 77 files linted (from `--format json`), 0
errors. Type-aware linting is off, so this diff cannot move any verdict
in untouched files.
- Gates, all exit 0: `check:control-bytes`, `check:new-line-citations`
(0 new), `check-changeset-presence`, `check:changeset-claims`,
`check:lockfile-integrity`, `check:lockfile-dedupe`,
`check:installed-pin-claims`, `check:pending-changeset-literals`,
`quick-reference:check`.
- `check:changeset-claims` lists `5793`, `6361` and `7122` for naming
`pnpm-lock.yaml`. I read each paragraph, and none is falsified by this
diff.
- `check:spec-floors`: NOT MEASURED. It refuses on an unbuilt workspace
(`no-artifact`), and it reads `@objectstack/spec` floors only, which
this diff does not move.
- **Ablation 1:** I restored the five-spelling ladder with
`ablation-replace.mjs` (anchor 1 to 0, blob `0b7bc68a9ed1` to
`3b936fc60bb8`). Result: `Tests 5 failed | 2 passed (7)`, with every
failure being `aggregate() resolved; it was expected to throw
AnalyticsResultShapeError`. The two greens are the positive pin and the
control. The restore is proven: blob equals HEAD `0b7bc68a9ed1` and `git
diff HEAD` is empty.
- **Ablation 2:** I deleted the `catch` rethrow. Result: the same 5 fail
and 2 pass, because the error is answered by the fallback. The restore
is proven the same way.
- Both ablations run from source through a relative `./index` import, so
no `dist` leg applies.

## Acceptance notes

- `analytics.meta` and `analytics.explain` are not called anywhere in
this repo, so they have nothing to tighten.
- The pending `.changeset/7122-objectstack-family-17-3-0.md` still says
the branches were kept "rather than deleted" and frames the question as
one about servers. It is true of its own change and will publish in the
same release as this one, so the new changeset names it and supersedes
it rather than editing it.
- The existing `aggregate-capability.test.ts` case "a `{ success, data:
{ rows } }` envelope is still a result" stays green. It is the server's
wire envelope, which the installed client unwraps. After this change it
is a positive control, not tolerance.
- Dependents' type-check: `plugin-charts`, `plugin-dashboard` and
`components` read `aggregate()` results through the `DataSource`
interface and do not depend on this package. The only export-surface
change is one added class: `export` lines in the diff are `+1 -0`, and
the `aggregate` signature is untouched. No export-star re-export exists,
and no other declaration of the name exists, so no importer's type-check
input changes. The full workspace Type Check runs in CI.

Session: `https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC` (dev
run dispatched by the `domain:ui` seat 1).

---
_Generated by [Claude
Code](https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[fields] Lookup 下拉候选列含 lookup 字段时,每条候选单独发一次请求(N+1)

2 participants