Skip to content

fix(core): resolve an import lookup cell by the target object's unique fields, an autonumber included - #28112

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-27947-import-lookup-unique-fields
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-27947-import-lookup-unique-fields

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #27947

Clause-②: no
Metadata-protocol: none
Skills-docs: none — skills/** and content/docs/** (outside the generated references/ and the release-owned releases/) carry no sentence listing the import lookup candidate fields; grepped for the fixed keys, reference_not_found, reference_ambiguous and the lookup-cell wording.

维护者速读

  • 改了什么:数据导入的 lookup 单元格匹配,在原有候选(id、lookup 的 displayField、目标对象标题字段、name/title/label/full_name/email/username)全部落空后,再按目标对象字段声明顺序尝试其唯一字段(unique 声明了约束的字段,以及未写 unique 的 autonumber 字段)。
  • 为什么改:单元格写的是目标记录的编号(如 ACC-000012)或其它唯一字段时,今天一律报 reference_not_found;记录标题一改,之前导出的文件就全部导不回来。
  • 风险与回滚:只在原有候选全部未命中时才多查几次;原有候选命中时行为不变,多条命中仍报 reference_ambiguous,不会静默挑一条。回滚即还原 import-runner.ts 这一处候选列表。
  • 先看:packages/core/src/utils/import-runner.ts 的 isUniqueReferenceField(autonumber 默认唯一、unique: false 退出)与 resolveRef 候选列表末尾一行;其次 import-runner-reference-unique.test.ts。
  • 两分钟验证:pnpm --filter @objectstack/core exec vitest run src/utils/import-runner-reference-unique.test.ts,预期 6 个用例全绿。
  • 需要你做的:无,席位落地。

What changes

resolveRef in packages/core/src/utils/import-runner.ts asked the id, the lookup's displayField, the target's title field, then name / title / label / full_name / email / username, and stopped. A cell holding the target's autonumber or another unique field answered reference_not_found.

The target's unique fields are now appended to the candidate list, read once per target per import off the same declaration the exposure check already reads (servesReferenceTarget), so no extra metadata read:

  • a field whose unique declares a constraint, judged by the spec's own isUniqueDeclared (true, 'organization', 'global');
  • an autonumber field that omits unique: the spec makes it unique: 'organization' by contract (FieldSchema's autonumber default). The default is applied in the predicate too, so a declaration read unparsed still counts;
  • an autonumber declared unique: false has opted out (the spec's display-only ordinal) and is not asked.

They come after every existing candidate, in the target's declared field order, de-duplicated against the existing list (a unique email is asked once, in its fixed place), and virtual fields are still skipped. The existing rules are unchanged: the first field to match wins, and a field matching more than one record is reference_ambiguous.

No spec key, no published shape and no public export changes.

Tests

New packages/core/src/utils/import-runner-reference-unique.test.ts (6 cases), beside the #24981 title pins:

  • an autonumber cell ACC-000012 resolves and stores the matched id, asked after every existing candidate;
  • another declared-unique field (unique: true) resolves;
  • order control: a cell that is one record's name and another record's autonumber links the name match, and the autonumber is never asked;
  • a cell matching nothing still answers reference_not_found, after asking the unique fields in declared order, the unique email once, the opted-out autonumber never;
  • an opted-out autonumber's value answers reference_not_found;
  • a unique field two records answer is reference_ambiguous and links neither.

Measured on main 8cb8c7f before the fix: 4 of 6 red, the autonumber cell answering reference_not_found (expected ok: true, received code: "reference_not_found", field: "account").

Ablations on the fix commit, each restored from HEAD after the run:

Ablation Red
drop the unique-field candidates 4 (autonumber, unique, not-found order, ambiguity)
ask the unique fields first 5, the order-control pin among them
ignore the unique: false opt-out 2 (opt-out pin, declared-order pin)
silently pick the first of several matches 1 (ambiguity pin)
drop the de-duplication 4

Acceptance notes

  • A cell that matches nothing now costs one query more per unique field of the target before it answers reference_not_found (misses are not cached, as before, so a repeated unmatched cell repeats them). Matching cells cost what they did.
  • An organization-scoped unique field read across organizations can answer two records; that is reported reference_ambiguous, never a silent pick.

Verification

  • Pins and ablations as above.
  • Local gates: every family node scripts/pm/dispatch-gates.mjs --commands derives for this diff, exit codes captured, reconciled with --ran; pnpm lint. Counts are in the os-dev report on the card.

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4czVnUdQQCVKSW5J2uk

…s, an autonumber included

resolveRef asked the id, the lookup's displayField, the target's title field
and the fixed human keys, then stopped, so a cell holding the target's
autonumber or another unique field answered reference_not_found. The target's
unique fields (isUniqueDeclared, plus an autonumber that omits unique, the
spec's contract default) are now asked last, in declared order, de-duplicated,
virtual fields still skipped. First match wins; more than one record on a
field is still reference_ambiguous.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4czVnUdQQCVKSW5J2uk
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 30, 2026
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/core, touching 11 documentable anchor(s).

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it))
  • content/docs/api/wire-format.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it))
  • content/docs/data-modeling/fields.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it))
  • content/docs/data-modeling/import-mappings.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it), /:object/import/jobs (route, bridged from symbol runImport — its route source's handler names it))
  • content/docs/permissions/system-context.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it), /:object/import/jobs (route, bridged from symbol runImport — its route source's handler names it))
  • content/docs/protocol/objectql/state-machine.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it), /:object/import/jobs (route, bridged from symbol runImport — its route source's handler names it))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v12.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it))
  • content/docs/releases/v17/17-6.mdx (via runImport (symbol, a top-level function), /:object/import (route, bridged from symbol runImport — its route source's handler names it))
  • content/docs/releases/v18/18-1.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it))
  • content/docs/releases/v18/18-2.mdx (via /:object/import (route, bridged from symbol runImport — its route source's handler names it))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 39 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 121d1a5ffbf716bf1c3fc56e17f6eafe9fff804d → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 16bd145f90875d530b5971bed276f25d3d59600a — the merge of head 721c634a00b7e03ebd2e3790e1f11a6c37321771 into base 121d1a5ffbf716bf1c3fc56e17f6eafe9fff804d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 16bd145f90875d530b5971bed276f25d3d59600a && git checkout 16bd145f90875d530b5971bed276f25d3d59600a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 121d1a5ffbf716bf1c3fc56e17f6eafe9fff804d 721c634a00b7e03ebd2e3790e1f11a6c37321771 && git checkout -B drift-repro 121d1a5ffbf716bf1c3fc56e17f6eafe9fff804d && git merge --no-ff 721c634a00b7e03ebd2e3790e1f11a6c37321771

node scripts/docs-audit/affected-docs.mjs --json 121d1a5ffbf716bf1c3fc56e17f6eafe9fff804d

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 121d1a5ffbf716bf1c3fc56e17f6eafe9fff804d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-elon-musk · session_016WV4czVnUdQQCVKSW5J2uk

Seat ACCEPT · PR #28112 · head 721c634a00b7e03ebd2e3790e1f11a6c37321771 · 2026-10-30T14:12Z

维护者速读

  • 改了什么:数据导入时,查找(lookup)单元格除了 id、显示字段、标题字段和 name/email 等常用字段外,最后还会按目标对象的唯一字段匹配,自动编号(如 ACC-000012)也包括在内。
  • 为什么改:Import lookup matcher never tries the target object's unique fields (an autonumber included) #27947。记录标题改过之后,以前导出的文件按编号再导入会报「找不到引用」。
  • 风险与回滚:只在原有候选全部落空后才多试唯一字段;先匹配到的仍然优先;一个值匹配到多条仍报歧义;显式 unique: false 的自动编号不参与。回滚即 revert。
  • 先看:packages/core/src/utils/import-runner.ts 的 isUniqueReferenceField 和候选列表末尾。
  • 两分钟验证:导入一个查找列写自动编号的文件,能正确关联;写一个不存在的值,仍报 reference_not_found。
  • 需要你做的:无(按本席落地授权落地)。

席位核对

  • Shape: draft → main, Fixes #27947. 3 files, +188/−6: import-runner.ts (+38/−6), a new test file, a patch changeset for @objectstack/core. check-governed-merges --pr 28112: NOT governed.
  • Declarations: Clause-②: no, Metadata-protocol: none, Skills-docs: none (no docs page lists the lookup candidates; the docs-drift pages re-read: import-mappings.mdx names none of them).
  • Diff read (seat): unique = the spec's isUniqueDeclared (true / 'organization' / 'global') or an autonumber that omits unique (the spec default); appended after the existing candidates in declared order, de-duplicated, virtual fields still skipped; the first match wins and a multi-match is still reference_ambiguous. No spec key or exported shape changes.
  • Tests: 6 pins (autonumber, unique: true, order control, no-match + query order, opt-out autonumber, ambiguity); 5 ablations each red, restored. Measured on main first: the autonumber cell answered reference_not_found (4 of 6 new pins red).
  • Local (seat subagent, report 6523790377): 61/61 derived gates exit 0 after a full build, 0 NOT MEASURED; pnpm lint 0; core typecheck 0; core 2879/2879.
  • CI: the first head's Build Core job was cancelled at its time limit; the seat merged main into the branch through the relay (pr_update_branch, branch was 14 behind), so this head is that merge only. On this head: 48 check-runs, 38 success, 10 skipped, 0 failed. mergeable_state: clean.
  • Accepted note: a cell matching nothing now runs one more query per unique field before reference_not_found; misses were already uncached. Recorded in the PR's Acceptance notes; not a defect at import volumes.
  • Landing: non-governed, under the maintainer's landing grant for this seat term (2026-10-30 10:20Z, seat-1 thread). Ready + auto-merge through the relay.

ACCEPT

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 30, 2026 14:13
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit d85ff19 Oct 30, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-27947-import-lookup-unique-fields branch October 30, 2026 14:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Import lookup matcher never tries the target object's unique fields (an autonumber included)

1 participant