/** * @utils —— 纯函数,零依赖(编码规范 §2 / §5.1:叶子包)。 * 无业务语义;有业务语义的东西不属于这里。 */ /** 穷尽性检查:switch 漏分支时编译期报错 */ export function assertNever(value: never, message = '未覆盖的分支'): never { throw new Error(`${message}: ${JSON.stringify(value)}`); } /** 类型守卫:滤掉 null / undefined */ export function isDefined(value: T | null | undefined): value is T { return value !== null && value !== undefined; } /** 按键分组 */ export function groupBy(items: readonly T[], key: (item: T) => K): Record { const out = {} as Record; for (const item of items) { const k = key(item); (out[k] ??= []).push(item); } return out; } /** 稳定排序的键值对遍历(避免 Object.keys 顺序依赖) */ export function sortedEntries(record: Readonly>): Array<[string, V]> { return Object.entries(record).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)); } export type TopoOrderResult = | { readonly ok: true; readonly order: readonly string[] } | { readonly ok: false; readonly cycle: readonly string[] }; /** * 拓扑排序(Kahn 算法)—— 给定节点集合与「这个节点依赖谁」,排出一个安全的处理序 * (被依赖的排前面)。 * * 同族算法已有一份在 `apps/api/src/modules/process/internal/domain/routing.ts` 的 * `advanceOrder`(那是流程图专用的,找不齐拓扑序时把剩下的节点原样缀在末尾 —— * 流程图的容错策略)。这里是它的**通用版**,故意搬到叶子包:两个不同层级的模块 * (`activity-registry` 装包静态审 / `loop-engine` 配方求值)都要用同一个算法判「排不排得出来」, * 而模块分层不许它们互相 import ——图算法本身不含业务语义,该住在 `@utils`。 * * **不兜底**:排不出来就是这几个节点之间有环,原样报出来,由调用方决定怎么处置 * (装包时机检报错 / 运行时交人),不在这一层替调用方决定「怎么办」。 * `dependsOn` 返回的名字如果不在 `nodes` 里,当作没这条依赖(忽略,不会因此误报成环)。 */ export function topoOrder(nodes: readonly string[], dependsOn: (node: string) => readonly string[]): TopoOrderResult { const known = new Set(nodes); const indegree = new Map(nodes.map((n) => [n, 0])); const dependents = new Map(); for (const node of nodes) { for (const dep of dependsOn(node)) { if (!known.has(dep)) continue; indegree.set(node, (indegree.get(node) ?? 0) + 1); (dependents.get(dep) ?? dependents.set(dep, []).get(dep)!).push(node); } } const ready = nodes.filter((n) => indegree.get(n) === 0); const order: string[] = []; const placed = new Set(); while (ready.length > 0) { const id = ready.shift()!; if (placed.has(id)) continue; placed.add(id); order.push(id); for (const dependent of dependents.get(id) ?? []) { const left = (indegree.get(dependent) ?? 0) - 1; indegree.set(dependent, left); if (left === 0) ready.push(dependent); } } const stranded = nodes.filter((n) => !placed.has(n)); return stranded.length > 0 ? { ok: false, cycle: stranded } : { ok: true, order }; }