现象
用户通过数据导入(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_machine initialStates 校验。
影响面
候选修法(需拍板)
不能照抄种子的一刀切豁免。 种子是平台/包策展的既成事实,无条件豁免合理;用户导入的意图是二义 的:
历史数据迁移 → 导入的是既成事实,应豁免 state_machine(和种子同理);
批量新建 → 真的在创建新记录,应走 initialStates(否则用户能批量绕过 FSM 入口)。
所以应该是一个用户可选开关 ,而非默认豁免。好消息:
倾向:加显式开关(默认关),复用 #3433 的引擎豁免管道。
复现
对一个声明了 state_machine + initialStates: ['prospecting'](events 含 insert)的对象,通过导入 API 批量导入若干 stage: 'closed_won' 的记录 → 每行 400 invalid_initial_state,import 结果 per-row errored,历史行全部丢失。
参考
🤖 Generated with Claude Code
现象
用户通过数据导入(CSV / 批量 API)导入历史 mid-lifecycle 记录时,凡是记录的状态字段值 ∉ 对象
state_machine.initialStates,该行被拒。典型场景:迁移一批已closed的历史工单、已closed_won的历史商机、已completed的历史项目——这些正是数据迁移的核心内容,却被 FSM 入口校验挡下。与 #3433(种子重放被 initialStates 拒)完全同构,只是换了入口:#3433 修了种子写路径,但数据导入这条平行路径没修。区别是导入不静默——import-runner 有 per-row 结果,会报
invalid_initial_state,所以用户看得到"导入 100 行,拒了 80 行",但合法的历史迁移场景被功能性挡住。机制
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)。state_machine:SeedLoaderService.SEED_OPTIONS带ExecutionContext.seedReplay→ 引擎传skipStateMachine→ rule-validator 跳过。packages/rest/src/import-runner.ts:265skipAutomations,没有seedReplay/skipStateMachine。导入写入经createManyData/insertManyData(packages/metadata-protocol/src/protocol.ts:3377/3405)→ 引擎 → 照跑state_machineinitialStates 校验。影响面
initialStates的对象 + 用户导入其历史态数据 = 丢行。历史数据迁移(平台的常见 onboarding 场景)被 FSM 挡住。state_machine校验规则在 INSERT 上是 no-op —— 无法强制记录的初始状态(declared ≠ enforced) #3165 是新能力,鼓励用),所以这个缺口会随采用面扩大。候选修法(需拍板)
不能照抄种子的一刀切豁免。 种子是平台/包策展的既成事实,无条件豁免合理;用户导入的意图是二义的:
state_machine(和种子同理);initialStates(否则用户能批量绕过 FSM 入口)。所以应该是一个用户可选开关,而非默认豁免。好消息:
seedReplay → skipStateMachine),import 只需在开关打开时往writeCtx塞对应 context 旗标即可,几乎零引擎改动。runAutomations/run_automations(import-runner.ts:104、rest-server.ts:3833)映射到skipAutomations。平行地加一个treatAsHistorical(或skipLifecycleValidation)选项,映射到skipStateMachine/seedReplaycontext,默认关(保持新建走 FSM),迁移历史数据时由用户显式开。import_job记录该标志(便于审计 + undo 语义一致)。倾向:加显式开关(默认关),复用 #3433 的引擎豁免管道。
复现
对一个声明了
state_machine+initialStates: ['prospecting'](events 含 insert)的对象,通过导入 API 批量导入若干stage: 'closed_won'的记录 → 每行400 invalid_initial_state,import 结果 per-row errored,历史行全部丢失。参考
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