Skip to content

searchAll's title rendering reads only the deprecated displayNameField, so a canonically-designated primary title never titles a ⌘K hit #8786

Closed

Description

@hotlong

Filing unassigned; domain:* routing is the triage seat's. Found while implementing #7643 (which fixed the RECALL half of searchAll and deliberately left titling alone — different concern, same function).

The defect

packages/metadata-protocol/src/protocol.ts, searchAll's renderTitle, resolves a hit's title from this candidate list:

const candidates = [
    obj.displayNameField,
    'name', 'full_name', 'title', 'subject', 'label', 'company',
].filter((c) => typeof c === 'string' && hasField(c));

Under ADR-0079, nameField is the CANONICAL primary-title pointer and displayNameField is the deprecated alias. This list reads only the alias.

Why it is reachable rather than theoretical

provisionPrimary — the ADR-0079 designation seat the SchemaRegistry runs on every object at registration (materializeBaseLayer, objectql/src/registry.ts) — writes nameField only:

packages/spec/src/data/display-name.ts:301   return { ...objectMeta, nameField: resolved };
packages/spec/src/data/display-name.ts:314   return { ...objectMeta, fields, nameField: 'name' };

It never stamps displayNameField. So for an object that declares nameField (or has one designated) and does not also carry the deprecated alias, obj.displayNameField is undefined, the entry is filtered out, and title resolution falls through to the conventional-name list.

Impact is confined to objects whose primary title is NOT one of name / full_name / title / subject / label / company. For those, a ⌘K hit falls all the way through to return String(row.id) — the palette shows a raw id where the object's own declared primary title exists and is populated. Objects using a conventional name are unaffected, which is why this has stayed invisible.

The fix

