Commit Graph
23 Commits
Author SHA1 Message Date
digital-garbageandClaude Opus 5 f128eb2d74 sampling: cancelling is ordinary, so it is checked like everything else
Stopping a generation is a normal thing to do -- a bad seed, a wrong prompt,
a rental minute -- so it is not an error path that can be left approximate.
A leak here is invisible: the run AFTER a cancel just behaves slightly wrong.

The sampler says a run was cancelled and re-raises. It is not ours to
absorb; ComfyUI ends the queue item. But a run that simply stops with
nothing said is indistinguishable from one that broke.

Verified against the real engine: a 40-step generation interrupted mid-way
reports execution_interrupted, and the next one completes as run 2. Tests
cover what must be left behind, which is nothing -- no wrappers on the shared
model after five cancelled runs, the sharp anchor put back on a denoiser that
outlives the run, and no modifier marked as dropped, because a cancelled
modifier did not fail and disabling it next time would punish the user for
stopping.

Temp previews needed no change: core's PreviewImage already writes to the
temp directory under a unique prefix, so where the app SHOWS them is a UI
decision and is recorded as one rather than built here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 18:27:31 +03:00
digital-garbageandClaude Opus 5 0f31547e1e foundation: two the whole-tree pass found, both against the real engine
The run id froze after the first generation. Runs were marked by the
modifiers node, and ComfyUI CACHES a node whose inputs have not changed --
only the seed changes between most generations, so that node executed once
and every later run reported under the first one's id. Per-run log dedup
degraded to per-process with it, which is precisely the v4 fault log.py's
own comment describes, reintroduced through a caching interaction the fix
did not account for.

The sampler marks the run now. It is what generates, and it re-executes
whenever anything about the generation differs. Verified through the real
PromptExecutor with caching on: three seeds, three run ids. The
dropped-modifier record is also emptied there, because it travels on the
model and ComfyUI may hand the same model back -- a modifier that failed
last time deserves this time.

And a bad settings payload was only caught during execution, after the
checkpoint had already loaded. On a rented GPU that is the cost paid before
the guard fires, one layer up from the sampling case this project already
guards. ComfyUI calls validate_inputs inside validate_prompt, so the payload
is now understood before the queue accepts it: a typo'd module id is refused
with nothing loaded. Both doors check the same things, and a test pins that
they agree.

There was no test file for runs at all, which is why the first fault could
be introduced by a fix and survive a green suite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 18:14:41 +03:00
digital-garbageandClaude Opus 5 cf318c8c0d graph: the pipeline is slots, so a built-in node can be swapped or removed
v4 could not remove a built-in node from the pipeline and the reason was
structural: the wiring lived in code as a CORE dict nobody could reach, so
"remove this" had nothing to delete and "use a different one" had nothing to
change. Code is not a thing a user can edit.

The pipeline is now data. Three operations over it, each refusing with a
reason rather than half-doing the job:

build turns slots into the prompt ComfyUI queues, checking every link's type
and refusing a graph that cannot work. replace points a slot at any node
producing what its consumers read -- the built-in ones have no special
standing, which is what makes them replaceable rather than merely present.
remove takes a slot out and rewires what it fed to what fed it, when the
node has exactly one input matching each CONSUMED output; v4 required one
for every output a class could produce, so a spare FLOAT nobody wired made a
node unremovable.

Demonstrated against real schemas: FunPack's VAE loader swapped for core's
VAELoader, the modifiers node removed with the sampler rewired to read the
model directly, and removing the empty latent refused because nothing
upstream could feed what it fed.

Then run for real. The default pipeline, with three loader slots replaced by
one checkpoint slot, built from data, accepted by ComfyUI's own
validate_prompt, and executed by its own engine to execution_success and a
picture. Every wiring and type validated on the first attempt; the only
things ComfyUI objected to in the untouched default were the three unset
file pickers, which is correct -- a fresh install cannot know which model
you own.

Added a checkpoint loader, because the split loaders are right for LTX and
H3 and cannot read an SD-family single file at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 18:06:25 +03:00
digital-garbageandClaude Opus 5 090669e950 sampling: a sampler that samples, and hosts what it says it can host
v4's Chain Sampler became a hub: 8183 lines, roughly seventy optional
inputs, a _sample_chunk that grew six ALG arguments, and a hardcoded list of
knobs silently inert on one model family. Every technique that needed the
loop was added to it, so it collected all of them.

This one picks an algorithm and a schedule from ComfyUI's own registries --
so a sampler it gains appears here without an edit -- and runs them. Three
things stop it becoming a hub again. It never learns what a model-side
modifier is. Sampler-side ones reach it through ONE call site,
chain.process(ctx, latent), which cannot grow because the step travels in a
context object rather than in the signature. And it declares the hook points
it offers, so core hands it only what fits and it never names a modifier.

Some modifiers genuinely cannot ride on a model: they need the step index,
the schedule, or a second pass. Those attach here instead, and one asking
for a hook this sampler does not offer is absent and said so, not
half-running.

Verified against ComfyUI's own guider path with real weights: bit-identical
output. That took correcting the method, not the code -- see below.

MEASUREMENT, corrected: sampling on this Mac's MPS backend is
NONDETERMINISTIC. The same node with the same seed and the same inputs
differs run to run: 2.42 by max latent difference, 0.0008 by mean pixel gap.
Comparing by MAX said this sampler diverged from core by 14.2 and it was
about to be "fixed"; by MEAN against a measured noise floor it is 0.00000
while core against itself is 0.00183. Every comparison here now runs a
control twice first. Re-checked the Detail modifier the same way: 145x the
floor, so that claim stands.

Also matched what core's sampler actually does and this did not:
fix_empty_latent_channels using the ratios our own empty-latent node reports,
the preview and progress callback, the move back to the intermediate device,
and dropping the ratio keys so nothing rescales twice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 17:51:02 +03:00
digital-garbageandClaude Opus 5 78c43acd67 contract: a colour type, and which run this is
Colour is a preference, so it is a setting: hex in, hex out, one spelling,
refused if it is not one. A module handed "red" in one place and "#f00" in
another guesses wrong somewhere.

Image and mask are NOT settings and deliberately have no type. They are
data -- tensors the app uploads and the graph wires -- and ComfyUI already
has socket types for both. Putting a drawn mask in a JSON settings payload
would be inventing a second way to move something that already moves.

Runs now have an id. Previews, progress, results and log lines all have to
be attributable to one generation, and ComfyUI gives a node no way to ask
which run it is in. So FunPack marks its own, every log record carries it,
and the history can be filtered to a single run. Readable rather than a
uuid, because it is going to be read: "run 7" is findable in a wall of
output and a hex string is not.

The colour element was caught by the registry-driven rule test before it was
finished -- an element with no catalogue demo is neither shown nor attacked,
so coverage keeps up with the registry instead of lagging behind it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 17:29:38 +03:00
digital-garbageandClaude Opus 5 fae5ecf12d sampling: pin that the guard never swallows a cancel
Cancel reaches a hook as an exception like any other, and a guard that
caught it would leave someone pressing stop on a run that will not stop.

It works today only because ComfyUI made InterruptProcessingException a
BaseException rather than an Exception -- a detail this code depends on and
does not control. Widening the guard to BaseException would break Cancel
with nothing else failing, so the property is now a test rather than an
accident.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 17:19:07 +03:00
digital-garbageandClaude Opus 5 74671329f9 log: a severity, a source, and a message
[Log] Info: FunPack Diffusion Model Loader | flux.safetensors loaded, attention sage
    [Log] Alert: MiniMaxH3ImageToVideo node | no "first_image" input is connected
    [Log] Warning: FunPack LoRA Loader | style.safetensors is empty and will not load
    [Log] Critical: torch | cannot allocate 14 GB (3 GB free); the run will not start

The difference between the middle two is the one that matters. Alert is a
choice you may not have made on purpose. Warning is something that will NOT
do what its name says -- the level v4 needed and did not have, which is how
560 handlers came to swallow a failure and carry on, so a feature could be
switched on, do nothing, and never say so.

Info is not decoration either: "loaded on GPU using sage attention" is how
an attention setting that did nothing gets noticed months before the
pictures look wrong enough to investigate.

Every call site moved -- there is no note() left to be vague through -- and
the loaders now say what they actually loaded rather than only what they
were asked for. Records are kept in a bounded ring as well as printed, so a
long rental session cannot turn the log into a memory leak, and served at
/funpack/api/log so a person can read it somewhere other than a terminal
full of ComfyUI's own output. There is a page for it.

Also dropped the preflight this was traded for: min and max are already
declared and already enforced at both doors, and a second protection layer
around a check that exists is ceremony.

Two mistakes worth keeping: a log line reached into the model's shape to
describe it and could have failed the load it was only narrating; and both
new pages passed `children` to an element that takes `rows`, which the kit
accepted and silently dropped. It throws on an unknown element and not on an
unknown prop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 17:09:33 +03:00
digital-garbageandClaude Opus 5 def940d9d5 foundation: a module cannot end a run, unless you asked for that
Catching a modifier that fails to INSTALL was never the hard part. The one
that costs a rental is the hook that works at step 0 and raises at step 3,
after the GPU time is spent -- which the Detail modifier does on any video
latent, because core's sharpen kernel cannot take a 5-D tensor.

A module is now handed a patcher that guards every hook it installs, so it
does not opt in to being safe: a module that could forget to is a module
that can break the foundation. A hook that raises is dropped for the rest of
that run, reported once with its traceback, and sampling continues. Thirty
steps on the latent that used to end the run now complete untouched.

Recorded per RUN, not per process, so a long session reports every
generation that went inert instead of only the first after a restart.

Full control turns all of it off. A one-step schedule, a width the model
cannot use, a modifier that throws -- someone exploring the edges wants the
real behaviour and the real traceback, not a careful refusal. One switch
rather than one per guard, because a half-disabled safety net is harder to
reason about than either extreme. It is asked for by capability, so nothing
names the module that answers, and a build without it installed has nobody
saying yes -- which reads as guarded.

Honest about its own limit: a hook shape with no known neutral result is
installed unguarded and named in the status, rather than counted as safe.

Two test faults fixed while writing this. The assertions compared conds_out
against the list passed in -- the same object the hook mutates -- so they
were true whatever the hook did. And the registry fakes were hand-rolled and
missing methods the real one has, which passed until the code called one.
They use the real Registry now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 13:52:18 +03:00
digital-garbageandClaude Opus 5 a81ac48083 model support is a folder, and a modifier you can watch working
Decode now takes the MODEL, so a provider answers "is this mine" by identity
instead of by shape. H3 was claiming any latent that happened to have two
parts -- right today because it is the only thing producing one, wrong the
moment a second AV model exists, and wrong in the way that reads as an
unrelated fault rather than an error. It declines when the model is not
wired, and the node says that is what is missing.

That completes the shape a model module should have: one folder saying how
this model behaves at each stage -- what it is, how its latent is built, how
its output is decoded -- instead of that knowledge spreading through the
conditioning and sampling code as special cases.

Added a modifier that runs on SD1.5, because a feature nobody can watch is a
feature nobody can check. It declares no traits, so it applies everywhere,
which is the other half of the rule ALG demonstrates by narrowing. Measured
on this machine at a fixed seed: off and on differ, 0.4 gives a visibly
richer picture that still reads correctly, and 1.2 blows the highlights --
so the range is -1..1, set from what was measured rather than from a round
number. A slider whose top half destroys the image lies about its range.

Two things found while building it. ComfyUI has a real circular import
between nodes_latent and nodes_post_processing that resolves only because
init_extra_nodes reaches one first; a custom node has to break it the same
way. And the accumulation bug returned through a third door: stripping
handled keyed wrappers but not the anonymous function lists on
model_options, so chaining doubled the effect. Tagging is now generic -- it
looks for our mark rather than for a list of hook names that would need
keeping current.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 13:33:09 +03:00
digital-garbageandClaude Opus 5 37430137cb foundation: run it, and fix what running it found
First real generation through FunPack's nodes: SD1.5 on this Mac, real
weights, a real picture of an apple. Everything before this was static.

