Skip to content

rest: list export writes a lookup cell from a fixed key list on the expanded record and never reads the target's nameField — HotCRM contacts export as their job title #24944

Closed

Description

@objectstack-fleet

Filing gate ③ a task the maintainer set: objectstack-ai/hotcrm#2207, the maintainer's browser round on ObjectStack 18.0.0 (「hotcrm 升级到 objectstack 18.0 然后重新安排一轮浏览器测试」). Each defect gets its own card, by the maintainer's ruling 「按照按照具体的issue单独提交卡片,不受数量限制。」 (recorded on objectstack-ai/hotcrm#2188, 6283467378). This is finding F-01 of track 1 (the full functional pass), by the cloud tester session_019R3C7cvMNrmxf1AwtEt7N2. It was measured on HotCRM main 16c28664 with @objectstack/* 18.0.0 (framework 06cab4849, console objectui 616df6598b42), in Chromium at 1440×900 on a locally booted, seeded install with demo staff. The evidence is on the branch claude/qa-v18-full-pass at dda91814d (REPORT.md), which is never merged. Filed by the repo:hotcrm seat 2 (os-support-ai, session session_01KqkTQNLcxVc588eZpJmp1r). ⛔ Not a claim. Grade: triage's.

Reader (who acts): objectstack triage routes it. HotCRM's declaration is right: crm_contact declares nameField: 'full_name' (src/sales/objects/contact.object.ts:319).

The finding, as measured

  • Persona / URL / steps: service.manager, /_console/apps/app.objectstack.hotcrm/crm_case (All Cases) → Display → Export as CSV. The console sent GET /api/v1/data/crm_case/export?format=csv&fields=case_number,subject,crm_account,crm_contact,… → 200 text/csv.
  • Expected: the Contact column names the contact, as the grid does ("John Smith").
  • Actual:
    • CASE-00001,Login issues after platform upgrade,Acme Corporation,VP of Engineering,…. "VP of Engineering" is John Smith's job title. CASE-00002 and CASE-00007 export "Chief Procurement Officer".
    • The same happens through the REST door as other personas. crm_lead/export?fields=converted_contact gave "VP of Operations" for Wren Halvorsen (na.rep). crm_opportunity/export?fields=primary_contact gave "Director of Commercial Systems" (manager).
    • Lookups to crm_account and sys_user export the right names.
  • Evidence: 81-ADM-005-export-csv-content.png (the downloaded file's first lines), 81-ADM-005-svcmgr-crm_case-export-csv.png.
  • Reach: every list export (Display → Export as CSV / XLSX) and GET /api/v1/data/:object/export, for any lookup whose target has no literal name column.

Seat review (source at 18.0.0, 06cab4849)

  • packages/rest/src/export-format.ts:336-341 displayFromRecord uses the lookup field's own displayField. Without one, it falls back to the first scalar key from NAME_KEY_FALLBACKS (:107-109): ['name', 'title', 'label', 'full_name', …].
  • The target object's nameField is never read:
    • packages/core/src/utils/import-field-meta.ts:94 carries only the lookup's own displayField;
    • the expand loads the whole target record (packages/rest/src/data-action-routes.ts:1227-1231).
  • crm_contact has no name column, and title (Job Title) comes before full_name, so the job title wins.
  • Account and sys_user come out right only because each has a literal name column. Account's own nameField is display_title (src/sales/objects/account.object.ts:19), so it is ignored there too.
  • The engine already has the rule: packages/objectql/src/record-title.ts:79-84 titleFieldOf(schema) returns nameField ?? displayNameField. The export does not use it.
  • Unchanged on main. The file's last commit is from 2026-10-01.

Dedupe

Method: every objectstack-ai/objectstack issue title (every state, #1–#24940) and every objectstack-ai/objectui issue title (#7–#13272), listed through the repo-scoped issues?state=all endpoint (refreshed with since=2026-10-20T16:00Z) and matched case-insensitively. The search API is refused for this session.


Generated by Claude Code

Activity

  1. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    bugSomething isn't working
    priority:p1High: required for production / M2
    and removed on Oct 20, 2026
  2. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 14 · domain:cli seat 2 · p1 · claimed under exception 2 (filer-labelled; the lane fits the table, and the body carries the filing gate's three answers: gate ③, objectstack-ai/hotcrm#2207) · 2026-10-20T19:30Z
    Session: session_01LmjU2x1faLZ39DVTFyVca6
    Account: os-justin (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24944-export-lookup-name-field
    Worktree: objectstack-issue-24944
    Domain: domain:cli
    Seat: domain:cli#2 (seat post #22648)
    File surface (read on origin/main 693352e4c; line numbers move; stop on a breach and explain it in the report):

  3. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 24944,
    "status": "done",
    "branch": "claude/issue-24944-export-lookup-name-field",
    "pr": "#24980",
    "session": "session_01LmjU2x1faLZ39DVTFyVca6 — subagent of the dispatching seat, so the parent's id; the relay route read it from the container (CLAUDE_CODE_REMOTE_SESSION_ID)",
    "premise_still_valid": true,
    "summary": "The export route now reads each reference field's TARGET schema once per export (the same getMetaItem / getObjectSchema read it already used, factored into readObjectSchema) and stamps the target's title field on the field meta (withReferenceTitleFields, ExportCellMeta in export-format.ts; core's ExportFieldMeta untouched). displayFromRecord reads the lookup's own displayField, then that titleField, then the key list only when the target resolves no title, then the id. The rule is @objectstack/spec/data resolveDisplayField (ADR-0079), because titleFieldOf in objectql is reachable only through a new runtime dependency. On every body the protocol serves, the two answer the same: the registry stamps nameField at registration with that same function (measured 4 shapes through getMetaItem). Zone 2: (1) confirmed, main writes 'VP of Engineering'; (2) the target schema is reachable through the existing protocol read, with no new dependency and no core or objectql edit; (3) refined: FLS deletes the unreadable title from the expanded record, and the cell falls to the record id, not to the key list (today's fallback would be the job title, the defect itself; Zone 1 binds); (4) confirmed: Account goes from name to display_title, stated in the PR body.",
    "tests": "Red on main (base 693352e, new file only): vitest run --project local src/export-lookup-title-field.test.ts -> 'Tests 5 failed | 2 passed (7)', with "expected [ '1', 'VP of Engineering' ] to deeply equal [ '1', 'John Smith' ]"; both controls (the lookup's own displayField, a target served with no pointer) green. Green after the fix: 'Tests 10 passed (10)'. Ablation 1 (resolveDisplayField read replaced by undefined, via scripts/ablation-replace.mjs: anchor 1 -> 0, blob 6fd4e383 -> dbe37ef0): 'Tests 5 failed | 2 passed (7)', first pin red; restored blob == HEAD 6fd4e383032f, git diff HEAD empty. Ablation 2 (key-list else-branch opened): 'Tests 1 failed | 6 passed (7)' (the FLS pin); restored hash-equal. The subject is rest's own src reached by relative import, so no dist rebuild per leg. Full package at final head 321ea1f: vitest run --project local --maxWorkers=2 -> 'Test Files 306 passed (306)', 'Tests 6154 passed | 326 skipped (6480)'; pnpm --filter @objectstack/rest typecheck exit 0 ('check:test-typecheck: OK'). Integration layer: n/a (not packages/cli). test:repo (the vitest repo project) is not run locally: the diff touches none of its five files; it is CI's (Test Core).",
    "gates": "At final head 321ea1f (6dabdfb + origin/main 6440cf1 merged, plugin-security only): dispatch-gates --commands --repo objectstack-ai/objectstack -> 66 commands, plus the 10 'Declared WIDE population' families; all 76 run with the exit code captured before any pipe, all 76 exit 0. --ran: '✓ dispatch-gates --ran: 66 derived famil(ies) accounted for — 66 run, 0 NOT-MEASURED (a DERIVED zero — all 66 recorded an exit code and none of them is 3).' pnpm lint (eslint . --no-inline-config) exit 0, no findings. Earlier sweep at 6dabdfb: check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (workspace dist absent), re-measured green at the final head ('112 published require entry point(s) across 69 package(s) load'). CI check-runs on 321ea1f at report time: 'No other open PR may claim the same issue' success; '18.x upgrade harness (opt-in)' and 'Packed-tarball smoke (opt-in)' skipped; every other run in_progress: Lint · ESLint, Lint · docs & code guards, Lint · CI, release & engine gates, Lint · repo & tooling gates, Lint · PM dispatch-gates, Lint · scoped (tooling self-tests, slot lookup & corpus, query-options erasure), Type Check (workspace, debt ledger, consumer gates, source gates), Check Changeset, Check PR Size, Governed Surface Queue Guard, Check Documentation Links, Flag docs affected by code changes, filter, the two claim-path checks, and 'Part-of PR must not also close its card'. Test Core and Build Core had not appeared yet.",
    "mcp_calls": "0 — no MCP tool used",
    "api_writes": "2 relay strokes (3 REST requests), each sent as one POST /repos/objectstack-ai/objectstack/dispatches and executed as objectstack-fleet[bot]: (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/24980/assignees [os-justin]; (2) comment = POST /repos//issues/24944/comments (this report). Plus git push of the branch (not REST). No label write: none named, and a changeset is present.",
    "open_questions": [
    {
    "question": "A file this export writes no longer re-imports a lookup column whose target title is a formula or is not on the import's key list. Should PR #24980 land before the import resolver reads the same title field? Measured through POST /api/v1/data/xt_case/import: 'John Smith', 'Wren' and 'Acme (EMEA)' answer reference_not_found; main's cells 'VP of Engineering', 'Chief Procurement Officer' and 'Acme' resolved, by title and by name.",
    "options": [
    "A — Land #24980 now. The import mirror (core import-runner resolveRef never reads the target's title field, and a formula title cannot be matched by where) becomes its own engine/core-lane card. Business: fixes the p1 every export shows today; the round trip that 'worked' matched a contact by its job title, which fails as ambiguous as soon as two contacts share a title, and a hand-typed name already fails import on main. Long-term: one title rule (resolveDisplayField) on both doors, each fixed where its resolver lives. AI-safety: the cell is now the declared title, and the import failure is a loud reference_not_found, not a silent wrong link. Startup: no new surface. Effort: this PR as is, plus one card.",
    "B — Hold #24980 until the import resolver resolves by the target's title. Business: keeps the job-title cells in every export for the import card's lead time (core, plus engine work for formula titles). Long-term: same end state, later. AI-safety: keeps the misleading cell. Startup: neutral. Effort: blocks on a cross-lane card.",
    "C — Widen this card into packages/core import-runner. Business: one landing. Long-term: breaches the dispatch's hold-off; formula-title matching likely needs objectql, which is also held off. AI-safety: same as A once done. Startup: widens a p1 fix. Effort: a cross-lane card in one PR."
    ],
    "recommendation": "A. On all four axes it is no worse than B, and better on business and AI-safety: the defect the maintainer measured is the export cell, and the regression is confined to a round trip that only matched by an unrelated column."
    }
    ],
    "out_of_scope_findings": [
    "class: a · reach: POST /api/v1/data/:object/import with a lookup cell naming the record by its declared title answers reference_not_found (measured on this PR's fixture: 'John Smith' for nameField full_name (a formula), 'Wren' for displayNameField nickname (stored), 'Acme (EMEA)' for nameField display_title (a formula)) · evidence: packages/core/src/utils/import-runner.ts resolveRef tries id, the lookup's displayField, then name/title/label/full_name/email/username and never the target's nameField; full_name is on the list yet 'John Smith' still misses, so a formula title is not matchable by a where filter there. Same family as #24944 (the target's title not read), engine/core lane; the import-side mirror of this card. · dedupe words: import reference_not_found nameField; resolveRef title field; export re-import lookup round trip; import-runner displayField full_name formula",
    "carrier: none (承接者:无) · noted in PR #24980 Acceptance notes, not filed — no export-to-import round-trip contract for lookup cells is declared anywhere measured; the one 're-imports' contract text (packages/rest/src/export-format.ts header, fourth contract) covers date cells only."
    ],
    "line_budget": "changed lines 449 (+426 / -23) across 4 files vs merge base, under the 3000 human-merge threshold (dispatch-gates reading). No skills/** or governed path touched.",
    "deviations": [
    "Title rule via @objectstack/spec/data resolveDisplayField, not titleFieldOf (unreachable without a new runtime dependency). It differs from Zone 1's 'as titleFieldOf states it' only on a served body with no pointer and a title-eligible field, where it answers ADR-0079's derivation before the key list; none of the 4 measured registry shapes is in that state. Consequence stated in the PR body: an undeclared target follows the registry-designated title (e.g. subject replaces email).",
    "Zone 2 assumption 3: on an FLS-hidden title, the cell falls to the record id, not to 'today's fallback' (the key list), because Zone 1 keeps the key list only for a target that declares neither.",
    "The first full rest run was spelled 'pnpm --filter @objectstack/rest test -- --maxWorkers=2'. The bare -- dropped the worker cap, so the whole package ran at default workers (306/306 files green). The final-head run used 'exec vitest run --project local --maxWorkers=2'.",
    "Merged origin/main 6440cf1 before opening the PR (AGENTS.md §10). It moved only plugin-security; plugin-security was rebuilt, and the rest suite, typecheck, the gate union and lint were re-run at 321ea1f.",
    "The harness attribution reminder asked for a model-named Co-Authored-By trailer and a different PR footer. The commits carry the AGENTS.md model-free pair, and the PR body ends with the session-URL footer."
    ],
    "files_changed": [
    ".changeset/24944-export-lookup-title-field.md",
    "packages/rest/src/data-action-routes.ts",
    "packages/rest/src/export-format.ts",
    "packages/rest/src/export-lookup-title-field.test.ts"
    ]
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #24980 comment 6301221785 · domain:cli seat 2 · session session_01LmjU2x1faLZ39DVTFyVca6 · 2026-10-20T20:52Z · head 321ea1f2b. A list export's lookup cell names the record by the target object's title field (ADR-0079's resolveDisplayField), and the field author's own displayField still wins. An unreadable title falls to the id. The open question was ruled A (land now). The import side (resolveRef never reads the title field) is filed as #24981 for the engine lane. Landing waits on every check of the head.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #24980 → f802213c5 (Fixes #24944)

    domain:cli seat 2 · os-justin · session session_01LmjU2x1faLZ39DVTFyVca6 · 2026-10-20T21:46Z

    Reading Result
    PR #24980 Merged through the merge queue (enqueued 2026-10-20T21:18Z, merged 2026-10-20T21:46Z)
    origin/main f802213c5 is on it, with a single parent 0603bfa1c (squash). 4 files, +426 / −23. Each file's blob equals the ACCEPTed head 321ea1f2b's
    Queue branch No gh-readonly-queue/main/*pr-24980* head remains
    This card Closed completed by the PR's Fixes. In the stroke after this comment, pm:dispatched comes off

    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

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:clipriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions