You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Import wizard: "Match on" offers a multi-value or structured field the server refuses as a match key #27567
Filing gate: ① product defect (user-visible), split from #27465 (the server half).
Business reason
Who: anyone importing into an object with a multiple: true lookup, a multiselect / tags / checkboxes field, or a json-like field, in "Update existing" or "Update if matched, else create" mode (reported by an Enterprise customer on 17.3.0).
What happens today: the wizard offers such a field as a match key. After #27465's server half lands, ticking it gets the whole import refused with 400 INVALID_REQUEST ("matchFields names "work_object" (lookup, multiple: true), which holds a list or a structured value ..."); before it, every row failed on a raw SQL-binding error. Either way the wizard offered a choice the server can never serve.
Without the fix: the user still picks a key that cannot work and only learns it at Validate.
Where
ui/packages/plugin-grid/src/ImportWizard.tsx (the options step): const matchable = useMemo(() => fields.filter((f) => mappedFieldNames.has(f.name)), ...) offers every mapped field.
The wizard's field shape does not carry multiple (see the refusedOptionValue docblock in the same file), so it cannot tell a multiple: true lookup apart.
Rule to follow (the server's, one definition)
A field cannot be a match key when it is stored as a JSON column: isMultiValueField(field) or STRUCTURED_JSON_TYPES.has(field.type) (both @objectstack/spec/data). Server-side the same rule is multiValueMatchKeyRefusal in @objectstack/core.
Acceptance
The wizard's field shape carries multiple from the object schema.
"Match on" lists only mapped fields the rule above admits; a multi-value or structured field is never offered.
No new widget, dialog or hook: a filter on the existing matchable list.
Reader who acts
domain:ui seat, next ui card in the import wizard.
reach
Public door: the Import wizard's "Match on" list, measured on the card's object (customer report on #27465); the server refusal it now meets is pinned in packages/rest/src/import-multi-value-match-key.test.ts.
Dedupe
Search API is unavailable from this session; read instead: the 500 most recent issues (open and closed) by title, grep import × match|wizard|multi → 4 hits: #27562 (the server-half PR), #27465 (the server half), #27434 (console import matcher never tries unique fields: a different defect), #27190 (exchange-rate import). No duplicate.
Claim: PM loop round 1 · 2026-10-29T14:43Z
Session: session_01Utp6CY2n1JmZZKrHgtp9jC (seat; implementation by a card thread, mode:thread)
Account: os-bill
Branch: claude/project-thread-w0so7g (the card thread's harness push branch, self-reported — the measured platform exception to claude/issue-N-slug)
Worktree: objectstack-issue-27567
Domain: domain:ui
Seat: domain:ui#2
File surface: ui/packages/plugin-grid/src/ImportWizard.tsx (+ test beside), ui/packages/app-shell/src/views/importTargetFields.ts (+ importTargetFields.test.ts; the module that builds the wizard's field shape), ui/packages/app-shell/src/console/ai/ExcelImportBar.tsxnormalizeFields — one line carrying multiple, agreed with seat 3 (domain:ui#3), a root changeset; no package.json change (plugin-grid already imports @objectstack/spec/data) (stop on breach; explain in the report)
Container & model: M, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate), effort: high
Clause-②: yes (widening) — corrected by the seat from the dev's measurement: ImportWizardProps is re-exported by name from @object-ui/plugin-grid's entry, and its fields[number] gains the optional multiple?: boolean; the claim first read no
Metadata-protocol: none (expected; the dev measures from the diff)
Responsibility: ui/packages/plugin-grid/src/ImportWizard.tsx (matchable offers every mapped field) | the server refusal from PR #27562 is the rule to read | every importer choosing "Match on" on an object with multi-value or structured fields, today
Thread-read: 6498869186
Premise: holds — PR #27562 (server half) is merged; matchable on origin/main filters only by mapping
Serial constraints cleared: none — no open PR or live claim touches plugin-grid/src/ImportWizard.tsx (seat 4 handed this card over)
Dispatch order: /mnt/project-files/ui-seat2/dispatch-27567.md in the Claude Code project (not on GitHub); its ruling section quotes the triage/maintainer text above verbatim.
{
"issue": 27567,
"status": "done",
"branch": "claude/project-thread-w0so7g",
"pr": "https://github.com/objectstack-ai/objectstack/pull/27645",
"session": "session_01Utp6CXKADE9Bt6J32Lacv5 (card thread; seat session_01Utp6CY2n1JmZZKrHgtp9jC)",
"premise_still_valid": true,
"summary": "The import wizard's \"Match on\" list now filters the mapped fields with isMatchKeyField = not STRUCTURED_JSON_TYPES.has(type) and not isMultiValueField({type, multiple}), the same two @objectstack/spec/data definitions the server's multiValueMatchKeyRefusal composes; plugin-grid already depended on @objectstack/spec, so no package.json change. ImportWizardProps fields gain an optional multiple flag (Clause-2 yes (widening), amended on Claim 6499806710), carried only when true by both hosts that build the field shape: importTargetFields (console list import) and ExcelImportBar normalizeFields (AI Excel import, cleared by seat 3). The refusedOptionValue docblock now says the cell check does not read multiple, so its behaviour is unchanged.",
"tests": "pre-merge head 06d76839: pnpm --dir ui exec vitest run --maxWorkers=2 packages/plugin-grid/ packages/app-shell/src/views/importTargetFields.test.ts packages/app-shell/src/console/ai/__tests__/ under the lock -> VERDICT command-exit 0, Test Files 305 passed (305), Tests 2530 passed (2530). Merged head 0c73a9da: new pin + importWriteModes-12697 + importTargetFields.test.ts + both ExcelImportBar test files -> Test Files 5 passed (5), Tests 31 passed (31); the main merge moved no plugin-grid file. type-check @object-ui/app-shell + @object-ui/plugin-grid on 0c73a9da -> VERDICT command-exit 0. Ablation (ablation-replace.mjs, anchor 'mappedFieldNames.has(f.name) && isMatchKeyField(f)' -> 'mappedFieldNames.has(f.name) /* ABLATED_27567 */'): anchor 1 -> 0, blob bff5282cf050 -> cd3b19e941f3, pin red (Tests 1 failed, received owners/stages/labels/regions/meta/site extra); restored blob == HEAD bff5282cf050, git diff HEAD empty, pin green again. ui vitest resolves @object-ui/* to src, so no rebuild between legs. An earlier run had ExcelImportBar.sharedSelect-11865 red (7) because the first ExcelImportBar edit always wrote multiple:false; fixed by writing the key only when true (commit 2 of the branch), green since.",
"gates": "local, head 0c73a9da: node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 | node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 | node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 | node scripts/check-closing-keyword-parity.mjs :: exit 0 | node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 | node scripts/check-comment-mask-corpus.mjs :: exit 0 | node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 | node scripts/check-empty-changeset.mjs --self-test :: exit 0 | node scripts/check-sdui-manifest.mjs :: exit 0 | node scripts/check-sdui-manifest.mjs --self-test :: exit 0 | node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 | node scripts/release-pending-publish.mjs --self-test :: exit 0 | pnpm --dir ui exec vitest run scripts/ :: exit 0 | pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 | pnpm check:changeset-gate-self-tests :: exit 0 | pnpm check:closed-ceremony-text :: exit 0 | pnpm check:cross-package-test-inputs :: exit 0 | pnpm check:dispatcher-error-vocabulary :: exit 0 | pnpm check:driver-memory-census :: exit 0 | pnpm check:engine-double-contract :: exit 0 | pnpm check:gitlink-declared :: exit 0 | pnpm check:nul-bytes :: exit 0 | pnpm check:pm-changeset-deadline-census :: exit 0 | pnpm check:published-files :: exit 0 | pnpm check:query-options-erasure :: exit 0 | pnpm check:refd-timer-probe :: exit 0 | pnpm check:skill-identifier-liveness :: exit 0 | pnpm check:test-source-alias :: exit 0 | pnpm check:tier-file-adoption :: exit 0 | pnpm check:type-check-coverage :: exit 0 | pnpm check:type-check-debt :: exit 0 | pnpm check:undeclared-dep-imports :: exit 0 | pnpm check:watch-hint-literal :: exit 0 | pnpm check:where-matcher :: exit 0 | reconciliation: dispatch-gates --ran -> 34 derived famil(ies) accounted for, 34 run, 0 NOT-MEASURED, 0 UNRUN. Pre-merge head 06d76839: the same 34, all exit 0. check:nul-bytes exit 0. eslint on the five touched source files: 0 errors. CI check-runs on PR #27645 head 0c73a9da: in_progress at report time (not awaited, per os-dev).",
"line_budget": "n/a, no skills/** or ledgered file touched",
"files_changed": [
".changeset/27567-import-match-on-list-fields.md",
"ui/packages/plugin-grid/src/ImportWizard.tsx",
"ui/packages/plugin-grid/src/ImportWizard.matchKeyFields-27567.test.tsx",
"ui/packages/app-shell/src/views/importTargetFields.ts",
"ui/packages/app-shell/src/views/importTargetFields.test.ts",
"ui/packages/app-shell/src/console/ai/ExcelImportBar.tsx"
],
"deviations": [
"Clause-2 measured yes (widening), claim said no (expected): ImportWizardProps is re-exported by name from the plugin-grid entry and gains an optional key; seat amended Claim 6499806710 before the PR",
"file surface widened by seat to ExcelImportBar.tsx (one line, cleared with seat 3)",
"no assignee and no reviewer on the PR, per the maintainer's routing rule: the harness's post-PR assign/review step skipped",
"test:repo not run locally (diff touches no repo-project test); CI's"
],
"dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm",
"mcp_calls": "2: mcp__github__issue_read (get, get_comments); no write tool",
"api_writes": "2: relay pr_create (POST /repos/objectstack-ai/objectstack/pulls, draft) -> #27645; relay comment (POST /repos/objectstack-ai/objectstack/issues/27567/comments) = this report. git push is not a REST write.",
"open_questions": [],
"out_of_scope_findings": [
"observation, not filed: a saved server mapping whose upsertKey names a list field is applied as authored (setMatchFields from m.upsertKey) and the server refuses it naming the mapping; the wizard does not edit authored mappings · carrier: none · noted in PR Acceptance notes",
"observation, not filed: app-shell views/*.test.ts (importTargetFields.test.ts among them) are in neither app-shell tsc program (--listFiles); vitest runs them · carrier: none · noted in PR Acceptance notes"
]
}
Activity
objectstack-fleet commentedon Oct 29, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: 业务对象与记录 | none | P②
Triage: p2 ·
domain:ui·area:records· 2026-10-29T14:06Z⛔ Not a claim.
multiple: truelookup ticked as a match field fails every row withOperator "=" … received an array#27465. This is the ui half of an Enterprise customer's report (/import upsert: amultiple: truelookup ticked as a match field fails every row withOperator "=" … received an array#27465).bug,pm:queue.Generated by Claude Code
objectstack-fleet commentedon Oct 29, 2026
domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC
Claim: PM loop round 1 · 2026-10-29T14:43Z
Session:
session_01Utp6CY2n1JmZZKrHgtp9jC(seat; implementation by a card thread,mode:thread)Account:
os-billBranch:
claude/project-thread-w0so7g(the card thread's harness push branch, self-reported — the measured platform exception toclaude/issue-N-slug)Worktree:
objectstack-issue-27567Domain:
domain:uiSeat:
domain:ui#2File surface:
ui/packages/plugin-grid/src/ImportWizard.tsx(+ test beside),ui/packages/app-shell/src/views/importTargetFields.ts(+importTargetFields.test.ts; the module that builds the wizard's field shape),ui/packages/app-shell/src/console/ai/ExcelImportBar.tsxnormalizeFields— one line carryingmultiple, agreed with seat 3 (domain:ui#3), a root changeset; nopackage.jsonchange (plugin-grid already imports@objectstack/spec/data) (stop on breach; explain in the report)Container & model:
M,mode:thread,model: TIER_DEFAULT(dispatch-gates --tier: no path-derived mandate),effort: highClause-②: yes (widening) — corrected by the seat from the dev's measurement:
ImportWizardPropsis re-exported by name from@object-ui/plugin-grid's entry, and itsfields[number]gains the optionalmultiple?: boolean; the claim first readnoMetadata-protocol: none (expected; the dev measures from the diff)
Responsibility:
ui/packages/plugin-grid/src/ImportWizard.tsx(matchableoffers every mapped field) | the server refusal from PR #27562 is the rule to read | every importer choosing "Match on" on an object with multi-value or structured fields, todayThread-read: 6498869186
Premise: holds — PR #27562 (server half) is merged;
matchableonorigin/mainfilters only by mappingSerial constraints cleared: none — no open PR or live claim touches
plugin-grid/src/ImportWizard.tsx(seat 4 handed this card over)Dispatch order:
/mnt/project-files/ui-seat2/dispatch-27567.mdin the Claude Code project (not on GitHub); its ruling section quotes the triage/maintainer text above verbatim.Generated by Claude Code
chore(changeset): #27567 declares Clause-②: yes (widening) for the ne…
fix(app-shell): the AI Excel import carries `multiple` only when true…
test(plugin-grid): the #27567 pin stubs the Radix pointer helpers wit…
objectstack-fleet commentedon Oct 29, 2026
domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC
os-dev-report
{ "issue": 27567, "status": "done", "branch": "claude/project-thread-w0so7g", "pr": "https://github.com/objectstack-ai/objectstack/pull/27645", "session": "session_01Utp6CXKADE9Bt6J32Lacv5 (card thread; seat session_01Utp6CY2n1JmZZKrHgtp9jC)", "premise_still_valid": true, "summary": "The import wizard's \"Match on\" list now filters the mapped fields with isMatchKeyField = not STRUCTURED_JSON_TYPES.has(type) and not isMultiValueField({type, multiple}), the same two @objectstack/spec/data definitions the server's multiValueMatchKeyRefusal composes; plugin-grid already depended on @objectstack/spec, so no package.json change. ImportWizardProps fields gain an optional multiple flag (Clause-2 yes (widening), amended on Claim 6499806710), carried only when true by both hosts that build the field shape: importTargetFields (console list import) and ExcelImportBar normalizeFields (AI Excel import, cleared by seat 3). The refusedOptionValue docblock now says the cell check does not read multiple, so its behaviour is unchanged.", "tests": "pre-merge head 06d76839: pnpm --dir ui exec vitest run --maxWorkers=2 packages/plugin-grid/ packages/app-shell/src/views/importTargetFields.test.ts packages/app-shell/src/console/ai/__tests__/ under the lock -> VERDICT command-exit 0, Test Files 305 passed (305), Tests 2530 passed (2530). Merged head 0c73a9da: new pin + importWriteModes-12697 + importTargetFields.test.ts + both ExcelImportBar test files -> Test Files 5 passed (5), Tests 31 passed (31); the main merge moved no plugin-grid file. type-check @object-ui/app-shell + @object-ui/plugin-grid on 0c73a9da -> VERDICT command-exit 0. Ablation (ablation-replace.mjs, anchor 'mappedFieldNames.has(f.name) && isMatchKeyField(f)' -> 'mappedFieldNames.has(f.name) /* ABLATED_27567 */'): anchor 1 -> 0, blob bff5282cf050 -> cd3b19e941f3, pin red (Tests 1 failed, received owners/stages/labels/regions/meta/site extra); restored blob == HEAD bff5282cf050, git diff HEAD empty, pin green again. ui vitest resolves @object-ui/* to src, so no rebuild between legs. An earlier run had ExcelImportBar.sharedSelect-11865 red (7) because the first ExcelImportBar edit always wrote multiple:false; fixed by writing the key only when true (commit 2 of the branch), green since.", "gates": "local, head 0c73a9da: node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 | node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 | node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 | node scripts/check-closing-keyword-parity.mjs :: exit 0 | node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 | node scripts/check-comment-mask-corpus.mjs :: exit 0 | node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 | node scripts/check-empty-changeset.mjs --self-test :: exit 0 | node scripts/check-sdui-manifest.mjs :: exit 0 | node scripts/check-sdui-manifest.mjs --self-test :: exit 0 | node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 | node scripts/release-pending-publish.mjs --self-test :: exit 0 | pnpm --dir ui exec vitest run scripts/ :: exit 0 | pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 | pnpm check:changeset-gate-self-tests :: exit 0 | pnpm check:closed-ceremony-text :: exit 0 | pnpm check:cross-package-test-inputs :: exit 0 | pnpm check:dispatcher-error-vocabulary :: exit 0 | pnpm check:driver-memory-census :: exit 0 | pnpm check:engine-double-contract :: exit 0 | pnpm check:gitlink-declared :: exit 0 | pnpm check:nul-bytes :: exit 0 | pnpm check:pm-changeset-deadline-census :: exit 0 | pnpm check:published-files :: exit 0 | pnpm check:query-options-erasure :: exit 0 | pnpm check:refd-timer-probe :: exit 0 | pnpm check:skill-identifier-liveness :: exit 0 | pnpm check:test-source-alias :: exit 0 | pnpm check:tier-file-adoption :: exit 0 | pnpm check:type-check-coverage :: exit 0 | pnpm check:type-check-debt :: exit 0 | pnpm check:undeclared-dep-imports :: exit 0 | pnpm check:watch-hint-literal :: exit 0 | pnpm check:where-matcher :: exit 0 | reconciliation: dispatch-gates --ran -> 34 derived famil(ies) accounted for, 34 run, 0 NOT-MEASURED, 0 UNRUN. Pre-merge head 06d76839: the same 34, all exit 0. check:nul-bytes exit 0. eslint on the five touched source files: 0 errors. CI check-runs on PR #27645 head 0c73a9da: in_progress at report time (not awaited, per os-dev).", "line_budget": "n/a, no skills/** or ledgered file touched", "files_changed": [ ".changeset/27567-import-match-on-list-fields.md", "ui/packages/plugin-grid/src/ImportWizard.tsx", "ui/packages/plugin-grid/src/ImportWizard.matchKeyFields-27567.test.tsx", "ui/packages/app-shell/src/views/importTargetFields.ts", "ui/packages/app-shell/src/views/importTargetFields.test.ts", "ui/packages/app-shell/src/console/ai/ExcelImportBar.tsx" ], "deviations": [ "Clause-2 measured yes (widening), claim said no (expected): ImportWizardProps is re-exported by name from the plugin-grid entry and gains an optional key; seat amended Claim 6499806710 before the PR", "file surface widened by seat to ExcelImportBar.tsx (one line, cleared with seat 3)", "no assignee and no reviewer on the PR, per the maintainer's routing rule: the harness's post-PR assign/review step skipped", "test:repo not run locally (diff touches no repo-project test); CI's" ], "dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm", "mcp_calls": "2: mcp__github__issue_read (get, get_comments); no write tool", "api_writes": "2: relay pr_create (POST /repos/objectstack-ai/objectstack/pulls, draft) -> #27645; relay comment (POST /repos/objectstack-ai/objectstack/issues/27567/comments) = this report. git push is not a REST write.", "open_questions": [], "out_of_scope_findings": [ "observation, not filed: a saved server mapping whose upsertKey names a list field is applied as authored (setMatchFields from m.upsertKey) and the server refuses it naming the mapping; the wizard does not edit authored mappings · carrier: none · noted in PR Acceptance notes", "observation, not filed: app-shell views/*.test.ts (importTargetFields.test.ts among them) are in neither app-shell tsc program (--listFiles); vitest runs them · carrier: none · noted in PR Acceptance notes" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 29, 2026
domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC
ACCEPT → PR #27645 comment 6502285953 (head
0c73a9da); contract review (declaration, TIER_DEFAULT) PASS 6502273608; landing through the merge queue.Generated by Claude Code
objectstack-fleet commentedon Oct 29, 2026
domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC
Landed · 2026-10-29T16:38Z
4d1f52c4458372169bf641f58db312734da5fb10completedby the PR'sFixeslineState:
pm:dispatchedremoved; the assignee stays as the record of who carried it.Generated by Claude Code