What it proved, on a real model rather than a fixture: the empty latent
takes its shape from SD1.5's own format with no family branch anywhere
(1, 4, 64, 64, an IMAGE latent from a pack built for video); the traits read
off the live model are predict_eps and spatial_latent; and ALG is correctly
absent, saying "needs temporal_latent", rather than running on a model with
no time axis.

What it found: the absolute-core-import bug again, this time INSIDE A
FUNCTION BODY. It imported fine, registered fine, passed 258 tests, and
raised at execute time -- during the generation. A suite that imports every
module cannot see an import that only happens when a node runs, so the check
for it is now static and walks the AST: no module may name `core` anywhere,
at any nesting, except through the shim.

The dev preview loaded one module in nine. It runs system python, which has
neither ComfyUI nor torch, so every module that imports either was absent
and the app looked almost empty while reporting nothing. It now re-execs
under ComfyUI's interpreter. Looking at the app is meant to be how the app
gets tested; it has to see what ComfyUI sees.

ALG asked for a mount point no region offered, so its panel vanished in
silence. The shell now offers one per stage, and a modules page lists what
loaded, what asked for a mount nothing offers, and what failed to import --
absence is correct behaviour and indistinguishable from "never installed",
so somewhere has to say which.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 13:20:50 +03:00
digital-garbageandClaude Opus 5 fc977f4068 sampling: check the values where they are used, not only where they are made
The NaN guard was on one of two doors. A FUNPACK_SETTINGS socket is typed by
a string tag, so a dict reaches the load node without necessarily having
passed through the settings node -- a raw API prompt does exactly that.

Through that door, until_sigma = NaN made ALG a permanent no-op: sigma > nan
is False at every step, so the anchor was never loosened, and the node
reported "1 modifier applied: alg". Nothing about the run looked wrong,
which is the only reason this is worth a commit of its own.

Values are now checked against the declaration at the point they are
consumed. The producer still checks too; that is not redundant, it is where
a person editing JSON gets told immediately.

Also recorded ALG's standing assumption rather than leaving it implicit: the
anchor is latent frame 0. True of everything it can currently be wired into,
untrue in general, and the denoise_mask already carries what would be needed
to do it properly. Left undone because nothing yet pins elsewhere, and a
derivation with no consumer cannot be checked against reality.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 13:05:28 +03:00
digital-garbageandClaude Opus 5 134cee78b7 output: decode, which is the last thing FunPack needs to own
Core already turns images into a video and writes the file, and handles
containers, codecs and muxing properly. Writing our own would be a worse
copy of tested code, so there is no FunPack save node and no reason for VHS
either.

What core cannot generalise is decoding a latent that is not one tensor: an
AV latent has two branches wanting different VAEs, and their arrangement is
the model's business. Same rule as the empty latent -- a model module may
claim it, and H3 now does. A nested latent nobody claims is refused rather
than handed to vae.decode, which would raise somewhere confusing or quietly
decode one branch as the picture. A missing audio VAE is named, because
returning no audio looks exactly like a model that makes none.

With this the pipeline closes end to end using FunPack's eight nodes and
core's samplers and video nodes -- verified by loading both together. That
matters more than the node count: until now nothing FunPack produced could
be measured, only reasoned about.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 13:02:11 +03:00
digital-garbageandClaude Opus 5 749b1b5db9 alg: the first real modifier, and the first module that is only behaviour
An i2v model shown a sharp reference can satisfy the objective by barely
moving. ALG low-passes the anchor while sigma is high, removing the detail
that shortcut needs, and hands the sharp one back once the picture settles.

It attaches to the model, so it applies to whatever sampler is wired, and it
requires temporal_latent and nothing else -- not "for LTX", not "for H3".
Any model with a time axis and a pinned anchor gets it, including ones
nobody here has heard of. A module with no ComfyUI node of its own: settings
and behaviour, which is what most modules should be.

The packed-layout path is NOT ported. v4 reached into a model-specific
arrangement of the latent to find the video stream; where the anchor is not
a plain [B, C, T, H, W] tensor this stands aside and says so once, rather
than reshaping something it is guessing about.

Tested as far as a machine with no GPU can: the filter really low-passes,
only the anchor frame changes, the choice follows sigma, and the whole thing
runs through ComfyUI's real KSAMPLER with only the innermost loop stubbed --
so the anchor a denoiser sees differs between an early step and a late one.
Whether it looks better needs the rental.

Also guarded route registration. PromptServer.instance only exists once a
server has been built, and importing `server` successfully is not the same
as running inside one -- with ComfyUI on the path the unguarded attribute
took the whole pack down.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 12:56:50 +03:00
digital-garbageandClaude Opus 5 b3afba43e7 sampling: tag and strip, because the clone only protected the original
The claim in the last commit was too strong. Cloning stops this node
touching the model everyone else holds; it does nothing about the node's own
output coming back in. add_wrapper_with_key APPENDS under the key and a
clone carries the list, so chaining two of these -- two passes, a flattened
subgraph -- installed the same wrapper twice and ran it twice per step at
double strength, while both nodes reported "1 modifier(s) applied".

That is the accumulation this design is supposed to prevent, arriving
through a different door. Every install is now preceded by stripping our own
namespaced keys and saying how many were replaced. Foreign wrappers are
never in scope, which is what the namespace was for.

NaN passed validation: it compares False against both bounds, so a range
check alone never fires, and json.loads accepts NaN and the infinities by
default. A NaN strength reached a modifier and would have poisoned sampling
with nothing reported. Non-finite numbers are now refused by name.

A settings payload of the wrong shape raised AttributeError rather than the
error reporting everything else here uses; the socket type is only a string
match, so anything can arrive on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 12:41:18 +03:00
digital-garbageandClaude Opus 5 8521bdf899 sampling: modifiers attach to the model, so no sampler can own them
Two nodes. One carries the values a person picked, as a single validated
payload rather than a socket per setting -- a socket per setting means this
node's shape changes whenever any module gains an option, and every saved
workflow refers to sockets that moved.

The other reads what announced itself, filters it by what the model actually
is, orders it by declared relations, and installs each one through ComfyUI's
own patcher_extension wrappers. The consequence is checked against comfy's
real prepare_model_patcher and WrapperExecutor rather than against our own
dict: a model that has been through this node runs every per-step modifier
inside ComfyUI's own SamplerCustomAdvanced. The sampler is not consulted, so
it cannot own them.

Installing happens on a clone, and clone() copies the wrapper lists per key,
so the model everyone else holds is untouched. Fifty runs leave zero
wrappers behind. The accumulation that once degraded output across a hundred
generations and survived every reset short of a restart is structurally
impossible here rather than something to remember to undo.

The payload is validated against the same declaration that renders the
panel: an undeclared key, a wrong type, a value outside its own bounds and a
bool passed as a number are all refused, and all problems are reported at
once. v4 validated none of it, which is why it needed a hand-mirrored table
of every default and a list of keys that no longer meant anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 12:35:28 +03:00
digital-garbageandClaude Opus 5 9e08f4dd0e latent: a provider that breaks on its own model must stop the node
A provider that raised was treated exactly like one that said "not my
model", so the node fell through and derived a shape for the very model the
provider existed to handle -- and reported success. For H3 at length=124
that is 31 latent frames against a real grid of 37: a 16% shorter video,
with nothing downstream saying anything.

The protocol is now stated: return None for "not mine", and let anything
else raise. Recognition happens before anything that can fail, so an
exception means the model was already claimed, and the node refuses rather
than substituting a generic shape.

execute() had no test at all -- the dispatch between claimed and derived is
the point of the node, and both earlier tests called around it. It now runs
through the real registry, and the refusal test fails against the old
fall-through.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 12:27:16 +03:00
digital-garbageandClaude Opus 5 fa2caa8860 latent: an empty latent the model decides the shape of
Derived from latent_format where that is possible, which is every
single-tensor model: SDXL, Wan21 and LTXV all come out matching the numbers
core's own nodes compute, including models that ship after this.

H3 cannot be derived and is the reason modules exist. Its latent is a
NestedTensor of a video and an audio branch, its video channel count is not
the latent_channels its format reports, and its length snaps to a frame
grid. So modules/models/minimax_h3 claims it and builds it, importing the
grid from ComfyUI rather than copying a second definition of it.

Nothing outside that folder names a model, and nothing here assumes video:
a 2-D latent format yields an image latent with no branch anywhere.

Modules reach core through modules/_core.py. ComfyUI imports a pack by file
location, so an absolute `import core` in a module resolves under pytest and
not under ComfyUI -- the node then never registers, silently. That cost the
same bug twice, so there is now a test that loads the pack the way ComfyUI
does, in an isolated subprocess whose cwd is NOT the repo. The first version
of that test passed with the bug reintroduced, because cwd was on sys.path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 12:21:25 +03:00
digital-garbageandClaude Opus 5 20a4c39db3 loaders: compute_dtype cast the input and left the weights behind
set_model_compute_dtype sets force_cast_weights on purpose: it is what
casts each layer's weights to match. Clearing it afterwards left the model
casting its INPUT to the chosen dtype while the weights stayed as loaded,
so the first Linear of the first sampling step raised "mat1 and mat2 must
have the same dtype".

Picking any compute dtype other than default therefore broke sampling on a
full-VRAM load. It survives on constrained hardware only because the
low-VRAM path forces the cast back on by itself, which is why nobody hit it.

Straight port from v4, where the same line has shipped since 3837f8f. The
branch had no test at all; it now has one that fails against the old line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 12:06:45 +03:00
digital-garbageandClaude Opus 5 e272a3b443 loaders: three the attacker found, and the silence around one of them
A backend that carries a zero-copy container path lost it: wrap_attn offers
that path only when the OVERRIDE has the attribute, so dropping it selects
the backend, computes the right answer, and never uses the thing the backend
exists for. ComfyUI forwards it in set_model_optimized_attention; now so do we.

An unknown encoder family fell back to stable_diffusion and reported success.
A family mismatch in this project reads as an unrelated fault, so it refuses
and lists what this ComfyUI actually knows.

fp16 accumulation is one flag for the whole interpreter, so a per-node
checkbox cannot mean "this model": a second loader changes the first model's
maths, and loader caching outlives the run. It cannot be scoped, so it is at
least no longer silent -- every transition says what it really covers. The
honest fix is to move it to a machine-level setting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 03:41:04 +03:00
digital-garbageandClaude Opus 5 ab9cbdb556 loaders: the four FunPack loaders, as V3 nodes
Ported from v4, minus two mechanisms that are no longer needed. The LoRA
loader takes one LoRA and chains, so the FlexibleOptionalInputType dict
subclass that lied to ComfyUI's validator has nothing left to do. The CLIP
loader uses numbered slots like core's DualCLIPLoader instead of a JSON
array in a string widget.

Autogrow looked like the right answer for the encoder list and is not: it
sets force_input on any widget input, so a list of combos renders as sockets
and the file picker stops being a picker. Every core use of it is a
connection type.

GGUF and SLA are deliberately not here. Both are capabilities rather than
loader options, and both belong in modules of their own.

The tests follow what the loaders PRODUCE rather than stopping at the call:
the attention override is carried into comfy's real dispatch and checked
against every backend this machine registers, because an override that only
fails at the first attention call fails at sampling, not at load.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 03:32:06 +03:00
digital-garbageandClaude Opus 5 7330ba0c18 core: nodes are announced by modules, like everything else
ComfyUI's V3 entrypoint replaces NODE_CLASS_MAPPINGS, which must not exist
at all: nodes.py checks it first and reaches comfy_entrypoint only in an
elif, so an empty dict registers nothing and reports no error.

A module declares NODES for graph structure and SETTINGS for preferences,
and the two never mix -- a saved workflow stores class_type and input
names, so letting a new setting add a socket would rot existing graphs.

Traits now come from two places: facts core can read off any model, and
providers announced by a model's own module. Core names no model, so
supporting a new one is a folder rather than an edit here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 03:03:19 +03:00
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
digital-garbage d5effb0378 core: serve the app, with the module allowlist as the wall
Static serving routes through a pure resolve(), so the extension allowlist and
the traversal guard are tested without aiohttp. modules/ serves .js only: a
module cannot ship a stylesheet even if one is written.
2026-08-28 14:18:55 +03:00