三面拆分⑰:编辑即其身 —— 保存写回本版,新版本只出自「新建」

用户问:为什么编辑一个版本、保存就自动多一个副本?那不是我要的。
旧机制「保存恒新增版本号」改为:

- savePackage:pkg.version 已存在 → **就地写回该版**(号不变、状态不变);
  `asNew: true`(新建按钮)或新流程首版才铸新版本号(服务端算,不生效)
- 底线三道,一道不少:
  ① **在途钉版不可变** —— 该版有未完结工作流(按 (包,版本) 钉定义)→ 409,
    一字不落库;「就地覆盖会改掉在途正在读的编排」由这道闸守,不再靠只插不改一刀切
  ② **生效版必须始终可运行** —— 写回生效版跑闭合闸,不闭合 412 带整份报告;
    未生效版画图不检查、随手存
  ③ **陈旧编辑不复活** —— 流程存在而版本号已被清理 → 409(实测:停在已删旧版上的
    标签页发来保存,把老内容静默复活成了新版本;StaleVersionError 拦死)
- store:新增 updatePackageDefinition(只许命中现存版本)+ countPinnedInFlight
- 前端:保存按钮「保存 v{n}」;新建走 asNew;文案全线「新建才长版本」
- 测试:save-package.test 六例(写回/钉版/热改闭合/asNew/防复活/首版);
  过程中揪出双 API 进程并存(旧进程霸端口跑旧代码)——已重启归一

实测:改名保存 → 版本列表仍唯一 v4 生效中,名字已写回;八例端到端全绿;
五闸退出码全过(api:check 提交后转绿)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
2026-07-28 20:11:41 +08:00
co-authored by Claude Fable 5
parent 2246b26438
commit a10a7b2855
16 changed files with 312 additions and 36 deletions
@@ -5,7 +5,7 @@
*/
import type { ProcessNode, ProcessPackage, StaffId, TerminalState, WorkflowState } from '@contracts';
import type { ProcessService, StartWorkflowInput } from '../../contract.ts';
import { CallInterfaceViolationError, NotClosedError, PositionVacantError } from '../../contract.ts';
import { CallInterfaceViolationError, NotClosedError, PinnedVersionError, PositionVacantError, StaleVersionError } from '../../contract.ts';
import type {
ActivityDirectory,
ApprovalGateway,
@@ -61,7 +61,7 @@ export function createProcessService(deps: ProcessServiceDeps): ProcessService {
return hit;
}
/** 下一个版本号 —— 保存恒新增,不覆盖任何在途任务正在读的定义 */
/** 下一个版本号 —— 只在显式新建 / 新流程首版时用(编辑即其身,写回不长号) */
async function nextVersion(id: string): Promise<number> {
const all = await deps.store.listPackages();
return all.filter((pkg) => pkg.id === id).reduce((max, pkg) => Math.max(max, pkg.version), 0) + 1;
@@ -298,9 +298,40 @@ export function createProcessService(deps: ProcessServiceDeps): ProcessService {
* 拿闭合校验拦保存,画到一半就存不了(实测 412 卡死编辑)。
* 存下来的是**未生效**的新版本 —— 完整性闸在激活那一步。
*/
async savePackage(pkg) {
const saved: ProcessPackage = { ...pkg, version: await nextVersion(pkg.id), state: 'retired' };
await deps.store.savePackage(saved);
async savePackage(pkg, opts) {
const existing = opts?.asNew === true ? undefined : await deps.store.getPackage(pkg.id, pkg.version);
if (existing === undefined) {
/*
* 流程已存在而这个版本号查无此版 = 陈旧编辑器(实测:停在已清理旧版上的
* 标签页发来保存,把老内容静默复活成了新版本)—— 拒掉,让人刷新后再改。
* 只有整条流都不存在(新流程首版)或显式 asNew 才走插入。
*/
if (opts?.asNew !== true) {
const newest = await deps.store.getPackage(pkg.id);
if (newest !== undefined) throw new StaleVersionError(pkg.id, pkg.version);
}
// 显式新建 / 新流程首版:插入下一个版本号,不生效 —— 新版本只出自这条路
const saved: ProcessPackage = { ...pkg, version: await nextVersion(pkg.id), state: 'retired' };
await deps.store.savePackage(saved);
return saved;
}
/*
* **编辑即其身**(产品决策 2026-07-28 第四轮):保存写回当前版本,号不变,
* 不再每存一次长一版。两道底线:
* ① 在途钉版不可变 —— 工作流按 (包, 版本) 钉定义,有未完结的就不许改字
* ② 生效版必须始终可运行 —— 写回生效版要过闭合闸(未生效版画图不检查)
*/
const pinned = await deps.store.countPinnedInFlight(pkg.id, pkg.version);
if (pinned > 0) throw new PinnedVersionError(pkg.id, pkg.version, pinned);
const saved: ProcessPackage = { ...pkg, version: existing.version, state: existing.state };
if (existing.state === 'active') {
const report = await this.validate(saved);
if (!report.closed) throw new NotClosedError(report);
}
await deps.store.updatePackageDefinition(pkg.id, pkg.version, saved);
return saved;
},