A derivation branch (and its ELSE) now runs its action and then each
entry of an optional extra_actions list, in order; each action sees the
previous one's write. The library editor gets "+ And" rows, the canvas
modal shows every action and gives each its own value override (keyed
"bi.K" / "else.K"), and WP Debug lists every action a branch ran.
$name.neg / $name.K.neg reads the negatives a variable carries. It rides
the accessor grammar's axis slot, so "neg" is reserved as an accepts
group name. Reading .neg is not a read of the variable, so it carries no
negatives along. Works in values, combines, the Assembler's negative
template and conditions; autocomplete offers it after the dot.
Both stamp schema v8. Cascade fixers and scans now walk the real
derivation action shape (action, extra_actions, else) instead of a stale
"actions" list, and read-only previews keep a var chip's accessor.
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.
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.
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.