Wildcard options, fixed values and combines can carry a `negative`, and a
derivation action gains the `negative` ("Add to negative") mode. The
engine files negatives per variable in `__wp_negatives__`, carries them
across nodes, and the Assembler gets a `negative` output built from the
variables its template rendered, filled into a `negative_template` at
`$negatives`. Negatives resolve on their own seed stream and refs inside
them resolve quietly, so picks never shift. Injector rows can set a
negative, the Prompt Cleaner cleans an optional negative with its own
rule column, and the preview endpoint returns the negatives table.
Schema v8 stamps payloads that use a negative.
An accepts axis rolls one tag per pick (read as $var.AXIS), but a constraint
sourced from it folded over the option's whole menu instead of that rolled tag,
so $outfit.SHOES and the constrained $shoes could disagree. The source now
sends only the rolled winner; a target accepts axis OR-folds so a multi-tag
option survives when any alternative is allowed, then its own roll is pinned to
the allowed tag -- $source.AXIS == $target.AXIS under a diagonal. Classify
groups are unchanged (multiply).
The assembler also rendered $var.AXIS empty: __wp_axes__ crosses the socket on
ContextPayload.internals but the node only merged the internal-flag map; it now
merges the whole carve-out so the accessor resolves.
Import: the community validators silently dropped tag_group_kinds (and the
constraint target_select) because the lenient payload object never listed them,
degrading a shared accepts axis to classify. Both are now in the v2/v1
validators + the engine-parity fixtures, with preservation tests. Engine math
mirrored in TS via the shared corpus.
The preview tokenizer never carried the axis accessor, so every consumer
rendered the variable and left ".SHOES" beside it as text — which is why both
the PREVIEW and RESOLVED lines read "a white t-shirt and denim skirt.SHOES".
It now carries `axis` the same way it already carried `index`, and the combine
editor resolves an axis read to a tag rather than to the variable's value. An
axis the producer does not declare stays verbatim: swallowing it would delete
text the user typed.
Also undoes the leak from the previous commit. strip_internals' result IS the
variable surface — the assembler's variables panel enumerates it, so keeping
__wp_axes__ / __wp_picks__ there listed them as $__wp_axes__ chips. The tables
are re-attached by with_resolver_tables at the one call site that needs them.
The template input now renders through RichTextInput, so it has the booru tag
autocomplete, `$var` chips with producer attribution, and the same editing
behaviour as every other WP text surface.
No payload transition. It is the SAME `template` widget: same name, same
position in `widgets_values`, same plain string value — only the renderer
changed. A workflow saved before this loads its template back untouched;
verified by loading a hand-written pre-change workflow JSON.
The seam is `widgetType`. The frontend picks a widget constructor with
`t.widgetType ?? t.type` while the socket keeps `t.type`, so tagging the input
spec routes the lookup to us without touching what the socket accepts. It has
to go through `extra_dict`: V3's `widget_type=` parameter would ALSO change
the socket, because `WidgetInput.get_io_type` returns widget_type when set —
the input would stop being STRING and no upstream STRING output could feed it.
`extra_dict` is merged last in `Input.as_dict` and never consulted by
`get_io_type`. The new schema test asserts both halves together, since either
one alone passes while the feature is broken.
Keeping the link input needs one more thing: ComfyUI only calls
`addInput(name, spec.type, {widget})` when the widget the factory returns has
no `socketless` flag, and `createDomWidgetHost` hardcoded `socketless: true`.
Hence the new `socketed` option — correct for every other WP widget, exactly
wrong for this one. While a link IS connected the editor goes read-only with
a placeholder saying so, matching the stock multiline widget. Derived from the
link rather than the widget's `computedDisabled`, which is recomputed lazily
during a draw and still read `true` a second after the link was removed.
Why a separate file and not `assembler.ts`: the helper widget must stay
socketless (it is an editor, not a value), and the two widgets need opposite
answers to that one question.
The helper's template mutators lose their `inputEl` branches. Those had been
dead for a while — the frontend moved widget values into a store, so the
textarea at `widget.inputEl` is detached and unrendered, and each of those
functions was really working through the `w.value` write sitting next to it.
Insert is the exception: it needs a caret, so it asks the editor through a
WeakMap hook instead of replacing the whole string.
Verified live on frontend 1.48.6 with Vue Nodes on: legacy value restores,
STRING link connects and disconnects, serialize round-trips, clear / remove /
insert all propagate, `$` autocomplete lists upstream vars with attribution,
tag autocomplete and its legend work, and a full Context → assembler chain
previews correctly.
Three coordinated changes to the WP_* node family.
1. New WP_SeedList node
Emits a list of N derived seeds for a downstream loop (is_output_list).
Three independent override toggles (override_seed / override_count /
override_strategy) each mirror the wired Loop's base_seed / count /
strategy. Connecting loop_config auto-flips override_count +
override_strategy ON so the natural intent of wiring in a loop gets
the defaults right; override_seed stays explicit opt-in.
All emitted seeds are masked to 50 bits via the new MAX_SAFE_SEED
constant in engine.seed_derive so values sit inside ComfyUI's
frontend randomize range and copy-paste reproduces identically in a
standalone sampler.
2. Context Loop type split + downstream Context seed widget lock
Split WP_CONTEXT_LOOP_CONFIG (wire payload) from a new
WP_CONTEXT_LOOP_WIDGET (DOM-widget-only TYPE). Loop's own widget
mounts on the widget type; the wire type has no factory so consumers
like WP_SeedList render their loop_config socket as a plain input
instead of being hidden by a fallback-undefined widget.
New graph walker hasUpstreamLoopOverridingSeed: WP_Context's widget
glue greys out the local seed stock widget when an upstream Loop has
override_seed ON, so users read "this value is dead while the Loop
drives it" without inspecting the Loop config separately.
3. wp_ prefix sweep + tooltips across every WP_* node input
Custom DOM widget input names now carry the wp_ prefix so canvas
socket labels read as branded WP widgets: modules -> wp_modules,
rows -> wp_rows, viewer -> wp_viewer, cleaner -> wp_cleaner,
context_loop_config -> wp_context_loop_config. Every stock input
across the node family gets a tooltip describing what it does and
when to wire it from another node. Tests + comfy_api stub updated.
CLAUDE.md notes the wp_ convention for future custom widgets.
Bundle size budget bumped 388 -> 395 KB to fit the SeedList Vue chunk
(~4.9 KB net gzip) + override lock UI; entry stays at ~4 KB gzipped.
User-reported bug: a variable marked internal in an upstream Context
node could not be read by downstream Combine / Derivation modules —
they saw it as missing. The previous design treated the
PIPELINE_CONTEXT socket as the public boundary and stripped both
engine `__` keys AND user-flagged internal vars at socket emit.
Correct semantic: internal = "don't render as standalone $var in the
final prompt." It does NOT mean "don't propagate through context."
Downstream nodes still need to read internal vars to compose them
into other vars (e.g. a Combine builds `$subject` from an internal
`$style` + a public `$pose`). Only the final PromptAssembler must
skip internal vars at template substitution time.
Implementation:
- engine/context.py: split the filter into two helpers.
`strip_engine_internals` drops only `__`-prefixed engine keys —
used at the socket boundary. `strip_internals` keeps the original
dual-layer behavior (engine + user-flagged) — used at the prompt-
render boundary.
- wp_nodes/types.py: socket emit now uses `strip_engine_internals`,
so user-flagged internal vars cross node boundaries as regular
context keys. `__wp_internal_flags__` is added to
`_CROSS_NODE_INTERNAL_KEYS` so downstream PromptAssemblers can
still know which vars to filter at render.
- wp_nodes/assembler_node.py: before resolving variables, merges
the cross-node `__wp_internal_flags__` back into a copy of the
socket context and calls `strip_internals` — drops both engine
keys and user-flagged internal vars. Net effect on the rendered
prompt: unchanged from the user's perspective (internal vars
still don't appear as standalone $vars).
- tests/test_context.py: regression tests for the new helper +
propagation behavior.