**字段只声明,不做任何要求**(用户定):编排层不再碰字段 ——
· 激活闸只剩两面:结构闭合(dangling-node / multi-default-edge)与活动装载
(unknown-activity);字段类的检查(同名不同型、容忍分支被必填依赖)全部退役。
· 节点属性卡的「数据接口」从「推来源」改成**纯声明**:照活动契约列出吃什么、吐什么,
不标必填、不推来源、不提补全 —— 缺了怎么补是**活动内实现**的事,不在这一层现形。
· 字段类型支持数组:统一写成 `T[]`(如 `any[]`),出口口的字段也带上类型
(原来只有字段名)。实测 A2 的 benchmark 口显示「rows any[] · needSourcing boolean」。
**详情页改三栏**:左窄轨版本(132px) | 中流程图 | 右节点属性。
· 版本条一版一行(v几 · 状态 · 点/边计数),动作只在**看着的那一版**上展开 ——
窄栏放不下每行一排钮,也不该让人误点别版。
· 删掉「激活 · 双判据」整卡:没选节点时,属性栏就地列出还差哪几条(点一条定位过去);
两面都齐时盖一枚小检验章,不再占一整张卡。
· 删掉节点拣选行 —— 直接在图上点节点。
· 属性卡用包壳绝对定位:行高由中栏的图定,属性长了在卡内滚,不把图拉长
(实测卡高 467、明细内容 1039、可滚,图框不受影响)。窄轨里属性小卡改单列竖排。
· 顺带校正:上一轮那条「图框下沿对齐左列底栏顶线」的 20px 留白,
三栏改版后左栏已无底栏,没有对齐对象了 —— 改回四边等距(18px)。
**版本可删可改名**(新):
· 删:生效版不可删(先停用,ActiveVersionError → 409)、在途钉版不可删
(那些单要按它跑完,PinnedVersionError → 409);删掉正看的那版会落到剩下的最新版,
一版不剩就回货架。
· 改名:名字是**这条流的身份**,一次改及于所有版本 —— 版本列表上只显示 v1/v2…,
若只改一版的名,列表上什么也看不见,人会以为没生效。
· 5 条单测守两道闸与改名口径;实测走查:改名落库 → 改回;生效版删 409、停用版删 200。
process CONTRACT.md 与产品设计.md 同步。五闸绿。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
172 lines
7.5 KiB
TypeScript
172 lines
7.5 KiB
TypeScript
/**
|
|
* module: process —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。
|
|
*/
|
|
import type { ProcessNode, ProcessPackage, StaffId, TerminalState, WorkflowState } from '@contracts';
|
|
import type { NodeInstance } from './internal/domain/owner-resolution.ts';
|
|
import type { ActivityContractFace } from './internal/domain/closure.ts';
|
|
export type Clock = {
|
|
now(): Date;
|
|
};
|
|
export type IdGenerator = {
|
|
next(): string;
|
|
};
|
|
/** 工作流实例的持久面 —— 崩溃后可续跑 */
|
|
export type WorkflowRecord = {
|
|
readonly id: string;
|
|
readonly packageId: string;
|
|
readonly packageVersion: number;
|
|
readonly subject: string;
|
|
readonly state: WorkflowState;
|
|
/** 来源:凡单必有来源(触发器 / 登记人 / 上游系统) */
|
|
readonly source: {
|
|
kind: 'periodic' | 'event' | 'registration';
|
|
ref: string;
|
|
by?: string;
|
|
};
|
|
readonly initiation: Record<string, unknown>;
|
|
/**
|
|
* 工作组绑定的**快照**(ADR-011):起单时按来源人装载,之后改组不影响这条单。
|
|
* 留痕要能解释「当时为什么派给他」,而不是被今天的组织调整改写(同 AM 9.1 版本信封)。
|
|
*/
|
|
readonly workGroup?: Record<string, string>;
|
|
/**
|
|
* 节点指派 `{ 节点id → 员工id }` —— **起单时定死某几个节点归谁**。
|
|
* 分派仍是运营时的事:这只是在轮值之前插一句「这个节点就他了」,
|
|
* 指的人不在岗则退回轮值(单不能没人接)。测试台用它把整条流走给指定账号。
|
|
*/
|
|
readonly assignments?: Record<string, string>;
|
|
/** 各节点输出口载荷 —— 边映射装配的来源 */
|
|
readonly ports: Record<string, Record<string, unknown>>;
|
|
readonly vars: Record<string, unknown>;
|
|
readonly startedAt: string;
|
|
readonly finishedAt?: string;
|
|
};
|
|
/**
|
|
* 流程运行时 —— **执行面的可替换切面**(M5-1):按结构调度节点、条件出边选路、
|
|
* 失败走向、审批裁决、状态聚合。自持状态机(internal/runtime/self-hosted-runtime)
|
|
* 是默认实现;Mastra 编译产物(workflow-compiler)将是第二实现 ——
|
|
* 八例端到端是两个实现的等价性护栏。
|
|
* 定义生命周期(保存/激活/校验)不在此内:那决定「编译哪一份定义」,与引擎选型无关。
|
|
*/
|
|
export type FlowRuntime = {
|
|
/** 起单后进入起点(起单前置 —— 校验/建单/留痕 —— 归 usecase,不在此内) */
|
|
begin(record: WorkflowRecord, pkg: ProcessPackage): Promise<void>;
|
|
onTaskTerminal(taskId: string, terminal: TerminalState, ports: Record<string, Record<string, unknown>>): Promise<WorkflowState | undefined>;
|
|
applyFailure(workflowId: string, nodeId: string, instance: number, node: Extract<ProcessNode, {
|
|
kind: 'activity';
|
|
}>, terminal: TerminalState): Promise<WorkflowState>;
|
|
onApprovalVerdict(workflowId: string, nodeId: string, verdict: 'pass' | 'reject', payload: Record<string, unknown>): Promise<WorkflowState>;
|
|
refreshState(workflowId: string): Promise<WorkflowState>;
|
|
terminate(workflowId: string, reason: string): Promise<WorkflowState>;
|
|
};
|
|
export type ProcessStore = {
|
|
savePackage(pkg: ProcessPackage): Promise<void>;
|
|
/** 就地写回一版的定义(编辑即其身)—— 撞不存在的版本报错,不静默插入 */
|
|
updatePackageDefinition(id: string, version: number, pkg: ProcessPackage): Promise<void>;
|
|
/** 钉在该版上的未完结工作流数 —— 在途钉版不可变的判据 */
|
|
countPinnedInFlight(id: string, version: number): Promise<number>;
|
|
/** 只改状态,不动定义 —— state 在列上与 definition 里各存一份,两处要一起改 */
|
|
setPackageState(id: string, version: number, state: 'active' | 'retired'): Promise<void>;
|
|
deletePackage(id: string, version: number): Promise<void>;
|
|
/** 改名及于这条流的所有版本 —— 名字是流的身份,不是版本的属性 */
|
|
renameFlow(id: string, name: string): Promise<void>;
|
|
getPackage(id: string, version?: number): Promise<ProcessPackage | undefined>;
|
|
listPackages(): Promise<ProcessPackage[]>;
|
|
insertWorkflow(record: WorkflowRecord): Promise<void>;
|
|
getWorkflow(id: string): Promise<WorkflowRecord | undefined>;
|
|
listWorkflows(limit: number): Promise<WorkflowRecord[]>;
|
|
updateWorkflow(id: string, patch: Partial<WorkflowRecord>): Promise<WorkflowRecord>;
|
|
listInstances(workflowId: string): Promise<NodeInstance[]>;
|
|
insertInstance(workflowId: string, instance: NodeInstance): Promise<void>;
|
|
updateInstance(workflowId: string, nodeId: string, instance: number, patch: Partial<NodeInstance> & {
|
|
taskId?: string;
|
|
}): Promise<void>;
|
|
findInstanceByTask(taskId: string): Promise<{
|
|
workflowId: string;
|
|
nodeId: string;
|
|
instance: number;
|
|
} | undefined>;
|
|
nodeOwners(packageId: string): Promise<Record<string, string>>;
|
|
setNodeOwner(packageId: string, nodeId: string, staffId: string | undefined): Promise<void>;
|
|
};
|
|
/** 活动契约面 —— app 注入 `activity-registry`(L4):编排校验与起单都要它 */
|
|
export type ActivityDirectory = {
|
|
faces(): Promise<Map<string, ActivityContractFace>>;
|
|
};
|
|
/** 岗位与在岗人员 —— app 注入 `org`(L1):负责人四策略解析的候选集 */
|
|
export type OrgDirectory = {
|
|
positions(): Promise<Set<string>>;
|
|
candidates(position: string): Promise<Array<{
|
|
staffId: string;
|
|
name: string;
|
|
openTasks: number;
|
|
}>>;
|
|
/** 某人的工作组绑定(ADR-011)—— 起单时按来源人取,取不到即不装载 */
|
|
workGroupBinding(staffId: string): Promise<Readonly<Record<string, string>> | undefined>;
|
|
/** 在岗与否 —— 绑定的人已离岗就不硬用,退回四策略 */
|
|
isActive(staffId: string): Promise<boolean>;
|
|
};
|
|
/** 起单与终态 —— app 注入 `task`(L6) */
|
|
export type TaskGateway = {
|
|
start(input: {
|
|
activityId: string;
|
|
ownerId: StaffId;
|
|
title: string;
|
|
workflowId: string;
|
|
nodeId: string;
|
|
data: Record<string, unknown>;
|
|
message?: string;
|
|
}): Promise<{
|
|
id: string;
|
|
}>;
|
|
};
|
|
/** 审批点 —— app 注入 `human-tool`(L3):审批单卡到审批人工位 */
|
|
export type ApprovalGateway = {
|
|
raise(input: {
|
|
workflowId: string;
|
|
nodeId: string;
|
|
approverId: StaffId;
|
|
subject: string;
|
|
basis: ReadonlyArray<{
|
|
k: string;
|
|
v: string;
|
|
}>;
|
|
}): Promise<{
|
|
id: string;
|
|
}>;
|
|
};
|
|
/**
|
|
* 调用接口的入参校验 —— app 注入 ajv。
|
|
* 「凡单必有来源」的另一半是「凡单必合契约」:不合契约**不得起单**,
|
|
* 且必须在**建工作流之前**判掉,否则会留下一条永远推不动的在途单。
|
|
*/
|
|
export type SchemaValidator = {
|
|
validate(schema: Record<string, unknown>, value: unknown): {
|
|
ok: boolean;
|
|
errors: string[];
|
|
};
|
|
};
|
|
/** S-FEEL —— app 注入 `@sfeel`:流转零 LLM */
|
|
export type Sfeel = {
|
|
condition(expression: string, scope: Readonly<Record<string, unknown>>): boolean;
|
|
expression(expression: string, scope: Readonly<Record<string, unknown>>): unknown;
|
|
};
|
|
/** 流转记入事件 —— app 注入 `event`(L1) */
|
|
export type FlowEventPublisher = {
|
|
publish(input: {
|
|
workflowId: string;
|
|
kind: string;
|
|
detail: Record<string, unknown>;
|
|
}): Promise<void>;
|
|
};
|
|
/** 终态回执 —— 登记人收消息 */
|
|
export type ReceiptNotifier = {
|
|
notify(input: {
|
|
to: StaffId;
|
|
workflowId: string;
|
|
subject: string;
|
|
state: WorkflowState;
|
|
}): Promise<void>;
|
|
};
|
|
export type { TerminalState };
|