Archived
画图不检查,激活才查完整 —— 保存 bug 的根治 + 异常看板
你存不了图的根因:保存跑闭合校验,而画图必然经过不完整的中间态
(节点没接完边、活动没安装)—— 画到一半就 412 卡死。实测同一份图:
改前 412 拒存;改后存为未生效的新版,报告随行。
## 模型改成三段
画图 = 只创建节点和边,图上真正生效可编辑的只有**名字**(双击不再出岗位一栏;
边上的字是辅助阅读的注记;岗位/判据/映射这些**闭环要素在节点属性里决定**;
分配到人是运营的事,设计侧不碰)
保存 = 不检查、不生效:恒存为新版本号,谁也伤不着 —— 因此不设确认卡,直接存;
任何版本都可编辑(在旧版上改 = 以旧版为底另起一版)
激活 = 完整性闸:不闭合 412 拦下并点名缺什么;过了即独占生效
## 异常看板(左,与版本卡同列同风)
画图不拦,看板替你盯着:逐条列这一版缺什么,**点一条定位到那个节点的属性**;
零异常时给空占位说「可以激活」。版本行的「激活」按钮按闭合灰显 ——
不闭合点不了,亮了才能上线。实测:v4 两条异常全列出、激活灰;闭合的 v2/v1 亮。
## 顺手修一个画布盲区
packageToGraph 以手绘存证为呈现真相,**语义里有、存证里没有的节点画布看不见**:
异常看板指着一个图上不存在的节点,点了也定位不到(实测:API 直存的包新增节点后
picker 里根本没有它)。补画:缺席节点按 60px 一档排在图纸下缘 —— 位置丑没关系,
看得见才能修。实测点异常 → 选中 A8 → 五张属性小卡弹出。
## 布局照你定的
row2 = 异常看板(1/3,与版本卡对齐)+ 节点属性卡(2/3,与流程图同宽);
属性卡内部小卡**等高 250px**,输入/输出超高的内容自己滚(实测五张全 250)。
种子改为保存后显式激活;NotClosedError 文案「不得保存」→「不得激活」。
typecheck ✓ lint ✓ depcruise ✓ 498 测试 ✓
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -61,8 +61,8 @@ const pkg = (over: Partial<ProcessPackage> = {}): ProcessPackage =>
|
||||
const contracts = new Map([['a4', face()]]);
|
||||
const positions = new Set(['BD', 'PM', 'FAE', 'PRM', 'OP']);
|
||||
|
||||
describe('checkClosure —— 不闭合不得保存', () => {
|
||||
it('必填全映射、岗位存在即可保存', () => {
|
||||
describe('checkClosure —— 不闭合不得激活', () => {
|
||||
it('必填全映射、岗位存在即可激活', () => {
|
||||
const report = checkClosure({ pkg: pkg(), contracts, positions });
|
||||
expect(report.violations).toEqual([]);
|
||||
expect(report.closed).toBe(true);
|
||||
|
||||
@@ -53,17 +53,16 @@ export type StartWorkflowInput = {
|
||||
|
||||
export type ProcessService = {
|
||||
/**
|
||||
* 保存 —— **严格检查 + 即刻生效**,不闭合抛 `NotClosedError`。
|
||||
* 恒产生新版本号(服务端算,不采信入参的 version);返回实际落的那一版。
|
||||
* 落下来的新版**即为生效版**:同流的旧生效版随之停用(一条流恰一个生效版本)。
|
||||
* 保存 —— **不检查、不生效**:恒存为新版本号(服务端算,不采信入参的 version),
|
||||
* 状态为未生效。画图的中间态天然不完整,保存不设闸;完整性闸在 `setPackageState` 激活时。
|
||||
*/
|
||||
savePackage(pkg: ProcessPackage): Promise<ProcessPackage>;
|
||||
listPackages(): Promise<ProcessPackage[]>;
|
||||
getPackage(id: string, version?: number): Promise<ProcessPackage | undefined>;
|
||||
/**
|
||||
* 版本启停 —— 运行时开关,**不改定义一个字**。
|
||||
* 激活某版:同流其余版本一并停用(恰一个生效);停用生效版:整条流下线,新起单被拒。
|
||||
* 在途工作流按启动时钉住的版本跑完,启停对它们零影响。
|
||||
* 激活先过闭合校验(不闭合抛 `NotClosedError`),过了即独占:同流其余版本一并停用;
|
||||
* 停用生效版:整条流下线,新起单被拒。在途按启动时钉住的版本跑完,启停零影响。
|
||||
*/
|
||||
setPackageState(id: string, version: number, state: 'active' | 'retired'): Promise<ProcessPackage[]>;
|
||||
/** 画图即校验 —— 与前端画布同源;只算不存 */
|
||||
@@ -110,10 +109,10 @@ export class CallInterfaceViolationError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/** 不闭合不得**保存** —— 没有发布这道门,闸就落在保存上 */
|
||||
/** 不闭合不得**激活** —— 画图与保存不拦(中间态天然不完整),上线这一步拦 */
|
||||
export class NotClosedError extends Error {
|
||||
constructor(readonly report: ClosureReport) {
|
||||
super(`流程包不闭合,不得保存:${report.violations.map((v) => v.message).join('; ')}`);
|
||||
super(`流程包不闭合,不得激活:${report.violations.map((v) => v.message).join('; ')}`);
|
||||
this.name = 'NotClosedError';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,18 +287,14 @@ export function createProcessService(deps: ProcessServiceDeps): ProcessService {
|
||||
* 而「旧任务走完旧版本」正是要防这个。恒新增之后,旧版本是不可变的历史,
|
||||
* 在途任务读得到的永远是它启动那天的那一份。
|
||||
*/
|
||||
/**
|
||||
* 保存**不检查**:画图必然经过不完整的中间态(节点没接完边、活动还没安装),
|
||||
* 拿闭合校验拦保存,画到一半就存不了(实测 412 卡死编辑)。
|
||||
* 存下来的是**未生效**的新版本 —— 完整性闸在激活那一步。
|
||||
*/
|
||||
async savePackage(pkg) {
|
||||
const report = await this.validate(pkg);
|
||||
if (!report.closed) throw new NotClosedError(report);
|
||||
const saved: ProcessPackage = { ...pkg, version: await nextVersion(pkg.id), state: 'active' };
|
||||
const saved: ProcessPackage = { ...pkg, version: await nextVersion(pkg.id), state: 'retired' };
|
||||
await deps.store.savePackage(saved);
|
||||
// 新版即生效 → 旧生效版随之停用:一条流恰一个生效版本(要回退就去版本列表切)
|
||||
const siblings = await deps.store.listPackages();
|
||||
for (const sib of siblings) {
|
||||
if (sib.id === saved.id && sib.version !== saved.version && sib.state === 'active') {
|
||||
await deps.store.setPackageState(sib.id, sib.version, 'retired');
|
||||
}
|
||||
}
|
||||
return saved;
|
||||
},
|
||||
|
||||
@@ -307,6 +303,9 @@ export function createProcessService(deps: ProcessServiceDeps): ProcessService {
|
||||
const target = all.find((p) => p.id === id && p.version === version);
|
||||
if (target === undefined) throw new Error(`流程包版本不存在:${id}@${version}`);
|
||||
if (state === 'active') {
|
||||
// **激活才查完整性**:不闭合不得激活 —— 画图不拦,上线拦
|
||||
const report = await this.validate(target);
|
||||
if (!report.closed) throw new NotClosedError(report);
|
||||
// 激活即独占:同流其余生效版一并停用 —— 「一次只能激活一个版本」
|
||||
for (const sib of all) {
|
||||
if (sib.id === id && sib.version !== version && sib.state === 'active') {
|
||||
|
||||
Reference in New Issue
Block a user