diff --git a/docs/web-cad-implementation-plan.zh-CN.md b/docs/web-cad-implementation-plan.zh-CN.md index e8694ea..d942833 100644 --- a/docs/web-cad-implementation-plan.zh-CN.md +++ b/docs/web-cad-implementation-plan.zh-CN.md @@ -1476,7 +1476,7 @@ SQLite 是运行时的主存储,不要求项目包直接暴露数据库内部 |---|---|---|---|---| | P4-01 | 实现 Document/Object/Container/Feature/Shape | 领域包和序列化投影 | G1 | UUID、类型、标签和生命周期稳定 | | P4-02 | 实现 Property/Link/Expression/Unit | 元数据、校验和单位换算 | P4-01 | Data/View 编辑器由 metadata 驱动 | -| P4-03 | 实现依赖 DAG、脏标记和拓扑调度 | recompute scheduler | P4-01 | 只重算受影响闭包 | +| P4-03 | 实现依赖 DAG、脏标记和拓扑调度 | recompute scheduler、稳定 `levels` | P4-01 | 只重算受影响闭包;同层对象无依赖,可安全交给并行 Worker | | P4-04 | 实现事务、撤销/重做和保存点 | Command Bus、undo journal | P4-03 | 100 次撤销/重做可回到原 hash | | P4-05 | 实现 Body/Tip/特征顺序/可见性 | PartDesign 领域规则 | P4-01/P3-04 | 非法多实体和 Tip 操作被拒绝 | | P4-06 | 实现诊断树和失败恢复 | Diagnostic model、repair actions | P4-03 | 失败节点可定位、回退或修复 | diff --git a/src/facade/dependencyGraph.ts b/src/facade/dependencyGraph.ts index e249239..7f6af8e 100644 --- a/src/facade/dependencyGraph.ts +++ b/src/facade/dependencyGraph.ts @@ -11,6 +11,7 @@ export type DependencyEdge = { export type RecomputePlan = { affected: string[] order: string[] + levels: string[][] cycles: string[][] } @@ -85,7 +86,14 @@ export class DependencyGraph { if (!cyclic.has(nodeId)) order.push(nodeId) } for (const nodeId of affected) visitDependencies(nodeId) - return { affected: [...affected], order, cycles } + const levelByNode = new Map() + for (const nodeId of order) { + const level = Math.max(0, ...this.dependenciesOf(nodeId).filter((dependency) => affected.has(dependency)).map((dependency) => (levelByNode.get(dependency) ?? 0) + 1)) + levelByNode.set(nodeId, level) + } + const levels: string[][] = [] + for (const nodeId of order) { const level = levelByNode.get(nodeId) ?? 0; (levels[level] ??= []).push(nodeId) } + return { affected: [...affected], order, levels, cycles } } findCycles(scope: Set = this.nodes): string[][] { diff --git a/src/facade/recomputeEngine.ts b/src/facade/recomputeEngine.ts index 7f49bfe..c655f04 100644 --- a/src/facade/recomputeEngine.ts +++ b/src/facade/recomputeEngine.ts @@ -49,6 +49,7 @@ export type RecomputeExecutionResult = { status: RecomputeExecutionStatus affected: string[] order: string[] + levels: string[][] completed: string[] failed: string[] skipped: string[] @@ -116,6 +117,7 @@ export class RecomputeCoordinator { status, affected: plan.affected, order: plan.order, + levels: plan.levels, completed, failed, skipped, diff --git a/tests/facade.test.ts b/tests/facade.test.ts index 24b86fc..3ad3b6d 100644 --- a/tests/facade.test.ts +++ b/tests/facade.test.ts @@ -240,9 +240,12 @@ test('dependency graph propagates dirty state and orders dependencies', () => { const plan = graph.plan(['pad']) assert.deepEqual(plan.cycles, []) assert.deepEqual(plan.order, ['pad', 'pocket', 'fillet', 'body']) + assert.deepEqual(plan.levels, [['pad'], ['pocket'], ['fillet'], ['body']]) assert.deepEqual(plan.affected, ['pad', 'pocket', 'fillet', 'body']) graph.addEdge({ sourceId: 'pad', targetId: 'body', relation: 'expression' }) assert.deepEqual(graph.findCycles(new Set(['pad', 'pocket', 'fillet', 'body'])), [['pad', 'pocket', 'fillet', 'body']]) + const branched = new DependencyGraph([{ sourceId: 'left', targetId: 'root', relation: 'link' }, { sourceId: 'right', targetId: 'root', relation: 'link' }], ['root', 'left', 'right']) + assert.deepEqual(branched.plan(['root']).levels, [['root'], ['left', 'right']]) }) test('generation-aware recompute cancels an older run before accepting its result', async () => {