/** * 模拟主工程的“实例/任务”台账。 * * 主工程语义:回边 reopen = 回到 A4 **重开一个新实例**(新 instance 号、新任务 ID, * 旧实例留痕不动)。这里用一个进程内表来记账,用来验证「哪条路线能自然地拿到新实例号」。 */ export type NodeId = 'A4' | 'A5' | 'A6'; export type InstanceRow = { /** 实例号:同一节点上单调递增,A4#1 / A4#2 ... */ instanceNo: number; nodeId: NodeId; /** 任务 ID:每个实例一个,全局唯一 */ taskId: string; /** 哪个 run 起的它(路线 B 会出现多个 runId) */ runId: string; /** 哪一轮循环起的它(路线 A 才有意义) */ iteration: number; /** 由哪条边重开(E10 / E12);首开为 null */ reopenedBy: string | null; status: 'open' | 'closed'; outcome?: string; openedAt: number; closedAt?: number; }; export class Ledger { readonly rows: InstanceRow[] = []; private seq = 0; openInstance(args: { nodeId: NodeId; runId: string; iteration: number; reopenedBy?: string | null; }): InstanceRow { const instanceNo = this.rows.filter((r) => r.nodeId === args.nodeId).length + 1; const row: InstanceRow = { instanceNo, nodeId: args.nodeId, taskId: `T-${args.nodeId}-${String(++this.seq).padStart(3, '0')}`, runId: args.runId, iteration: args.iteration, reopenedBy: args.reopenedBy ?? null, status: 'open', openedAt: Date.now(), }; this.rows.push(row); return row; } closeTask(taskId: string, outcome: string): InstanceRow { const row = this.rows.find((r) => r.taskId === taskId); if (!row) throw new Error(`ledger: unknown taskId ${taskId}`); row.status = 'closed'; row.outcome = outcome; row.closedAt = Date.now(); return row; } dump(title: string): void { console.log(`\n ── 台账 ${title} ──`); console.log( ' ' + ['节点', '实例', '任务ID', '轮次', '重开自', '状态', '结论', 'runId'].join('\t'), ); for (const r of this.rows) { console.log( ' ' + [ r.nodeId, `#${r.instanceNo}`, r.taskId, `it=${r.iteration}`, r.reopenedBy ?? '-', r.status, r.outcome ?? '-', r.runId.slice(0, 8), ].join('\t'), ); } } } /** 外部注入的“决策脚本”:模拟人在另一个调用链里点了按钮 */ export type Decision = | { node: 'A4'; price: number } | { node: 'A5'; verdict: 'pass' | 'reject' } | { node: 'A6'; outcome: 'won' | 'revise' }; export class DecisionScript { private i = 0; constructor(private readonly script: Decision[]) {} next(expectNode: NodeId): Decision { const d = this.script[this.i++]; if (!d) throw new Error(`decision script exhausted at ${expectNode}`); if (d.node !== expectNode) { throw new Error(`decision script mismatch: expect ${expectNode}, got ${d.node}`); } return d; } get consumed(): number { return this.i; } } export const log = (...args: unknown[]): void => console.log(...args); export const banner = (s: string): void => console.log(`\n${'='.repeat(78)}\n${s}\n${'='.repeat(78)}`);