Repository navigation
fix(core): the import resolves a lookup cell by the target object's stored title field - #25033
Conversation
… field resolveRef asks the lookup target's ADR-0079 title field (resolveDisplayField, read off the same getMetaItem declaration the exposure check already reads) after the id and the lookup's own displayField. A candidate the target declares virtual (a formula) is never asked by where: the engine's filter seam refuses it by the same spec predicate (isVirtualSearchField), so the query could only answer a refusal the resolver read as "field absent". Claude-Session: https://claude.ai/code/session_01QK6pdbxKZiHdFApL8RYVh9 Co-authored-by: Claude <noreply@anthropic.com>
Wren resolves through a stored displayNameField / nameField after the id and the lookup's displayField; a formula title (John Smith, Acme (EMEA)) is never asked by where and still answers reference_not_found; two records sharing a stored title answer reference_ambiguous; a target whose exposure refuses list is still never read. Claude-Session: https://claude.ai/code/session_01QK6pdbxKZiHdFApL8RYVh9 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK6pdbxKZiHdFApL8RYVh9 Co-authored-by: Claude <noreply@anthropic.com>
…reads it as field-addressed Claude-Session: https://claude.ai/code/session_01QK6pdbxKZiHdFApL8RYVh9 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 28 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 22bbec1fd962b90bd7c40a6b1ea284dc32cecdde && git checkout 22bbec1fd962b90bd7c40a6b1ea284dc32cecdde
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4d991588ffcdedce1b79fc96ba2779c67c4f8d62 9a67cf4cee9f26f8f2fa934db729a13ced60e3af && git checkout -B drift-repro 4d991588ffcdedce1b79fc96ba2779c67c4f8d62 && git merge --no-ff 9a67cf4cee9f26f8f2fa934db729a13ced60e3af
node scripts/docs-audit/affected-docs.mjs --json 4d991588ffcdedce1b79fc96ba2779c67c4f8d62
|
席位核对Seat review ·
Generated by Claude Code |
Part of #24981
Clause-②: no
The import door now honours the title rule ADR-0079 already declares; no schema, export or published accept set moves, and nothing narrows.
What this delivers (the decision-independent half)
The import's lookup resolution (
resolveRef,packages/core/src/utils/import-runner.ts) now asks the target object's title field after the id and the lookup's owndisplayField, and before the fallback keys. The title is resolved by ADR-0079's one rule,resolveDisplayFieldfrom@objectstack/spec/data. Only a STORED title is asked.Wren(target titledisplayNameField: 'nickname', a stored text field) now resolves. Onmainit answeredreference_not_found, becausenicknamewas never asked.John SmithagainstnameField: 'full_name',Acme (EMEA)againstnameField: 'display_title') is not asked bywhere. The swallowedINVALID_FIELDquery is gone, and the cell still answersreference_not_found.reference_ambiguous. Neither is linked.name,title,label,full_name,email,username) and its order are unchanged. The exposure check from security(import): an import's reference resolution matches a lookup cell against a target whose exposure refuses reads (census row 7 of #22661) #22739 still runs first and still withholds a target whose declared exposure does not servelist.What is left (with the maintainer)
Q1 on the card asks how a FORMULA title should be matched (options A, B-cap, C and D in the first dev report and in the seat's analysis). Nothing is built for it here: no scan, no search door, no new refusal code. The card's second Wanted bullet ("A formula title is matched by the engine's own record-title resolution") is not delivered by this PR, so #24981 remains open after it merges.
Mechanism (measured in this run)
servesReferenceTargetalready read the target'sgetMetaItemitem for itsenableblock. It now also returns the title field (resolveDisplayField(item)) and the field map from that same read. No extra metadata read is made. Pinned: two rows asking one target cost onegetMetaItemcall.withReferenceTitleFields(packages/rest/src/export-format.ts) resolves a lookup cell's title withresolveDisplayFieldover thegetMetaItemitem (readObjectSchemainpackages/rest/src/data-action-routes.ts). So the import asks the field the export wrote the cell from.isVirtualSearchField(@objectstack/spec/data, overSEARCH_VIRTUAL_TYPES, which holdsformula). It is the same predicate the engine's filter seamassertFilterIsMaterializable(packages/objectql/src/filter-comparand-shape.ts) uses to refuse awhere, so there is no second list.packages/corecannot import objectql, because objectql depends on core. The spec-level predicate the engine itself reads is the one reused.full_nameis also on the fallback list, so skipping only the rung would still sendwhere full_name. The skip changes no verdict. The engine refuses everywhereover a formula at that seam, which its own comment calls "the one place EVERY caller-suppliedwherepasses through". The resolver'scatchread that refusal as "field absent" and moved on.Pins (
packages/core/src/utils/import-runner-reference-title.test.ts)The fake
findDataanswers like the engine: equality over the stored row, and a recordedINVALID_FIELD/ 400 refusal for awhereover a formula field.Wrenresolves through a storeddisplayNameField: 'nickname', and throughnameField: 'nickname'(the alias as the registry normalizes it). The asked keys areid, then the lookup'sdisplayField(first_name), thennickname.getMetaItemis called once for two rows.John Smith(formulafull_name) andAcme (EMEA)(formuladisplay_title) answerreference_not_found. The find calls never name the formula field. The asked keys are exactlyidplus the fallback list minus the formula member, and no refusal was produced.code: 'reference_ambiguous'.enable: { apiEnabled: false }and a stored title answersreference_not_foundfor a match and for a miss.findDatais never called, andgetMetaItemis called once.Ablations
The fix was committed first. Both mutations went through
scripts/ablation-replace.mjs: the anchor hit 1 and fell to 0, and the blob moved. Each restore is proven by blob hash equal to HEAD and an emptygit diff HEAD. The subject is imported by relative path fromsrc/, so nodist/leg applies....(target.titleField ? [target.titleField] : []),deleted): 4 failed, 3 passed. Both Wren pins went red withreference_not_foundforperson. The stored-id pin went red. The ambiguity pin went red withreference_not_foundin place ofreference_ambiguous. Restored: blobd9b249d961d4equals HEAD.isVirtualSearchFieldfilter on the candidate list dropped): 2 failed, 5 passed. Both formula pins went red: "expected [ 'id', 'full_name', ... ] to not include 'full_name'", and the same fordisplay_title. Restored: blobd9b249d961d4equals HEAD.Tests and gates (final head
9a67cf4ce)pnpm --filter @objectstack/core typecheck: exit 0. The test layer compiles undertsconfig.test.json(the new file is in that program), with 4 of 4 pinned signatures held.pnpm --filter @objectstack/core exec vitest run --project local --maxWorkers=2: 109 files, 2630 tests passed.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderivation gave 68 commands. All 68 were run at9a67cf4ce, and every one exited 0.--ranreconciliation: "68 derived famil(ies) accounted for — 68 run, 0 NOT-MEASURED (a DERIVED zero — all 68 recorded an exit code and none of them is 3)".745620facwent red oncheck:error-code-casing. The new test spelled a field-addressed code with a shorthandfieldkey, which the gate does not read as field-addressed.9a67cf4cespellsfield: column.eslint --no-inline-configover the two touched source files: exit 0. The repo-widepnpm lintis CI's.Acceptance notes
import-runner.ts(+63/-16) was read hunk by hunk and kept unchanged as the first commit. Every claim above was measured again in this run.reference_not_foundtext does not say why the cell cannot match. The first dev measuredContact: no record matches "John Smith". No wording changes here. Whether the refusal should name the formula title belongs with Q1's answer; the seat's analysis lists it as a confidence gap.VP of Engineeringlinks a contact by its jobtitle). This PR leaves that unchanged, by ruling.resolveRef'scatchreads everyfindDatathrow as "field absent". Only theINVALID_FIELDpath is measured.runImportthrough@objectstack/core:packages/rest/src/import-integration.test.ts,packages/plugins/plugin-auth/src/admin-import-users.test.tsand the dogfood import-reference pins. No export, type or wire shape of@objectstack/corechanged, so CI's Test Core carries them.origin/mainwas merged at73432299cbefore this PR opened. Two later commits onmaintouch neitherpackages/corenorpackages/spec.Generated by Claude Code