Skip to content

refactor(spec)!: 双源清账 C12 — FieldMapping 三源改名为 ConnectorFieldMapping / ImportFieldMapping (#4703) - #4710

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-4703-field-mapping-tri-source
Aug 2, 2026
Merged

refactor(spec)!: 双源清账 C12 — FieldMapping 三源改名为 ConnectorFieldMapping / ImportFieldMapping (#4703)#4710
os-zhuang merged 2 commits into
mainfrom
claude/issue-4703-field-mapping-tri-source

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#4703

#4535 的 C12 簇。基线 16 → 14,其余 14 行一字未动。

复核结论:三侧确实是两个概念,改名而非收敛

PM 的定位表我对 post-C9 的 origin/main 逐条自验过,完全属实:

声明形态authorable key
./sharedshared/mapping.zod.ts:102,plain z.object4
./integrationintegration/connector.zod.ts:105Base.extend({dataType, required, syncMode})7
./datadata/mapping.zod.ts:97独立strictObject4

./shared./integration 确实是基与超集,所以「该不该收敛」是个真问题。我的结论是不能,两个方向都不行:

  • 把基加宽到 7 个 key —— automation/sync.zod.tsdata/external-lookup.zod.ts 也 extend 这个基,等于把 connector 的同步语义(syncMode / required / dataType)推给 ETL 同步和外部查找;
  • 把 connector 侧收窄到 4 个 key —— 那是退役三个 live key,不是命名修复,得走 ADR-0049 那一整套。

./data 根本不是同一个概念。三条硬证据我都实测并钉住了:

  1. transform 同名不同类型 —— ./shared+./integration 收判别联合 { type: 'cast', targetType };./dataTransformType 普通枚举 + 平铺 params 袋子,默认 'none'。互相都解析不过
  2. 基数不同 —— ./datasource/targetstring | string[](一个目标字段可由多列 split/join 合成),另两侧只收 string
  3. 未知键失败模式相反 —— ./datastrictObject(未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001),未知键 throw 并给出别名/拼写处方;另两侧是 plain z.object,静默 strip。同一个 typo,一边硬报错一边什么都不做。

ADR-0112 D9(a):基保留裸名,两个领域专用侧加领域前缀。这不是新发明 —— 同仓 data/external-lookup.zod.ts:86ExternalFieldMappingSchema extend 的是同一个基,正因为带前缀,它从来没进过基线

integration/FieldMapping -> integration/ConnectorFieldMapping (7 keys)
data/FieldMapping -> data/ImportFieldMapping (4 keys)
shared/FieldMapping -> 原样不动

11 个 key 逐条核对(4 + 7,一个不少)

authorable-surface.json 总量 8265 → 8265,只有 11 行改名,没有任何增减:

defkey状态
integration/ConnectorFieldMappingsourcetargettransformdefaultValuedataTyperequiredsyncMode7/7 承接
data/ImportFieldMappingsourcetargettransformparams4/4 承接
shared/FieldMappingsourcetargettransformdefaultValue未进承接表,原样

零 tombstone、零 ADR-0087 conversion —— check:spec-changes 全程绿,没有任何 key 离开契约。

承接表:发现并修了两个只在「多条目」下才成立的洞

本簇是 RENAMED_DEFS(#4684)的第一个真实消费者,也是它第一次同时装两条。装上之后有两条规则才开始有意义,原表都没有:

洞 1 —— 两个 source 指向同一个 target,checkRenameTable 不报错。 这不是两次改名,是一次合并,而且它恰好击穿承接表存在的理由:build-schemas.ts 把快照 carry 进一个按新 key 索引prev map,于是两个 def 下同名属性的条目会塌成一条 —— 活下来的 [RETIRED] 状态是最后 carry 的那个。一个在 A 下是 live、在 B 下已 tombstone 的 key,合并后读作「早就退役了」,检查 (b)(每个 live → retired 都要有已注册的 conversion)就永远不会为它触发。那正是「承接表替一个 key 离开契约背书」,是它唯一不许干的事。现在直接拒。

洞 2 —— 链式改名(A → B → C)诊断错误。 原先它已经会红,但报的是「B 没被产出」,把链误诊成拼写错误 —— 而按那个提示去改(删掉 A → B)会让 A 的 key 真的消失。carry 是单趟的,链本来就不支持,现在按名报出来。

两条都在 scripts/renamed-defs.test.ts 补了单测(该文件 10 → 17 条),另加一条「不要把 extend 的误判成 rename source」的用例,以及一条拿真实验证器跑 committed 表自身的自洽检查。

extend 场景本身没有洞:target 的 key 集是基的超集,carry 的每个 key 都找得到;基自己既不是 source 也不是 target,原样产出。

回归 pin:三源,三对入口两两覆盖,全部 sabotage 验过

#4642 已证本包编译期 pin 空转(tsconfig.json 排除 **/*.test.ts,vitest 不开 typecheck),而 FieldMapping类型,运行时看不见。所以照 PR #4695 / #4689TypeScript compiler API 做符号身份解析,含两道防空转守卫(expect(moduleSym).toBeTruthy()origins.size > 20)。

C9 那条通用不变式 pin 的 KNOWN_STILL_DUAL_SOURCE 已按设计清空为 []

sabotage 实测输出

1. ./integration 重新加回 export type FieldMapping(旧名兼容别名——本簇明令禁止):

× no name resolves to two declarations across ./shared and ./integration (types included)
× no name resolves to two declarations across ./shared, ./integration and ./data (types included)
AssertionError: expected [ 'FieldMapping' ] to deeply equal []
AssertionError: FieldMapping must be gone from ./integration: expected 'src/integration/connector.zod.ts:149' to be undefined
Tests 2 failed | 46 passed (48)

C9 的清单 pin 和新的三源 pin 同时变红 —— 那个强制握手是活的。

2. ./data 重新加回 FieldMapping 类型 + schema 别名:

× each entry exposes exactly one field-mapping name, and not the others’
× no name resolves to two declarations across ./shared, ./integration and ./data (types included)
AssertionError: FieldMapping must be gone from ./data: expected 'src/data/mapping.zod.ts:253' to be undefined

3. 概念差异 pin —— 把 ./datatransform「顺手统一」到共享判别联合:

× `transform` means a discriminated union on two sides and a flat enum on the third
× ./data accepts arrays for source/target where the other two take a single string
AssertionError: expected undefined to be 'none'
"message": "Invalid input: expected object, received string"

4. 把 ./datasource/target 收窄成单个 string:

× ./data accepts arrays for source/target where the other two take a single string
"message": "Invalid input: expected string, received array"

5. 把 ./datastrictObject 改回 z.object:

× an unknown key THROWS on ./data and is silently stripped by the other two
AssertionError: expected true to be false

6. 防空转守卫本身(把 ./data 入口指向不存在的文件):

AssertionError: ./data module symbol must resolve: expected undefined to be truthy
Tests 1 failed | 47 skipped (48)

承接表的 sabotage(对改名前的快照跑,否则表已对新快照惰性)

7a. 两条承接都在 —— 只报「新增未记录」,没有任何 key 丢失,正好 11 条:

❌ authorable-surface.json is out of date (11 key(s) not recorded).
+ data/ImportFieldMapping:params …(4)
+ integration/ConnectorFieldMapping:dataType …(7)

7b. 删掉 data/FieldMapping 那条承接:

❌ 4 authorable key(s) disappeared from the contract:
- data/FieldMapping:params
- data/FieldMapping:source
- data/FieldMapping:target
- data/FieldMapping:transform

7c. 声明了改名,却把 syncMode 从新 def 里删掉(检查 (a0)):

❌ 1 authorable key(s) were lost by a declared def rename:
- integration/FieldMapping:syncMode → integration/ConnectorFieldMapping:syncMode (absent)

7d. 旧名留着不删(是拷贝,不是改名):

❌ 1 problem(s) in RENAMED_DEFS (scripts/lib/renamed-defs.ts):
- data/FieldMapping → data/ImportFieldMapping: the SOURCE def is still emitted by
this build. That is a copy, not a rename — and a copy is precisely the
dual-source shape this table must never be able to launder (#4411, #4446).

预期的「非回归」变化:文档页归属(#4696)

gen:docs 删掉了 content/docs/references/integration/mapping.mdx,内容并入 integration/connector.mdx这是修复,不是回归,机制已定位到行:

build-docs.ts:136schemaZodFileMap 是一个跨 category 的全局 map,按裸 schema 名索引。FieldMapping 被写了三次(data/mapping.zod.tsshared/mapping.zod.ts 两个 slug 都是 mapping,integration/connector.zod.ts slug 是 connector),最后写入者胜出 → 'mapping'。所以发射 integration 分类时,integration/FieldMapping.json 查到的 slug 是 mapping,被写进了 content/docs/references/integration/mapping.mdx —— 而 packages/spec/src/integration/ 下根本没有 mapping.zod.ts(该目录只有 connector*.ts),那是个查无此文件的幽灵页(所以它连 Source: 行都没有)。名字不再撞车后,ConnectorFieldMapping 正确落到它真正所在的 connector.mdx

顺手改了一处手写文档 content/docs/getting-started/quick-reference.mdx:28(FieldMappingImportFieldMapping),它直接点名了被改的导出。:217./shared 那行本来就正确,未动。

changeset:major,但作者写的元数据零迁移

据实定级,没有照抄 C9 或 C11:

  • major —— 两个 entry 的 TS 导出真的改名了,import { FieldMappingSchema } from '@objectstack/spec/data' 会编译失败。
  • 元数据零迁移 —— 11 个 authorable key 名字、类型、默认值、严格性全部不变。connectors[].fieldMappings[]mapping.fieldMapping[] 里已有的 stack metadata / sys_metadata 行 / 已发布应用逐字节不受影响。所以无 tombstone、无 conversion。
  • $id 迁移:…/integration/FieldMapping.json…/integration/ConnectorFieldMapping.json,…/data/FieldMapping.json…/data/ImportFieldMapping.json

changeset 里额外写了一条升级陷阱:编译报错后不要把 import 改指 @objectstack/spec/shared。那个名字解析得过去,但拿到的是——connector 侧会静默丢掉dataType / required / syncMode(基不是 .strict(),parse 时直接 strip),import 侧则会直接拒掉数组和枚举形式的 transform

门禁实测

命令结果
pnpm --filter @objectstack/spec buildexit 0
check:generated(8 项)✓ All 8 generated artifacts are up to date.
check:dual-source-exports✅ no new dual-source exports: 4328 names across 16 entry points — 166 re-exported, 14 accepted dual-source (baseline).
pnpm --filter @objectstack/spec testTest Files 293 passed (293) / Tests 7374 passed (7374)
pnpm --filter @objectstack/spec typecheckexit 0
check:strictness-ledger✓ 67 file(s) across 5 triaged directories — site counts match
check:liveness✓ all governed-type properties are classified …
check:empty-state✓ all classified (1 closed, 2 open, 4 output, 9 scope)
check:variant-docs✓ 19 discriminated union(s) — 8 governed, 11 exempt
check:exported-any✅ no exported type resolves to any: 1893 types + 1638 schemas
check:skill-examples✅ 202 prose examples type-check
全仓 turbo run typecheckTasks: 122 successful, 122 total
全仓 turbo run testTasks: 133 successful, 133 total

严格性台账:docs/audits/2026-07-unknown-key-strictness-ledger.md:475mapping.zod.ts(3 sites,authorable (p))未改 —— 纯改名不动 site 数,已实跑 check:strictness-ledger 确认,没有假设。

已合并 main(4 个新提交)并重验

合并后 packages/spec 两侧都动过(#4687 退役 IDataEngine.batch?),按 AGENTS.md §10 重跑了 pnpm install --frozen-lockfile + spec build + check:generated,没有走 git 的文本合并结果。新落地的 #4675 合并驱动已由 pnpm install 注册,node scripts/check-regen-pending.mjs exit 0。

origin/main 的 delta 复核:dual-source 基线恰好少两行、其余 14 行零改动;authorable-surface.json 恰好 11 增 11 删。

⛔ 未碰 content/docs/releases/;⛔ 未手编 packages/spec/authorable-surface.json(全部经 gen:schema 由承接表重新生成)。


Generated by Claude Code

…, C12)
`FieldMapping` / `FieldMappingSchema` were published by THREE entry points for
three different declarations — the #4411 trap, one entry worse than the usual
pair:
./shared the base, plain z.object, 4 keys
./integration Base.extend({ dataType, required, syncMode }), 7 keys
./data an independent strictObject, 4 keys — and a different CONCEPT:
the column mapping of a CSV/table import, not a connector's
remote-field mapping
Per ADR-0112 D9(a) the two domain-specific sides take a domain prefix and the
base keeps the bare name:
integration/FieldMapping -> integration/ConnectorFieldMapping
data/FieldMapping -> data/ImportFieldMapping
`shared/FieldMapping` is untouched — two other defs extend it, including
`data/ExternalFieldMapping`, which has never been in the baseline precisely
because it already carries a domain prefix.
dual-source-exports baseline: 16 -> 14.
Zero tombstones and zero ADR-0087 conversions: all eleven authorable keys
(7 + 4) carry over unchanged, so no authored metadata migrates. The rename is
carried through the two def-keyed ratchets by `RENAMED_DEFS` (#4684), which
gets its first entries beyond the original one — and with them the first two
rules that only bind when the table holds more than one entry: two sources onto
one target is rejected as a merge (it would collapse the carried key sets and
their retired states, blinding the "live -> retired needs a conversion" check),
and a chained rename is rejected by name rather than misdiagnosed as a typo.
Regression pins live in `src/integration/connector.test.ts` next to the #4684
block: a TypeScript compiler-API symbol-identity resolution over all three
pairs of entries (types are erased at runtime, and #4642 proved a compile-time
pin in this package is dead text), plus three runtime pins on the concept
differences that justify the rename — `transform`'s union-vs-enum split,
`./data`'s array cardinality, and its strictObject throwing where the other two
silently strip. The C9 `KNOWN_STILL_DUAL_SOURCE` handshake list is now empty.
`gen:docs` moves the connector field mapping from a phantom
`references/integration/mapping` page into `references/integration/connector`,
where the schema actually lives — #4696's bare-name global index resolving now
that the names are distinct.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0176qgxgCXTJCUv4YFLtusP9
@vercel

vercelBot commented Aug 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 2, 2026 8:11pm

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:data tests tooling size/l labels Aug 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via @objectstack/spec)
  • content/docs/automation/connectors.mdx(via @objectstack/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via packages/spec)
  • content/docs/concepts/north-star.mdx(via packages/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/cli.mdx(via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via packages/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via packages/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/spec)
  • content/docs/permissions/authorization.mdx(via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/rls.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx(via @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v16.mdx(via @objectstack/spec)
  • content/docs/releases/v17.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/spec)
  • content/docs/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

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

Labels

documentationImprovements or additions to documentationprotocol:datasize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec 双源清账 C12:FieldMapping / FieldMappingSchema(./data ≠ ./integration ≠ ./shared,三源)—— 2 条

2 participants

@os-zhuang@claude