Read the same precedence every other consumer of this pointer already uses — obj.nameField ?? obj.displayNameField — as the first candidate. Three call sites in this file already spell exactly that (the #4254 ingress gate at assertSearchFieldsAreSearchable, the engine's expandSearchOnAst, and, since #7643, searchAll's own search-field resolution one screen above renderTitle). This would make the fourth, in the same function as the third.

Suggested test: an object declaring nameField: 'company_name' with no displayNameField, seeded with a populated company_name, must produce a hit whose title is that value rather than the record id.

Scope note

Not fixed in PR for #7643 on purpose: that card is about RECALL (which rows come back), this is about PRESENTATION (what the returned row is called). They share a function but not a contract, and #7643's file surface was declared region-limited while another PR was open on the same file.

Activity

  1. added theissue type on Aug 15, 2026
  2. hotlong commented on Aug 15, 2026

    @hotlong
    ContributorAuthor

    Triage: lands in packages/metadata-protocol ⇒ domain:metadata; graded pm:queue, type Bug. Restores a canonical pointer the platform already declares (ADR-0079) — accept set unchanged ⇒ Bug, not Feature, and nothing here is escalable.

    Premise verified on origin/main @ 84cb121, not inherited

    Both load-bearing claims hold verbatim:

    packages/metadata-protocol/src/protocol.ts:8603-8606
      const candidates = [
          obj.displayNameField,
          'name', 'full_name', 'title', 'subject', 'label', 'company',
      ].filter((c): c is string => typeof c === 'string' && hasField(c));
    
    packages/spec/src/data/display-name.ts:301   return { ...objectMeta, nameField: resolved };
    packages/spec/src/data/display-name.ts:314   return { ...objectMeta, fields, nameField: 'name' };
    

    provisionPrimary does stamp nameField only. Zero-hit discipline satisfied — displayNameField returns 8 hits in that same file, so its absence from the two return statements is a real reading rather than a bad probe.

    ⚠️ One supporting claim in the card is measurably wrong — do not inherit it

    "Three call sites in this file already spell exactly that … This would make the fourth, in the same function as the third."

    Measured: one, not three.

    $ git grep -nE "nameField\s*\?\?" origin/main -- packages/metadata-protocol/src/protocol.ts
    :7221:  displayField: gate.schema?.nameField ?? gate.schema?.displayNameField,
    

    Counter-probe: nameField occurs only 5 times in the whole 16,480-line file, so the narrow match is real. The other two named symbols live in other packages — assertSearchFieldsAreSearchable in packages/lint and packages/spec/src/data/search-fields.ts, expandSearchOnAst in packages/objectql/src/engine.ts — and the third ("since #7643, searchAll's own search-field resolution") does not exist on main yet, because #7643 is still pm:dispatched.

    The defect itself is unaffected: the fix is still obj.nameField ?? obj.displayNameField as the first candidate, and the precedent for that spelling is still real — it is just cross-package precedent plus one local site, not a local pattern of three. The dev should not open the file expecting to find three.

    ⛔ Hard serial constraint — same function as an in-flight card

    #7643 is pm:dispatched on searchAll, the same function, and this card's own scope note records that #7643 declared a region-limited file surface precisely because another PR was open on this file. This card must therefore either wait for #7643 to land or declare renderTitle at region level and hold to it. Do not dispatch this in the same batch as #7643.

    Full picture for the file — four cards, regions listed on #8783. domain:metadata seat #6367 is vacant, so no seat currently holds this file's serial queue; flagged to the maintainer in this round's brief.

    Impact bound is accepted as filed and is genuinely narrow: only objects whose primary title is outside name / full_name / title / subject / label / company fall through to String(row.id). That narrowness is why it stayed invisible, and it is also why it grades pm:queue rather than a release nomination.


    Generated by Claude Code

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 15, 2026
  4. self-assigned this
    on Aug 15, 2026
  5. hotlong commented on Aug 15, 2026

    @hotlong
    ContributorAuthor

    Claim: PM loop round 5
    Session: session_01XeQRiAa7vYRVX5Fog7Zby8
    Branch: claude/issue-8786-searchall-title-namefield
    Worktree: objectstack-issue-8786
    Domain: domain:metadata
    File surface: packages/metadata-protocol/src/protocol.ts — region-declared: renderTitle inside searchAll, around :8640 — plus its co-located pins. ⛔ Stop on breach and report.
    Container & model: S, mode:subagent, model: opus — above the sonnet floor deliberately: the change alters what a user sees in ⌘K, and the nameField / displayNameField precedence is a judgement if an object carries both.
    Serial constraints cleared: ⛔ triage's hard serial behind #7643 is DISCHARGED — PR #8787 merged at 02:09Z, confirmed on both readings (timeline merge event + the artifact on origin/main). Concurrent region in this file: auditMetaItem :6091 (#8747, in flight) — ~2,500 lines away. diffMetaItem :15349 (#8671) has its PR open at #8797; no dev is editing the file. #8770 (#8421) is a held draft, parked.

    ⚠️ Triage's call-site count is now stale — I re-measured, and the news is good

    Triage measured one nameField ?? site and correctly flagged that the card's "three call sites in this file" claim was wrong. That measurement was taken at 84cb121 while #7643 was still pm:dispatched. #7643 has since landed, and it added exactly the site triage said "does not exist on main yet".

    Re-measured on the current origin/main f039a4ca:

    :7221   displayField: gate.schema?.nameField ?? gate.schema?.displayNameField
    :8684   displayField: obj.nameField ?? obj.displayNameField        <- NEW, from #7643
    :8640   obj.displayNameField,                                      <- the defect, unchanged
    

    Counter-probe: nameField occurs 7 times in the whole file (was 5 pre-#7643), so the narrow match is real rather than a bad pattern.

    The operative fact: searchAll now contains both the correct spelling and the broken one, 44 lines apart, in the same function. You are not importing a convention from another package — you are making renderTitle agree with the line 44 below it. Copy the spelling from :8684.

    So: the card says "three sites, this would be the fourth" — wrong. Triage says "one site, cross-package precedent" — true when written, now stale. Two sites, one of them in your own function. Verify all three line numbers from the tree yourself; the file has moved twice tonight and will move again.

    The defect (premise verified by triage on 84cb121, re-confirm on current main)

    renderTitle's candidate list reads obj.displayNameField — the deprecated alias — while provisionPrimary, the ADR-0079 designation seat the SchemaRegistry runs on every object at registration, stamps nameField only:

    packages/spec/src/data/display-name.ts:301   return { ...objectMeta, nameField: resolved };
    packages/spec/src/data/display-name.ts:314   return { ...objectMeta, fields, nameField: 'name' };
    

    Triage's zero-hit discipline: displayNameField returns 8 hits in display-name.ts, so its absence from those two returns is a real reading.

    Consequence: an object declaring nameField without the deprecated alias gets undefined, the entry is filtered out, and title falls through the conventional-name list to return String(row.id) — the palette shows a raw id where a declared, populated primary title exists.

    Scope — ⛔ presentation only

    #7643 fixed recall (which rows come back) and deliberately left titling alone. This card is titling. Same function, different contract. ⛔ Do not touch search-field resolution, the object sweep, caps, ordering, or anything #7643 just landed — that code is hours old and its pins are load-bearing.

    Impact bound — accepted as filed, and it bounds your test too

    Only objects whose primary title is outside name / full_name / title / subject / label / company are affected; everything else already resolves through the conventional list. That narrowness is why this stayed invisible, and it means your fixture must use a non-conventional title field or it will pass before and after.

    Verification bar

    The card names the pin and it is a good one: an object declaring nameField: 'company_name' with no displayNameField, seeded with a populated company_name, must produce a hit whose title is that value rather than the record id. Add the discriminating control in the same test — an object using a conventional name still titles correctly — so the pin shows you changed precedence rather than bypassing the candidate list.

    ⚠️ One judgement I want measured, not assumed: if an object carries both nameField and displayNameField and they name different fields, your change flips which one wins. Measure whether any object in the repo is in that state before deciding it is safe, and pin the precedence you land on. If real objects disagree, ⛔ stop and report — that is a behaviour change beyond this card.


    Generated by Claude Code

  6. hotlong commented on Aug 15, 2026

    @hotlong
    ContributorAuthor
    {
      "issue": 8786,
      "status": "done",
      "branch": "claude/issue-8786-searchall-title-namefield",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/8809",
      "premise_still_valid": true,
      "summary": "renderTitle's candidate list in searchAll now opens with `obj.nameField ?? obj.displayNameField` instead of the deprecated alias alone, matching the spelling #7643 landed 44 lines below it at :8699 (was :8684 pre-merge). Re-derived all four dispatch numbers from the tree before editing and they held exactly: :8640 defect, :8684 correct spelling, :7221 gate, :8552 searchAll, with `nameField` at 7 occurrences and exactly 2 `nameField ??` sites — the claim comment's corrected count, not the card's 'three' nor triage's stale 'one'. ZONE 3 measured rather than assumed: across 52 files carrying `displayNameField`, 38 declarations have a co-located `nameField`; 33 agree exactly and the 5 that disagree are ALL test fixtures that exist to pin precedence — every one already pinning `nameField` as the winner (`resolveDisplayField({nameField:'a',displayNameField:'b'}) === 'a'`). No real object is in the disagreeing state, so the precedence landed on is the platform's existing answer and no escalation was needed. Region held: only renderTitle and its own doc comment; nothing #7643 wrote was touched. origin/main was merged (it had advanced to caaae2cca under me) before the PR.",
      "tests": "All at HEAD cd974da6e, quoted from the post-merge union run (the merge commit is the final commit). `pnpm --filter @objectstack/metadata-protocol test` -> 'Test Files 92 passed (92) / Tests 1375 passed (1375)'. New file protocol.search-title-namefield.test.ts, 6 cases, all green in isolation. REVERSE VERIFICATION with the direction predicted before running and observed as predicted — restoring `obj.displayNameField,` alone turns exactly 2 of 6 red with the defect's own signature: \"expected 'acc_1' to be 'Acme Industrial'\" and \"expected 'OLD-11' to be 'TK-77'\", 'Tests 2 failed | 4 passed (6)'; the 4 green are the controls, which by design do not discriminate this fix. Fix was committed BEFORE the ablation and restored via `git checkout <branch> -- <path>`, `git status` clean after. Gate union re-derived against actual changed paths via scripts/pm/dispatch-gates.mjs, all green: check:nul-bytes, check:cross-package-test-inputs, check:durability-log-level, check:filter-alias-parity, check:changeset-gate-self-tests, check:objectui-changeset, check:query-options-erasure, check:engine-double-contract, check:type-check-coverage, check:type-check-debt. No dogfood ablation in this card (no dist/-resolved test), so nothing to rebuild on that axis; the type-check ratchet DID need the built closure and got it — `turbo run build --filter=./packages/* --filter=./packages/*/*`, 70/70 successful, then '33 ledger entries re-measured, 1926 raw tsc errors total, none above its recorded number'.",
      "open_questions": [],
      "out_of_scope_findings": [
        "NOT FILED (already tracked, no duplicate): check:type-check-debt reports @objectstack/lint's TEST_DEBT records 20 while tsc now measures 19 — a 1-error surplus another card's improvement opened. The gate itself calls it informational and cites its own tracking issue #6376, and closing it (`--lower`) would be an unrelated ledger edit in this PR, so I left it. Flagging so the PM can decide whether to queue the `--lower`."
      ]
    }

    Gate-list delta worth the PM's attention: the dispatch prompt named the gates from dispatch-gates.mjs against protocol.ts alone. Re-deriving against my actual changed paths (source + new test + changeset) surfaced four families the prompt could not have listed — the three .changeset families (check:changeset-gate-self-tests, check:objectui-changeset, and the adr-0087 / no-major / empty-changeset trio) plus the convention-triggered check:query-options-erasure and the check:type-check-coverage / check:type-check-debt pair, which fire because the change adds a test file. The last one mattered: @objectstack/metadata-protocol sits in the DEBT ledger, so a new test file genuinely moves that ratchet, and it refuses to measure at all on an unbuilt worktree. Ran green after a full closure build.


    Generated by Claude Code


    Generated by Claude Code

  7. hotlong commented on Aug 15, 2026

    @hotlong
    ContributorAuthor

    ACCEPTED — PR #8809 marked ready and queued. All 26 gate jobs read job-by-job; zero failures.

    PM session session_01XeQRiAa7vYRVX5Fog7Zby8, round 5.

    You resolved the one stop-condition by measurement, which is exactly what it was for

    I made this a hard stop: if an object carries both pointers naming different fields, your change flips which one wins — measure whether any object is in that state, and if real objects disagree, stop and report.

    You measured it rather than reasoning about it. Across 52 files carrying displayNameField, 38 declarations have a co-located nameField:

    • 33 agree exactly, each under an [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField) comment;
    • 5 disagree, and all five are test fixtures that exist precisely to pin precedence — every one of them already pinning nameField as the winner, matching resolveDisplayField({nameField:'a', displayNameField:'b'}) === 'a' as pinned in display-name.test.ts.

    So no real object is in the disagreeing state, and the precedence you landed on is the platform's existing answer rather than a new one. That is the difference between "the stop condition did not fire" and "I checked whether it fired" — and it is now pinned here too, so the next person does not have to re-derive it.

    The two controls that make the pin mean something

    The pin alone (nameField: 'company_name', no alias ⇒ title is Acme Industrial, not acc_1) would pass under a rewrite that short-circuits on the pointer and bypasses the candidate list entirely. Two of your six cases close that off:

    • an object with a conventional name and no pointer at all still titles correctly;
    • an empty pointer value falls through to the conventional entry — proving the pointer is a candidate in one ordered list, not a short-circuit return.

    Using non-conventional title fields (company_name, ref_no, ref_code) throughout was necessary rather than incidental: a conventional field would have passed identically before and after the fix, and the test would have proved nothing.

    Reverse verification: predicted before running, measured exactly 2 of 6 red, with the defect's own signature (expected 'acc_1' to be 'Acme Industrial', expected 'OLD-11' to be 'TK-77'). The 4 that stayed green are the controls, which by design do not discriminate this fix — worth saying, because a reverse verification where everything goes red is measuring the file, not the change.

    The line numbers held — and the corrected count was the operative one

    You re-derived all four numbers from the tree before editing and they held exactly (:8640 defect, :8684 correct spelling, :7221 gate, :8552 searchAll), with nameField at 7 occurrences and exactly 2 nameField ?? sites.

    That count is the thing worth recording: the card claimed three local call sites (wrong), triage corrected it to one plus cross-package precedent (true when written, stale by dispatch — #7643 landed and added the second), and the truth was two, one of them 44 lines below the defect in the same function. Re-measuring at dispatch turned "import a convention from another package" into "make this line agree with the one below it", which is a materially easier and safer change.

    Scope held

    Presentation only. #7643's recall contract untouched, its protocol.search-case-fold.test.ts pins still passing, search-field resolution / object sweep / caps / ordering unmodified. Region held to renderTitle, so the concurrent auditMetaItem and diffMetaItem work was unaffected — three cards in one 16,000-line file this round, no collisions.

    Noted, not actioned

    check:type-check-debt reports @objectstack/lint's TEST_DEBT records 20 while tsc measures 19 (-1) — a surplus another card's improvement opened. You left it, correctly: --lower in this PR would be an unrelated ledger edit. It is informational per the gate's own wording and tracked in #6376. Three separate devs have now reported it tonight, which is enough signal that it wants its own one-line card rather than a recurring footnote; I am carrying that rather than asking anyone to fold it in.

    Docs

    Drift check flagged 3 pages; no action — this changes which declared field titles a search hit, and no page documents that resolution order.

    Landing

    ready → auto-merge → merge queue. Confirmation on both readings — the timeline merge event and the artifact on origin/main.


    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

bugSomething isn't workingpm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions