DAG: expose deterministic recompute levels

This commit is contained in:
2026-08-02 17:42:10 -04:00
parent a4b9d04ff4
commit ed5b1f3b67
4 changed files with 15 additions and 2 deletions

View File

@@ -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 | 失败节点可定位、回退或修复 |

View File

@@ -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<string, number>()
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<string> = this.nodes): string[][] {

View File

@@ -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,

View File

@@ -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 () => {