Files
DumiFlex-ComfyUI-Wildcard-P…/engine/context.py
T
DumiFlex c4e1663210 fix(axes): carry axes across writers, resolve them in previews and the assembler
Three separate places dropped an axis read on the floor:

- The producer map replaced a variable's record wholesale on every later
  write, so a derivation or injector rebinding $outfit erased the wildcard's
  axes. The engine keys __wp_axes__ by binding at pick time and a rebind
  never clears it, so the axes must survive the override. Carried forward in
  both the graph walk and the same-node sibling merge.

- The static preview resolver knew only the numeric .K accessor, so
  "$outfit.SHOES" substituted $outfit and stranded ".SHOES" — the preview and
  resolved lines both read "a white t-shirt and denim skirt.SHOES". It now
  resolves an axis to its first tag, the same deterministic convention @{uuid}
  expansion already uses, and leaves an accessor it cannot resolve verbatim
  rather than deleting text.

- strip_internals runs at the prompt-render boundary and dropped every
  __-prefixed key, __wp_axes__ and __wp_picks__ included. The assembler
  therefore rendered "$outfit.SHOES" and "$outfit.0" as literal text while the
  identical template resolved one node upstream. Those two tables are read by
  key, never as variables, so keeping them cannot leak a $var.
2026-08-19 00:51:50 +03:00

82 lines
3.3 KiB
Python

"""Context type definitions and internals helpers.
The engine uses a flat ``dict[str, Any]`` for context. Keys starting with ``__``
are reserved for engine internals (seed, trace, internal flags). User-defined
variables use plain identifier names.
"""
from __future__ import annotations
from typing import Any, TypedDict
class ContextInternals(TypedDict, total=False):
"""Documents the reserved internal keys. Not enforced at runtime."""
__wp_node_seed__: int
__wp_internal_flags__: dict[str, bool]
__wp_trace__: list[dict[str, Any]]
Context = dict[str, Any]
#: Engine tables the text resolver reads by key rather than as variables.
#: `strip_internals` keeps these so accessor syntax (`$var.0`, `$var.AXIS`)
#: works at the prompt-render boundary, not just inside the pipeline.
_RESOLVER_TABLES = ("__wp_axes__", "__wp_picks__")
def strip_internals(ctx: dict[str, Any]) -> dict[str, Any]:
"""Return a copy of ``ctx`` with engine-only + user-flagged-internal keys removed.
Use at the *prompt render* boundary (PromptAssembler) — both engine
bookkeeping AND user-marked-internal vars are filtered:
1. ``__``-prefixed keys: engine bookkeeping (``__wp_node_seed__``,
``__wp_trace__``, ``__wp_internal_flags__``, …).
2. Keys whose name appears with ``True`` in the
``__wp_internal_flags__`` map: user-marked-internal bindings
from modules with ``instance.internal == True``. These vars
drive composition (other modules can read them) but must not
appear as standalone ``$var`` substitutions in the rendered
prompt — that's the "internal" UX promise.
For the *socket* boundary (PIPELINE_CONTEXT carrying state between
Context nodes), use ``strip_engine_internals`` instead — it drops
only the ``__``-prefixed engine keys so user-flagged internal vars
continue to propagate through downstream Context / Combine / etc.
until they reach a PromptAssembler that filters them at render
time.
"""
flags = ctx.get("__wp_internal_flags__")
internal_names: set[str] = set()
if isinstance(flags, dict):
internal_names = {k for k, v in flags.items() if v}
out = {
k: v for k, v in ctx.items()
if not k.startswith("__") and k not in internal_names
}
# The accessor tables are not variables — the resolver reads them off the
# ctx dict by key, and `_vars` already excludes every `__` name, so they
# can never surface as a `$var` substitution. Dropping them here is what
# made `$outfit.SHOES` and `$outfit.0` resolve in a Combine but render as
# literal text in the assembler: same grammar, different boundary.
for key in _RESOLVER_TABLES:
if key in ctx:
out[key] = ctx[key]
return out
def strip_engine_internals(ctx: dict[str, Any]) -> dict[str, Any]:
"""Return a copy of ``ctx`` with only the ``__``-prefixed engine keys removed.
User-flagged internal vars stay in the result — they need to
propagate across Context-node boundaries so downstream Combine /
Derivation / Constraint modules can read them. Only the final
PromptAssembler filters them out at render time (via
``strip_internals``) so they don't appear as standalone ``$var``
substitutions in the prompt text.
"""
return {k: v for k, v in ctx.items() if not k.startswith("__")}