Files
olivv-cs-ComfyUI-FunPack/core/relations.py
T
digital-garbage 835fab7864 core+shell: the bridge, and the first module through it
One declaration does three jobs: renders the panel, carries the defaults, names
the values that come back. A module writes no DOM and reads no DOM.

Validation refuses rather than repairs -- unknown type, a renderer its type does
not have, a label that is not text, a default outside its own bounds, a when
naming a key the module never declared. A module that is wrong does not get a
best-effort panel; it does not load, and says why once.

Ordering is by declared relation and stage, never priority numbers, and a cycle
drops only the modules in it. Compatibility is by trait, never model name.

The shell hands a module { composer, values, on, shell } and nothing else: no
node, no document, no kit internals. It cannot style anything because it is
never given anything that could.

catalogue/hide.html puts one good module and five broken ones through the real
shell and shows the result: one panel, five absent, no placeholders. The lint
fixtures make the scanner prove it catches each rule, so a clean scan means
something.

First slice: modules/system/appearance. Two settings, an optional ui.js, and
clicking Light actually turns the app light.
2026-08-28 19:58:06 +03:00

66 lines
2.5 KiB
Python

"""Ordering by declared relations, never by priority numbers.
A number is a global namespace: inserting one module between two others means
renumbering everything after it, and the numbers live in files that know nothing
about each other. A relation is local -- a module names only the neighbours it
actually cares about, and everything else sorts itself.
"""
from typing import Dict, Iterable, List, Tuple
from .contract import STAGES, ModuleSpec
class CycleError(ValueError):
"""Relations that cannot be satisfied in any order."""
def order(specs: Iterable[ModuleSpec]) -> Tuple[List[ModuleSpec], List[Tuple[ModuleSpec, str]]]:
"""(ordered, rejected) -- rejected carries the reason it could not be placed.
A module in a cycle is dropped rather than placed arbitrarily: an arbitrary
order is one that works until it does not, and by then nobody remembers the
order was never real.
"""
specs = list(specs)
by_id = {spec.id: spec for spec in specs}
rejected: List[Tuple[ModuleSpec, str]] = []
# Edges point from "runs earlier" to "runs later". `before` is expressed as
# the same edge from the other end, so the two spellings cannot disagree.
edges: Dict[str, set] = {spec.id: set() for spec in specs}
for spec in specs:
for earlier in spec.after:
if earlier in by_id:
edges[spec.id].add(earlier)
for later in spec.before:
if later in by_id:
edges[later].add(spec.id)
# A relation naming an absent module is not an error: modules come and go,
# and "after audio_clock" simply has nothing to wait for when audio_clock
# failed to load or was never installed.
stage_of = {spec.id: STAGES.index(spec.stage) for spec in specs}
ordered: List[ModuleSpec] = []
placed: set = set()
# Stage first, then declaration order, so a run without any relations is
# stable and readable rather than dependent on filesystem order.
remaining = sorted(specs, key=lambda s: (stage_of[s.id], s.id))
while remaining:
ready = [s for s in remaining if edges[s.id] <= placed]
if not ready:
for spec in remaining:
unmet = sorted(edges[spec.id] - placed)
rejected.append((spec, f"ordering cycle with {', '.join(unmet)}"))
break
# Among the ready, respect stage so a later stage never jumps ahead.
nxt = min(ready, key=lambda s: (stage_of[s.id], s.id))
ordered.append(nxt)
placed.add(nxt.id)
remaining.remove(nxt)
return ordered, rejected