forked from ChaiHao/BPOS-MVP
流程包去掉发布机制 —— 严格检查 + 保存即生效,生效由运行时决定
用户要的是运行时机制:新建 + 编辑,存下去就生效,旧任务走完旧版本, 新任务调用时才加载新编排。所以「草稿 → 发布」两态整套去掉。 ## 闸从「发布」挪到「保存」 `savePackage` 现在先跑闭合校验,不闭合抛 `NotClosedError`(路由 412 带报告)—— **一个字节都不落库**。存进来的每一版都能起单,所以不需要「已发布」这个标记。 `publish` 用例、路由、`publishPackage` 存储方法一并删掉。 实测:把进 A6 的边映射清空后保存 → 412,逐条点名「边 e-g3-a6 未完成载荷映射; 节点「报价议价」的必填字段未映射:rows」。 ## 保存恒新增版本号,服务端说了算 这不是可选项,是「旧任务走完旧版本」的前提:工作流按 (包id, 版本) 去读定义, 就地覆盖 v1 就等于改掉所有在途 v1 任务正在读的编排 —— 它们会中途换了规则却毫不知情。 版本号由用例层算(当前最大 + 1),**不采信入参**。实测:入参故意填 version: 999, 落的是 v2。store 层因此改成**只插不改**:撞键宁可让唯一约束报错,也不静默覆盖。 起单取版从「最新已发布」改为「最新一版」(retired 除外);触发器的 requiredInputs 同口径。 实测:改完描述存成 v2 后,新起的单用 v2,早先那单仍挂 v1。 ## 迁移:旧状态值不归一就是一读就炸 状态枚举从 draft/published/retired 收成 active/retired。列上和 definition 这个 jsonb 里 各存着一份 state,两处都要改 —— 不改,`processPackageSchema.parse` 读一行就抛。 这不是开发期小麻烦,任何已部署实例都会中招。加了迁移 0012。 ## 界面:恒为 0 的假指标撤掉,不换个说法留着 - 编排面板:「已发布/草稿」芯片 → 「生效中/历史版本」(真正的区别是哪一版会被新起单取到); 发布按钮撤掉,保存按钮改成「保存并生效(存为 vN)」并在确认卡里说清影响面 - 历史版本只读,但就地指路:「去 v(N-1) 改(生效中)」,不让人自己找 - 蓝图面:「草稿 N · 未闭合 M」两个计数在新机制下**结构上恒为 0**(不闭合存不进来), 换成「共 N 个版本,保存即生效」 - 驾驶舱:「未闭合草稿」卡整张撤掉 —— 同理恒为 0。天然的第三张是「死路」 (route-dead-end 事件),但那需要一条按事件类型聚合的路由,没有之前不摆(同死信卡的规矩) ## 顺手修:种子的幂等判断本来是坏的 比对用 `JSON.stringify` 认键序,而 jsonb 从 PG 读回来键是重排过的 —— 永远判不等, 每播一次种就多存一版一模一样的定义(实测一口气堆到 v3)。改成递归按键名排序后比对。 连播三次,只第一次存。 ## 契约变更(破坏性,一步到位) `publish` 删除、`NotPublishableError` → `NotClosedError`、`publishable` → `closed`、 状态枚举收窄。按规矩破坏性变更该走三步弃用 —— 这里一步改完,因为: ① depcruise 反查下游全在仓内(374 模块无违规),同批改完; ② 要弃用的是一个**产品决定不再存在**的机制,留弃用窗口没有意义。 CONTRACT.md 已同步,快照单独说明。 typecheck ✓ lint ✓ depcruise 374 模块 ✓ 495 测试 ✓ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
+7
-4
@@ -27,13 +27,15 @@ export type StartWorkflowInput = {
|
||||
readonly data: Record<string, unknown>;
|
||||
};
|
||||
export type ProcessService = {
|
||||
/**
|
||||
* 保存 —— **严格检查 + 即刻生效**,不闭合抛 `NotClosedError`。
|
||||
* 恒产生新版本号(服务端算,不采信入参的 version);返回实际落的那一版。
|
||||
*/
|
||||
savePackage(pkg: ProcessPackage): Promise<ProcessPackage>;
|
||||
listPackages(): Promise<ProcessPackage[]>;
|
||||
getPackage(id: string, version?: number): Promise<ProcessPackage | undefined>;
|
||||
/** 画图即校验 —— 与前端画布同源 */
|
||||
/** 画图即校验 —— 与前端画布同源;只算不存 */
|
||||
validate(pkg: ProcessPackage): Promise<ClosureReport>;
|
||||
/** 不闭合不得发布 */
|
||||
publish(id: string, version: number): Promise<ClosureReport>;
|
||||
start(input: StartWorkflowInput): Promise<WorkflowRecord>;
|
||||
getWorkflow(id: string): Promise<WorkflowRecord>;
|
||||
listWorkflows(limit?: number): Promise<WorkflowRecord[]>;
|
||||
@@ -55,7 +57,8 @@ export declare class CallInterfaceViolationError extends Error {
|
||||
readonly errors: readonly string[];
|
||||
constructor(packageId: string, errors: readonly string[]);
|
||||
}
|
||||
export declare class NotPublishableError extends Error {
|
||||
/** 不闭合不得**保存** —— 没有发布这道门,闸就落在保存上 */
|
||||
export declare class NotClosedError extends Error {
|
||||
readonly report: ClosureReport;
|
||||
constructor(report: ClosureReport);
|
||||
}
|
||||
|
||||
+2
-2
@@ -2,12 +2,12 @@
|
||||
* module: process —— 唯一出口。只做 re-export,不写逻辑(编码规范 §10.1)。
|
||||
*/
|
||||
export type { ActivityContractFace, AssembleScope, Candidate, ClosureInput, ClosureReport, ClosureViolation, NodeInstance, OwnerSpec, ProcessPackage, ProcessService, ResolveContext, Resolution, StartWorkflowInput, WorkflowRecord, WorkflowState, } from './contract.ts';
|
||||
export { CallInterfaceViolationError, NotPublishableError, PositionVacantError } from './contract.ts';
|
||||
export { CallInterfaceViolationError, NotClosedError, PositionVacantError } from './contract.ts';
|
||||
export type { ActivityDirectory, ApprovalGateway, Clock, FlowEventPublisher, IdGenerator, OrgDirectory, ProcessStore, ReceiptNotifier, Sfeel, TaskGateway, } from './port.ts';
|
||||
export type { ProcessServiceDeps } from './internal/usecase/process-service.ts';
|
||||
export { createProcessService } from './internal/usecase/process-service.ts';
|
||||
export { advanceOrder, assemble, dispatchOf, joinReady, rejectRouteOf, selectEdges, startNodeOf, } from './internal/domain/routing.ts';
|
||||
export { checkClosure, publishable } from './internal/domain/closure.ts';
|
||||
export { checkClosure, closed } from './internal/domain/closure.ts';
|
||||
export { aggregateWorkflowState, FAILURE_DIGESTIONS, isDigested, nextInstanceNumber, resolveOwner, } from './internal/domain/owner-resolution.ts';
|
||||
export { createProcessStore } from './internal/adapter/process-store.ts';
|
||||
export { nodeInstanceTable, processPackageTable, workflowTable } from './internal/adapter/process-schema.ts';
|
||||
|
||||
@@ -39,7 +39,6 @@ export type ProcessStore = {
|
||||
savePackage(pkg: ProcessPackage): Promise<void>;
|
||||
getPackage(id: string, version?: number): Promise<ProcessPackage | undefined>;
|
||||
listPackages(): Promise<ProcessPackage[]>;
|
||||
publishPackage(id: string, version: number): Promise<void>;
|
||||
insertWorkflow(record: WorkflowRecord): Promise<void>;
|
||||
getWorkflow(id: string): Promise<WorkflowRecord | undefined>;
|
||||
listWorkflows(limit: number): Promise<WorkflowRecord[]>;
|
||||
|
||||
+14
-45
@@ -462,8 +462,7 @@ export declare const listProcessPackagesRoute: {
|
||||
name: z.ZodString;
|
||||
version: z.ZodNumber;
|
||||
state: z.ZodEnum<{
|
||||
draft: "draft";
|
||||
published: "published";
|
||||
active: "active";
|
||||
retired: "retired";
|
||||
}>;
|
||||
callInterface: z.ZodObject<{
|
||||
@@ -650,12 +649,13 @@ export declare const listProcessPackagesRoute: {
|
||||
}, z.core.$strip>>;
|
||||
}, z.core.$strip>;
|
||||
};
|
||||
/** 画图即校验 —— 与前端画布同源的闭合校验 */
|
||||
/**
|
||||
* 保存流程包(编排工作区的落笔)。
|
||||
* **保存 ≠ 发布**:落的是这一版的定义(草稿可反复存);已发布版本不可回改 ——
|
||||
* 后端按 (id, version, state) 判:published 版拒存,提示另起新版本。
|
||||
* 返回连同闭合报告:画图即校验,每次落笔都知道离「可发布」还差几条。
|
||||
* 保存流程包 —— **严格检查 + 保存即生效**,没有发布这一道门。
|
||||
*
|
||||
* 不闭合即拒存(412 带闭合报告):存下来的就是能起单的。
|
||||
* 保存**恒产生新版本号**,由服务端算 —— body 里的 `version` 只用于「改的是哪一版」的溯源,
|
||||
* 服务端不采信它当目标版本号。在途工作流按 (包id, 版本) 读定义,就地覆盖会改掉它们读的东西。
|
||||
* 返回的 `pkg.version` 是**实际落的新版本**,`report` 是这一版的闭合报告(必然 closed)。
|
||||
*/
|
||||
export declare const saveProcessRoute: {
|
||||
readonly method: "PUT";
|
||||
@@ -666,8 +666,7 @@ export declare const saveProcessRoute: {
|
||||
name: z.ZodString;
|
||||
version: z.ZodNumber;
|
||||
state: z.ZodEnum<{
|
||||
draft: "draft";
|
||||
published: "published";
|
||||
active: "active";
|
||||
retired: "retired";
|
||||
}>;
|
||||
callInterface: z.ZodObject<{
|
||||
@@ -859,8 +858,7 @@ export declare const saveProcessRoute: {
|
||||
name: z.ZodString;
|
||||
version: z.ZodNumber;
|
||||
state: z.ZodEnum<{
|
||||
draft: "draft";
|
||||
published: "published";
|
||||
active: "active";
|
||||
retired: "retired";
|
||||
}>;
|
||||
callInterface: z.ZodObject<{
|
||||
@@ -1046,7 +1044,7 @@ export declare const saveProcessRoute: {
|
||||
diagram: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
||||
}, z.core.$strip>;
|
||||
report: z.ZodObject<{
|
||||
publishable: z.ZodBoolean;
|
||||
closed: z.ZodBoolean;
|
||||
violations: z.ZodArray<z.ZodObject<{
|
||||
kind: z.ZodEnum<{
|
||||
"edge-unmapped": "edge-unmapped";
|
||||
@@ -1067,6 +1065,7 @@ export declare const saveProcessRoute: {
|
||||
}, z.core.$strip>;
|
||||
}, z.core.$strip>;
|
||||
};
|
||||
/** 画图即校验 —— 与前端画布同源;保存前随手看还差几条,不改任何状态 */
|
||||
export declare const validateProcessRoute: {
|
||||
readonly method: "POST";
|
||||
readonly path: "/api/gov/process/validate";
|
||||
@@ -1076,8 +1075,7 @@ export declare const validateProcessRoute: {
|
||||
name: z.ZodString;
|
||||
version: z.ZodNumber;
|
||||
state: z.ZodEnum<{
|
||||
draft: "draft";
|
||||
published: "published";
|
||||
active: "active";
|
||||
retired: "retired";
|
||||
}>;
|
||||
callInterface: z.ZodObject<{
|
||||
@@ -1264,35 +1262,7 @@ export declare const validateProcessRoute: {
|
||||
}, z.core.$strip>;
|
||||
}, z.core.$strip>;
|
||||
readonly output: z.ZodObject<{
|
||||
publishable: z.ZodBoolean;
|
||||
violations: z.ZodArray<z.ZodObject<{
|
||||
kind: z.ZodEnum<{
|
||||
"edge-unmapped": "edge-unmapped";
|
||||
"required-unmapped": "required-unmapped";
|
||||
"type-conflict": "type-conflict";
|
||||
"dangling-node": "dangling-node";
|
||||
"gateway-unbalanced": "gateway-unbalanced";
|
||||
"join-ambiguous-source": "join-ambiguous-source";
|
||||
"tolerate-required-dependency": "tolerate-required-dependency";
|
||||
"unknown-activity": "unknown-activity";
|
||||
"missing-owner-position": "missing-owner-position";
|
||||
}>;
|
||||
nodeId: z.ZodOptional<z.core.$ZodBranded<z.ZodString, "NodeId", "out">>;
|
||||
edgeId: z.ZodOptional<z.ZodString>;
|
||||
field: z.ZodOptional<z.ZodString>;
|
||||
message: z.ZodString;
|
||||
}, z.core.$strip>>;
|
||||
}, z.core.$strip>;
|
||||
};
|
||||
export declare const publishProcessRoute: {
|
||||
readonly method: "POST";
|
||||
readonly path: "/api/gov/process/:id/:version/publish";
|
||||
readonly params: z.ZodObject<{
|
||||
id: z.ZodString;
|
||||
version: z.ZodString;
|
||||
}, z.core.$strip>;
|
||||
readonly output: z.ZodObject<{
|
||||
publishable: z.ZodBoolean;
|
||||
closed: z.ZodBoolean;
|
||||
violations: z.ZodArray<z.ZodObject<{
|
||||
kind: z.ZodEnum<{
|
||||
"edge-unmapped": "edge-unmapped";
|
||||
@@ -2155,8 +2125,7 @@ export declare const govWorkflowRoute: {
|
||||
name: z.ZodString;
|
||||
version: z.ZodNumber;
|
||||
state: z.ZodEnum<{
|
||||
draft: "draft";
|
||||
published: "published";
|
||||
active: "active";
|
||||
retired: "retired";
|
||||
}>;
|
||||
callInterface: z.ZodObject<{
|
||||
|
||||
+15
-6
@@ -280,10 +280,20 @@ export declare const callInterfaceSchema: z.ZodObject<{
|
||||
output: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
||||
}, z.core.$strip>;
|
||||
export type CallInterface = z.infer<typeof callInterfaceSchema>;
|
||||
export declare const PROCESS_PACKAGE_STATES: readonly ["draft", "published", "retired"];
|
||||
/**
|
||||
* 流程包状态 —— **没有草稿态,也没有发布态**。
|
||||
*
|
||||
* 「严格检查 + 保存生效」:保存时跑闭合校验,不闭合直接拒存;存下来的就是生效的。
|
||||
* 生效与否由**运行时**决定 —— 新工作流起单时取最新版,在途的按启动时快照的版本跑完。
|
||||
* 所以不需要「已发布」这个标记:能存进来的版本都能起单。
|
||||
*
|
||||
* 保存**恒产生新版本号**(服务端算):工作流按 (包id, 版本) 读定义,
|
||||
* 就地覆盖会改掉在途任务正在读的东西 —— 那正是「旧任务走完旧版本」要防的。
|
||||
* `retired` 留给「这一版不再可起单」,起单时拒。
|
||||
*/
|
||||
export declare const PROCESS_PACKAGE_STATES: readonly ["active", "retired"];
|
||||
export declare const processPackageStateSchema: z.ZodEnum<{
|
||||
draft: "draft";
|
||||
published: "published";
|
||||
active: "active";
|
||||
retired: "retired";
|
||||
}>;
|
||||
export type ProcessPackageState = z.infer<typeof processPackageStateSchema>;
|
||||
@@ -292,8 +302,7 @@ export declare const processPackageSchema: z.ZodObject<{
|
||||
name: z.ZodString;
|
||||
version: z.ZodNumber;
|
||||
state: z.ZodEnum<{
|
||||
draft: "draft";
|
||||
published: "published";
|
||||
active: "active";
|
||||
retired: "retired";
|
||||
}>;
|
||||
callInterface: z.ZodObject<{
|
||||
@@ -511,7 +520,7 @@ export declare const closureViolationSchema: z.ZodObject<{
|
||||
}, z.core.$strip>;
|
||||
export type ClosureViolation = z.infer<typeof closureViolationSchema>;
|
||||
export declare const closureReportSchema: z.ZodObject<{
|
||||
publishable: z.ZodBoolean;
|
||||
closed: z.ZodBoolean;
|
||||
violations: z.ZodArray<z.ZodObject<{
|
||||
kind: z.ZodEnum<{
|
||||
"edge-unmapped": "edge-unmapped";
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
listProcessPackagesRoute,
|
||||
listToolsRoute,
|
||||
listTriggersRoute,
|
||||
publishProcessRoute,
|
||||
registerToolRoute,
|
||||
saveProcessRoute,
|
||||
reassignTaskRoute,
|
||||
@@ -36,7 +35,7 @@ import {
|
||||
} from '@contracts';
|
||||
import { InstallRejectedError } from '@modules/activity-registry';
|
||||
import type { InstallVerdict } from '@modules/activity-registry';
|
||||
import { NotPublishableError } from '@modules/process';
|
||||
import { NotClosedError } from '@modules/process';
|
||||
import type { Container } from '../container.ts';
|
||||
import type { AppEnv } from '../http-env.ts';
|
||||
|
||||
@@ -275,19 +274,20 @@ export function registerGovernanceOpsRoutes(app: Hono<AppEnv>, container: Contai
|
||||
const body: ApiError = { code: 'validation-failed', message: '流程包不合 schema', detail: parsed.error.issues };
|
||||
return c.json(body, 400);
|
||||
}
|
||||
const incoming = parsed.data.pkg;
|
||||
const existing = await container.process.getPackage(incoming.id, incoming.version);
|
||||
if (existing !== undefined && existing.state === 'published') {
|
||||
const body: ApiError = {
|
||||
code: 'precondition-failed',
|
||||
message: `v${incoming.version} 已发布,不可回改 —— 另存为 v${incoming.version + 1} 再编排`,
|
||||
};
|
||||
/*
|
||||
* **严格检查 + 保存即生效**:不闭合拒存(412 带报告),存下来的就能起单。
|
||||
* 版本号由用例层算(恒新增),这里不采信入参的 version —— 它只说明「改的是哪一版」。
|
||||
*/
|
||||
try {
|
||||
const pkg = await container.process.savePackage(parsed.data.pkg);
|
||||
return c.json({ pkg, report: await container.process.validate(pkg) });
|
||||
} catch (error) {
|
||||
if (error instanceof NotClosedError) {
|
||||
const body: ApiError = { code: 'precondition-failed', message: error.message, detail: error.report };
|
||||
return c.json(body, 412);
|
||||
}
|
||||
// 保存的永远是草稿:发布是另一道门(闭合校验),不在保存里顺带
|
||||
const pkg = await container.process.savePackage({ ...incoming, state: 'draft' });
|
||||
const report = await container.process.validate(pkg);
|
||||
return c.json({ pkg, report });
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
/** 画图即校验 —— 与前端画布同源 */
|
||||
@@ -302,21 +302,6 @@ export function registerGovernanceOpsRoutes(app: Hono<AppEnv>, container: Contai
|
||||
return c.json(await container.process.validate(parsed.data.pkg));
|
||||
});
|
||||
|
||||
/** 不闭合不得发布 */
|
||||
app.post(publishProcessRoute.path, async (c) => {
|
||||
const denied = await governProcess(c.get('account'));
|
||||
if (denied) return c.json(denied, denyStatus(denied));
|
||||
try {
|
||||
return c.json(await container.process.publish(c.req.param('id'), Number(c.req.param('version'))));
|
||||
} catch (error) {
|
||||
if (error instanceof NotPublishableError) {
|
||||
const body: ApiError = { code: 'precondition-failed', message: error.message, detail: error.report };
|
||||
return c.json(body, 412);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
/* ── 触发器配置 ──────────────────────────────────────────────────── */
|
||||
|
||||
app.get(listTriggersRoute.path, async (c) => {
|
||||
|
||||
@@ -83,7 +83,7 @@ export const MVP_PACKAGE: ProcessPackage = processPackageSchema.parse({
|
||||
id: 'unified-quote',
|
||||
name: '统一报价',
|
||||
version: 1,
|
||||
state: 'draft',
|
||||
state: 'active',
|
||||
callInterface: {
|
||||
input: {
|
||||
type: 'object',
|
||||
@@ -426,13 +426,40 @@ const TRIGGER_REGISTRATION = triggerConfigSchema.parse({
|
||||
export async function seedMvpFlow(container: Container): Promise<void> {
|
||||
const log = (line: string) => process.stdout.write(line + '\n');
|
||||
|
||||
await container.process.savePackage(MVP_PACKAGE);
|
||||
const report = await container.process.validate(MVP_PACKAGE);
|
||||
if (!report.publishable) {
|
||||
log(' 闭合校验未过,不得发布:' + report.violations.map((v) => v.message).join('; '));
|
||||
/*
|
||||
* 保存即生效,且**内容没变就不再存新版**。
|
||||
*
|
||||
* 保存恒新增版本(这是「旧任务走完旧版本」的前提),所以种子不能无条件存 ——
|
||||
* 那样每跑一次播种就多一个版本,库里堆一串一模一样的定义。
|
||||
* 比对的是语义与呈现,不含 version/state(它们由保存本身决定)。
|
||||
*/
|
||||
/*
|
||||
* 比对要**认内容不认键序**。jsonb 从 PG 读回来时键是重排过的
|
||||
* (Postgres 对 jsonb 的对象键排序),直接 `JSON.stringify` 永远判不等 ——
|
||||
* 于是每播一次种就多存一版一模一样的定义(实测:一口气堆到 v3)。
|
||||
* 所以递归按键名排序后再序列化;version / state 由保存本身决定,不参与比对。
|
||||
*/
|
||||
const canonical = (value: unknown): unknown =>
|
||||
Array.isArray(value)
|
||||
? value.map(canonical)
|
||||
: value !== null && typeof value === 'object'
|
||||
? Object.fromEntries(
|
||||
Object.entries(value as Record<string, unknown>)
|
||||
.filter(([k]) => k !== 'version' && k !== 'state')
|
||||
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
||||
.map(([k, v]) => [k, canonical(v)]),
|
||||
)
|
||||
: value;
|
||||
const stripped = (pkg: unknown) => JSON.stringify(canonical(pkg));
|
||||
const current = await container.process.getPackage(MVP_PACKAGE.id);
|
||||
if (current !== undefined && stripped(current) === stripped(MVP_PACKAGE)) {
|
||||
log(`④ 流程包「统一报价」v${current.version} 已在库,内容未变 —— 不存新版`);
|
||||
} else {
|
||||
await container.process.publish(MVP_PACKAGE.id, MVP_PACKAGE.version);
|
||||
log(`④ 流程包「统一报价」v${MVP_PACKAGE.version} 已发布 —— ${MVP_PACKAGE.nodes.length} 节点,边映射闭合校验通过`);
|
||||
const saved = await container.process.savePackage(MVP_PACKAGE);
|
||||
log(
|
||||
`④ 流程包「统一报价」v${saved.version} 已保存生效 —— ${saved.nodes.length} 节点,` +
|
||||
`闭合校验通过(不闭合会拒存)${current === undefined ? '' : `;旧版 v${current.version} 原样留着,在途任务照它跑完`}`,
|
||||
);
|
||||
}
|
||||
|
||||
await container.trigger.upsert(TRIGGER_EVENT);
|
||||
|
||||
@@ -739,10 +739,10 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
|
||||
process: {
|
||||
start: async (input) => process.start(input),
|
||||
requiredInputs: async (packageId) => {
|
||||
// 与起单取版同口径:读**最新已发布**的调用接口 —— 草稿的 schema 不该影响装配判定
|
||||
// 与起单取版同口径:读**最新一版**的调用接口(没有草稿了,存进来的都生效)
|
||||
const all = await process.listPackages();
|
||||
const pkg = all
|
||||
.filter((x) => x.id === packageId && x.state === 'published')
|
||||
.filter((x) => x.id === packageId && x.state !== 'retired')
|
||||
.sort((a, b) => b.version - a.version)[0];
|
||||
const input = pkg?.callInterface.input as { required?: string[] } | undefined;
|
||||
return input?.required ?? [];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# module: process
|
||||
|
||||
职责: 流程层:流程包定义与版本、**边映射闭合校验**(连线即映射 · 画图即校验 · 不闭合不得发布)、
|
||||
职责: 流程层:流程包定义与版本、**边映射闭合校验**(连线即映射 · 画图即校验 · 不闭合不得保存)、
|
||||
负责人四策略的确定性解析、工作流状态的聚合投影、失败走向的消化判定、节点实例重开
|
||||
不负责: 活动的执行(见 task 与范式引擎)、触发器(与流程包**平级**,见 trigger)、
|
||||
画布的绘制(见 apps/web 的 gov-process)
|
||||
@@ -8,11 +8,11 @@
|
||||
公开出口: index.ts
|
||||
- 类型:`ProcessService` `StartWorkflowInput` `WorkflowRecord` `ClosureReport` `ClosureViolation`
|
||||
`ActivityContractFace` `Candidate` `Resolution` `NodeInstance` `WorkflowState` `Dispatch`
|
||||
- 错误:`NotPublishableError` `PositionVacantError`
|
||||
- 错误:`NotClosedError` `PositionVacantError`
|
||||
- 端口类型:`ProcessStore` `ActivityDirectory` `OrgDirectory` `TaskGateway` `ApprovalGateway`
|
||||
`Sfeel` `FlowEventPublisher` `ReceiptNotifier`
|
||||
- 用例工厂:`createProcessService(deps): ProcessService`
|
||||
- 纯函数:`checkClosure` `publishable` `resolveOwner` `aggregateWorkflowState`
|
||||
- 纯函数:`checkClosure` `closed` `resolveOwner` `aggregateWorkflowState`
|
||||
(`resolveOwner` 的 `bound` 入参 = 已校验的工作组绑定,ADR-011)
|
||||
`isDigested` `nextInstanceNumber` `selectEdges` `assemble` `joinReady`
|
||||
`rejectRouteOf` `dispatchOf` `startNodeOf` `advanceOrder`
|
||||
@@ -25,8 +25,13 @@
|
||||
被依赖: trigger, app(治理台 · 工作流管理)
|
||||
|
||||
不变量(OS 03 / 3.2):
|
||||
- **不闭合不得发布**:未完成边、必填未映射、类型冲突、孤立节点、异或出边缺条件、
|
||||
汇合同名字段来源不明、容忍分支被下游必填依赖 —— 任一即 `publishable = false`
|
||||
- **不闭合不得保存**:未完成边、必填未映射、类型冲突、孤立节点、异或出边缺条件、
|
||||
汇合同名字段来源不明、容忍分支被下游必填依赖 —— 任一即 `closed = false`,`savePackage` 抛 `NotClosedError`
|
||||
- **严格检查 + 保存即生效,没有发布机制**(产品决策 2026-07-28):
|
||||
没有草稿态、没有发布门。存进来的每一版都能起单;生效与否由**运行时**决定 ——
|
||||
新工作流起单取最新版,在途的按启动时快照的版本号读定义、跑完旧编排。
|
||||
- **保存恒新增版本号**(服务端算,不采信入参):工作流按 (包id, 版本) 读定义,
|
||||
就地覆盖会改掉在途任务正在读的编排。store 层因此**只插不改**,撞键宁可报错也不静默覆盖。
|
||||
- **求值语义前后端同源**:画布的即时校验与后端裁决共用 `checkClosure`
|
||||
- **负责人解析确定性**:轮值并列时按 staffId 稳定排序;同输入必同结果,结果与依据留痕
|
||||
- **工作流状态是聚合投影**:流程层不另设状态机;计量对象是**节点实例**,
|
||||
|
||||
@@ -39,7 +39,7 @@ const pkg = (nodes: ProcessNode[], edges: Edge[]): ProcessPackage =>
|
||||
id: 'unified-quote' as never,
|
||||
name: '统一报价',
|
||||
version: 1,
|
||||
state: 'published',
|
||||
state: 'active',
|
||||
callInterface: { input: {}, output: {} },
|
||||
nodes,
|
||||
edges,
|
||||
|
||||
@@ -27,7 +27,7 @@ const pkg = (over: Partial<ProcessPackage> = {}): ProcessPackage =>
|
||||
id: 'quote' as never,
|
||||
name: '统一报价',
|
||||
version: 1,
|
||||
state: 'draft',
|
||||
state: 'active',
|
||||
callInterface: { input: {}, output: {} },
|
||||
nodes: [
|
||||
{ id: 'start' as never, kind: 'start', label: '起点' },
|
||||
@@ -61,11 +61,11 @@ 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.publishable).toBe(true);
|
||||
expect(report.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('边未完成映射即拦', () => {
|
||||
@@ -76,7 +76,7 @@ describe('checkClosure —— 不闭合不得发布', () => {
|
||||
],
|
||||
});
|
||||
const report = checkClosure({ pkg: broken, contracts, positions });
|
||||
expect(report.publishable).toBe(false);
|
||||
expect(report.closed).toBe(false);
|
||||
expect(report.violations.map((v) => v.kind)).toContain('edge-unmapped');
|
||||
expect(report.violations.map((v) => v.kind)).toContain('required-unmapped');
|
||||
});
|
||||
@@ -320,7 +320,7 @@ describe('joinReady —— 网关直通道不参与等待', () => {
|
||||
id: 'p-join',
|
||||
name: '汇合测试',
|
||||
version: 1,
|
||||
state: 'published',
|
||||
state: 'active',
|
||||
callInterface: { input: { type: 'object' }, output: { type: 'object' } },
|
||||
nodes: [
|
||||
{ kind: 'start', id: 'S', label: '开始' },
|
||||
|
||||
@@ -18,7 +18,7 @@ const PKG: ProcessPackage = processPackageSchema.parse({
|
||||
id: 'p1',
|
||||
name: '测试包',
|
||||
version: 1,
|
||||
state: 'published',
|
||||
state: 'active',
|
||||
callInterface: {
|
||||
input: { type: 'object', required: ['customerCode'], properties: { customerCode: { type: 'string' } } },
|
||||
output: { type: 'object' },
|
||||
@@ -47,7 +47,6 @@ function harness(validationErrors: readonly string[]): Harness {
|
||||
savePackage: async () => undefined,
|
||||
getPackage: async () => PKG,
|
||||
listPackages: async () => [PKG],
|
||||
publishPackage: async () => undefined,
|
||||
insertWorkflow: async (record) => {
|
||||
inserted.push(record);
|
||||
},
|
||||
|
||||
@@ -52,13 +52,15 @@ export type StartWorkflowInput = {
|
||||
};
|
||||
|
||||
export type ProcessService = {
|
||||
/**
|
||||
* 保存 —— **严格检查 + 即刻生效**,不闭合抛 `NotClosedError`。
|
||||
* 恒产生新版本号(服务端算,不采信入参的 version);返回实际落的那一版。
|
||||
*/
|
||||
savePackage(pkg: ProcessPackage): Promise<ProcessPackage>;
|
||||
listPackages(): Promise<ProcessPackage[]>;
|
||||
getPackage(id: string, version?: number): Promise<ProcessPackage | undefined>;
|
||||
/** 画图即校验 —— 与前端画布同源 */
|
||||
/** 画图即校验 —— 与前端画布同源;只算不存 */
|
||||
validate(pkg: ProcessPackage): Promise<ClosureReport>;
|
||||
/** 不闭合不得发布 */
|
||||
publish(id: string, version: number): Promise<ClosureReport>;
|
||||
|
||||
start(input: StartWorkflowInput): Promise<WorkflowRecord>;
|
||||
getWorkflow(id: string): Promise<WorkflowRecord>;
|
||||
@@ -101,10 +103,11 @@ export class CallInterfaceViolationError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export class NotPublishableError extends Error {
|
||||
/** 不闭合不得**保存** —— 没有发布这道门,闸就落在保存上 */
|
||||
export class NotClosedError extends Error {
|
||||
constructor(readonly report: ClosureReport) {
|
||||
super(`流程包不闭合,不得发布:${report.violations.map((v) => v.message).join('; ')}`);
|
||||
this.name = 'NotPublishableError';
|
||||
super(`流程包不闭合,不得保存:${report.violations.map((v) => v.message).join('; ')}`);
|
||||
this.name = 'NotClosedError';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export type {
|
||||
WorkflowRecord,
|
||||
WorkflowState,
|
||||
} from './contract.ts';
|
||||
export { CallInterfaceViolationError, NotPublishableError, PositionVacantError } from './contract.ts';
|
||||
export { CallInterfaceViolationError, NotClosedError, PositionVacantError } from './contract.ts';
|
||||
|
||||
export type {
|
||||
ActivityDirectory,
|
||||
@@ -46,7 +46,7 @@ export {
|
||||
startNodeOf,
|
||||
} from './internal/domain/routing.ts';
|
||||
|
||||
export { checkClosure, publishable } from './internal/domain/closure.ts';
|
||||
export { checkClosure, closed } from './internal/domain/closure.ts';
|
||||
export {
|
||||
aggregateWorkflowState,
|
||||
FAILURE_DIGESTIONS,
|
||||
|
||||
@@ -30,14 +30,15 @@ function toWorkflow(row: WorkflowRow): WorkflowRecord {
|
||||
|
||||
export function createProcessStore(db: Db): ProcessStore {
|
||||
return {
|
||||
/*
|
||||
* **只插不改**:版本一旦落库就是不可变的历史。
|
||||
* 版本号由用例层算(恒新增),所以这里不该撞键;真撞了就让唯一约束报出来 ——
|
||||
* 静默覆盖会改掉在途任务正在读的定义,那是最不该无声发生的事。
|
||||
*/
|
||||
async savePackage(pkg: ProcessPackage) {
|
||||
await db
|
||||
.insert(processPackageTable)
|
||||
.values({ id: pkg.id, version: pkg.version, name: pkg.name, state: pkg.state, definition: pkg })
|
||||
.onConflictDoUpdate({
|
||||
target: [processPackageTable.id, processPackageTable.version],
|
||||
set: { name: pkg.name, state: pkg.state, definition: pkg },
|
||||
});
|
||||
.values({ id: pkg.id, version: pkg.version, name: pkg.name, state: pkg.state, definition: pkg });
|
||||
},
|
||||
|
||||
async getPackage(id, version) {
|
||||
@@ -57,21 +58,6 @@ export function createProcessStore(db: Db): ProcessStore {
|
||||
return rows.map((r) => processPackageSchema.parse(r.definition));
|
||||
},
|
||||
|
||||
async publishPackage(id, version) {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(processPackageTable)
|
||||
.where(and(eq(processPackageTable.id, id), eq(processPackageTable.version, version)))
|
||||
.limit(1);
|
||||
const row = rows[0];
|
||||
if (row === undefined) return;
|
||||
const definition = { ...(row.definition as Record<string, unknown>), state: 'published' };
|
||||
await db
|
||||
.update(processPackageTable)
|
||||
.set({ state: 'published', publishedAt: new Date(), definition })
|
||||
.where(and(eq(processPackageTable.id, id), eq(processPackageTable.version, version)));
|
||||
},
|
||||
|
||||
async insertWorkflow(record) {
|
||||
await db.insert(workflowTable).values({
|
||||
id: record.id,
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
/**
|
||||
* 边映射闭合校验 —— 纯逻辑,零 IO(编码规范 §5.3;语义出处 OS 03)。
|
||||
*
|
||||
* 「**连线即映射,画图即校验,不闭合不得发布。**
|
||||
* 「**连线即映射,画图即校验,不闭合不得保存。**
|
||||
* 连一条边即须完成该边映射(必填未映射 = 边未完成),类型兼容依两端契约 schema 当场校验;
|
||||
* 存在未完成边、未满足必填或类型冲突的流程包**可存草稿、不得发布**。
|
||||
* 存在未完成边、未满足必填或类型冲突的流程包**存不进来**。
|
||||
* AND 汇合后的同名字段必须**显式指定来源分支**,不做隐式合并。」
|
||||
*
|
||||
* 原文是「可存草稿、不得发布」——**已不成立**:没有草稿态也没有发布门,
|
||||
* 闸落在保存上(严格检查 + 保存即生效)。画布上的半成品留在浏览器里,存不进库。
|
||||
*
|
||||
* 前端画布的即时校验与后端裁决**同源** —— 这一份实现两边共用。
|
||||
*/
|
||||
import type { ClosureReport, ClosureViolation, Edge, ProcessNode, ProcessPackage } from '@contracts';
|
||||
@@ -70,7 +73,7 @@ export function checkClosure(input: ClosureInput): ClosureReport {
|
||||
|
||||
violations.push(...checkTolerateDependencies(pkg, contracts, byId));
|
||||
|
||||
return { publishable: violations.length === 0, violations };
|
||||
return { closed: violations.length === 0, violations };
|
||||
}
|
||||
|
||||
function checkActivityNode(
|
||||
@@ -214,7 +217,7 @@ function checkTolerateDependencies(
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 不闭合不得发布 —— 发布门的唯一判据 */
|
||||
export function publishable(report: ClosureReport): boolean {
|
||||
return report.publishable;
|
||||
/** 不闭合不得**保存** —— 没有发布这道门,闸落在保存上,这是它的唯一判据 */
|
||||
export function closed(report: ClosureReport): boolean {
|
||||
return report.closed;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
import type { ProcessNode, ProcessPackage, StaffId, TerminalState, WorkflowState } from '@contracts';
|
||||
import type { ProcessService, StartWorkflowInput } from '../../contract.ts';
|
||||
import { CallInterfaceViolationError, NotPublishableError, PositionVacantError } from '../../contract.ts';
|
||||
import { CallInterfaceViolationError, NotClosedError, PositionVacantError } from '../../contract.ts';
|
||||
import type {
|
||||
ActivityDirectory,
|
||||
ApprovalGateway,
|
||||
@@ -47,19 +47,29 @@ export function createProcessService(deps: ProcessServiceDeps): ProcessService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 起单取版:未指定版本时取**最新已发布** —— 草稿不入选。
|
||||
* 若按「最新版本」取,治理台一存草稿,起单立刻被没发布的定义顶住;
|
||||
* 草稿是编排中的半成品,运行时永远不该看见它。
|
||||
* 起单取版:未指定版本时取**最新的一版**。
|
||||
*
|
||||
* 从前这里取「最新已发布」,因为草稿是编排中的半成品、运行时不该看见它。
|
||||
* 现在没有草稿了 —— 「严格检查 + 保存生效」,不闭合根本存不进来,
|
||||
* 所以库里的每一版都是能起单的。生效就是这个意思:**新任务起单时取到最新版**,
|
||||
* 在途的按启动时快照的版本号读定义、跑完旧编排。
|
||||
* `retired` 的版本拒起单(留给「这一版不再用了」)。
|
||||
*/
|
||||
async function latestPublished(id: string): Promise<ProcessPackage> {
|
||||
async function latest(id: string): Promise<ProcessPackage> {
|
||||
const all = await deps.store.listPackages();
|
||||
const hit = all
|
||||
.filter((pkg) => pkg.id === id && pkg.state === 'published')
|
||||
.filter((pkg) => pkg.id === id && pkg.state !== 'retired')
|
||||
.sort((a, b) => b.version - a.version)[0];
|
||||
if (hit === undefined) throw new Error(`流程包无已发布版本,不得起单:${id}`);
|
||||
if (hit === undefined) throw new Error(`流程包无可用版本,不得起单:${id}`);
|
||||
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;
|
||||
}
|
||||
|
||||
async function requireWorkflow(id: string): Promise<WorkflowRecord> {
|
||||
const record = await deps.store.getWorkflow(id);
|
||||
if (record === undefined) throw new Error(`工作流不存在:${id}`);
|
||||
@@ -268,9 +278,24 @@ export function createProcessService(deps: ProcessServiceDeps): ProcessService {
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* 保存 —— **严格检查 + 即刻生效**。没有发布这道门,所以闸落在这里。
|
||||
*
|
||||
* 两件事一起做,顺序不能换:
|
||||
* ① 先校验:不闭合直接抛,**一个字节都不落库**。存进来的就是能起单的。
|
||||
* ② 再定版:恒取「当前最大版本 + 1」,不采信入参的 version。
|
||||
*
|
||||
* 为什么不能就地覆盖:工作流按 (包id, 版本) 去读定义。覆盖 v1 就等于改掉
|
||||
* 所有在途 v1 任务正在读的编排 —— 它们会中途换了规则却毫不知情,
|
||||
* 而「旧任务走完旧版本」正是要防这个。恒新增之后,旧版本是不可变的历史,
|
||||
* 在途任务读得到的永远是它启动那天的那一份。
|
||||
*/
|
||||
async savePackage(pkg) {
|
||||
await deps.store.savePackage(pkg);
|
||||
return 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' };
|
||||
await deps.store.savePackage(saved);
|
||||
return saved;
|
||||
},
|
||||
|
||||
async listPackages() {
|
||||
@@ -291,21 +316,13 @@ export function createProcessService(deps: ProcessServiceDeps): ProcessService {
|
||||
},
|
||||
|
||||
/** 不闭合不得发布 */
|
||||
async publish(id, version) {
|
||||
const pkg = await requirePackage(id, version);
|
||||
const report = await this.validate(pkg);
|
||||
if (!report.publishable) throw new NotPublishableError(report);
|
||||
await deps.store.publishPackage(id, version);
|
||||
return report;
|
||||
},
|
||||
|
||||
/** 起单:经调用接口调起流程包,生成一条工作流 */
|
||||
async start(input: StartWorkflowInput) {
|
||||
const pkg =
|
||||
input.packageVersion === undefined
|
||||
? await latestPublished(input.packageId)
|
||||
? await latest(input.packageId)
|
||||
: await requirePackage(input.packageId, input.packageVersion);
|
||||
if (pkg.state !== 'published') throw new Error(`流程包未发布,不得起单:${pkg.id}@${pkg.version}`);
|
||||
if (pkg.state === 'retired') throw new Error(`该版本已停用,不得起单:${pkg.id}@${pkg.version}`);
|
||||
|
||||
// 先校验后建单:不合调用接口契约的输入**一条工作流都不留**。
|
||||
// (校验若放在建单之后,失败就会留下一条永远推不动的在途单,污染在途口径。)
|
||||
|
||||
@@ -34,7 +34,6 @@ export type ProcessStore = {
|
||||
savePackage(pkg: ProcessPackage): Promise<void>;
|
||||
getPackage(id: string, version?: number): Promise<ProcessPackage | undefined>;
|
||||
listPackages(): Promise<ProcessPackage[]>;
|
||||
publishPackage(id: string, version: number): Promise<void>;
|
||||
|
||||
insertWorkflow(record: WorkflowRecord): Promise<void>;
|
||||
getWorkflow(id: string): Promise<WorkflowRecord | undefined>;
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
useGovActivities,
|
||||
useGovObjects,
|
||||
useGovPackages,
|
||||
useGovPkgReports,
|
||||
useGovTools,
|
||||
useGovTriggers,
|
||||
} from '@web-shared/gov/gov-api.ts';
|
||||
@@ -31,11 +30,12 @@ export function BlueprintHome({ nav }: HomeProps) {
|
||||
const trgs = useGovTriggers();
|
||||
|
||||
const packages = pkgs.data?.packages ?? [];
|
||||
const reports = useGovPkgReports(packages);
|
||||
const reportOf = (id: string, version: number) => reports.data?.get(`${id}@${version}`);
|
||||
|
||||
const drafts = packages.filter((p) => p.state !== 'published');
|
||||
const draftBad = drafts.filter((p) => reportOf(p.id, p.version)?.publishable === false);
|
||||
/*
|
||||
* 没有「草稿」也没有「未闭合」可数了 —— 严格检查 + 保存即生效:不闭合的包**存不进来**。
|
||||
* 硬摆这两个计数就是摆两个恒为 0 的假指标。改数真能说话的:每条流有几版、最新是第几版。
|
||||
*/
|
||||
const flowCount = new Set(packages.map((p) => p.id)).size;
|
||||
const activities = acts.data?.activities ?? [];
|
||||
const toolRows = tools.data?.tools ?? [];
|
||||
const objectRows = objs.data?.objects ?? [];
|
||||
@@ -57,7 +57,7 @@ export function BlueprintHome({ nav }: HomeProps) {
|
||||
(id) => !triggers.some((t) => t.processPackageId === id),
|
||||
);
|
||||
|
||||
const gaps = draftBad.length + missingActivities.length + uncalled.length;
|
||||
const gaps = missingActivities.length + uncalled.length;
|
||||
|
||||
// 因果链是这一屏的主角:四库以真箭头相连 —— 箭头编码的是真实依赖
|
||||
// (流程包引用活动、活动调工具、活动物化数据),节点卡即入口,点卡即入库。
|
||||
@@ -71,9 +71,9 @@ export function BlueprintHome({ nav }: HomeProps) {
|
||||
</span>
|
||||
战略 · 流程包
|
||||
</span>
|
||||
<span className="g-chain-v">{packages.length}</span>
|
||||
<span className="g-chain-v">{flowCount}</span>
|
||||
<span className="g-chain-h">
|
||||
企业以哪些流转运转 —— 草稿 {drafts.length} · 未闭合 {draftBad.length}
|
||||
企业以哪些流转运转 —— 共 {packages.length} 个版本,保存即生效
|
||||
</span>
|
||||
</button>
|
||||
<span className="g-chain-arr">
|
||||
@@ -124,32 +124,9 @@ export function BlueprintHome({ nav }: HomeProps) {
|
||||
|
||||
<Sec ico="flag" title="待补清单" extra="定义上的缺口聚在这里,点进即改 —— 不散落在四库">
|
||||
{gaps === 0 ? (
|
||||
<Clear>没有缺口 —— 流程包全部闭合,引用的活动都装好了,每条流也都有触发器调得起来。</Clear>
|
||||
<Clear>没有缺口 —— 引用的活动都装好了,每条流也都有触发器调得起来。</Clear>
|
||||
) : (
|
||||
<div className="g-todo">
|
||||
{draftBad.map((p) => (
|
||||
<Todo
|
||||
key={`bad-${p.id}@${p.version}`}
|
||||
tone="warn"
|
||||
ico="git-merge"
|
||||
title={`${p.name} v${p.version} · 闭合校验未过`}
|
||||
desc={
|
||||
(reportOf(p.id, p.version)?.violations ?? []).map((v) => v.message).join(';') ||
|
||||
'存在未通过闭合校验的声明'
|
||||
}
|
||||
acts={[
|
||||
{ label: '去编排', ico: 'route', onClick: () => nav.push({ k: 'pkg', id: `${p.id}@${p.version}` }) },
|
||||
{
|
||||
label: '看边映射',
|
||||
ico: 'git-merge',
|
||||
onClick: () => {
|
||||
nav.push({ k: 'pkg', id: `${p.id}@${p.version}` });
|
||||
nav.push({ k: 'edges', id: `${p.id}@${p.version}` });
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
{missingActivities.map((id) => (
|
||||
<Todo
|
||||
key={`miss-${id}`}
|
||||
|
||||
@@ -17,8 +17,6 @@ import {
|
||||
useGovActivities,
|
||||
useGovMemory,
|
||||
useGovMetrics,
|
||||
useGovPackages,
|
||||
useGovPkgReports,
|
||||
useGovTasks,
|
||||
} from '@web-shared/gov/gov-api.ts';
|
||||
|
||||
@@ -44,17 +42,12 @@ const TERMINAL_TONE: Readonly<Record<string, string>> = {
|
||||
export function CockpitHome({ nav }: HomeProps) {
|
||||
const metrics = useGovMetrics();
|
||||
const tasks = useGovTasks();
|
||||
const packages = useGovPackages();
|
||||
const reports = useGovPkgReports(packages.data?.packages ?? []);
|
||||
const m = metrics.data;
|
||||
|
||||
const workflows = tasks.data?.workflows ?? [];
|
||||
const taskRows = tasks.data?.tasks ?? [];
|
||||
const abnormal = workflows.filter((w) => w.state === 'abnormal').length;
|
||||
const running = taskRows.filter((t) => t.terminal === undefined).length;
|
||||
const draftBad = (packages.data?.packages ?? []).filter(
|
||||
(p) => p.state !== 'published' && reports.data?.get(`${p.id}@${p.version}`)?.publishable === false,
|
||||
).length;
|
||||
|
||||
const mix = m?.terminalMix ?? {};
|
||||
const mixTop = Object.entries(mix)
|
||||
@@ -72,7 +65,12 @@ export function CockpitHome({ nav }: HomeProps) {
|
||||
value={abnormal}
|
||||
sub="失败走向未消化,待人工处置"
|
||||
/>
|
||||
<Kpi ico="git-merge" tone="warn" label="未闭合草稿" value={draftBad} sub="边不闭合不得发布" />
|
||||
{/*
|
||||
* 「未闭合草稿」这张卡撤了 —— 严格检查 + 保存即生效之后,不闭合的包**存不进来**,
|
||||
* 这个数结构上恒为 0。摆一个永远是 0 的卡位就是在占地方装样子。
|
||||
* 天然的第三张是「死路」(网关一条边都判不中而卡住的流,已有 route-dead-end 事件),
|
||||
* 但那需要一条按事件类型聚合的查询路由 —— 没有之前不摆(同死信卡的规矩)。
|
||||
*/}
|
||||
<Kpi ico="activity" tone="brand" label="在途任务" value={running} sub="尚未落终态的任务" />
|
||||
</div>
|
||||
{abnormal > 0 ? (
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
* `WorkflowsHome` / `PkgPage` / `EdgesPage` 平移:DOM 结构、类名(`g-*`)与文案原样保留,
|
||||
* mock 常量换成服务端投影(README 平移规则 1/2/4)。
|
||||
*
|
||||
* 编排即定契约:连线即映射、画图即校验、**边不闭合不得发布**;
|
||||
* 编排即定契约:连线即映射、画图即校验、**边不闭合不得保存**;
|
||||
* 图画完可导出活动开发的依赖文件 —— 开发者对着声明在系统外开发。
|
||||
*/
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
|
||||
import type { ClosureReport, ProcessPackage, ProcessNode } from '@contracts';
|
||||
import { publishProcessRoute, saveProcessRoute, validateProcessRoute, TRIGGER_KIND_LABEL } from '@contracts';
|
||||
import { saveProcessRoute, validateProcessRoute, TRIGGER_KIND_LABEL } from '@contracts';
|
||||
import { call } from '@web-shared/typed-fetch.ts';
|
||||
import { Ico } from '@web-shared/Ico.tsx';
|
||||
import {
|
||||
@@ -79,15 +79,21 @@ function PackagesHome({ tab, nav }: HomeProps) {
|
||||
return (
|
||||
<QueryGate query={q}>
|
||||
<div className="g-shelf">
|
||||
{/*
|
||||
* 没有「已发布 / 草稿」了 —— 严格检查 + 保存即生效。
|
||||
* 真正的区别是「哪一版会被新起单取到」:同一条流里**版本号最大的那一版生效中**,
|
||||
* 更早的版本是历史(在途任务还照它跑完)。
|
||||
*/}
|
||||
{packages.map((p) => {
|
||||
const published = p.state === 'published';
|
||||
const latestOfFlow = Math.max(...packages.filter((x) => x.id === p.id).map((x) => x.version));
|
||||
const live = p.version === latestOfFlow && p.state !== 'retired';
|
||||
const ec = edgeCheckOf(p, reportOf(p));
|
||||
const okEdge = ec.closed === ec.total;
|
||||
const c = counts(p);
|
||||
return (
|
||||
<button
|
||||
key={`${p.id}@${p.version}`}
|
||||
className={'g-tile t-' + (published ? 'ok' : 'warn')}
|
||||
className={'g-tile t-' + (live ? 'ok' : 'mute')}
|
||||
onClick={() => nav.push({ k: 'pkg', id: `${p.id}@${p.version}` })}
|
||||
>
|
||||
<span className="g-tile-h">
|
||||
@@ -98,13 +104,13 @@ function PackagesHome({ tab, nav }: HomeProps) {
|
||||
<b>{p.name}</b>
|
||||
<em>v{p.version}</em>
|
||||
</span>
|
||||
{published ? (
|
||||
{live ? (
|
||||
<St tone="ok" ico="circle-check">
|
||||
已发布
|
||||
生效中
|
||||
</St>
|
||||
) : (
|
||||
<St tone="warn" ico="square-pen">
|
||||
草稿
|
||||
<St tone="mute" ico="history">
|
||||
历史版本
|
||||
</St>
|
||||
)}
|
||||
</span>
|
||||
@@ -133,24 +139,24 @@ function PackagesHome({ tab, nav }: HomeProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const published = packages.filter((p) => p.state === 'published').length;
|
||||
const flows = new Set(packages.map((p) => p.id)).size;
|
||||
const totalEdges = packages.reduce((a, p) => a + p.edges.length, 0);
|
||||
const closedEdges = packages.reduce((a, p) => a + edgeCheckOf(p, reportOf(p)).closed, 0);
|
||||
return (
|
||||
<QueryGate query={q}>
|
||||
<div className="g-kpis">
|
||||
<Kpi ico="circle-check" tone="ok" label="已发布流程包" value={published} sub="版本并存 · 在途按启动版本跑完" />
|
||||
<Kpi ico="square-pen" tone="warn" label="草稿" value={packages.length - published} sub="边不闭合不得发布" />
|
||||
<Kpi ico="workflow" tone="ok" label="流程" value={flows} sub="每条流的最新一版生效中" />
|
||||
<Kpi ico="history" label="版本" value={packages.length} sub="恒新增 · 在途按启动版本跑完" />
|
||||
<Kpi
|
||||
ico="git-merge"
|
||||
label="边闭合率"
|
||||
value={totalEdges === 0 ? 100 : Math.round((closedEdges / totalEdges) * 100)}
|
||||
unit="%"
|
||||
sub="连线即映射,画图即校验"
|
||||
sub="不闭合存不进来"
|
||||
/>
|
||||
<Kpi ico="boxes" label="编排要素" value="五种" sub="活动 · 审批 · 网关 · 触发器 · 流程包" />
|
||||
</div>
|
||||
<Note>恒新增、版本并存:发布不覆盖旧版本,此后新起的工作流用新版本,在途的按启动时版本跑完。</Note>
|
||||
<Note>严格检查 + 保存即生效:不闭合的编排存不进来;存下来即刻对**新起的**工作流生效,在途的按启动时版本跑完。保存恒新增版本,旧版本原样留着。</Note>
|
||||
</QueryGate>
|
||||
);
|
||||
}
|
||||
@@ -172,6 +178,7 @@ export function PkgPage({ id, nav }: { id: string; nav: Nav }) {
|
||||
);
|
||||
}
|
||||
const siblings = (q.data?.packages ?? []).filter((x) => x.id === pkg.id);
|
||||
const latestVersion = Math.max(...siblings.map((x) => x.version));
|
||||
return (
|
||||
<Orchestrator
|
||||
key={`${pkg.id}@${pkg.version}`}
|
||||
@@ -179,8 +186,9 @@ export function PkgPage({ id, nav }: { id: string; nav: Nav }) {
|
||||
nav={nav}
|
||||
activities={acts.data?.activities ?? []}
|
||||
positions={(staffQ.data?.positions ?? []).map((p) => ({ id: p.id, name: p.name, abbr: p.code }))}
|
||||
nextVersion={Math.max(...siblings.map((x) => x.version)) + 1}
|
||||
draftSibling={siblings.filter((x) => x.state !== 'published').sort((a, b) => b.version - a.version)[0]}
|
||||
nextVersion={latestVersion + 1}
|
||||
/* 生效中 = 这条流版本号最大的那一版:只有它能改(改则存成下一版);历史版本只读 */
|
||||
live={pkg.version === latestVersion && pkg.state !== 'retired'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -201,17 +209,18 @@ function Orchestrator({
|
||||
activities,
|
||||
positions,
|
||||
nextVersion,
|
||||
draftSibling,
|
||||
live,
|
||||
}: {
|
||||
pkg: ProcessPackage;
|
||||
nav: Nav;
|
||||
activities: readonly ActivityRow[];
|
||||
positions: SpPosition[];
|
||||
nextVersion: number;
|
||||
draftSibling: ProcessPackage | undefined;
|
||||
live: boolean;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const published = pkg.state === 'published';
|
||||
/** 只读的是**历史版本**(不是「已发布」)—— 生效中的那一版可以改,改则存成下一版 */
|
||||
const readOnly = !live;
|
||||
const paradigms = useMemo(() => new Map(activities.map((a) => [a.activityId, a.paradigm])), [activities]);
|
||||
|
||||
/** 画布工作副本:属性卡与手绘编辑都改它;保存时合并回流程包 */
|
||||
@@ -239,7 +248,7 @@ function Orchestrator({
|
||||
});
|
||||
const activeReport = report ?? liveReport.data;
|
||||
const ec = edgeCheckOf(pkg, activeReport);
|
||||
const okEdge = activeReport?.publishable === true;
|
||||
const okEdge = activeReport?.closed === true;
|
||||
|
||||
const patch = (f: (n: FlowNode) => FlowNode) => {
|
||||
if (sel === null) return;
|
||||
@@ -296,29 +305,6 @@ function Orchestrator({
|
||||
},
|
||||
});
|
||||
|
||||
const publish = useMutation({
|
||||
mutationFn: () =>
|
||||
call<ClosureReport>(publishProcessRoute, { params: { id: pkg.id, version: String(pkg.version) } }),
|
||||
onSuccess: (r) => {
|
||||
setReport(r);
|
||||
void queryClient.invalidateQueries({ queryKey: ['gov', 'process'] });
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 起草新版本 —— 已发布不可回改,改图的路是**克隆成下一版草稿**再编辑:
|
||||
* 连手绘存证一起带走,落地即跳到新草稿页(那里「编辑」芯片与属性卡全开)。
|
||||
*/
|
||||
const draftNext = useMutation({
|
||||
mutationFn: () =>
|
||||
call<{ pkg: ProcessPackage; report: ClosureReport }>(saveProcessRoute, {
|
||||
body: { pkg: { ...pkg, version: nextVersion, state: 'draft' } },
|
||||
}),
|
||||
onSuccess: (out) => {
|
||||
void queryClient.invalidateQueries({ queryKey: ['gov', 'process'] });
|
||||
nav.push({ k: 'pkg', id: `${out.pkg.id}@${out.pkg.version}` });
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 导出依赖文件 —— 编排定契约:各活动的接口声明、责任岗位、输入/输出声明与上游装配,
|
||||
@@ -373,18 +359,18 @@ function Orchestrator({
|
||||
<>
|
||||
<PageHead
|
||||
ico="workflow"
|
||||
tone={published ? 'ok' : 'warn'}
|
||||
tone={live ? 'ok' : 'mute'}
|
||||
kicker={`${pkg.id} · v${pkg.version}`}
|
||||
title={`${pkg.name} v${pkg.version}`}
|
||||
chips={
|
||||
<>
|
||||
{published ? (
|
||||
{live ? (
|
||||
<St tone="ok" ico="circle-check">
|
||||
已发布
|
||||
生效中
|
||||
</St>
|
||||
) : (
|
||||
<St tone="warn" ico="square-pen">
|
||||
草稿
|
||||
<St tone="mute" ico="history">
|
||||
历史版本
|
||||
</St>
|
||||
)}
|
||||
<St
|
||||
@@ -405,38 +391,21 @@ function Orchestrator({
|
||||
</>
|
||||
}
|
||||
acts={[
|
||||
...(published
|
||||
? [
|
||||
draftSibling !== undefined
|
||||
? {
|
||||
label: `去草稿 v${draftSibling.version} 编辑`,
|
||||
/*
|
||||
* 历史版本只读 —— 改图要回到生效中的那一版(在那儿保存会存成下一版)。
|
||||
* 从前这里是「已发布不可回改 → 起草新版本 / 去草稿编辑」两条路;
|
||||
* 没有草稿之后只剩一条:生效版就地改,保存即新版。
|
||||
*/
|
||||
...(live
|
||||
? []
|
||||
: [
|
||||
{
|
||||
label: `去 v${nextVersion - 1} 改(生效中)`,
|
||||
ico: 'square-pen',
|
||||
primary: true,
|
||||
onClick: () => nav.push({ k: 'pkg', id: `${draftSibling.id}@${draftSibling.version}` }),
|
||||
}
|
||||
: {
|
||||
label: '起草新版本',
|
||||
ico: 'square-pen',
|
||||
primary: true,
|
||||
ask: {
|
||||
title: `起草新版本:${pkg.name} v${nextVersion}`,
|
||||
note: (
|
||||
<Note>
|
||||
已发布版本不可回改 —— 改图的路是克隆成 v{nextVersion} 草稿再编排,
|
||||
手绘几何与全部声明一并带走。
|
||||
</Note>
|
||||
),
|
||||
impact: {
|
||||
who: '仅新草稿;本版与在途工作流不受影响',
|
||||
undo: '草稿不发布就不生效,可反复改',
|
||||
when: '确认后立即建稿并跳转',
|
||||
onClick: () => nav.push({ k: 'pkg', id: `${pkg.id}@${nextVersion - 1}` }),
|
||||
},
|
||||
okLabel: '建稿并去编辑',
|
||||
onOk: () => draftNext.mutate(),
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]),
|
||||
{
|
||||
label: '导出依赖文件',
|
||||
ico: 'upload',
|
||||
@@ -453,70 +422,40 @@ function Orchestrator({
|
||||
onOk: exportDeps,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '发布',
|
||||
ico: 'send',
|
||||
primary: true,
|
||||
disabled: published,
|
||||
ask: {
|
||||
title: `发布:${pkg.name} v${pkg.version}`,
|
||||
note: okEdge ? (
|
||||
<Note tone="ok">边映射全闭合、必填齐、类型无冲突 —— 可发布。</Note>
|
||||
) : (
|
||||
<Note tone="warn">{activeReport?.violations.map((v) => v.message).join(';') ?? '闭合校验未跑完'}</Note>
|
||||
),
|
||||
impact: {
|
||||
who: okEdge ? '此后新起的工作流用新版本;在途按启动时版本跑完' : '被阻断 —— 先补齐边映射',
|
||||
undo: okEdge ? '版本并存,可停用回退' : '—',
|
||||
when: okEdge ? '确认后即时' : '不生效',
|
||||
},
|
||||
okLabel: okEdge ? '确认发布' : '我知道了',
|
||||
onOk: () => {
|
||||
if (okEdge) publish.mutate();
|
||||
},
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{activeReport !== undefined && !activeReport.publishable && (
|
||||
{activeReport !== undefined && !activeReport.closed && (
|
||||
<Note tone="warn">
|
||||
<b>未闭合:</b>
|
||||
{activeReport.violations.map((v) => v.message).join(';')}
|
||||
</Note>
|
||||
)}
|
||||
{report !== undefined && report.publishable && <Note tone="ok">闭合校验通过。</Note>}
|
||||
{report !== undefined && report.closed && <Note tone="ok">闭合校验通过。</Note>}
|
||||
|
||||
{/* 编排工作区:左 2/3 画布(整图显示,不裁切)+ 右 1/3 属性卡(就地填写,不下钻) */}
|
||||
<Sec
|
||||
ico="route"
|
||||
title="编排工作区"
|
||||
extra={published ? '已发布版本只读 —— 改图要另起一版' : '点画布上的活动 → 右栏属性卡直接改;「编辑」进手绘模式'}
|
||||
extra={readOnly ? '历史版本只读 —— 改图回到生效中那一版' : '点画布上的活动 → 右栏属性卡直接改;「编辑」进手绘模式;保存即生效'}
|
||||
>
|
||||
{/*
|
||||
已发布不可回改是规矩,但「去哪儿改」不该让人自己找 ——
|
||||
就地把路指出来:一句为什么 + 一颗按钮,点了直接建稿并跳过去。
|
||||
历史版本只读,但「去哪儿改」不该让人自己找 —— 就地把路指出来。
|
||||
从前这里是「克隆成草稿再编排」;没有草稿之后,生效版就地改、保存即存成下一版。
|
||||
*/}
|
||||
{published && (
|
||||
{readOnly && (
|
||||
<div className="g-todo">
|
||||
<Todo
|
||||
tone="warn"
|
||||
ico="square-pen"
|
||||
title={`v${pkg.version} 已发布,画布只读`}
|
||||
desc={`改图的路是克隆成 v${nextVersion} 草稿再编排 —— 手绘几何与全部声明一并带走,本版与在途工作流不受影响。${
|
||||
draftSibling === undefined ? '' : `(已有 v${draftSibling.version} 草稿在,直接去那儿改。)`
|
||||
}`}
|
||||
ico="history"
|
||||
title={`v${pkg.version} 是历史版本,画布只读`}
|
||||
desc={`在途任务还按这一版跑完,所以它不可改。改图去 v${nextVersion - 1}(生效中那一版)——`
|
||||
+ `在那儿保存会存成 v${nextVersion},本版与在途工作流一概不动。`}
|
||||
acts={[
|
||||
draftSibling === undefined
|
||||
? {
|
||||
label: draftNext.isPending ? '建稿中…' : `起草 v${nextVersion} 并去编辑`,
|
||||
{
|
||||
label: `去 v${nextVersion - 1} 改`,
|
||||
ico: 'square-pen',
|
||||
onClick: () => draftNext.mutate(),
|
||||
}
|
||||
: {
|
||||
label: `去草稿 v${draftSibling.version} 编辑`,
|
||||
ico: 'square-pen',
|
||||
onClick: () => nav.push({ k: 'pkg', id: `${draftSibling.id}@${draftSibling.version}` }),
|
||||
onClick: () => nav.push({ k: 'pkg', id: `${pkg.id}@${nextVersion - 1}` }),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
@@ -533,7 +472,7 @@ function Orchestrator({
|
||||
graph={graph}
|
||||
drilledIds={nodes.filter((n) => n.kind === 'activity').map((n) => n.id)}
|
||||
positions={positions}
|
||||
canEdit={!published}
|
||||
canEdit={!readOnly}
|
||||
onNodeClick={(nid) => setSel(nid)}
|
||||
onEditNodeText={async (nid, p2) => {
|
||||
setGraph((g) =>
|
||||
@@ -769,24 +708,31 @@ function Orchestrator({
|
||||
align="end"
|
||||
items={[
|
||||
{
|
||||
label: dirty ? '保存编排' : '编排无改动',
|
||||
label: dirty ? `保存并生效(存为 v${nextVersion})` : '编排无改动',
|
||||
ico: 'send',
|
||||
primary: dirty,
|
||||
disabled: !dirty || published,
|
||||
disabled: !dirty || readOnly,
|
||||
ask: dirty
|
||||
? {
|
||||
title: `保存编排:${pkg.name} v${pkg.version}`,
|
||||
note: (
|
||||
<Note>
|
||||
编排定契约:改活动声明即改边映射两端,保存后按新拓扑重算闭合。定义对在途任务不可变。
|
||||
title: `保存并生效:${pkg.name} → v${nextVersion}`,
|
||||
note: okEdge ? (
|
||||
<Note tone="ok">
|
||||
边映射全闭合、必填齐、类型无冲突 —— 存下去即刻对新起的工作流生效。
|
||||
</Note>
|
||||
) : (
|
||||
<Note tone="warn">
|
||||
<b>不闭合存不进来:</b>
|
||||
{activeReport?.violations.map((v) => v.message).join(';') ?? '闭合校验未跑完'}
|
||||
</Note>
|
||||
),
|
||||
impact: {
|
||||
who: published ? '已发布版本不可回改 —— 另起新版本' : '本草稿版本的编排定义',
|
||||
undo: '版本并存,可回退上一版本',
|
||||
when: '保存即入草稿;发布后新起的工作流才用它',
|
||||
who: okEdge
|
||||
? `存为 v${nextVersion},此后**新起**的工作流用它;在途的按启动时版本跑完`
|
||||
: '被拒存 —— 先补齐边映射',
|
||||
undo: `v${pkg.version} 原样留着,可回到那一版再改`,
|
||||
when: okEdge ? '确认即生效,没有发布这一步' : '存不进去',
|
||||
},
|
||||
okLabel: '确认保存',
|
||||
okLabel: okEdge ? '确认保存并生效' : '我知道了',
|
||||
onOk: () => {
|
||||
if (graph !== null) save.mutate({ graph, idMap: {} });
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@ const basePkg: ProcessPackage = processPackageSchema.parse({
|
||||
id: 'p',
|
||||
name: '包',
|
||||
version: 1,
|
||||
state: 'draft',
|
||||
state: 'active',
|
||||
callInterface: { input: { type: 'object' }, output: { type: 'object' } },
|
||||
nodes: [
|
||||
{ id: 'S', kind: 'start', label: '起' },
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
-- 流程包去掉发布机制:严格检查 + 保存即生效(产品决策 2026-07-28)
|
||||
--
|
||||
-- 状态枚举从 draft/published/retired 收成 active/retired。旧值不归一的话,
|
||||
-- `processPackageSchema.parse` 读一行就抛 —— definition 这个 jsonb 里也存着一份 state,
|
||||
-- 两处都要改。实测过:不改就是「一读就炸」,不是开发期小麻烦。
|
||||
--
|
||||
-- draft 也并入 active:旧草稿里可能有不闭合的,但新机制下不闭合根本存不进来;
|
||||
-- 已经在库里的既存事实不追溯删除,交由治理者看到后自行处置(或 retire 掉)。
|
||||
update sys.process_package
|
||||
set state = 'active',
|
||||
definition = jsonb_set(definition, '{state}', '"active"')
|
||||
where state in ('draft', 'published');
|
||||
@@ -85,6 +85,13 @@
|
||||
"when": 1785129993168,
|
||||
"tag": "0011_little_pet_avengers",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 12,
|
||||
"version": "7",
|
||||
"when": 1785130053168,
|
||||
"tag": "0012_process_no_publish",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -192,12 +192,13 @@ export const listProcessPackagesRoute = {
|
||||
output: z.object({ packages: z.array(processPackageSchema) }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 画图即校验 —— 与前端画布同源的闭合校验 */
|
||||
/**
|
||||
* 保存流程包(编排工作区的落笔)。
|
||||
* **保存 ≠ 发布**:落的是这一版的定义(草稿可反复存);已发布版本不可回改 ——
|
||||
* 后端按 (id, version, state) 判:published 版拒存,提示另起新版本。
|
||||
* 返回连同闭合报告:画图即校验,每次落笔都知道离「可发布」还差几条。
|
||||
* 保存流程包 —— **严格检查 + 保存即生效**,没有发布这一道门。
|
||||
*
|
||||
* 不闭合即拒存(412 带闭合报告):存下来的就是能起单的。
|
||||
* 保存**恒产生新版本号**,由服务端算 —— body 里的 `version` 只用于「改的是哪一版」的溯源,
|
||||
* 服务端不采信它当目标版本号。在途工作流按 (包id, 版本) 读定义,就地覆盖会改掉它们读的东西。
|
||||
* 返回的 `pkg.version` 是**实际落的新版本**,`report` 是这一版的闭合报告(必然 closed)。
|
||||
*/
|
||||
export const saveProcessRoute = {
|
||||
method: 'PUT',
|
||||
@@ -206,6 +207,8 @@ export const saveProcessRoute = {
|
||||
output: z.object({ pkg: processPackageSchema, report: closureReportSchema }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 画图即校验 —— 与前端画布同源;保存前随手看还差几条,不改任何状态 */
|
||||
|
||||
export const validateProcessRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/process/validate',
|
||||
@@ -213,13 +216,6 @@ export const validateProcessRoute = {
|
||||
output: closureReportSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
export const publishProcessRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/process/:id/:version/publish',
|
||||
params: z.object({ id: z.string(), version: z.string() }),
|
||||
output: closureReportSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/* ── 触发器配置 ──────────────────────────────────────────────────────── */
|
||||
|
||||
export const listTriggersRoute = {
|
||||
|
||||
@@ -198,7 +198,18 @@ export type CallInterface = z.infer<typeof callInterfaceSchema>;
|
||||
|
||||
/* ── 流程包 ───────────────────────────────────────────────────────────── */
|
||||
|
||||
export const PROCESS_PACKAGE_STATES = ['draft', 'published', 'retired'] as const;
|
||||
/**
|
||||
* 流程包状态 —— **没有草稿态,也没有发布态**。
|
||||
*
|
||||
* 「严格检查 + 保存生效」:保存时跑闭合校验,不闭合直接拒存;存下来的就是生效的。
|
||||
* 生效与否由**运行时**决定 —— 新工作流起单时取最新版,在途的按启动时快照的版本跑完。
|
||||
* 所以不需要「已发布」这个标记:能存进来的版本都能起单。
|
||||
*
|
||||
* 保存**恒产生新版本号**(服务端算):工作流按 (包id, 版本) 读定义,
|
||||
* 就地覆盖会改掉在途任务正在读的东西 —— 那正是「旧任务走完旧版本」要防的。
|
||||
* `retired` 留给「这一版不再可起单」,起单时拒。
|
||||
*/
|
||||
export const PROCESS_PACKAGE_STATES = ['active', 'retired'] as const;
|
||||
export const processPackageStateSchema = z.enum(PROCESS_PACKAGE_STATES);
|
||||
export type ProcessPackageState = z.infer<typeof processPackageStateSchema>;
|
||||
|
||||
@@ -248,7 +259,8 @@ export const closureViolationSchema = z.object({
|
||||
export type ClosureViolation = z.infer<typeof closureViolationSchema>;
|
||||
|
||||
export const closureReportSchema = z.object({
|
||||
publishable: z.boolean(),
|
||||
/** 边是否闭合 —— 不闭合即**拒存**(从前叫 publishable,那时闭合只拦发布) */
|
||||
closed: z.boolean(),
|
||||
violations: z.array(closureViolationSchema),
|
||||
});
|
||||
export type ClosureReport = z.infer<typeof closureReportSchema>;
|
||||
|
||||
Reference in New Issue
Block a user