Combine handler now honors two new instance fields the v2 modal will
write:
- template_override (string | None) — full per-instance template
replacement; falls back to payload.template when null. Same precedence
pattern wildcard's option_weights and fixed_values' values_overrides
use.
- locked_seed (int | None) — pins {a|b|c} resolution so the same seed
reproduces the same roll. RNG derived via shared
engine.modules._seed.derive_module_rng using output_var as the
per-module key. Without locked_seed, falls back to the chain seed
(existing behavior).
Both paths feed the SAME derive_module_rng(seed, output_var), so locking
with locked_seed = chain_seed reproduces the unlocked pick exactly —
that's the contract the lock UX relies on.
5 new tests: template_override precedence, null collapse, locked_seed
determinism, statistical independence across seeds, unlocked uses
chain seed.
Combine semantics: read upstream bindings + emit one composed
output_var. When a template references a var nothing upstream has
bound (e.g. user mistypes "$colur" or removes a wildcard), the
resolver silently collapsed the token to empty string and the
combined output came out wrong with no signal — user had to
reverse-engineer "why is half my prompt blank?"
Add a `unknown_var` warning at the lone _resolve_var call site:
type "unknown_var", severity "warn", detail carries the var name +
the surface. Surface-agnostic — wildcards, derivations, and
constraints get the same signal when they reference a var that
isn't in ctx. Internal-key lookups (`__wp_*` engine bookkeeping)
are still skipped silently — those legitimately won't be in vars.
The TODO at the original site (before this commit) acknowledged
the silent-empty behaviour was a gap pending user feedback. User
asked for it explicitly today.
Tests:
- combine handler now asserts the warn fires + carries the right
detail when a template var is missing
- sanity test that bound vars don't trigger the warning
CombineHandler now calls resolve_text(template, build_resolve_ctx(ctx, surface="combine"))
replacing the manual $TOKEN_RE substitution loop. Unknown $vars emit "" (resolve_text
contract) rather than passing through unchanged; @{uuid} refs in combine surface are
lenient — emit empty string + ref_out_of_surface warning.
DerivationHandler wraps action.value through resolve_text with surface="derivation"
before applying the replace/append/prepend mode, enabling $var interpolation and
{a|b|c} picks inside action values.
ConstraintHandler is a pure metadata pass-through that records reweighting matrices
for future WildcardHandler consumption. It never processes user-authored text strings,
so no migration is required (no-op verdict).
Tests updated: combine tests drop ctx=None path and the pass-through unknown-var
assertion (both incompatible with resolve_text contract); derivation tests add
__wp_rng__ + __wp_warnings__ to all ctx fixtures; two new combine surface tests
added to tests/test_handlers.py per task spec.