Files
Claude 7b02fcfd58 feat(engine): per-variable negatives for send-to-negative
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.
2026-09-30 00:50:47 +00:00

88 lines
3.6 KiB
Python

"""WP_Debug — terminal node that emits the PipelineContext for UI inspection."""
import json
from comfy_api.latest import io # pyright: ignore[reportMissingImports]
from engine.syntax.types import ListVar
from wp_nodes.types import DebugViewerInput, PipelineContext
class WPDebug(io.ComfyNode):
"""Inspect the context at any point in the chain. Terminal output node."""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="WP_Debug",
display_name="WP Debug",
category="wildcard-pipeline",
inputs=[
PipelineContext.Input(
"context",
tooltip=(
"The resolved $variable context from any upstream "
"WP Context / Loop / Injector chain. Required — "
"without it the node has nothing to inspect."
),
),
DebugViewerInput.Input("wp_viewer", socketless=True),
],
outputs=[],
is_output_node=True,
not_idempotent=True,
)
@classmethod
def execute(cls, context, wp_viewer):
del wp_viewer # accepted for widget binding parity; no runtime use
# Flatten the typed ContextPayload into a single dict the
# frontend `DebugViewer.vue` consumes. User-facing variables sit at
# the top level; everything else rides on `__`-prefixed keys the
# viewer reads by name (see `src/components/debug/debug-model.ts`):
# __wp_trace__ / __wp_warnings__ every module + injector row and
# warning in the chain
# __wp_picks__ wildcard picks, by module id
# __wp_ref_log__ nested @{} picks, by owner uid
# __wp_nodes__ one {node_id, seed} per Context
# __wp_internal_flags__ variables marked internal
# __wp_axes__ rolled accepts-axis tags
# __wp_constraint_hits__ targets each constraint reached
# __wp_negatives__ each variable's negative words
# __wp_multi__ multi-pick variables' items
# __wp_node_seed__ / __wp_loop_index__
# `__wp_debug_version__` lets the viewer tell this shape from a
# cached snapshot written by an older build.
flat: dict = {}
multi: dict = {}
for key, value in (context.context or {}).items():
if isinstance(value, ListVar):
multi[key] = {"items": list(value.items), "sep": value.sep}
flat[key] = value
debug = context.debug or {}
internals = context.internals or {}
flat["__wp_debug_version__"] = 2
for key in ("__wp_trace__", "__wp_warnings__", "__wp_ref_log__", "__wp_nodes__"):
if debug.get(key):
flat[key] = debug[key]
for key in (
"__wp_picks__",
"__wp_internal_flags__",
"__wp_axes__",
"__wp_constraint_hits__",
"__wp_negatives__",
):
if internals.get(key):
flat[key] = internals[key]
if "__wp_loop_index__" in internals:
flat["__wp_loop_index__"] = internals["__wp_loop_index__"]
if multi:
flat["__wp_multi__"] = multi
if "node_seed" in debug:
flat["__wp_node_seed__"] = debug["node_seed"]
snapshot = json.dumps(flat, default=str, indent=2)
return io.NodeOutput(ui={"wp_debug_snapshot": [snapshot]})