Skip to content

fix(plugin-list): filter candidates read the declared displayField, and no list-column options (objectui#10545, objectui#10547) - #10627

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-10545-filter-candidates-displayfield
Sep 25, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-10545-filter-candidates-displayfield

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #10545
Fixes #10547
Clause-②: no

One claim for two cards on one branch: objectui#10547 is folded into objectui#10545 (its triage: 「Same file as #10545: serial, or one claim」). There is one commit per card and one changeset.

What changed

objectui#10545, commit 0814660:

  • ListView, the objectDef.fields branch of the filter/sort candidate builder (candidateFields): the candidate reads displayField: field.displayField and nothing else. It no longer reads display_field, reference_field or id_field, and the candidate carries no idField.
  • UserFilters, resolveFields: reads displayField = fieldDef.displayField and nothing else. The idField thread is gone (the local, the returned member, the internal ResolvedField member and the prop handed to LookupValuePicker), because its only writer was the id_field read.
  • The objectui#7642 census KEEP comments at both sites are replaced. The new comments say what the code reads now and why, and cite this card.

objectui#10547, commit 61ce977:

  • ListView, the !objectDef?.fields columns-only fallback of the same builder: no longer reads options: buildOptions(fieldName, f.options) off a list column. Select options reach the candidate from the object definition once it loads. This reuses the objectui#7531 ruling (5809501335, letter "remove").

One changeset, .changeset/10545-filter-candidates-displayfield.md: patch for @object-ui/plugin-list, with one paragraph per card. It says plainly that a def carrying only the refused display_field (or only reference_field) no longer sets the picker's display field, that id_field no longer re-keys the picker, and that a list column's options is no longer read.

Two merges of origin/main (1336ba8, f0930bb). The first one (1336ba8) picked up this seat's objectui#10597, which touches ListView.tsx in a different region. Neither merge had conflicts.

The contract, re-measured on the installed @objectstack/spec 17.4.0

FieldSchema.safeParse of a lookup def (name, type: 'lookup', label, reference) plus one key:

key result
none (control) success
zzz_not_a_real_key (control) unrecognized_keys, no hint
displayField success
display_field unrecognized_keys: "Did you mean display_field → displayField?"
reference_field unrecognized_keys: "Did you mean reference_field → referenceVia?"
id_field unrecognized_keys, no hint
idField unrecognized_keys, no hint

ListColumnSchema.safeParse of a column (field, label, type: 'select'): the bare column → success; a nonsense key → unrecognized_keys; with options → unrecognized_keys: "Unrecognized key(s) on this list column: options." There is no rename hint. The declared keys are field,label,width,align,hidden,sortable,resizable,wrap,type,pinned,summary,prefix,link,action.

The pins restate the refusals as lit-control assertions, so the next spec bump re-derives them.

Why the display read can be displayField alone

  • It matches plugin-grid. RELATIONAL_META_READ_SET in relationalMetaKeys.ts classifies displayField as spec, and it is the only display spelling the grid's copy set (RELATIONAL_META_KEYS, via applyRelationalMeta) reads off a def (objectui#7155). reference_field is no-producer there and is not copied. So these two sites are no stricter and no looser than the grid's reader.
  • A stored pre-strict display_field still arrives as displayField when it comes through the in-repo serve paths. normalizeSchemaReferenceKeys folds it once at ingestion (objectui#7650), in ObjectStackAdapter.getObjectSchema and in the MetadataProvider object paths. Its alias probe maps display_field to displayField. reference_field and id_field probe onto no declared key, so the fold leaves them on the document and logs a dev-mode no-declared-twin warning (objectui#8938). That is the one condition the objectui#7642 PM ruling set for any consumer-side retirement in this family, and it now holds.
  • A def that reaches these sites some other way (a host DataSource that bypasses the adapter, or an objectDef passed straight to the exported UserFilters) is held to the same FieldSchema contract (AGENTS.md 0.1). The changeset says so.

What the sibling idField read becomes: removed, not re-pointed

  • FieldSchema declares no id column for a lookup: it refuses both id_field and idField, measured above. So the read targeted no declared key, and re-pointing it to idField would put a new read on an undeclared spelling. The same reasoning is recorded at ObjectChart and resolveActionParams.
  • The consumer is LookupValuePicker (@object-ui/components). It resolves identity with field.idField || "id", then r.id ?? r._id. Without the read it always keys by id. On a spec-compliant def the read was always undefined, so nothing changes there.
  • Producers of id_field on an object def: no non-test hit in objectui. In objectstack origin/main the only hits are the liveness ledger note, tests, and the unreleased FIELD_KEY_GUIDANCE row in authoring-key-lint.ts, which says: "id_field was never a FieldSchema key: a lookup stores the referenced record's id … the picker resolves record identity itself."
  • It matches the grid: the grid's copy set copies no id column, and no grid consumer reads id_field.
  • UserFilterFieldSchema (the view's own userFilters.fields entry) declares only field,label,type,options,showCount,defaultValues, and it refuses idField too. So removing ResolvedField.idField drops no authored value.

Pins, and the before/after

  • packages/plugin-list/src/__tests__/ListView.declaredDisplayField-10545.test.tsx: the candidate FilterBuilder receives once the definition loads (5 tests).
  • packages/plugin-list/src/__tests__/UserFilters.declaredDisplayField-10545.test.tsx: the toolbar lookup picker's resolved label, and the $filter key of its label lookup (5 tests).
  • packages/plugin-list/src/__tests__/ListView.columnsOnlyOptions-10547.test.tsx: the columns-only candidate before load, plus the lit control where options arrive with the definition (3 tests).

Each ablation leg was run under the shared verify lock. It checks out the BASE version of each source file with the pins unchanged, proves on disk that the swap landed (grep -c of the pre-fix and post-fix lines), and restores with git checkout HEAD --. The restore is proved by an empty git diff HEAD and by blob hashes equal to HEAD's, inside an EXIT INT TERM trap.

  • objectui#10545, at 0814660, sources at f169a03:
    • HEAD: PINS_HEAD_EXIT=0, 10 passed (10).
    • BASE: PINS_BASE_EXIT=1, 8 failed and 2 passed (10). The 2 that pass are the spec lit-control test and the picker-default control.
    • Messages on BASE:
      • "expected undefined to be 'full_name'" (the positive pin);
      • "expected 'full_name' to be undefined" (the display_field and reference_field negative legs);
      • "expected 'code' to be undefined" (id_field);
      • "Expected: "Ada Lovelace" Received: "ada"" (the picker's positive pin);
      • "expected 'a1' not to be 'a1'" (id_field re-keyed the lookup, so the label never resolved).
  • objectui#10547, at f0930bb, ListView.tsx at 1336ba8:
    • HEAD: PIN10547_HEAD_EXIT=0, 3 passed (3).
    • Pre-fix: PIN10547_PREFIX_EXIT=1, 1 failed and 2 passed. The failing test is "a column carrying options offers the same candidates as a plain one": "expected [ …(2) ] to be undefined". The spec test and the lit control stay green in both legs.

Gates, at f0930bb (the last commit)

  • pnpm exec vitest run --maxWorkers=2 packages/plugin-list/ (lock): exit 0, Test Files 97 passed (97), Tests 1144 passed (1144).
  • pnpm turbo run type-check --filter @object-ui/plugin-list --concurrency=2 (lock): exit 0.
    • The log echoes tsc --noEmit && tsc -p tsconfig.test.json, then Tasks: 14 successful, 14 total.
    • tsc -p tsconfig.test.json --listFiles lists all 3 new pin files.
  • check:control-bytes, check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:new-line-citations, check:handler-key-reads, check:changeset-claims, check:pending-changeset-literals: all exit 0. The line-citation gate printed "VERDICT new-cross-file-line-citations: 0 new citation(s)".
  • Added by re-derivation from .github/workflows/: check:unreferenced-sources, check:phantom-deps, check:self-import, check:designer-field-key-parity, and the check-changeset-presence / -no-major / -fixed / -overwrite scripts. All exit 0. Presence printed "5 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)".
  • eslint, plain form, on the 5 changed source and test files: exit 0, 0 errors.
    • This is a measured narrowing, not a skip. ① The population is from eslint's own config: files: ['**/*.{ts,tsx}'] covers all 5. ② --format json reports 5 files. ③ The config enables no type-aware linting (projectService / parserOptions.project count 0), so this diff cannot move any untouched file's verdict.
    • The warnings (183 on ListView.tsx, 30 on UserFilters.tsx) equal the same files at BASE. The new test files have 0.
  • The full pnpm lint, full pnpm test and the other workflows are CI's. There was no browser run. The pins render the real ListView, UserFilters and LookupValuePicker under happy-dom.

Acceptance notes

  • class b, handed to the seat, not filed here: plugin-form deriveMasterDetail (deriveColumns and hydrateColumns) still reads d?.display_field || d?.reference_field and never the declared displayField. This is the same seam, at a site this card does not claim. It is a source reading on this tree, not probed. Seam: spec:FieldSchema.displayField → renderer:plugin-form deriveColumns / hydrateColumns (GridColumn.displayField).
  • @object-ui/fields LookupField and LookupCellRenderer still read reference_field as a second display leg (graded no-producer in the grid's register). Not touched; no carrier.
  • ObjectChart and resolveActionParams keep the display_field / reference_field legs behind displayField (objectui#7435). The tree now holds two dispositions for the same refused spellings; this card follows the grid's. No carrier.
  • The pending, empty-frontmatter census record .changeset/lucky-donkeys-shave.md (objectui#7642) describes both plugin-list sites as KEEP. This PR supersedes that for these two sites. The file declares no package, so it publishes into no CHANGELOG. It is not edited here.
  • In the same candidateFields memo, the older objectui#6837 note beside referenceTo still says the columns-only branch "is NOT narrowed". objectui#7531 and objectui#10547 have since narrowed it. Not edited (outside the claimed lines); no carrier.

Generated by Claude Code

…nd no id column (objectui#10545)

The definition-branch filter candidate in ListView and resolveFields in
UserFilters read a lookup's display field as display_field, then
reference_field, spellings FieldSchema refuses, and never displayField, the
one it declares. Both now read displayField alone, the single spelling
plugin-grid's copy set reads (objectui#7155). Neither reads id_field any
more: FieldSchema declares no id column for a lookup (it refuses idField and
id_field alike), so the value picker keys the lookup by its id default.

The objectui#7642 census KEEP comments at both sites are replaced by what
the code reads now and why.

Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
Co-authored-by: Claude <noreply@anthropic.com>
…lter-candidates-displayfield

Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
Co-authored-by: Claude <noreply@anthropic.com>
…ff a list column (objectui#10547)

Before the object definition loads, ListView builds its filter and sort
candidates from the declared columns. That fallback read `options` off each
column, a key ListColumnSchema refuses with unrecognized_keys. Reusing the
objectui#7531 ruling (remove), the read is dropped: a select field's options
reach the filter builder from the object definition once it loads.

Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
Co-authored-by: Claude <noreply@anthropic.com>
…lter-candidates-displayfield

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

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 7 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/7070-no-invented-gantt-date-fields.md

  • names plugin-list/src/ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    • app-shell/src/views/ObjectView.tsx — the console object page. The inline branch becomes ganttViewOptions, the sibling of calendarViewOptions and timelineViewOptions: the declared block spread whole, title floored at 'name', no date field invented. - plugin-list/src/ListView.tsx — the render branch AND the capability gate. - plugin-view/src/ObjectView.tsx — generateViewSchema, the authored object-view element route, which bypasses ListView entirely.

.changeset/7499-gantt-non-axis-floors-omitted.md

  • names plugin-list/src/ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    • plugin-list/src/ListView.tsx — the object-gantt render branch. - plugin-view/src/ObjectView.tsx — generateViewSchema, the authored object-view element route, which bypasses ListView entirely.

.changeset/7773-kanban-adapter-groupfield-write.md

  • names ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    Who is NOT affected — the boundary is node-local. Every VIEW-LEVEL groupField read is untouched and still live: it is a legacy alias of the spec's groupByField on the kanban view config, mapped by normalize-list-view.ts, and both adapters still resolve lanes through it (ObjectView.tsx's kanbanCfg.groupField ||, ListView.tsx's groupByField || groupField). Authoring options.kanban.groupField on a list-view or object-view keeps working exactly as documented in packages/plugin-list/README.md. groupField is dead only on the generated object-kanban NODE.

.changeset/7780-object-kanban-record-source.md

.changeset/8990-object-kanban-groupby-optional.md

  • names packages/plugin-list/src/ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    • packages/plugin-list/src/ListView.tsx generates the node as groupBy: laneField. objectDef loads asynchronously, so laneField is undefined on every load until it lands, and stays undefined whenever the object offers no stageField hint and none of status / stage / state / phase. The renderer serves that node; both published faces refused it. - content/docs/utilities/data-objectstack.mdx documents an object-kanban node that is exactly { type, dataSource }, with no groupBy. ⚠️ This one is weaker and is cited for what it is: that fragment is still refused after this change, at RECORD_SOURCE_REQUIRED, because dataSource is not a rung of the record-source ladder. It shows a lane-less board is a documented authoring; it is not a document this change admits.

.changeset/list-user-actions-collision-5398.md

  • names ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    The harvest now reads the object block only. Both userActions read sites in ListView.tsx carry a comment naming the collision, and __tests__/ListView.userActionsCollision.test.tsx pins each clause of it: the two shapes, a producer that manufactures the view one, the harvest's blindness to it, and the projection that must keep the object's operand with a toolbar block — or an empty block — present on the view.

.changeset/listview-comment-pair-4559.md

  • names ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    Two comment corrections in ListView.tsx (objectui#4559, objectui#4966). No runtime behaviour changes and the emitted bundle is byte-identical; the published .d.ts does change, which is why this is a patch rather than an empty frontmatter.

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 402a266c9 (merge-base with origin/main): 5 file(s) changed outside .changeset/, read against 1481 pending declaration(s) that publish a body (2065 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3049.7 KB 3104.5 KB
Main entry chunk (gzip) 147.9 KB 350 KB
Entry file index-EOf6GpzP.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.57KB 6.15KB
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.17KB 10.58KB
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.39KB 1.21KB
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) 546.99KB 130.83KB
core (index.js) 9.22KB 3.71KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.91KB 62.28KB
fields (index.js) 259.05KB 65.69KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
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.33KB 3.07KB
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.40KB 14.61KB
plugin-charts (index.js) 74.94KB 20.89KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.50KB 35.37KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 232.96KB 61.65KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 147.92KB 37.83KB
plugin-gantt (index.js) 169.62KB 41.91KB
plugin-grid (index.js) 215.37KB 58.92KB
plugin-kanban (index.js) 48.26KB 15.04KB
plugin-list (index.js) 114.44KB 28.25KB
plugin-map (index.js) 22.42KB 7.38KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.67KB 8.95KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 87.84KB 21.96KB
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) 116.21KB 38.14KB
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) 2.52KB 1.31KB
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: f0930bbb64db7594d8cc2d966cd4facf64cfd390

Reviewed from git only: refs/review/pr-10627 (head f0930bb) against merge-base b1030c71a617c4507e5d220319b728ebf367baf1; refs/review/main at ac2d6f1. Spec symbols measured on an installed @objectstack/spec@17.4.0 (the shared tree at /home/user/objectui carries no node_modules; the copy under objectui-issue-10580/node_modules/.pnpm/@objectstack+spec@17.4.0_… was read, not touched, with a probe script run from this scratch directory). Six files in the PR: one changeset, ListView.tsx, UserFilters.tsx, three new pins. No existing test is edited or deleted.

① Derived judgments

Card objectui#10545 (chain head), commit 0814660. Binding triage 5831112408: 「read the declared displayField (spec first, objectui AGENTS.md 0.1), ⛔ with no fallback to the refused spellings; prefer the reader plugin-grid converged on (relationalMetaKeys.ts). Say what the sibling idField read becomes.」

  1. packages/plugin-list/src/ListView.tsx:3742 — the objectDef.fields candidate now reads displayField: field.displayField and nothing else (base read field.display_field || field.reference_field). RIGHT. Measured on 17.4.0: FieldSchema.safeParse of a lookup def accepts displayField; refuses display_field (unrecognized_keys, hint to displayField), reference_field (hint to referenceVia), id_field and idField (no hint), with the bare def and zzz_not_a_real_key as lit controls. No fallback remains, as the triage ordered. The bag is the object-schema def: objectDef is set from dataSource.getObjectSchema(schema.objectName) at ListView.tsx:1880-1882.
  2. packages/plugin-list/src/UserFilters.tsx:311 — resolveFields reads displayField = fieldDef.displayField alone (base fieldDef.display_field ?? fieldDef.reference_field). RIGHT, same contract; ListView passes its objectDef at ListView.tsx:4224-4226.
  3. "Prefer the reader plugin-grid converged on": plugin-grid/src/relationalMetaKeys.ts:331 grades displayField spec as the only display spelling the copy set reads; reference_field is no-producer (:328) and never copied. plugin-list does not depend on plugin-grid (its package.json names no plugin-grid), and applyRelationalMeta copies the reference/reference_to pair rather than the candidate's referenceTo, so the dev converged on the SAME single spelling instead of importing the function. Judged as satisfying the triage's "prefer": no third spelling was introduced.
  4. The sibling idField read: REMOVED, not re-pointed. ListView.tsx:3665 (candidate type has no idField), UserFilters.tsx:39-49 (ResolvedField has no idField), :372 (return), :600-606 (the field handed to LookupValuePicker carries no idField). Judged against the spec: FieldSchema refuses both id_field and idField (measured), so there is no declared spelling to re-point to, and the tree's other two object-def sites say the same (app-shell/src/utils/resolveActionParams.ts:327-333, plugin-charts/src/ObjectChart.tsx:332-338: idField "is NOT a leg this read may gain"). Against consumers: the only consumer is components/src/custom/filter-builder.tsx:2073 field.idField || "id", :2098 r?.[idField] ?? r?.id ?? r?._id, :2145 $filter: { [idField]: … }; FilterBuilder's fields type keeps idField optional (:76), so dropping it is type-safe. In-repo producers of id_field on an object def: none outside tests (the only non-test hits are the two KEEP readers and prose); every idField: producer in the tree sits on an action-param, inline-grid-column or widget contract, not on objectDef.fields. Behaviour lost for a real consumer: only a stored pre-strict document (served verbatim, left on the def by the choke point with a dev-mode no-declared-twin warning, core/src/utils/reference-keys.ts:366-376) or a host DataSource that hand-feeds id_field; for those the picker now keys by id instead of that column. That flip is declared in the changeset (lines 27-31). The removal is consistent with the reader the triage told the dev to prefer: the grid's copy set copies no id column (idField is widget-contract, readers LOOKUP_EDITOR_ONLY, relationalMetaKeys.ts:352), and since objectui#7155/feat(types, fields, plugin-grid)!: converge the lookup dialect on the spec's camelCase #7641 the form/grid widget reads idField camel only, so a stored id_field already reached no reader on that path. RIGHT, with one recorded tension: the objectui#7642 PM ruling (5542090184) made "an ingestion choke point canonicalises the alias first" the standing test for a consumer-side retirement, and id_field cannot be canonicalised (no declared twin, reference-keys.ts:130-133, :164-173); the disposition here therefore rests on the later, card-specific triage (which left the choice to the claimer and asked it to be stated) plus AGENTS.md #0.1, not on finding(plugin-charts, plugin-form, plugin-list, app-shell): six sites read the retired snake lookup keys off an OBJECT-schema def, where FieldSchema refuses them #7642's test. It also leaves id_field split 2/2 across the four object-def readers (ObjectChart.tsx:339 and resolveActionParams.ts:676 still read it). Not a defect of this PR; worth its own card (the PR body files no carrier for it).
  5. Wider than the claim's "the lookup field-def read only": the idField thread through ResolvedField, the return object and the LookupValuePicker prop. All in the two claimed files, all dead once the id_field read went, stated in the PR body. Not asked, explained, harmless.
  6. The objectui#7642 census KEEP comments at both sites are replaced (ListView.tsx:3725-3741, UserFilters.tsx:297-310). Each sentence checked: objectDef is the getObjectSchema def — true; FieldSchema declares displayField and refuses the two snake spellings with the stated hints — true (measured); "the single spelling plugin-grid's copy set reads (RELATIONAL_META_READ_SET, objectui#7155)" — true (the copy set is derived from that table; displayField is its only display entry); "supersedes the objectui#7642 census KEEP, which held only while this chain had no displayField leg" — a fair reading of the KEEP's ground ("no camel leg here, so retiring these reads deletes the only read"), the serve-path half of that ground being answered by the next sentence; "A stored pre-strict display_field is folded onto displayField once, at ingestion (normalizeSchemaReferenceKeys, objectui#7650), never here" — true: data-objectstack/src/index.ts:4865 calls it inside getObjectSchema, canonicalizeRetiredFieldKeys (reference-keys.ts:334-399) folds by alias probe (lowercase, strip _/-/space), and display_field probes onto declared displayField; "FieldSchema declares none [id column] for a lookup (it refuses idField and id_field alike)" — true; "plugin-grid's copy set copies no id column" — true. UserFilters.tsx:301-303 "a host's objectDef is held to the same contract (AGENTS.md #0.1)" — a policy statement consistent with #0.1. The replacement comments read true.
  7. Nothing asked is missing: declared read, no fallback, idField disposition stated, pins beside the existing tests, one changeset.

Card objectui#10547 (member), commit 61ce977. Binding triage 5831160366, reusing ruling 5809501335 on objectui#7531 (letter remove): 「drop the options read so select options come from the object definition once it loads.」

  1. ListView.tsx:3696-3705 — the !objectDef?.fields fallback no longer emits options: buildOptions(fieldName, f.options) (the line sat between :3702 and :3703 at base); the new comment at :3704 cites the ruling. RIGHT. ListColumnSchema.safeParse of {field,label,type:'select'} plus options is refused with unrecognized_keys ['options'], no hint; the bare column parses and a nonsense key is refused as controls; declared keys measured as field,label,width,align,hidden,sortable,resizable,wrap,type,pinned,summary,prefix,link,action. buildOptions stays live in the definition branch (:3712 options: buildOptions(key, field.options)), so the options do arrive once the def loads. The ruling's stop condition (census producers of the key on a list column first) was answered on the card body (0 list-column producers, 6 line-item/flow-config hits on other contracts) and adopted by the triage; the PR body does not re-run it, which the triage did not ask for.
  2. The existing objectui#7531 pin (__tests__/ListView.columnsOnlyRelationalReads-7531.test.tsx) asserts nothing about a column's options, so it is not weakened. The older objectui#6837 note at ListView.tsx:3719-3723 still says the columns branch "is NOT narrowed"; stale since finding(plugin-list): ListView's declared-columns branch reads reference_to and reference off a list COLUMN, and ListColumnSchema declares neither #7531, outside the claimed lines, noted by the PR body. Not a defect.

Existing pins edited or deleted: none (file list: 3 added tests, 0 modified). No guard weakened.

New pins can fail (each read in full at head):

  • __tests__/ListView.declaredDisplayField-10545.test.tsx (5 tests): lit control = the spec test (bare and declared defs parse, each refused spelling yields unrecognized_keys [key]) and the positive displayField to 'full_name'; negative legs = display_field-only and reference_field-only give undefined, and id_field leaves candidate.idField undefined. It waits for referenceTo === 'crm_account' (:109-112) so the candidate provably comes from the definition branch. At base the positive and the three negative legs fail (base reads display_field || reference_field and id_field).
  • __tests__/UserFilters.declaredDisplayField-10545.test.tsx (5 tests): renders the real UserFilters and LookupValuePicker; the record carries a distinct value under every field the picker could read (:33); control = bare def resolves 'ada' (the picker's name default); positive = displayField resolves 'Ada Lovelace'; negative = the two snake spellings resolve 'ada'; id_field leg asserts $filter keys equal ['id'] (:100-101). Non-vacuity asserted at :59. At base the $filter key would be code and the label never resolves.
  • __tests__/ListView.columnsOnlyOptions-10547.test.tsx (3 tests): spec control, the pre-load candidate list equal for a plain column and one carrying options with options undefined (:131-132), and the lit control where options arrive with the definition (:142-155). At pre-fix the carrying column's candidate has an array.

Clause-②: ResolvedField is an unexported interface (UserFilters.tsx:39); UserFiltersProps is untouched; the ListView candidate type is a local inline type; no schema, type or export moves.

② Semver level

.changeset/10545-filter-candidates-displayfield.md: '@object-ui/plugin-list': patch. RIGHT under AGENTS.md 版本号策略: no major (CI Changeset Bump Policy = the no-major check, green); the precedent for the same seam is patch (.changeset/7531-listview-column-undeclared-reads.md, the same fallback in the same package; plugin-grid and components took patch in 7357-retire-field-metadata-depends-on.md for consumer narrowing), and only changes that moved @object-ui/types went minor (7155-converge-lookup-dialect-camelcase.md). Source files of one released package changed; one changeset declared; content/docs/** untouched.

Changeset prose, sentence by sentence:

  • Title "list filters label a lookup's values by the displayField the object definition declares" — TRUE.
  • "Once a list view's object definition loads, the filter builder's candidates and the toolbar's lookup filter read each lookup field's display field off that definition." — TRUE (ListView.tsx:3708-3743, UserFilters.tsx:254-311).
  • "They read display_field, then reference_field, and never displayField, the only one of the three @objectstack/spec's FieldSchema declares." — TRUE (base diff; spec probe).
  • "So a spec-compliant lookup's display field never reached the value picker, which fell back to its default, name." — TRUE (filter-builder.tsx:2072).
  • "Both now read displayField, and only it." — TRUE.
  • "This is the same single spelling @object-ui/plugin-grid reads off a definition." — TRUE of plugin-grid's own copy set (relationalMetaKeys.ts:331, :415-420); the @object-ui/fields widgets the grid's editor feeds still read reference_field as a second leg (LookupField.tsx:332, fields/src/index.tsx:2517), which the PR body names as a boundary.
  • "FieldSchema refuses the other two: display_field with a rename to displayField, and reference_field with a pointer to referenceVia." — TRUE (measured).
  • "Behaviour change: a field definition that carries only the refused display_field no longer sets the picker's display field, and neither does one that carries only reference_field. The picker falls back to name." — TRUE.
  • "A stored definition served through the ObjectStack adapter still works, because that adapter folds display_field onto displayField when the schema is loaded." — TRUE for display_field (data-objectstack/src/index.ts:4865; the fold never overwrites a differing displayField, reference-keys.ts:379-397). It is silent about a stored reference_field-only def, which the fold leaves and which the previous sentence already declares lost. Not false.
  • "A definition that reaches the list any other way, such as a host DataSource or an objectDef passed straight to UserFilters, needs displayField." — TRUE.
  • "The two readers also stop reading id_field as the lookup's id column." — TRUE.
  • "FieldSchema declares no id column for a lookup and refuses both id_field and idField, so the picker now always keys a lookup by id, its default." — TRUE (measured; filter-builder.tsx:2073).
  • "A definition that carried id_field used to make the picker match and emit that column's values instead." — TRUE (base idField = fieldDef.id_field; filter-builder.tsx:2098, :2145).
  • "Also (objectui#10547): before a list view's object definition loads, the filter builder's candidates come from the view's declared columns, and that fallback no longer reads options off a list column." — TRUE.
  • "ListColumnSchema refuses the key with unrecognized_keys, so a spec-compliant view never carries it." — TRUE (measured).
  • "A select field's options now reach the filter builder only from the object definition, once it loads, which is where they already came from after the load." — TRUE (ListView.tsx:3712).

No README or content/docs prose is added by the diff.

③ Boundary flags

  • PR body flags and open questions. The body lists no open questions. Its "Acceptance notes" check out against the tree at head: plugin-form/src/deriveMasterDetail.ts:255 and :311 still read d?.display_field || d?.reference_field and never displayField (a real class-b sibling, correctly handed on rather than fixed here); @object-ui/fields LookupField.tsx:332 and index.tsx:2517 keep the reference_field leg; ObjectChart.tsx:339, :386-387 and resolveActionParams.ts:675-676 keep the snake legs (two dispositions now coexist for display_field/reference_field, and, new with this PR, for id_field); .changeset/lucky-donkeys-shave.md has an empty frontmatter, describes both plugin-list sites as KEEP, and is not in the file list; the objectui#6837 "is NOT narrowed" note at ListView.tsx:3719-3723 is still there. One PR-body sentence is FALSE: "Two merges of origin/main … The second one picked up this seat's objectui#10597". git log 0814660..1336ba8 contains 6d1e77c fix(plugin-list): … (objectui#10512) (#10597) and git log 61ce977..f0930bb does not, so the FIRST merge (1336ba8) carried fix(plugin-list): a held user filter does not narrow the fetch on a dataset-bound chart (objectui#10512) #10597; the second (f0930bb) touches no plugin-list file. Provenance only; nothing in the tree depends on it; the seat should correct the body. The body's spec table matches my probe exactly (including "no hint" for id_field/idField and the ListColumnSchema key list); UserFilterFieldSchema keys measured as field,label,type,options,showCount,defaultValues, and it refuses idField (with a hint to field, which the body omits; not false). The body's claim about id_field hits in objectstack origin/main was not verified here (outside this review's read set). @object-ui/types still declares reference_field on the widget contract LookupFieldMetadata (types/src/field-types.ts:711) and on a view field type (types/src/views.ts:205, zod/views.zod.ts:91); neither is the object-schema def these two sites read, so the retirement here is not the objectui#7155 second-contract shape, but the seat should know the spelling is still published on those bags.
  • CI at head f0930bb. First poll (12:5xZ): 42 check-runs, 30 success, 3 skipped, 9 in progress. Re-polled once at 12:57Z: 43 check-runs, 40 success, 3 skipped (Test (coverage), dependabot, Test (coverage shard …/4)), 0 failure, 0 in progress. Lint, Type Check, all 8 test shards, dist pins, Build & E2E, Line Citation Gate, Changeset Declaration/Fixed Group/Bump Policy/Claim Re-read and Spec Main Shape Gate are all green.
  • Draft: yes, draft: true (state open, mergeable: true, mergeable_state: behind).
  • Clause-②: no holds. No accept set moves (no spec or @object-ui/types file touched); no published export moves (ResolvedField unexported, UserFiltersProps unchanged, LookupValuePicker/FilterBuilder untouched, candidate type local).
  • Main since merge-base: two commits (402a266 objectui#6900 app-shell, ac2d6f1 objectui#10567 fields/i18n), 29 files, zero overlap with the PR's six files. Main did not move under the PR's files.

Implemented-by: claude/issue-10545-filter-candidates-displayfield
Reviewed-by: session_01KUxVUa7e39aNjhkKi1gsoy

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 25, 2026 13:03
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit dddbc27 Sep 25, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-10545-filter-candidates-displayfield branch September 25, 2026 13:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment