fix(#2492): split rebuild-queue DAG lines on fan-out points, not just WCC
nodeComponents() split a DAG into weakly-connected components via deps edges, but post #2476/#2450 every agent's rebuild subgraph hangs off a shared MetaLock node via AfterOk, so the whole meta-update cascade is one connected component and rendered as a single wall-of-chips line. Add a second pass (splitFanOut) that further splits a component's topo-ordered nodes on out-degree>1 points: a node with more than one direct dependent renders as its own one-node line, and each dependent becomes the root of an independent line. Purely deps-structure-driven, same as the existing WCC split - no agent-field grouping involved. A component with no fan-out (the common single-agent case) is unaffected.
This commit is contained in:
parent
419c9659a3
commit
990f021c32
1 changed files with 62 additions and 2 deletions
|
|
@ -178,6 +178,64 @@ function entryAgents(entry) {
|
||||||
return seen.join(',');
|
return seen.join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Further split one weakly-connected component's topo-ordered nodes on
|
||||||
|
// *fan-out* points — a node with more than one direct dependent — so a
|
||||||
|
// shared gate/lock node (e.g. MetaLock, which every agent's rebuild
|
||||||
|
// subgraph now hangs off via AfterOk since the meta-update cascade was
|
||||||
|
// folded into one in-DAG growth instead of separate per-agent child DAGs)
|
||||||
|
// doesn't merge N otherwise-independent per-agent chains into one
|
||||||
|
// wall-of-chips line. Pure
|
||||||
|
// `deps`-structure-driven, same as the WCC split above — no `agent` field
|
||||||
|
// involved. Rule: a node with out-degree > 1 renders as its own one-node
|
||||||
|
// line; each of its direct dependents becomes the root of an independent
|
||||||
|
// line, walked forward until the next fan-out point or a dead end. A
|
||||||
|
// component with no fan-out (the common single-agent case) comes back
|
||||||
|
// unchanged as one line.
|
||||||
|
function splitFanOut(orderedNodes) {
|
||||||
|
const ids = new Set(orderedNodes.map((n) => n.id));
|
||||||
|
const children = new Map(orderedNodes.map((n) => [n.id, []])); // dep -> direct dependents
|
||||||
|
for (const n of orderedNodes) {
|
||||||
|
for (const d of n.deps || []) {
|
||||||
|
if (children.has(d)) children.get(d).push(n.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const byId = new Map(orderedNodes.map((n) => [n.id, n]));
|
||||||
|
const indeg = new Map(orderedNodes.map((n) => [n.id, 0]));
|
||||||
|
for (const n of orderedNodes) {
|
||||||
|
for (const d of n.deps || []) {
|
||||||
|
if (ids.has(d)) indeg.set(n.id, indeg.get(n.id) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const roots = orderedNodes.filter((n) => indeg.get(n.id) === 0).map((n) => n.id);
|
||||||
|
|
||||||
|
const lines = [];
|
||||||
|
const visited = new Set();
|
||||||
|
function walk(startId, chain) {
|
||||||
|
let cur = startId;
|
||||||
|
while (cur != null && !visited.has(cur)) {
|
||||||
|
visited.add(cur);
|
||||||
|
const kids = children.get(cur) || [];
|
||||||
|
if (kids.length > 1) {
|
||||||
|
if (chain.length) lines.push(chain);
|
||||||
|
lines.push([byId.get(cur)]);
|
||||||
|
for (const k of kids) walk(k, []);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chain.push(byId.get(cur));
|
||||||
|
cur = kids.length === 1 ? kids[0] : null;
|
||||||
|
}
|
||||||
|
if (chain.length) lines.push(chain);
|
||||||
|
}
|
||||||
|
for (const r of roots) walk(r, []);
|
||||||
|
// Any leftover (shouldn't happen for a DAG reachable from its roots, but
|
||||||
|
// guard against a dep loop / disconnected leftover rather than dropping
|
||||||
|
// nodes from the display).
|
||||||
|
for (const n of orderedNodes) {
|
||||||
|
if (!visited.has(n.id)) lines.push([n]);
|
||||||
|
}
|
||||||
|
return lines.length ? lines : [orderedNodes];
|
||||||
|
}
|
||||||
|
|
||||||
// Split a DAG's nodes into its actual weakly-connected subgraphs, using the
|
// Split a DAG's nodes into its actual weakly-connected subgraphs, using the
|
||||||
// `deps` edges the backend provides — not an inferred heuristic like
|
// `deps` edges the backend provides — not an inferred heuristic like
|
||||||
// grouping by `n.agent`. A DAG with independent subgraphs (e.g. a
|
// grouping by `n.agent`. A DAG with independent subgraphs (e.g. a
|
||||||
|
|
@ -185,7 +243,9 @@ function entryAgents(entry) {
|
||||||
// component per subgraph; a single connected DAG stays one component. Each
|
// component per subgraph; a single connected DAG stays one component. Each
|
||||||
// component's nodes come back topo-sorted (Kahn's algorithm, falling back to
|
// component's nodes come back topo-sorted (Kahn's algorithm, falling back to
|
||||||
// original array order for ties) so a chain renders in actual dependency
|
// original array order for ties) so a chain renders in actual dependency
|
||||||
// order rather than raw array order.
|
// order rather than raw array order. Each component is then further split
|
||||||
|
// on fan-out points (see `splitFanOut`) so a shared gate node doesn't merge
|
||||||
|
// independent branches into one line.
|
||||||
function nodeComponents(nodes) {
|
function nodeComponents(nodes) {
|
||||||
if (!nodes.length) return [];
|
if (!nodes.length) return [];
|
||||||
const byId = new Map(nodes.map((n) => [n.id, n]));
|
const byId = new Map(nodes.map((n) => [n.id, n]));
|
||||||
|
|
@ -243,7 +303,7 @@ function nodeComponents(nodes) {
|
||||||
for (const cn of compNodes) {
|
for (const cn of compNodes) {
|
||||||
if (remaining.has(cn.id)) ordered.push(cn);
|
if (remaining.has(cn.id)) ordered.push(cn);
|
||||||
}
|
}
|
||||||
components.push(ordered);
|
for (const line of splitFanOut(ordered)) components.push(line);
|
||||||
}
|
}
|
||||||
return components;
|
return components;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue