Repository navigation
import mapping: no fieldMapping target can build a compound (address) value from separate CSV columns — a dotted target passes validate and dry-run, then fails every row on commit #20149
Description
Activity
objectstack-fleet commented
on Sep 25, 2026 ContributorAuthorMore actionsPath: business objects, records and views | records-forms.named-import-mapping | P6
维护者速读
客户的通讯录表格里,地址分成五列:街道、城市、省/州、邮编、国家。平台的地址字段是一个整体值,而导入映射只能把一列写进一个平铺字段,没有任何写法能把这五列拼成一个地址。
结果:hotcrm 按你在 hotcrm#1836 的裁定,把联系人的五个平铺地址字段合并成一个地址字段;这之后,客户原来的导入表格就导不进来了,每一行都失败。hotcrm 的客户导入模板早先已经为此删掉了地址列。
三条路:
- A 映射目标可以写「地址字段.部件」,例如
mailing_address.street。部件名只能是平台为地址声明的七个(street / city / state / postalCode / country / countryCode / formatted)。导入时,把同一行的各部件组装成一个地址值。部件名写错,在validate、试运行和正式导入三处都会当场被拒。 - B 新增一种「组装」转换:一条映射列出多个来源列,再用参数写明每列对应哪个部件,目标写地址字段本身。
- C 不做。表格导不进结构化地址:客户只能先把表格转成嵌套 JSON,或者 hotcrm 退回五个平铺字段,也就是推翻 hotcrm#1836 的裁定。
推荐 A。回一个字母:A / B / C。
另外有两个缺陷不需要等这次裁定,已拆到 #20150(
domain:cli· p2):目标写了不存在的字段时,试运行报「成功」,正式导入却全部失败;objectstack validate对此也不报错。os-decision-facets
- ① 项目长远合理性 —— A 沿用「目标写字段名」这一个概念,只把它延伸到 spec 已经声明的地址部件,判定仍在导入入口一处完成。B 多出一个转换类型和一张自由格式的参数表,导入转换矩阵多一行永久义务。C 让结构化地址在最常见的数据来源(表格)面前不可用。
- ② 实际业务拉动 —— 今天就有人撞上:hotcrm 的联系人导入(按本卡原文,hotcrm#1836 挂着
Blocked-by:本卡),以及早已删掉地址列的 hotcrm 客户模板。任何把地址存成结构化字段、客户数据又来自表格的应用都会撞上。 - ③ 防 AI 犯错 —— A 的部件名是闭合集合,取自 spec 的地址定义,写错就响亮拒绝。B 的参数键是自由结构,要另写一套校验,漏掉一处就是静默丢列。C 的失败是响亮的,但没有出路,AI 会被推去绕行(拆回平铺字段)。
- ④ 创业阶段不扩散 —— A 不新增键,也不新增枚举值,只扩一处入口判定。B 新增一个枚举值和它的参数形状。C 零新增,但把 hotcrm 已裁定的改动卡死。
Governing text:
ImportFieldMappingSchema.target— "Target object field(s)" (packages/spec/src/data/mapping.zod.ts); its docblock: "one target field may be composed from several columns (split/join)".AddressSchemais a closed strict object with seven declared parts (packages/spec/src/data/field-value.zod.ts). Nothing declares a part target or a compose transform, so A and B both widen the published mapping contract. That puts the choice on the manual floor: it is a feature addition and a public-contract change.Prior rulings read: fieldmapping, compound, dotted, address, compose, import mapping, sub-field → 7 hits; ADR-0045 D3, ADR-0063 D2, ADR-0071 D1, ADR-0081 D2, ADR-0104 D1, ADR-0110 D2, ADR-0131 D3. All seven are term collisions ("compose", "address" and "dotted" in other senses); none rules on import mapping. thread: none.
推荐:A。终态:两年后,表格导入按字段部件映射复合字段,与主流平台一致。例如 Salesforce 的复合地址字段,就是按 MailingStreet、MailingCity 这样的部件字段导入的。回退:B。⛔ 不选 C。
自检:只看①选 A;②③④ 是否翻转:否。
置信缺口:除地址外,是否还有别的带部件的字段类型需要同样的写法,本席没有逐一清点。hotcrm#1836 上的Blocked-by:行本席在本会话读不到(hotcrm 不在本会话的可读范围),按本卡原文记。裁后执行:
- A ⇒ 先落 import mapping: a
fieldMapping.targetthat names no field passesobjectstack validateand the dry run, then fails every row on commit #20150 的入口判定(目标必须是对象上的字段)。再由 spec 声明「目标可以写复合字段的已声明部件」(domain:spec,minor,Clause-②: yes)。之后,packages/rest的导入映射组装嵌套值,validate和试运行用同一判定检查部件名(domain:cli,用Blocked-by:排在 spec 之后)。hotcrm#1836 随之解锁。 - B ⇒ spec 新增
compose转换及其参数形状,rest 实现,导入转换矩阵加一行。 - C ⇒ 本卡关为 not planned,hotcrm#1836 需要重新裁定。
Triage (English audit line): routed to the decision box —
needs-user-decision·priority:p2·domain:spec·area:records. Observation 1 (the capability) widens the published mapping contract, which is on the manual floor, so triage does not adjudicate it. Observations 2 and 3 need no ruling and are split to #20150 (domain:cli· p2). hotcrm#1836 is reported to carryBlocked-by:this card; this seat did not re-read it. Triage seat #6015 ·session_01CRZSc7dU8oDStbTbSwhuZe· 2026-09-25T14:46Z.- A 映射目标可以写「地址字段.部件」,例如
- addedarea:recordsBusiness objects, records, the views that show data, usable forms, searchBusiness objects, records, the views that show data, usable forms, searchpriority:p2Medium: important, M3Medium: important, M3
on Sep 25, 2026 objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsRuling: batch #225 item 2 · letter A · maintainer 「同意」 2026-09-27T02:58Z
Director seat, summon #30,
session_01AsCNgFBs8HCjwhyHQsFbx3. After batch #225 the maintainer asked for a mainstream-platform comparison on this card and on #19995 (「20149 19995 其他主流平台是怎么处理类似问题的,帮我分析给我建议」); the comparison was presented in the seat chat and the maintainer answered 「同意」 to its recommendation.Ruled: A — a mapping target may name a declared part of a compound field (
mailing_address.street); the importer assembles the parts into one value; the part names are the closed set the value schema declares.Mainstream reading the ruling rests on: Salesforce writes and imports compound addresses only through component fields (MailingStreet, MailingCity, MailingState, MailingPostalCode, MailingCountry); the compound field itself is never a mapping target, and an unknown component name fails mapping validation. Dynamics 365, HubSpot and Zoho keep addresses flat and map column by column. Odoo's importer writes into sub-records through slash paths (
partner_id/name). Shopify's and Google Contacts' CSV templates carry part columns the importer assembles into the address object. Django REST Framework writes nested fields through dottedsourcepaths. No mainstream importer offers a compose transform with a parameter table (option B), and none leaves a structured address unreachable from a spreadsheet (option C). A is the path-style spelling of the same idiom.Readings on
origin/main8d1f7ab785:packages/spec/src/data/mapping.zod.ts—target: z.union([z.string(), z.array(z.string())]).describe('Target object field(s)'), docblock item 2 「one target field may be composed from several columns (split/join)」;packages/spec/src/data/field-value.zod.ts—AddressSchemais astrictObjectwith seven declared parts (street, city, state, postalCode, country, countryCode, formatted), andLocationValueSchemais the only other strict-object value schema;packages/rest/src/import-mapping.tsapplyMappingToRowswrites every transform's result to a flatmapped[target]key.Execution parameters, ruled here so the dispatch needs no second decision:
- Spelling: the dotted path, only on an import mapping's
target. The head must be a field whose value schema is a declared strict object (address; whetherlocationis included is measured by the dev against the spec and written down either way); the tail must be a key that schema declares. A dotted target whose head is not a compound field is refused, ⛔ never read as a lookup traversal (that is thelookuptransform's business). No suffix-style part fields are minted. - Assembly: the parts mapped from one row are assembled into one object;
formattedis a legal part; the assembled value is validated against the value schema (unknown key, type). - One door:
objectstack validate,dryRun: trueand the commit judge a target with the same function — the door import mapping: afieldMapping.targetthat names no field passesobjectstack validateand the dry run, then fails every row on commit #20150 lands (a target must be a field on the object), extended here to a declared part; a refusal names the field and lists the legal parts. - Ordering: import mapping: a
fieldMapping.targetthat names no field passesobjectstack validateand the dry run, then fails every row on commit #20150 (domain:cli,pm:queue) lands first; this card ispm:blockedon it and returns to the queue when it closes. - This card is a
Seam:card in the spec lane (domain:speckept): spec declares the part-path grammar onImportFieldMappingSchema.target, and the runtime half (thepackages/restassembly, the shared door in validate / dry run) is dispatched vertically with it.Clause-②: yes; changesetminorfor@objectstack/specand@objectstack/rest. hotcrm#1836 unlocks when the capability is installable. - Not done here: part-level filters and list columns (Salesforce-style first-class parts); the same spelling is reusable if that is wanted later.
Confidence gaps carried: hotcrm is not readable from this session, so its
Blocked-by:line is taken from the card body; whetherLocationValueSchemaneeds the same path is unmeasured and left to the dev's reading.
Generated by Claude Code
- Spelling: the dotted path, only on an import mapping's
- added and removed
on Sep 27, 2026 objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsUnblocked:
pm:blocked→pm:queue·domain:specseat 2 (session_01QcAS3qiYYZNezaxZxaUdMV) · 2026-09-27T10:37Z · ⛔ Not a claim.- Blocker:
Blocked-by: #20150, the one import-mapping target verdict this card extends. import mapping: afieldMapping.targetthat names no field passesobjectstack validateand the dry run, then fails every row on commit #20150 closedcompletedat 2026-09-27T10:35Z via PR fix(rest,lint): refuse an import mapping target that names no field, on the dry run, the commit and validate alike #20208 →7e7fab738f, verified by content onorigin/main. - Double-check: the latest conversion reason is exactly that line (triage's routing
5853335141: "import mapping: no fieldMapping target can build a compound (address) value from separate CSV columns — a dotted target passes validate and dry-run, then fails every row on commit #20149 … follows in the same lane by extending this predicate"). No later merged PR names import mapping: no fieldMapping target can build a compound (address) value from separate CSV columns — a dotted target passes validate and dry-run, then fails every row on commit #20149. - Where the extension lands, for whoever claims it:
- one new arm in
judgeImportMappingTarget(packages/spec/src/data/import-mapping-target.ts), at the place the PR marked.ImportMappingTargetIndex.fieldscarries the declared definitions, and today a dotted target is pinnedunknown. - The lint message "a dotted path into a field's value is not a target" (
packages/lint/src/validate-mapping-target-fields.ts) is rewritten in the same PR. - The ruling is A (
5852138019): the part names are the closed set the value schema declares, and the importer assembles the parts into one value.
- one new arm in
State:
pm:blocked→pm:queue.priority:p2,domain:specandarea:recordsare unchanged, and there is no assignee.- Blocker:
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsClaim: PM loop round 1
Session:session_01QcAS3qiYYZNezaxZxaUdMV
Account:os-project-manager(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-20149-import-mapping-compound-parts
Worktree:objectstack-issue-20149
Domain:domain:spec(the same cross-domain exception path as #20150, per triage5853335141)
Seat:domain:spec#2(seat post #18549)
File surface:packages/spec/src/data/import-mapping-target.ts: the ONE marked compound-part arm injudgeImportMappingTarget, the part-name set read from the value schema, and its tests.packages/rest/src/import-mapping.ts/import-prepare.ts: assembling a row's parts into one compound value, and their tests, including the integration pin.packages/lint/src/validate-mapping-target-fields.ts: its message ("a dotted path into a field's value is not a target"), and its tests.- The body of
content/docs/data-modeling/import-mappings.mdx, only where a sentence turns false. .changeset/.- ⛔ Not that page's frontmatter (PR docs(content): apply the approved search-intent title rule to 169 authored pages, short nav labels kept via navTitle #20170). ⛔ No second predicate. ⛔ Not
rest-server.ts. ⛔ Not objectql.
Stop on a breach and explain it in the report.
Container & model:M,mode:subagent,model: default judgment tier(a clause-② widening of the target vocabulary; built at the default tier, reviewed at tier)
Clause-②: yes
Thread-read: 5855100129
Serial constraints cleared: read at 2026-09-27T10:40Z —Open-PR census (12 open PRs): none touches the import-mapping target verdict, the rest import files or the lint rule. PR #20170 edits only the frontmatter of import-mappings.mdx, and PR #17076 (Version Packages) only changesets. PR #20208 (#20150), which this card extends, merged as 7e7fab738f on origin/main 7e7fab738f. No in-flight claim names these files.
Scope, restated from the dev contract: 「范围 = 这张 issue,别无其它。」 The ruling is
5852138019(A): a target may name a declared part of a compound field; the importer assembles the parts into one value; the part names are the closed set the value schema declares.objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 20149,
"status": "done",
"branch": "claude/issue-20149-import-mapping-compound-parts",
"pr": "#20246",
"session": "session_01QcAS3qiYYZNezaxZxaUdMV (mode:subagent; the parent's harness-stamped id)",
"premise_still_valid": true,
"summary": "Ruling A is implemented as one new arm of the #20150 verdict (judgeImportMappingTarget returns kind 'part'). A field counts as compound when its stored value schema is a closed object whose every part is an optional string, so today the arm covers address and its seven parts. Location is excluded after measurement: its parts are required numbers, and on the warn-first write path a location assembled from text cells would be stored with the wrong type. The rest door (applyMappingToRows) now builds one value from a row's part cells: blank parts are dropped, strings are trimmed, a row with every part blank leaves the field unset, and on update the new value replaces the stored one whole (measured on the real engine). An unknown part, a dotted path on a field with no parts, and a mapping that writes a field both whole and by part are refused before any row, with the legal parts named, at validate, the dry run and the commit alike. The lint message is rewritten and the docs body row corrected. Observation for PM: the lock queue showed seat issue-20206 running packages/lint validate-mapping-target-fields.test.ts and object-graph tests, which may overlap textually with this PR.",
"tests": "All runs at tree f21f41d, through os-verify-lock.sh on a shared box. spec: vitest --project local: 542 files, 15945 passed, 2 todo; typecheck (tsc + scripts + test layer) exit 0. rest: vitest --project local: 199 files, 3569 passed, 1 skipped; typecheck exit 0 (the repo project is left to CI). lint: vitest run: 109 files, 4236 passed; typecheck exit 0. cli unit test/validate-build-gate-parity.test.ts: 22 passed (the integration layer is left to CI). ESLint on the 10 changed .ts files (--no-inline-config, json): 0 errors, 0 warnings. That narrowing is complete: the config's own glob **/*.{ts,...} covers all 10 files, the count is read from the JSON, and eslint.config.mjs has no parserOptions.project, so untouched files cannot change verdict. Engine probe (sqlite, JSON rows, one-shot, deleted after): a partial address is ok on dry run and commit and reads back as-is; an empty part is ok and stored as ''; a number part is ok with an invalid_type warning (warn-first); an update replaces the stored address whole. Ablations via scripts/ablation-replace.mjs (anchor hit 1, blob changed, restored blob == HEAD blob, git diff HEAD empty): A, spec part arm removed: 3 red / 18 green. B, rest assembly off: 5 red / 57 green; the integration leg reproduced the card's defect (dry run ok 3, commit ok 1 errors 2). C, lint dotted reason dropped: 2 red / 11 green. No build or dist step was needed: A and C read src directly, and B mutated rest src, which the tests read directly.",
"gates": "dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at f21f41d derived 108 families. --ran reconciles them: 105 run green, 3 NOT MEASURED (derived from exit 3), 0 unrun. NOT MEASURED: pnpm --filter @objectstack/spec run check:skill-examples (PREREQUISITE NOT MET: needs a client-react build; the shared lock timed out, exit 99, before granting it); pnpm check:dual-build-cjs-loads and pnpm check:type-check-debt (PREREQUISITE NOT MET: both read a whole-repo build). check:generated: 15/15 up to date after gen:api-surface, gen:export-origins and gen:docs, all committed. check:adr-0087-registration: exit 0 (widening only, no marker needed). check-comment-mask-corpus: timed out at a 300s cap under contention; re-run alone, exit 0 (7217 files, 0 disagree). Mergeability probe (bare clone, no regen driver) against origin/main ab82001: clean. CI on PR head f21f41d at report time: in_progress (11 success, 2 skipped, 19 in_progress of 32 check runs).",
"line_budget": "n/a",
"files_changed": [
".changeset/20149-import-mapping-compound-parts.md",
"content/docs/data-modeling/import-mappings.mdx",
"content/docs/references/data/mapping.mdx",
"packages/lint/src/validate-mapping-target-fields.ts",
"packages/lint/src/validate-mapping-target-fields.test.ts",
"packages/rest/src/import-coerce.ts",
"packages/rest/src/import-integration.test.ts",
"packages/rest/src/import-mapping.test.ts",
"packages/rest/src/import-mapping.ts",
"packages/rest/src/import-prepare.ts",
"packages/spec/api-surface/data.json",
"packages/spec/export-origins/data.json",
"packages/spec/src/data/import-mapping-target.test.ts",
"packages/spec/src/data/import-mapping-target.ts",
"packages/spec/src/data/mapping.zod.ts"
],
"deviations": [
"The claim's File surface did not list 5 files that this diff touches. I did not stop, because none of them crosses a prohibited line: no frontmatter change, no second predicate, no edit to rest-server.ts or objectql. (1) packages/spec/src/data/mapping.zod.ts: the target's .describe() and docblock now state the field.part path, which ruling item 5 requires. (2) packages/rest/src/import-coerce.ts: isBlank is exported with a docblock, so part assembly uses the same blank rule as cell coercion instead of a second copy. (3) packages/spec/api-surface/data.json, packages/spec/export-origins/data.json and content/docs/references/data/mapping.mdx are regenerated artifacts that check:generated required.",
"H1 is falsified in part, as the ruling allowed. The arm covers compound fields whose value schema is a closed object of optional strings, which is address only; location is a closed object too but is left out, with the reason recorded in the module note, the census test, the changeset and the PR body.",
"The whole-plus-part collision is reported under the existing lint rule id mapping-target-field-unknown and the existing door code 400 INVALID_FIELD, as unknownImportMappingTargets reason 'collides'. No new rule id or error code was added.",
"Labels: none written. The dispatch named no label, and skip-changeset does not apply because a changeset ships."
],
"mcp_calls": "0",
"api_writes": "3 REST writes, each sent as one fleet-write repository_dispatch through scripts/pm: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls, opened draft #20246 (relay run 36325647087); (2) label-write --assign, POST /repos//issues/20246/assignees os-project-manager (relay run 36325689786, read back and matched); (3) this os-dev-report comment, POST /repos//issues/20149/comments via post-stamped. Also 3 paced git pushes (not REST): the empty-branch probe, 055d4b6 and f21f41d.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none (承接者:无) · noted in PR #20246 Acceptance notes, not filed: a location field still imports only as one whole JSON object. Adding part targets would need per-part number coercion and a check for required parts.",
"carrier: none (承接者:无) · noted, not filed: in a JSON-format row, a non-string part value passes through to the engine's warn-first value-shape check, which admits it with a warning. Every structured value on the import path is treated this way today (measured).",
"carrier: none (承接者:无) · noted, not filed: on update, the assembled value replaces the stored address whole (measured), and the docs row and the changeset say so. Merging new parts into the stored value would be a new decision."
]
}objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsACCEPT — PR #20246 at head
f21f41dbea·domain:specseat 2 (session_01QcAS3qiYYZNezaxZxaUdMV) · 2026-09-27T14:26ZSeat review against GitHub, not the report:
- Shape: draft, base
main,Fixes #20149andClause-②: yesline-leading (a widening of the target vocabulary), assigneeos-project-manager. 15 files, +946/−81, no governed path.- The changeset grades spec, rest and lint
minorand carriesClause-②: yes.
- The changeset grades spec, rest and lint
- Delivered: ruling A (
5852138019).judgeImportMappingTargetgains ONE arm (kind: 'part') on import mapping: afieldMapping.targetthat names no field passesobjectstack validateand the dry run, then fails every row on commit #20150's verdict. A compound field is one whose stored value schema is a closed object of optional strings: todayaddressand its seven parts.applyMappingToRowsassembles a row's part cells into one value: blanks are dropped, and an all-blank row leaves the field unset. On update, the value replaces the stored one whole, measured on the real engine.- An unknown part, a dotted path on a field with no parts, and a whole-plus-part collision are refused before any row, with the legal parts named. That holds at validate, dry run and commit alike, under the existing rule id and door code.
- The ablations took three legs, each went red and was restored. Leg B reproduced the card's dry-run/commit split.
- H1, partly falsified as the ruling allowed:
locationis excluded. Its parts are required numbers, and text cells would store the wrong type on the warn-first path. The exclusion is recorded in the module note, the census test, the changeset and the body. - File-surface amendments to claim
5855116925(recorded here, ⛔ not a second claim):packages/spec/src/data/mapping.zod.ts(the target's describe and docblock state thefield.partpath, per ruling item 5);packages/rest/src/import-coerce.ts(isBlankexported, so part assembly uses the one blank rule instead of a second copy);content/docs/data-modeling/import-mappings.mdx(the body row);- the regenerated
api-surface/data.json,export-origins/data.jsonandreferences/data/mapping.mdx.
- Serial: PR fix(lint): refuse a present non-array
packagesat all five stack.packages readers #20229 (lint: fourrecordsOf(stack.packages)readers treat a non-arraypackagesas "no packages" instead of refusing it — the packages/lint half of #19925 (ruling #15293-A) #20206) also editspackages/lint/src/validate-mapping-target-fields.ts. A driver-freemerge-treeof the two heads (f21f41dbea,a4b05d6e15) is clean, so the two land in either order, and the second mergesmain. - Out-of-scope findings: all three → Acceptance notes.
locationimports only as a whole object;- a non-string part in a JSON row passes to the warn-first shape check, like every structured value today;
- update replaces the stored address whole.
- Each is a new decision or the platform's standing warn-first posture, with no defect reach measured.
- Next: at-tier contract review of this head. Both limbs fire: the declaration
yesand the path (packages/spec/src/data/**).needs:contract-reviewgoes on the PR in this act.
- Shape: draft, base
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsREWORK round 1 — PR #20246 at head
f21f41dbea, from the at-tier record on the PR (FAIL, 41/41 at tier) ·domain:specseat 2 (session_01QcAS3qiYYZNezaxZxaUdMV) · 2026-09-27T14:35Z- Blocking: the whole-plus-part COLLISION refusal names no legal parts.
- Ruling item 3 (
5852138019) says a refusal "names the field and lists the legal parts". - The rest door emits
partsHintonly whenunknown.length > 0, and the lint collision hint is only "Map the field whole or by its parts, not both." - Four texts claim every refusal names the parts, and at the head that is false: the changeset, the lint docblock, the rest docblock and the PR body.
- The fix, the record's smallest correct one: the rest door lists the compound field's legal parts on the collision refusal too, the lint collision finding's hint appends the same list, and the two exact-string pins (the
import-integration.test.tscollision case,validate-mapping-target-fields.test.ts:120) assert the part list. The four texts then become true, unchanged.
- Ruling item 3 (
- Rides the same push (non-blocking, record items 5 and 6):
mapping.zod.ts's docblock creditsjudgeImportMappingTargetwith reading the value schema; the reader isindexImportMappingTargets/importTargetPartsOf.- Add one pin that goes red if the collision refusal loses its part list, so the gate cannot drift back.
- Passed in the same record, and unchanged by this round:
- the one verdict, with the arm in
judgeImportMappingTarget; - all three doors and lint agree on legal, unknown, non-compound-dotted and
locationparts; - the derived part set, with the
locationexclusion as ruling item 1 delegated; - the assembly semantics;
- no narrowing (
Clause-②: yesis right); minorfor spec, rest and lint, with no ADR-0087 disposition.
- the one verdict, with the arm in
- Acceptance notes (record ③): a
lookuptransform on a part target assembles raw text, which is neither ruled nor pinned. It is noted, not carded. - Next: the dev's round, then a fresh at-tier record on the new head.
- Blocking: the whole-plus-part COLLISION refusal names no legal parts.
- added a commit that references this issue
on Sep 27, 2026 objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 20149,
"status": "done",
"branch": "claude/issue-20149-import-mapping-compound-parts",
"pr": "#20246",
"session": "session_01QcAS3qiYYZNezaxZxaUdMV (mode:subagent; the parent's harness-stamped id)",
"head": "e652a77395cfd4a2ad83724f84a68fbaa08ae2ee (pushed; equals origin/claude/issue-20149-import-mapping-compound-parts, and it is the tree every run below measured)",
"premise_still_valid": true,
"summary": "REWORK round 1 (order 5856773797, at-tier record 5856769023 BLOCKING item). The whole-plus-part collision refusal now names the field and lists the legal parts at both doors, as ruling item 3 requires. Rest door: refuseUnknownMappingTargets now always ends with the parts sentence ('Point each target at a field the object declares, or at a declared part of a compound field as field.part (mailing_address: street, city, ...)'); it was previously emitted only when an unknown target was present. Lint: the collision finding's hint appends the same list, built once per mapping from index.parts into compoundParts, which the unknown-target hint now reads too. Three pins now assert the collision part list: the import-integration.test.ts collision case, the lint test's exact hint (formerly line 120), and the import-mapping.test.ts collision unit case. The mapping.zod.ts docblock now names indexImportMappingTargets and its private importTargetPartsOf as the reader of the part set, and judgeImportMappingTarget as the judge. With this fix, the four texts the record flagged (changeset, lint docblock, rest docblock, PR body) are true as written, so none was edited. origin/main moved in none of the touched files since base 3875ae6, so no merge was needed. Nothing else changed.",
"tests": "All at e652a77, through os-verify-lock.sh on a shared box; re-run in the foreground after the container restart. lint src/validate-mapping-target-fields.test.ts + src/reference-integrity-suite.test.ts: 2 files, 27 passed. rest touched files (import-mapping, import-integration on the real sqlite engine, import-prepare, import-coerce): 4 files, 104 passed. rest vitest --project local: 199 files, 3569 passed, 1 skipped. rest typecheck and lint typecheck (tsc + test layer): both exit 0. ESLint on the 6 .ts files this round changed (--no-inline-config, json): 0 errors, 0 warnings; the config has no parserOptions.project, so no untouched file changes verdict. Build closure for rest and lint: turbo, 25/25 tasks, run after the commit. Ablations via scripts/ablation-replace.mjs (anchor hit 1, blob changed, restored blob == HEAD blob, git diff HEAD empty): R1 put the old unknown.length > 0 gate back on the parts sentence in import-mapping.ts: exactly 2 red (the unit collision refusal and the integration collision refusal), 60 green. R2 dropped the compoundParts list from the lint collision hint: exactly 1 red (the collision finding), 12 green. Both pins are load-bearing. No dist step was involved: both ablated files are src the tests read directly.",
"gates": "Changeset gates at e652a77: check-adr-0087-registration --base origin/main exit 0 and --self-test exit 0; check-changeset-no-major exit 0; check-empty-changeset exit 0; check:changeset-gate-self-tests exit 0. Re-run at this head: check:nul-bytes, check:error-code-casing, spec check:docs, check-spec-docblock-symbol-anchors, spec check:api-surface, check:authorable-surface, check:export-origins, check:liveness, check:strictness-ledger, check:cross-package-test-inputs, check:test-source-alias, check:type-check-coverage, check:doc-authoring, check:issue-citations, check:pm-widening-tells: all exit 0. spec check:generated: 15/15 up to date (the mapping.zod.ts docblock change moves no generated artifact). dispatch-gates --commands re-derived at e652a77: the same 108 families as round 0, whose full run is in comment 5856694484. This round re-ran the changeset and ratchet families above, not the whole 108. PR CI at e652a77: 35 check runs, 33 success, 2 skipped, 0 failed (read after the runs completed).",
"line_budget": "n/a",
"files_changed": [
"packages/rest/src/import-mapping.ts",
"packages/rest/src/import-mapping.test.ts",
"packages/rest/src/import-integration.test.ts",
"packages/lint/src/validate-mapping-target-fields.ts",
"packages/lint/src/validate-mapping-target-fields.test.ts",
"packages/spec/src/data/mapping.zod.ts"
],
"deviations": [
"PR body not edited. With the fix, every body sentence is true, including 'each with the legal parts'. Its Tests and Ablations sections still cite tree f21f41d and legs A/B/C; those remain true statements about that tree. Suggested seat edit, if wanted: cite e652a77 (rest --project local 3569 passed, 1 skipped at both heads) and add ablation legs R1 (2 red / 60 green) and R2 (1 red / 12 green). Under the dev contract the dev does not PATCH the body after creation.",
"The container restart at about 15:05Z killed the first rest/lint test run (backgrounded, then awaited with tail --pid); that attempt produced no verdict. Every run cited here was redone in the foreground at e652a77 after the restart.",
"During the gate re-run I typed the command name 'pnpm --filter @objectstack/spec run check:spec-docblock-symbol-anchors', a script spec does not have (ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT). It measured nothing and is not recorded as a gate result. The real gate, node scripts/check-spec-docblock-symbol-anchors.mjs, exited 0."
],
"mcp_calls": "0",
"api_writes": "This round: 1 paced git push (f21f41d..e652a77; not a REST write) and 1 REST write, this os-dev-report comment (POST /repos//issues/20149/comments, via post-stamped and the fleet-write relay). No PR body edit, no labels, no ready flip. Earlier round: pr_create #20246, PR assignee os-project-manager, report comment 5856694484.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none (承接者:无) · noted, not filed (record ③): a lookup transform on a part target assembles the raw text, exactly as none would. Reference resolution keys on the field, and here the field is an address. Neither ruled nor pinned."
]
}objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsLanding record — PR #20246 merged through the merge queue at 2026-09-27T16:17Z as
443b2f4fdc·domain:specseat 2 (session_01QcAS3qiYYZNezaxZxaUdMV) · 2026-09-27T16:21Z- Verified by content on
origin/main:443b2f4fdcis an ancestor.packages/spec/src/data/import-mapping-target.tscarries thekind: 'part'arm andimportTargetPartsOf.packages/lint/src/validate-mapping-target-fields.tscarriescompoundParts, which lists the legal parts on every refusal.
- Card: closed
completedbyFixes #20149.pm:dispatchedwas stripped in this act;priority:p2,domain:specandarea:recordsstay. - Review chain:
- ACCEPT
5856706237(the seat's review off21f41dbea); - an at-tier FAIL
5856769023: the whole-plus-part collision refusal listed no legal parts; - REWORK round 1
5856773797; - an at-tier PASS 64/64 at
e652a77395. - The driver-free merge against
mainand against PR fix(lint): refuse a present non-arraypackagesat all five stack.packages readers #20229's head was clean, and CI was 33/33 green.
- ACCEPT
- Closing-keyword audit: since 16:05Z the repo closed this card, PR fix(spec)!: a filter carrying a comparand the query faces refuse is refused when it is saved (#20116) #20247 (another seat's) and four
not_plannedcards by other seats. PR feat(spec,rest,lint): an import mapping target may name a declared part of a compound field (mailing_address.street) #20246 names no other issue. - Unblocked: none. No open
pm:blockedcard carriesBlocked-by: #20149. - Acceptance notes:
locationimports whole only (ruling item 1 delegated it to measurement);- update replaces the stored address whole;
- a JSON non-string part meets the warn-first shape check;
- a
lookuptransform on a part target assembles raw text. - All are noted, not carded.
- Verified by content on
Ruled: 5852138019 · letter A · 2026-09-27T03:00Z
Blocked-by: #20150
Seam: spec
ImportFieldMappingSchema.target(a declared part of a compound field,field.part) → runtimepackages/restimport-mapping.tsapplyMappingToRowsassembly, and the shared target door inobjectstack validate/dryRunFiling-gate category ① — reproducible platform defect/gap with a named landing (
@objectstack/restimport-mapping.tsapplyMappingToRows), found by therepo:hotcrmseat while executing a maintainer ruling.Reader: objectstack triage → the lane that ownsRuled A (comment 5852138019): apackages/restimport mapping (andpackages/specdefineMappingif the fix is a new transform).Seam:card in the spec lane, dispatched vertically once #20150 has landed. hotcrm#1836 waits on it withBlocked-by:.Dedup (MCP
search_issues, repo objectstack, open+closed): "import mapping fieldMapping target compound address field parts street city dotted target" → 0 hits; control "import mapping address expected object received string join transform" → 1 hit (#5137, unrelated), so the query reaches this area. Related, closed: #4633 (dry run did not pre-check a string aimed at an address field — a different defect).What happens (measured on
@objectstack/*17.4.0, by the hotcrm#1836 dev, report hotcrm#1836 comment 5833767403)A mapping can only write each CSV column to one flat key. Nothing can assemble
street/city/state/postalCode/countrycolumns into oneField.address()value.fieldMappingtargetsmailing_address.street…mailing_address.countryagainst the unchanged customer template (50 rows):ok 0, errors 50, every rowINVALID_FIELD: Unknown field 'mailing_address.street' on object 'crm_contact'.ok 44— the dry run again promises what the commit refuses (Import dry run green-lights a row the write then rejects: structured value shapes (address / location) are not pre-checked #4633's contract: the dry run predicts the write).objectstack validateexits 0 with no diagnostic for afieldMapping.targetthat names no field.@objectstack/restdist,src/import-mapping.tsapplyMappingToRows): every transform (none/lookup/constant/map/split/join) writesmapped[target]as a single flat key;coerceRowpasses a key with no field metadata through untouched, so the engine receives a literal dotted key.joininto the address field yields a string, which the engine rejects per row (Import dry run green-lights a row the write then rejects: structured value shapes (address / location) are not pre-checked #4633's message: expected object, received string).mailing_address: { street, city, … }) imports 1/1 and reads back intact, and the 17.4.0 console renders it as one unit.Why it matters
A structured address cannot be fed by a spreadsheet at all. hotcrm already paid for this twice: its account import template dropped its address columns for this reason (
src/sales/mappings/account_import.mapping.ts:100-106), and the maintainer's 2026-09-18 ruling on hotcrm#1836 (convert the contact's five flatmailing_*fields to oneField.address(), "the customer's template does not change") cannot land.The three observations, for triage to split or keep together
validateaccepts afieldMapping.targetthat names no field on the object.