comfyui-mcp
Version:
Local-first, agent-native control plane for ComfyUI — MCP server + autonomous sidebar agent that drives your live graph in natural language on ANY LLM: Claude/ChatGPT/Gemini on your subscription (no API key), free local models via Ollama (fully offline),
558 lines • 32.3 kB
JavaScript
// In-place workflow flattener: resolve virtual wiring — rgthree/KJ Get/Set
// buses, Reroutes, and cg-use-everywhere (UE) broadcast links — into direct
// real links on the ORIGINAL UI graph, deleting the now-dead virtual nodes.
//
// This deliberately does NOT round-trip through the API converter
// (convertUiToApi → convertApiToUi): that regenerates the layout and throws
// away everything the author arranged. Litegraph groups are geometric (a
// titled bounding box — no node membership), so by never moving a kept node,
// positions/sizes/colors/groups/titles are preserved for free; the only
// visible change is the deleted virtual nodes leaving empty space.
//
// UE resolution strategy (see docs in the panel_flatten_workflow tool):
// 1. `extra.ue_links` — cg-use-everywhere writes its computed broadcast
// links there on every graph analysis, as objects
// { downstream, downstream_slot, upstream, upstream_slot, controller, type }
// where `upstream` is already the REAL producer (for Seed Everywhere the
// controller IS the producer — it owns the seed widget). Materializing
// these is the pack's own ground truth: zero re-implementation.
// 2. If UE sender nodes exist but ue_links is absent/stale → warn and leave
// those senders in place (never guess a broadcast match).
//
// Unlike the API converter, the resolver here stops at ANY real node
// regardless of mode: muted/bypassed toggle branches are the author's state
// and a wiring-only flatten must not collapse or drop them.
const GET_SET_TYPES = new Set([
"GetNode", "SetNode", "PRO_GetNode", "PRO_SetNode",
"SetNode_GetNode", "SetNode_SetNode",
]);
const isGetType = (t) => GET_SET_TYPES.has(t) && /get/i.test(t);
const isSetType = (t) => GET_SET_TYPES.has(t) && /set/i.test(t) && !/get/i.test(t);
const isRerouteType = (t) => t === "Reroute";
/** Wiring-only virtual node: exists purely to carry a connection. */
const isWiringVirtual = (t) => isRerouteType(t) || GET_SET_TYPES.has(t);
/**
* cg-use-everywhere sender detection — the registered classes, by name.
*
* This was briefly a PREFIX match, to avoid missing a variant like the registered
* "Seed Everywhere?" that the pack's own is_UEnode() overlooks. That traded one
* failure for a worse one: a foreign class merely NAMED "Seed Everywhere Helper"
* was classified as a sender, and a stale record naming it then satisfied the
* feed check and FABRICATED an edge. Membership cannot be inferred from a name,
* and no feed check can rescue a wrong classification.
*
* A closed set is now safe because an unrecognized class no longer disappears
* quietly: records present with no recognized sender are REPORTED by both tools.
* So a future variant degrades to a disclosed unknown, which is the failure we
* can live with, while a name-alike foreign class can no longer invent wiring.
*/
const UE_SENDER_CLASSES = new Set([
"Anything Everywhere",
"Anything Everywhere?",
"Anything Everywhere3",
"Prompts Everywhere",
"Seed Everywhere",
"Seed Everywhere?",
]);
export const isUeSender = (t) => UE_SENDER_CLASSES.has(t);
/**
* The UE senders that OWN the value they broadcast instead of relaying an input —
* the Seed Everywhere family holds its seed in a widget and IS its own producer.
* Every other sender relays whatever is wired into it.
*
* This gates the one path that accepts a record WITHOUT verifying the sender's
* incoming feed, so it stays the narrower of the two sets. A future
* self-producing variant that is not listed here simply takes
* the ordinary feed check and is refused with a warning — disclosed, not silent.
*/
export const isSelfProducingUeSender = (t) => t === "Seed Everywhere" || t === "Seed Everywhere?";
/**
* Flatten a UI workflow in place (on a deep copy): every consumer input fed
* through virtual wiring gets a fresh direct link to its real producer, UE
* broadcasts become real links, and the virtual nodes are deleted. Kept nodes
* are NEVER moved or otherwise altered (widgets, mode, size, color, title all
* untouched), so the author's formatting survives exactly.
*/
export function flattenUiWorkflow(input, opts = {}) {
const includeGetSet = opts.includeGetSet !== false;
const includeUe = opts.includeUe !== false;
const ui = JSON.parse(JSON.stringify(input));
const warnings = [];
const nodes = ui.nodes ?? [];
const nodesById = new Map(nodes.map((n) => [n.id, n]));
const linkMap = new Map();
for (const l of ui.links ?? [])
if (Array.isArray(l))
linkMap.set(l[0], l);
// bus name (Set node's first widget) -> the link id feeding that Set's input.
// Multiple Sets writing the same key: LAST one wins (matches litegraph's
// iteration order at prompt time) — warn so the author knows.
const busSource = new Map();
if (includeGetSet) {
for (const n of nodes) {
if (!isSetType(n.type))
continue;
const bus = n.widgets_values?.[0];
const inp = (n.inputs ?? []).find((i) => i.link != null);
if (bus == null || inp?.link == null)
continue;
if (busSource.has(String(bus))) {
warnings.push(`multiple Set nodes write bus "${String(bus)}" — the last one wins`);
}
busSource.set(String(bus), inp.link);
}
}
/** Walk upstream past Get/Set/Reroute to the first REAL node (any mode). */
const resolveReal = (linkId, depth = 0) => {
if (depth > 100)
return null;
const link = linkMap.get(linkId);
if (!link)
return null;
const src = nodesById.get(link[1]);
if (!src)
return { id: link[1], slot: link[2], type: link[5] };
if (includeGetSet && isGetType(src.type)) {
const bus = src.widgets_values?.[0];
const setLink = bus != null ? busSource.get(String(bus)) : undefined;
return setLink != null ? resolveReal(setLink, depth + 1) : null;
}
if (includeGetSet && (isSetType(src.type) || isRerouteType(src.type))) {
const inp = (src.inputs ?? []).find((i) => i.link != null);
return inp?.link != null ? resolveReal(inp.link, depth + 1) : null;
}
return { id: link[1], slot: link[2], type: link[5] };
};
/** Walk from a NODE + output slot past any virtual wiring to the real producer.
* A GetNode has no input to follow — its value arrives over the bus — so this
* mirrors resolveReal's Get handling instead of looking for an incoming link. */
const resolveRealFromNode = (nodeId, slot) => {
const n = nodesById.get(nodeId);
if (!n)
return { id: nodeId, slot };
if (includeGetSet && isGetType(n.type)) {
const bus = n.widgets_values?.[0];
const setLink = bus != null ? busSource.get(String(bus)) : undefined;
const r = setLink != null ? resolveReal(setLink) : null;
return r ? { id: r.id, slot: r.slot } : null;
}
if (includeGetSet && (isSetType(n.type) || isRerouteType(n.type))) {
const inp = (n.inputs ?? []).find((i) => i.link != null);
const r = inp?.link != null ? resolveReal(inp.link) : null;
return r ? { id: r.id, slot: r.slot } : null;
}
return { id: nodeId, slot };
};
// Take the MAXIMUM of the header and the ids actually present. `last_link_id`
// is only a header: a graph edited by tooling that did not maintain it can
// carry links above it, and a fresh id equal to an existing one yields two
// links with the same id — an ambiguous graph, produced silently.
let nextLinkId = Math.max(ui.last_link_id ?? 0, 0, ...linkMap.keys()) + 1;
const freshLinks = [];
let rewired = 0;
/** node id → how many of its inputs pass 1 disconnected because their virtual
* chain could not be resolved. Pass 2 needs this: it runs AFTER pass 1, so a
* sender whose second channel came through a dead chain would otherwise look
* single-fed and lose its channel caveat — while the converter, which
* validates before de-virtualizing, still sees two. Same graph, same answer. */
const clearedInputs = new Map();
const noteCleared = (nodeId) => clearedInputs.set(nodeId, (clearedInputs.get(nodeId) ?? 0) + 1);
const addDirectLink = (srcId, srcSlot, dstId, dstSlot, type) => {
const src = nodesById.get(srcId);
const dst = nodesById.get(dstId);
const dstInput = dst?.inputs?.[dstSlot];
const srcOutput = src?.outputs?.[srcSlot];
if (!src || !dst || !dstInput || !srcOutput)
return null;
const id = nextLinkId++;
const link = [id, srcId, srcSlot, dstId, dstSlot, type];
freshLinks.push(link);
dstInput.link = id;
srcOutput.links = [...(srcOutput.links ?? []), id];
// Index it: pass 1 REPLACES a virtual-fed input with a fresh direct link, and
// pass 2's staleness check resolves the sender's feed through this same map.
// Leaving fresh links out of it made a rewired feed look UNRESOLVABLE, which
// would refuse a perfectly live broadcast.
linkMap.set(id, link);
return id;
};
// ── Pass 1: rewire consumer inputs fed through Get/Set/Reroute chains ─────
if (includeGetSet) {
for (const node of nodes) {
if (isWiringVirtual(node.type))
continue;
for (let slot = 0; slot < (node.inputs?.length ?? 0); slot++) {
const inp = node.inputs?.[slot];
if (inp?.link == null)
continue;
const link = linkMap.get(inp.link);
if (!link)
continue;
const origin = nodesById.get(link[1]);
if (!origin || !isWiringVirtual(origin.type))
continue; // already direct
const resolved = resolveReal(inp.link);
if (!resolved) {
inp.link = null;
noteCleared(node.id);
warnings.push(`${node.type} #${node.id} input "${inp.name ?? slot}" fed by a dangling ${origin.type} chain — left unconnected`);
continue;
}
const added = addDirectLink(resolved.id, resolved.slot, node.id, slot, resolved.type ?? link[5]);
if (added == null) {
// The chain resolved, but the edge could not be built — that producer or
// output slot is not in the graph. Discarding the null left the virtual
// deleted, no replacement link, and the final scrub quietly clearing the
// consumer: a real connection gone with nothing said.
inp.link = null;
noteCleared(node.id);
const producer = nodesById.get(resolved.id);
warnings.push(`${node.type} #${node.id} input "${inp.name ?? slot}" resolves through ${origin.type} #${origin.id} to node #${resolved.id}${producer ? ` (${producer.type}) output slot ${resolved.slot}` : ""}, which this graph does not have — left unconnected`);
continue;
}
rewired++;
}
}
}
// ── Pass 2: materialize UE broadcasts from the pack's own computed list ───
const ueSenders = nodes.filter((n) => isUeSender(n.type));
const ueDeletable = new Set();
const allUeLinks = ui.extra?.ue_links ?? null;
if (includeUe && ueSenders.length === 0 && Array.isArray(allUeLinks) && allUeLinks.length > 0) {
// Records but no recognized sender. Skipping UE processing in silence is the
// worst outcome available: a real broadcast dropped with nothing said, and
// exactly what an unrecognized sender CLASS produces. A deleted sender
// (records genuinely stale) and an unknown class (records live) cannot be told
// apart here, so it is reported as the could-not-determine it is. The
// converter has the same backstop — a graph must not be disclosed by one tool
// and passed over in silence by the other.
warnings.push(`${allUeLinks.length} Use-Everywhere broadcast record(s) present but no node in this graph is recognized as a Use-Everywhere sender — either those senders were deleted (records stale) or they are a class this flattener does not know, so NOTHING was materialized from them and the inputs they name are left unconnected`);
}
if (includeUe && ueSenders.length) {
const ueLinks = allUeLinks;
// An EMPTY computed list is an ANSWER — the pack analysed the graph and
// recorded no broadcast — so there is nothing to materialize and nothing to
// report. Only an ABSENT (or non-array) list is an unknown. Folding the two
// together warned on a faithful graph, and disagreed with the converter.
// Either way the senders stay: an empty list is not a licence to delete them.
if (!Array.isArray(ueLinks)) {
warnings.push(`${ueSenders.length} Use-Everywhere sender(s) present but extra.ue_links is missing — ` +
`UE broadcasts NOT materialized (open the graph with cg-use-everywhere active and save/queue once, then retry); senders left in place`);
}
else if (ueLinks.length > 0) {
// A ue_links record is only as current as the last time the pack analysed
// the graph. Materializing one without checking it against the LIVE wiring
// invents a connection the graph does not have — which reads as a plausible
// graph that is silently wrong, worse than an obviously missing link. Same
// policy as convertUiToApi's materializer (issue #361); kept as a separate
// implementation because this one walks a different link representation and
// its failure action differs — it must never delete or disconnect anything,
// only decline to add.
const ueSenderIds = new Set(ueSenders.map((s) => s.id));
/** Senders with at least one record we could not confirm — kept in place so
* the user can re-save and recover the real broadcast. */
const unconfirmed = new Set();
/** Set when a refused record names NO sender, so we cannot tell WHICH one
* has to survive. Deleting any of them would erase the evidence of the
* broadcast we declined to materialize, so none is deleted. */
let blockSenderDeletion = false;
/** Senders whose channel could not be verified (several distinct producers,
* and a record does not name the input it came through). Disclosed once. */
const multiFeed = new Map();
/**
* THE single "is this producer real" question — the same one addDirectLink
* asks before it will build an edge. Counting a producer as confirmed here
* that the materializer would refuse is how the channel caveat came to
* claim confirmation it did not have.
*/
const producerConfirmed = (nodeId, slot) => {
const nd = nodesById.get(nodeId);
if (!nd)
return true; // outside this graph — nothing to judge here
if (!Array.isArray(nd.outputs))
return false;
return slot < nd.outputs.length;
};
/** null = confirmed against the live graph; otherwise the reason it is not. */
const unconfirmedReason = (uel, srcId, srcSlot) => {
const ctrlId = uel.controller;
if (ctrlId == null) {
// Unattributable: an unrelated sender sharing the producer proves
// nothing about THIS target. Only a self-producing sender names its own
// single channel unambiguously.
const up = nodesById.get(srcId);
return up && isSelfProducingUeSender(up.type)
? null
: "the record names no broadcast node of its own (an older cg-use-everywhere list), so it cannot be attributed to any sender here";
}
const ctrl = nodesById.get(ctrlId);
if (!ctrl || !ueSenderIds.has(ctrlId)) {
return `broadcast node #${ctrlId} is no longer a broadcast node in this graph`;
}
// The Seed Everywhere family owns its value, so it has no feed to compare.
// Restricted to that closed set: recognition is deliberately generous, and
// a merely name-alike class must not inherit an exemption from checking.
if (srcId === ctrlId && isSelfProducingUeSender(ctrl.type))
return null;
const feeds = (ctrl.inputs ?? []).filter((i) => i.link != null);
if (feeds.length === 0) {
return `${ctrl.type} #${ctrl.id} no longer has any incoming connection, so it broadcasts nothing`;
}
// Inputs pass 1 already disconnected count as unknown producers, not as
// absent ones — the record may well have belonged to one of them.
let unresolved = clearedInputs.get(ctrl.id) ?? 0;
let matched = false;
const distinct = new Set();
for (const f of feeds) {
const r = resolveReal(f.link);
// An UNRESOLVED input is not "no producer" — it is an unknown one, and
// so is one whose producer this flattener would REFUSE to wire from.
// Both count toward the channel ambiguity; letting them contribute
// nothing made a sender with one known and one unknown producer read as
// "one producer, nothing to confuse", silencing the caveat exactly
// where the risk is highest.
if (!r || !producerConfirmed(r.id, r.slot)) {
unresolved++;
continue;
}
distinct.add(`${r.id}:${r.slot}`);
if (r.id === srcId && r.slot === srcSlot)
matched = true;
}
if (matched) {
if (distinct.size + unresolved > 1) {
multiFeed.set(ctrl.id, {
type: ctrl.type,
confirmed: distinct.size,
unresolved,
});
}
return null;
}
return unresolved > 0
? `${ctrl.type} #${ctrl.id}'s own incoming connection could not be resolved, so the recorded producer node ${srcId} could not be confirmed`
: `${ctrl.type} #${ctrl.id} is now fed by a different producer than the recorded node ${srcId}`;
};
for (const uel of ueLinks) {
const dst = nodesById.get(uel.downstream);
const dstInput = dst?.inputs?.[uel.downstream_slot];
if (!dst || !dstInput) {
warnings.push(`ue_link → node #${uel.downstream} slot ${uel.downstream_slot} no longer exists — skipped`);
continue;
}
if (dstInput.link != null)
continue; // got a real link since analysis — UE wouldn't fire
// upstream is the real producer per the pack's analysis; if it is itself
// virtual wiring (producer behind a Get/Set), normalize through pass-1 logic.
let srcId = uel.upstream;
let srcSlot = uel.upstream_slot;
const up = nodesById.get(srcId);
if (up && isWiringVirtual(up.type)) {
const resolved = resolveRealFromNode(srcId, srcSlot);
if (!resolved) {
warnings.push(`ue_link upstream #${srcId} (${up.type}) is virtual wiring that could not be resolved to a real producer — ${dst.type} #${dst.id} input "${dstInput.name ?? uel.downstream_slot}" is left unconnected`);
if (uel.controller != null)
unconfirmed.add(uel.controller);
else
blockSenderDeletion = true;
continue;
}
srcId = resolved.id;
srcSlot = resolved.slot;
}
const why = unconfirmedReason(uel, srcId, srcSlot);
if (why) {
warnings.push(`ue_link → ${dst.type} #${dst.id} input "${dstInput.name ?? uel.downstream_slot}" could not be confirmed against the live graph: ${why} — left unconnected, and the sender is kept in place so a re-save can recover it`);
if (uel.controller != null)
unconfirmed.add(uel.controller);
else
blockSenderDeletion = true;
continue;
}
const added = addDirectLink(srcId, srcSlot, uel.downstream, uel.downstream_slot, uel.type ?? dstInput.type ?? "*");
if (added == null) {
// Confirmed, but the edge could not be built — the producer node or that
// output slot is not in the graph. Swallowing the null returned a
// FLATTENED graph missing a broadcast it claimed to resolve, and let the
// sender be deleted on top of it.
const srcNode = nodesById.get(srcId);
warnings.push(`ue_link → ${dst.type} #${dst.id} input "${dstInput.name ?? uel.downstream_slot}" names producer #${srcId}${srcNode ? ` (${srcNode.type}) output slot ${srcSlot}` : ""}, which this graph does not have — left unconnected, and the sender is kept in place`);
if (uel.controller != null)
unconfirmed.add(uel.controller);
else
blockSenderDeletion = true;
continue;
}
rewired++;
}
for (const [id, info] of multiFeed) {
// Say only what is true: this fires both for several CONFIRMED producers
// and for a channel that could not be resolved at all, and claiming every
// producer was confirmed in the second case would be a false statement in
// a disclosure.
const carries = info.unresolved > 0
? `${info.confirmed} confirmed producer(s) plus ${info.unresolved} input(s) whose own source could NOT be resolved`
: `${info.confirmed} different confirmed producers`;
warnings.push(`${info.type} #${id}: a broadcast could not be matched to a specific sender input — this sender carries ${carries}, and a ue_link record does not name the input a broadcast came through, so a saved list that went stale by SWAPPING its channels would look identical; re-save with cg-use-everywhere active if this sender's routing matters`);
}
// A sender is deletable only when it is not the real producer of any link
// (Seed Everywhere IS its own upstream — it must stay), and never when one
// of its records could not be confirmed: deleting it would destroy the only
// evidence of a broadcast we declined to materialize.
//
// PRE-EXISTING outgoing links count, not just the ones materialized here.
// Counting only freshLinks protected a sender that produced a MATERIALIZED
// link while deleting one that was already the real producer of an ordinary
// link — taking that link with it and silently disconnecting its consumer.
// (Reachable whenever a sender's record adds nothing: its target already had
// a real link, so the record is skipped, no fresh link is created, and the
// sender looked unused.) A node that really produces a connection is not
// virtual wiring, whatever its class.
const liveOrigins = new Set();
for (const l of freshLinks)
liveOrigins.add(l[1]);
for (const l of ui.links ?? [])
if (Array.isArray(l))
liveOrigins.add(l[1]);
// A non-empty list is not proof that it accounts for EVERY sender: one that
// predates a sender has no row for it. Deleting such a sender would remove a
// node whose broadcasts were never materialized — a silent, unrecoverable
// edit. A sender that reaches nothing also has no row, and the two cannot be
// told apart, so it is kept and reported.
const attributed = new Set();
for (const r of ueLinks) {
if (r?.controller != null)
attributed.add(r.controller);
else {
// A controllerless record still accounts for its sender when the
// producer IS that sender (the self-producing shape accepted above) —
// judged on the NORMALIZED producer, since the record may name a virtual
// in front of it. Reading the raw field here let such a record be
// materialized correctly and then reported as if the sender had no
// record at all: a false disclosure, which is the failure mode that
// teaches people to ignore the true ones. (The converter normalizes in
// its equivalent; this is the site that was left behind.)
const p = resolveRealFromNode(r?.upstream, r?.upstream_slot);
if (p && isSelfProducingUeSender(nodesById.get(p.id)?.type ?? "")) {
attributed.add(p.id);
}
}
}
for (const s of ueSenders) {
if (attributed.has(s.id))
continue;
warnings.push(`${s.type} #${s.id} is a Use-Everywhere sender, but no record in extra.ue_links names it — either it currently broadcasts to nothing or the saved list predates it, so nothing was wired from it and it is kept in place`);
}
for (const s of ueSenders) {
if (blockSenderDeletion)
break;
if (!attributed.has(s.id))
continue; // unaccounted for — never delete it
if (!liveOrigins.has(s.id) && !unconfirmed.has(s.id))
ueDeletable.add(s.id);
}
}
}
// ── Deletion + purge ───────────────────────────────────────────────────────
const removedIds = new Set();
let getset = 0;
let reroute = 0;
let ue = 0;
for (const n of nodes) {
if (includeGetSet && GET_SET_TYPES.has(n.type)) {
removedIds.add(n.id);
getset++;
}
else if (includeGetSet && isRerouteType(n.type)) {
removedIds.add(n.id);
reroute++;
}
else if (ueDeletable.has(n.id)) {
removedIds.add(n.id);
ue++;
}
}
ui.nodes = nodes.filter((n) => !removedIds.has(n.id));
const survivingLinks = [...(ui.links ?? []), ...freshLinks].filter((l) => Array.isArray(l) && !removedIds.has(l[1]) && !removedIds.has(l[3]));
// Two links sharing an id make the graph ambiguous: every consumer input and
// producer output list keyed by that id could mean either. The fresh-id
// allocator can no longer create one, but a graph edited by tooling that did
// not maintain link ids can arrive with one already — and returning it
// unchanged propagates the ambiguity from a pass whose whole job is to leave
// the wiring unambiguous. Repair it (re-point exactly the one consumer input
// and producer output slot involved) and say so; never fix it silently.
const seenLinkIds = new Set();
const repairedIds = [];
for (const l of survivingLinks) {
if (!seenLinkIds.has(l[0])) {
seenLinkIds.add(l[0]);
continue;
}
const clashing = l[0];
const fresh = nextLinkId++;
const tgtInput = nodesById.get(l[3])?.inputs?.[l[4]];
if (tgtInput?.link === clashing)
tgtInput.link = fresh;
const srcOut = nodesById.get(l[1])?.outputs?.[l[2]];
if (srcOut?.links) {
const at = srcOut.links.indexOf(clashing);
if (at >= 0)
srcOut.links[at] = fresh;
}
l[0] = fresh;
seenLinkIds.add(fresh);
repairedIds.push(clashing);
}
if (repairedIds.length > 0) {
warnings.push(`${repairedIds.length} link(s) in the source graph shared an id already used by another link (${[
...new Set(repairedIds),
].join(", ")}) — each duplicate was given a fresh id so the flattened graph is unambiguous, but the original file is malformed and anything else reading it by link id may disagree`);
}
const survivingIds = new Set(survivingLinks.map((l) => l[0]));
ui.links = survivingLinks;
ui.last_link_id = nextLinkId - 1;
// Scrub dangling link references on kept nodes (inputs fed by a deleted
// virtual node that pass-1 already rewired keep their fresh id; anything
// still pointing at a purged link gets cleared).
for (const n of ui.nodes) {
for (let slot = 0; slot < (n.inputs?.length ?? 0); slot++) {
const inp = n.inputs[slot];
if (inp.link == null || survivingIds.has(inp.link))
continue;
// This input claims a connection the flattened graph does not have. Say so —
// clearing it in silence is the same observation-failed-treated-as-a-no that
// this change removes everywhere else, and the converter already reports its
// equivalent.
//
// The question is asked about THIS INPUT's claim, not about the id in the
// abstract. An earlier version suppressed the report when the id existed
// ANYWHERE in the arriving graph, which let a bogus claim on an id belonging
// to a link into a DIFFERENT node be cleared silently — the same
// bare-identifier-vs-identifier-in-context mistake as the raw-vs-normalized
// producer bugs elsewhere in this pass, one level down. Whether the id was
// once a link somewhere says nothing about whether it fed this input.
warnings.push(`${n.type} #${n.id} input "${inp.name ?? slot}" claims link ${inp.link}, which is not a connection into this input in the flattened graph — left unconnected`);
inp.link = null;
}
for (const out of n.outputs ?? []) {
if (out.links)
out.links = out.links.filter((id) => survivingIds.has(id));
}
}
// The senders are gone — stale UE bookkeeping would confuse the pack's JS.
if (ui.extra && includeUe && ueSenders.length && ueDeletable.size === ueSenders.length) {
delete ui.extra.ue_links;
delete ui.extra.links_added_by_ue;
}
return {
graph: ui,
report: {
removed: { getset, reroute, ue },
added_links: freshLinks.length,
rewired_inputs: rewired,
kept_nodes: ui.nodes.length,
warnings,
},
};
}
//# sourceMappingURL=flatten-workflow.js.map