Skip to content

bug(plugin-grid): ObjectGrid's inline lookup picker ignores four more spec-declared field props — multiple, allowCreate, lookupPageSize, dependsOn are read on its own path and never copied #7154

Description

@os-warren

Provenance: measured while implementing objectui#6875, by the derivation that card
asked for. Filed unassigned. Not folded into #6875 — that card names five display/target
spellings and its fix closes those; these four are a different slice of the same seam and
each changes picker BEHAVIOUR, not just which display string renders.

What's measured

objectui#6875 replaced ObjectGrid's hand-kept RELATIONAL_META_KEYS with a table in
packages/plugin-grid/src/relationalMetaKeys.ts that classifies every key the grid's own
consumers read off the fieldMeta bag, plus a gate
(__tests__/relationalMetaCopySet.derivation.test.ts) that re-extracts that read set from
the consumer sources. Running the extractor over the three consumers on the grid's render
path — LookupCellRenderer (packages/fields/src/index.tsx), LookupField.tsx and
UserField.tsx — surfaced four keys that are:

  1. read on this path, off fieldMeta, and
  2. declared by @objectstack/spec 17.2.0's FieldSchema, so a spec-compliant
    producer can and does emit them, and
  3. never copied onto the column meta.

Verbatim from packages/fields/src/widgets/LookupField.tsx on dd35800af:

  • :259 — const multiple = fieldMeta?.multiple || false;
  • :270 — const explicitAllowCreate = fieldMeta?.allow_create ?? fieldMeta?.allowCreate;
  • :277 — const lookupPageSize: number | undefined = fieldMeta?.lookup_page_size ?? fieldMeta?.lookupPageSize;
  • dependsOn — read the same way, and the driver of the cascading picker filter.

All four are on FieldSchema's 71-property strict shape (measured on the installed
package, name/type/label as the positive control). None is on RELATIONAL_META_KEYS
even after #6875.

They are carried in the new table with verdict deferred and a note, so the gate keeps
them visible rather than letting them lapse back into "nobody noticed" — but deferred
means the value still does not arrive.

Why each is a user-visible defect, not a tidiness item

  • multiple — an author declaring a multi-value lookup gets a SINGLE-select inline picker
    in the grid. The form surface honours it; the grid does not.
  • allowCreate — an author setting allowCreate: false to opt a relation out of inline
    quick-create still gets the create affordance in the grid's picker, because the explicit
    value never arrives and isUserFacingReference() decides instead.
  • lookupPageSize — the author's page size is ignored; the picker uses its default.
  • dependsOn — the cascading filter the author declared does not scope the grid's picker.
    ⚠️ objectui#2215 is a closed card in this area ("Cascading lookup (dependsOn) broken in
    forms; table picker bypasses the dependent filter") — worth reading before acting, in
    case this is the grid-side residue of it rather than a fresh defect.

What is NOT measured here

  • No user report is attached. This is the same static producer-vs-consumer measurement
    objectui#6875 was, run one seam wider — the reachability half is measured (spec declares
    all four), the rendering half is not: nobody has yet rendered a grid over a multiple
    lookup and watched the picker.
  • Whether plugin-dashboard needs any of them: it does NOT on today's reading — its two
    widgets are read-only and reach no field EDITOR, so the picker keys have no reader on
    that path. That verdict is already recorded on CELL_RELATIONAL_META_KEYS and does not
    move with this card.

Shape of a fix

Mechanically small — flip four deferred verdicts to spec in
relationalMetaKeys.ts and the derived copy set picks them up, with no list to edit. The
work is the behaviour: each of the four wants a rendering test on the grid's own path, of
the kind objectui#6875 added for displayField
(__tests__/lookupDisplayFieldSpelling-6875.test.tsx), and dependsOn wants objectui#2215
read first.

⛔ Do not also copy the snake_case twins the same chains read (allow_create,
lookup_page_size, depends_on). FieldSchema declares none of them — they parse to
unrecognized_keys — so no producer can fill them, which is exactly why objectui#6875 left
reference_field and lookup_columns out. The gate proves that absence against the
installed spec.

Generated by Claude Code

Activity

  1. self-assigned this
    on Sep 1, 2026
  2. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    🔒 Claimed — domain:ui seat

    Unblocked: PR #7156 landed as 9700dd94e. Verified on main: relationalMetaKeys.ts now exists (absent at the pre-merge sha) carrying displayField ×4, descriptionField ×3, lookupColumns ×3, imported twice by ObjectGrid.tsx, with the derivation gate relationalMetaCopySet.derivation.test.ts present.

    The deferral that created this card is now the thing that protects it. I ruled these four out of objectui#6875 because each changes picker behaviour — multiple flips cardinality, allowCreate adds an affordance, lookupPageSize changes paging, dependsOn cascades a filter — and folding them in unverified would have shipped behaviour changes without the rendering proof displayField got. That is the exact failure class objectui#6875 existed to fix.

    What made deferring safe rather than merely cautious is that the derivation carries all four with verdict deferred, so the gate keeps them visible and they cannot lapse. That property is now on main and this card inherits it.

    ⚠️ Carried into the dispatch: each key needs its own rendering evidence, the way displayField got two columns differing only in spelling over one record. ⛔ Four verdict flips is not the deliverable — the copy set being derived makes the mechanical change trivial, which is precisely why the measurement is the work. And ⛔ dependsOn wants objectui#2215 read first (a closed prior card reporting the table picker bypassing the dependent filter).


    Generated by Claude Code

  3. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 7154,
      "status": "done",
      "branch": "claude/issue-7154-lookup-picker-remaining-keys",
      "pr": "https://github.com/objectstack-ai/objectui/pull/7167",
      "premise_still_valid": false,
      "summary": "Re-derived on origin/main head 51449a043. All four keys are spec-declared (FieldSchema 17.2.0, 71 strict props, positive control name/type/label) and read by LookupField at :259/:271/:277/:300 — but the card's fourth claim is false: all four ALREADY take effect in ObjectGrid's inline lookup picker with the copy set unchanged, so the deliverable is not four verdict flips. applyRelationalMeta writes onto the fieldMeta handed to CellRenderer (the read-only cell, whose LookupCellRenderer reads none of the four); the inline editor is a different seam — renderCellEditor looks the field up in objectSchema and spreads the whole def into the widget, so every key a def carries reaches LookupField regardless of the copy set. Both halves read objectSchema.fields[name], so copying could never rescue an editor the schema read did not already serve. Shipped: a rendering pin per key with its control, the four verdicts kept deferred with the measurement in their notes, and corrections to the two docblocks and three call-site comments that claimed the picker reads this bag — the sentences the card was filed against. dependsOn was NOT deferred: objectui#2215 was closed COMPLETED by PR objectui#2216 (form injects live dependentValues; every picker takes the dependsOn chain as a hard baseFilter), and the grid-side residue is measured — the key arrives and gates permanently because the grid supplies none of LookupField's three dependent-value channels. Filed as objectui#7165 rather than fixed, because saved-row vs staged-row is a seam decision.",
      "tests": "All runs through scripts/pm/os-verify-lock.sh, canonical repo-root vitest invocation. NO BUILD is involved in these runs: this repo's root vitest config aliases every @object-ui/* specifier to the sibling package's src/, so the mutated/edited source is what executes (the ablations state this rather than a dist rebuild). UNION at final commit 21509d3c8, echoed by the run itself: 'UNION AT SHA 21509d3c8' then 'Test Files 7 passed (7)' / 'Tests 35 passed (35)' over lookupPickerKeys-7154, relationalMetaCopySet.derivation, relationalMetaCopySet-6711, relationalMetaCopySet-6874, lookupDisplayFieldSpelling-6875, inlineEditLookupRepro, plugin-dashboard lookupRelationalMeta-6694. TYPECHECK: 'pnpm --filter @object-ui/plugin-grid run type-check' first returned NOT MEASURED — exit 2 with TS2307 'Cannot find module @object-ui/fields' etc., an unbuilt dependency closure, not a red gate; after 'pnpm --workspace-concurrency=2 --filter @object-ui/plugin-grid^... build' (exit 0) it re-ran to exit 0 with the script echoed ('tsc --noEmit && tsc -p tsconfig.test.json'). tsconfig.test.json includes the new test file — 1 hit under --listFiles — so the typecheck actually covers it. LINT: 'pnpm --filter @object-ui/plugin-grid run lint' exit 0, '731 problems (0 errors, 731 warnings)', all pre-existing. GATES: check:control-bytes OK (5940 files scanned), check:vi-mock-specifiers OK, check:vi-mock-inherit OK, check-changeset-no-major 'No changeset declares a major bump', check-changeset-fixed OK, check-changeset-presence exit 0 verbatim: '4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) ... Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate.' Plus a manual control-byte grep over the four changed files: no match. ABLATION A1 — direction predicted BEFORE running (the four behavioural cases red, the classification case green): stripped multiple/allowCreate/lookupPageSize/dependsOn from renderCellEditor's spread in ObjectGrid.tsx. Mutation proved on disk by anchor count 1 to 0 and injected-marker count 0 to 1 AND blob hash 94798f01 to 59059ebf (not by the editor's exit code). Observed exactly as predicted: 'Tests 4 failed | 1 passed (5)'. Restore proved by state: hash back to 94798f01 = the HEAD blob, and git diff HEAD / git diff --cached / git status --short all empty. ABLATION A2 — predicted only the classification case red, every behavioural case unchanged: flipped all four verdicts deferred to spec in relationalMetaKeys.ts. Mutation proved by marker count 4 to 0 and hash 4469a85a to da671dc3; observed 'Tests 1 failed | 12 passed (13)' against the pin file plus the derivation gate — i.e. flipping is behaviourally inert and the derivation gate stays green either way. Restore proved by the same three-way state check plus hash equality. Both scripts carried a trap on EXIT INT TERM restoring by absolute path against HEAD.",
      "mcp_calls": "6 — 2 search_issues (each returning its own known-hit control: #7154/#2215 and #6875/#6874/#6711, so search was live this session), 2 issue_write create, 1 create_pull_request, 1 add_issue_comment. All card and PR reads went through the zero-quota public-repo page payload; the REST probe returned 403 for this session and gh is absent.",
      "open_questions": [
        {
          "question": "The PR carries 'Fixes #7154', so merging closes the card — but it answers the card by falsifying it rather than by doing what it asked (four verdict flips). Is that the disposition you want?",
          "options": [
            "A. Keep 'Fixes #7154' — the question the card raised is answered and pinned, and the two live defects it uncovered are carried as objectui#7165 and objectui#7166.",
            "B. Downgrade to 'Part of #7154' and re-triage the card yourself against the measurement."
          ],
          "recommendation": "A, because the card's own acceptance criterion was rendering evidence per key, and that evidence exists and is pinned; leaving it open would leave a card whose stated defect is measured absent, while the two real defects now have their own cards. B costs a triage round and buys nothing the PR body does not already state."
        }
      ],
      "out_of_scope_findings": [
        "filed as #7165: a dependsOn lookup column is permanently uneditable in ObjectGrid — the key arrives and the picker gates forever, because the grid supplies none of LookupField's three dependent-value channels (PR objectui#2216 gave the form that injection; the grid never got it). Saved-row vs staged-row is a seam decision, spelled out in the issue.",
        "filed as #7166: 6 of the 10 keys ObjectGrid copies onto a column's fieldMeta have no reader on that bag (descriptionField, description_field, lookupColumns, lookupFilters, lookup_filters, id_field) — the objectui#6711 / objectui#6874 class. Two of them were added by objectui#6875 as its fix, so that card delivered 1 of 3; its displayField half is genuinely delivered and stays pinned."
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

  4. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    ✅ LANDED — PR #7167 squash-merged to main (47035ce79)

    Verified by content with live controls, never by sha (a squash creates a new commit, so a sha proves nothing about what is in the tree):

    probe result
    SUBJECT — new suite plugin-grid/src/__tests__/lookupPickerKeys-7154.test.tsx present 1
    CONTROL — prior suite lookupDisplayFieldSpelling-6875.test.tsx present in the same listing 1
    SUBJECT — corrected docblock, "Only the FIRST of those three is fed by this bag — objectui#7154" 1
    CONTROL — applyRelationalMeta still live in ObjectGrid.tsx 5

    ⚠️ One probe of mine was badly chosen, and I am recording it because it nearly produced a false negative

    My first subject marker was the phrase the PR said it removed — "dispatches the same bag" — expecting 0. It returned 1. Read carelessly, that says the correction did not land.

    It did land. The surviving hit is inside the correction itself:

    relationalMetaKeys.ts:51 — "This docblock used to say the inline editor "dispatches the same bag" into the two widgets. It does not, and the correction matters because…"

    ⇒ A phrase a correction quotes is not a usable subject marker for that correction. This seat's house style is to state the old claim before refuting it — which is good for readers and fatal for a naive grep. The re-probe used a marker the correction does not quote, and that is the reading above. Adding it to the instrument list beside "a zero can also mean the symbol moved" and "an empty output is not a 0".

    What this card actually delivered — the premise was falsified, and that is the result

    The card asked for four deferred → spec verdict flips. The PR flipped none, and that is correct: the rendering proof killed the premise. multiple, allowCreate, lookupPageSize and dependsOn already take effect in the inline picker with the copy set unchanged, because the picker is not fed that bag at all — renderCellEditor spreads the whole field def into the widget ({ name: ctx.column.accessorKey, ...fieldDef }), so every key a def carries reaches LookupField regardless of RELATIONAL_META_KEYS. fieldMeta goes to <CellRenderer> and nowhere else.

    Flipping the four would have written four members onto a bag whose consumer does not read them — the exact shape #6711 (reference_to_field) and #6874 (titleFormat) were each filed to retire. The two ablations settle it: stripping the four keys from renderCellEditor's spread turns the four behavioural cases red (4 failed | 1 passed), while flipping all four verdicts is behaviourally inert (1 failed | 12 passed — only the classification assertion moves).

    ⭐ This is also the second half of a correction this seat owes. #6875's landing note over-praised that card; #7154 measured that descriptionField and lookupColumns are inert on the fieldMeta bag, so #6875 delivered 1 of the 3 keys it shipped. The general rule, now paid for twice: a derivation establishes that a consumer READS a key; it does not establish that a given BAG is how the consumer gets it.

    Unblocking

    Both were filed rather than ridden into this PR, correctly — each is its own adjudication, and #7165 in particular is a seam decision (whether the grid feeds the saved row or the row plus its staged edits).

    Clearing pm:dispatched.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatfinding

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions