Repository navigation
Closed
Description
现象
用户通过数据导入(CSV / 批量 API)导入历史 mid-lifecycle 记录时,凡是记录的状态字段值 ∉ 对象 state_machine.initialStates,该行被拒。典型场景:迁移一批已 closed 的历史工单、已 closed_won 的历史商机、已 completed 的历史项目——这些正是数据迁移的核心内容,却被 FSM 入口校验挡下。
与 #3433(种子重放被 initialStates 拒)完全同构,只是换了入口:#3433 修了种子写路径,但数据导入这条平行路径没修。区别是导入不静默——import-runner 有 per-row 结果,会报 invalid_initial_state,所以用户看得到"导入 100 行,拒了 80 行",但合法的历史迁移场景被功能性挡住。
机制
- feat(validation): state_machine.initialStates enforces the FSM entry point on INSERT (#3165) #3188(实现 enhancement:
state_machine校验规则在 INSERT 上是 no-op —— 无法强制记录的初始状态(declared ≠ enforced) #3165)让state_machine.initialStates在 INSERT 上强制 FSM 入口:创建值必须 ∈ initialStates,否则400 invalid_initial_state(packages/objectql/src/validation/rule-validator.ts)。 - seed replay vs state_machine initialStates: mid-lifecycle fixture rows are silently rejected on INSERT (showcase kanban can never repopulate) #3433 让种子写路径豁免
state_machine:SeedLoaderService.SEED_OPTIONS带ExecutionContext.seedReplay→ 引擎传skipStateMachine→ rule-validator 跳过。 - 但数据导入的写入 context 没带这个旗标:
packages/rest/src/import-runner.ts:265只有const writeCtx = { ...(context ?? {}), skipAutomations: !runAutomations };
skipAutomations,没有seedReplay/skipStateMachine。导入写入经createManyData/insertManyData(packages/metadata-protocol/src/protocol.ts:3377/3405)→ 引擎 → 照跑state_machineinitialStates 校验。
影响面
- 任何声明了
initialStates的对象 + 用户导入其历史态数据 = 丢行。历史数据迁移(平台的常见 onboarding 场景)被 FSM 挡住。 - 声明 initialStates 的对象会越来越多(enhancement:
state_machine校验规则在 INSERT 上是 no-op —— 无法强制记录的初始状态(declared ≠ enforced) #3165 是新能力,鼓励用),所以这个缺口会随采用面扩大。 - 与种子不同,导入是用户显式高频操作,可见性更高(per-row 报错),但也更容易被当成"平台不支持历史数据导入"而流失。
候选修法(需拍板)
不能照抄种子的一刀切豁免。 种子是平台/包策展的既成事实,无条件豁免合理;用户导入的意图是二义的:
- 历史数据迁移 → 导入的是既成事实,应豁免
state_machine(和种子同理); - 批量新建 → 真的在创建新记录,应走
initialStates(否则用户能批量绕过 FSM 入口)。
所以应该是一个用户可选开关,而非默认豁免。好消息:
- 引擎管道已就绪(seed replay vs state_machine initialStates: mid-lifecycle fixture rows are silently rejected on INSERT (showcase kanban can never repopulate) #3433 建的
seedReplay → skipStateMachine),import 只需在开关打开时往writeCtx塞对应 context 旗标即可,几乎零引擎改动。 - 有现成的开关先例:import 已有
runAutomations/run_automations(import-runner.ts:104、rest-server.ts:3833)映射到skipAutomations。平行地加一个treatAsHistorical(或skipLifecycleValidation)选项,映射到skipStateMachine/seedReplaycontext,默认关(保持新建走 FSM),迁移历史数据时由用户显式开。 - 配套:导入向导(objectui)加一个复选框「作为历史数据导入(跳过生命周期校验)」;REST 导入 API 加对应参数;
import_job记录该标志(便于审计 + undo 语义一致)。
倾向:加显式开关(默认关),复用 #3433 的引擎豁免管道。
复现
对一个声明了 state_machine + initialStates: ['prospecting'](events 含 insert)的对象,通过导入 API 批量导入若干 stage: 'closed_won' 的记录 → 每行 400 invalid_initial_state,import 结果 per-row errored,历史行全部丢失。
参考
- seed replay vs state_machine initialStates: mid-lifecycle fixture rows are silently rejected on INSERT (showcase kanban can never repopulate) #3433(种子写路径豁免 state_machine;引擎
skipStateMachine管道)、enhancement:state_machine校验规则在 INSERT 上是 no-op —— 无法强制记录的初始状态(declared ≠ enforced) #3165 / feat(validation): state_machine.initialStates enforces the FSM entry point on INSERT (#3165) #3188(initialStates 强制)、packages/rest/src/import-runner.ts(writeCtx 构造点 + runAutomations 开关先例)、packages/metadata-protocol/src/protocol.ts(createManyData/insertManyData)。
🤖 Generated with Claude Code
Metadata
Metadata
Assignees
Labels
No labels
Activity
feat(rest): treatAsHistorical import option — skip the state machine …
feat(rest): preserve the original audit timeline for a historical imp…
test+docs: harden and document the historical-import preserveAudit se…
fix(objectql): the audit anchor is engine-owned — a declared created_…
fix(data): 审计锚点归引擎所有,lookup 必须能解析 (objectstack-ai#4447, objectstack-a…