Skip to content

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

@objectstack-fleet

Ruled: 5852138019 · letter A · 2026-09-27T03:00Z
Blocked-by: #20150
Seam: spec ImportFieldMappingSchema.target (a declared part of a compound field, field.part) → runtime packages/rest import-mapping.ts applyMappingToRows assembly, and the shared target door in objectstack validate / dryRun

Filing-gate category ① — reproducible platform defect/gap with a named landing (@objectstack/rest import-mapping.ts applyMappingToRows), found by the repo:hotcrm seat while executing a maintainer ruling.
Reader: objectstack triage → the lane that owns packages/rest import mapping (and packages/spec defineMapping if the fix is a new transform). Ruled A (comment 5852138019): a Seam: card in the spec lane, dispatched vertically once #20150 has landed. hotcrm#1836 waits on it with Blocked-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 / country columns into one Field.address() value.

  • fieldMapping targets mailing_address.street … mailing_address.country against the unchanged customer template (50 rows):
  • Mechanism (read in @objectstack/rest dist, src/import-mapping.ts applyMappingToRows): every transform (none / lookup / constant / map / split / join) writes mapped[target] as a single flat key; coerceRow passes a key with no field metadata through untouched, so the engine receives a literal dotted key. join into 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).
  • Control: the engine itself accepts the compound value — a nested JSON row (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 flat mailing_* fields to one Field.address(), "the customer's template does not change") cannot land.

The three observations, for triage to split or keep together

  1. Capability: a way for a mapping to write into a compound field's parts (a dotted target resolved into a nested value, or a compose transform).
  2. Dry run ≠ commit for a target naming no field (sibling of Import dry run green-lights a row the write then rejects: structured value shapes (address / location) are not pre-checked #4633).
  3. Author-time silence: validate accepts a fieldMapping.target that names no field on the object.

Activity

  1. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: 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)". AddressSchema is 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 不在本会话的可读范围),按本卡原文记。

    裁后执行:


    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 carry Blocked-by: this card; this seat did not re-read it. Triage seat #6015 · session_01CRZSc7dU8oDStbTbSwhuZe · 2026-09-25T14:46Z.

  2. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: 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 dotted source paths. 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/main 8d1f7ab785: 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 — AddressSchema is a strictObject with seven declared parts (street, city, state, postalCode, country, countryCode, formatted), and LocationValueSchema is the only other strict-object value schema; packages/rest/src/import-mapping.ts applyMappingToRows writes every transform's result to a flat mapped[target] key.

    Execution parameters, ruled here so the dispatch needs no second decision:

    1. 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; whether location is 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 the lookup transform's business). No suffix-style part fields are minted.
    2. Assembly: the parts mapped from one row are assembled into one object; formatted is a legal part; the assembled value is validated against the value schema (unknown key, type).
    3. One door: objectstack validate, dryRun: true and the commit judge a target with the same function — the door import mapping: a fieldMapping.target that names no field passes objectstack validate and 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.
    4. Ordering: import mapping: a fieldMapping.target that names no field passes objectstack validate and the dry run, then fails every row on commit #20150 (domain:cli, pm:queue) lands first; this card is pm:blocked on it and returns to the queue when it closes.
    5. This card is a Seam: card in the spec lane (domain:spec kept): spec declares the part-path grammar on ImportFieldMappingSchema.target, and the runtime half (the packages/rest assembly, the shared door in validate / dry run) is dispatched vertically with it. Clause-②: yes; changeset minor for @objectstack/spec and @objectstack/rest. hotcrm#1836 unlocks when the capability is installable.
    6. 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; whether LocationValueSchema needs the same path is unmeasured and left to the dev's reading.


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Unblocked: pm:blocked → pm:queue · domain:spec seat 2 (session_01QcAS3qiYYZNezaxZxaUdMV) · 2026-09-27T10:37Z · ⛔ Not a claim.

    State: pm:blocked → pm:queue. priority:p2, domain:spec and area:records are unchanged, and there is no assignee.

  4. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01QcAS3qiYYZNezaxZxaUdMV
    Account: os-project-manager (the seat's linked user as GET /user answers 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 triage 5853335141)
    Seat: domain:spec#2 (seat post #18549)
    File surface:

    • packages/spec/src/data/import-mapping-target.ts: the ONE marked compound-part arm in judgeImportMappingTarget, 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.

  5. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    os-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."
    ]
    }

  6. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #20246 at head f21f41dbea · domain:spec seat 2 (session_01QcAS3qiYYZNezaxZxaUdMV) · 2026-09-27T14:26Z

    Seat review against GitHub, not the report:

    • Shape: draft, base main, Fixes #20149 and Clause-②: yes line-leading (a widening of the target vocabulary), assignee os-project-manager. 15 files, +946/−81, no governed path.
      • The changeset grades spec, rest and lint minor and carries Clause-②: yes.
    • Delivered: ruling A (5852138019).
      • judgeImportMappingTarget gains ONE arm (kind: 'part') on import mapping: a fieldMapping.target that names no field passes objectstack validate and 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: today address and its seven parts.
      • applyMappingToRows assembles 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: location is 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 the field.part path, per ruling item 5);
      • packages/rest/src/import-coerce.ts (isBlank exported, 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.json and references/data/mapping.mdx.
    • Serial: PR fix(lint): refuse a present non-array packages at all five stack.packages readers #20229 (lint: four recordsOf(stack.packages) readers treat a non-array packages as "no packages" instead of refusing it — the packages/lint half of #19925 (ruling #15293-A) #20206) also edits packages/lint/src/validate-mapping-target-fields.ts. A driver-free merge-tree of the two heads (f21f41dbea, a4b05d6e15) is clean, so the two land in either order, and the second merges main.
    • Out-of-scope findings: all three → Acceptance notes.
      • location imports 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 yes and the path (packages/spec/src/data/**). needs:contract-review goes on the PR in this act.
  7. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    REWORK round 1 — PR #20246 at head f21f41dbea, from the at-tier record on the PR (FAIL, 41/41 at tier) · domain:spec seat 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 partsHint only when unknown.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.ts collision case, validate-mapping-target-fields.test.ts :120) assert the part list. The four texts then become true, unchanged.
    • Rides the same push (non-blocking, record items 5 and 6):
      • mapping.zod.ts's docblock credits judgeImportMappingTarget with reading the value schema; the reader is indexImportMappingTargets / 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 location parts;
      • the derived part set, with the location exclusion as ruling item 1 delegated;
      • the assembly semantics;
      • no narrowing (Clause-②: yes is right);
      • minor for spec, rest and lint, with no ADR-0087 disposition.
    • Acceptance notes (record ③): a lookup transform 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.
  8. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    os-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."
    ]
    }

  9. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Landing record — PR #20246 merged through the merge queue at 2026-09-27T16:17Z as 443b2f4fdc · domain:spec seat 2 (session_01QcAS3qiYYZNezaxZxaUdMV) · 2026-09-27T16:21Z

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:specpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions