Chat round (explain on selection, page starters, reactions, streamed notes, thinking), negatives on node fixed values and combines, Pack builder quality fixes from 31 real-model eval rounds, and small-model support (scoped tools, checked tool arguments on local servers, two-phase steps, fixed prefix shown in Test connection).
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.
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.
Samples always has a column for the output variable, so a one-module
stack no longer shows a table of bare seeds.
The seed trace now shows the nested @{ref} picks each module made, as a
tree, with what each ref expanded to. The engine records them only when
the caller seeds ctx["__wp_ref_log__"] (the Test Runner does; canvas runs
don't), and nested picks count toward their wildcard's pick totals.
A stack card now says when it reads a $var that no pin and no earlier
item sets, which is the usual reason a combine keeps producing the same
text, and the Outputs tab points at it when the output never changes.
The trace no longer shows "no change" under a module that wrote values.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xwq95EvrKu6WgE9D5rahKy
When the picked option carried no tag on a declared accepts axis, the run
warned "$outfit has no 'SHOES' axis — is that tag group marked
'accepts'?", sending users to fix a group that was fine. Wildcards now
record which accepts axes they declare (`__wp_axis_decl__`, carried across
nodes and held seeds like `__wp_axes__`), so the resolver can tell the
cases apart: an untagged pick becomes an info note, and a genuinely
unknown axis names the axes that do exist.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BxeNnfTMTyaKQwCYWA6frg
`$outfit.K.AXIS` compacted away picks whose option has no tag on the
axis, so every later index slid down and `$outfit.0.SHOES` could print
pick 1's shoe beside pick 0. Both the resolver (`get_axis`) and the
derivation condition reader now keep one slot per pick.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011fQ7Lkc8AxwXqsRZrFBxE9
get_axis sits beside get_var as a plain dict lookup over a projection
build_resolve_ctx makes at frame construction, so the resolver stays a pure
consumer with no ctx dependency. Keyed by binding, matching what the
reference addresses.
Branches before the var lookup: $outfit.SHOES must resolve even when
nothing bound $outfit itself, and must not trip unknown_var on the way
past. Never routes through deref_var_value either - an axis value is not a
ListVar, so the SP2a accessor contract is untouched.
A missing axis renders empty and warns unknown_tag_axis rather than
failing, matching how every other unresolvable reference behaves.
Replace the constraint one-shot/consumed-set model with a reach selector:
each constraint's target_select (default {mode:"all"}) decides which firing
target instances it covers, and every covering constraint is applied +
combined sequentially. A per-constraint hit counter (__wp_constraint_hits__)
replaces the consumed-set; reach is downstream-relative (a constraint is only
in ctx after its module ran).
- apply_constraints_for_target rewritten: hits/firing_uid/carrier_ctx params,
_occurrence_matches matcher (carrier_ctx None from callers — nested-pick
threading is a later task). consumed= kept accepted+ignored for back-compat.
- claim_carrier_constraints deleted (the positional spill failsafe is obsolete
without consumption); its import + both wildcard_handler call sites + the
_claim_carried helper removed.
- resolve.py nested path shares the ctx hit counter (get_constraint_hits) so
first/next coverage spans direct + nested encounters; carrier_ctx=None.
- pipeline.py finalisation now hits-based: constraint_never_applied when a
selector covered zero, new constraint_partial_reach when next N / pick
matched fewer than asked.
- cross-node: __wp_constraint_hits__ replaces __wp_consumed_constraints__ in
_CROSS_NODE_INTERNAL_KEYS; build_payload dedup also collapses partial_reach.
Existing one-shot tests adapted to the new semantics:
- test_constraint_first_instance: the implicit "fires once" tests re-expressed
via explicit target_select={mode:"first"}; added an all-mode-default test and
a partial_reach finalisation test. The pre-seeded-consumed no-op test became a
first-mode "counter already past 1" test (consumed-set concept gone).
- test_user_chain_simulation: consumed assertions → hit-count assertions; the
carrier-claim tests (no-spill, null-pick claim, source-not-picked, sibling
one-shots) rewritten to the new model — a carrier that doesn't resolve the ref
no longer "claims" anything, so the constraint applies on the next actual
resolution (or legitimately never fires → never_applied).
- test_module_interactions: "only first fires" composition test replaced with a
two-all-constraints-compose-on-one-target test (both excludes zero the pool →
excludes-all fallback), plus a single-all-constraint survivor test.
- Constraint first-instance one-shot: each constraint claims first
downstream target wildcard instance then is consumed. Engine tracks
via ctx __wp_consumed_constraints__ across pipeline + nested ref
resolves. Emits constraint_never_applied (info) for unused at end.
- Null wildcard option: is_null flag opts into no-output slot that
survives category filter + matrix constraint. Reserved sub_category
name guard forbids null as real sub-cat.
- Inverted nested-ref null filter (2026-05-25): null keyword in filter
list now EXCLUDES wildcard null option. Default keeps null in pool
alongside listed sub-cats.
Three improvements aimed at making constraints actually useful in
composed pipelines, with the conflict scanner keeping up.
## Phase A — nested constraints (engine)
Pre-2026-05 constraints only fired when the target wildcard ran as a
top-level chain module. Wildcards reached through `@{uuid}` refs from
inside another wildcard's option value silently bypassed every rule.
Composition (outfit-set wildcards, palette reuse) was a constraint
blind spot.
- New `engine/modules/_constraints.py` holds the shared application
logic (loop constraints by target uuid, look up source pick, apply
matrix/exception cells, emit unknown_constraint_source +
constraint_excludes_all_options warnings)
- `wildcard_handler.resolve` now delegates to the helper instead of
inlining the loop
- `engine/syntax/resolve._resolve_ref` calls the same helper after
fetching the catalog wildcard but before rolling — nested target
wildcards now get constrained identically to chain-level ones
- `ResolveContext` exposes `get_constraints()` + `get_picks()` so the
resolver has the back-channel it needs without depending on the
full pipeline ctx dict
## Phase B — `@{uuid:subcat[,subcat]}` filter
New optional ref syntax. `@{color:warm}` resolves the catalog wildcard
`color` but restricts its option pool to entries whose `sub_category`
is in the filter list. Same semantics as chain-level
`instance.category_filter`, but scoped per call site so one library
wildcard stays reusable.
- Python tokenizer regex extended to capture optional `:subcat,subcat`
group; `_resolve_ref` filters options pre-pick + pre-constraint
- TS `richTokenize.ts` mirrors the grammar so previews + RichTextInput
recognise the new form
- `extension/graph.ts:WC_REF_RE` + `preview-tokens.ts:REF_RE` updated
to skip the filter when extracting uuids (uuid is still group 1)
- Empty pool after filter → empty string output + new
`ref_subcategory_empty_pool` warning so the user sees an
unsatisfiable filter instead of falling back silently
## Phase C — scanner reach-set + classification refinement
Constraint scanner pre-fix:
- flagged `constraint_target_in_upstream` whenever target had ANY
upstream instance, even if a downstream / local-after / nested-after
instance was also present
- flagged `constraint_target_missing` for targets reachable only
through nested `@{}` refs
Both false-positives. Post-fix scanner computes
`localNestedReachAfter[i]` — transitive `@{}` walk through the local
catalog starting from modules at positions ≥ i — and uses it to:
- skip `target_in_upstream` when a bindable instance exists elsewhere
(downstream, local-after-self, nested-after)
- skip `target_missing` when target is reachable via nested ref from
any later chain wildcard
- still flag both when no bindable instance exists (sole-upstream,
truly-missing both remain caught)
## Tests
- 8 new Python tests under `TestNestedConstraints` +
`TestNestedSubcategoryFilter` (constraint applies through nested,
excludes-all warning, source-not-yet-picked warning, subcat narrows,
multi-subcat union, empty pool warning, empty filter no-op,
subcat+constraint composition)
- 7 new TS tests under `scanConflicts — constraint nested-reach
classification` (nested reachable doesn't flag missing, transitive
deep reach, upstream+downstream silent, upstream+local-after silent,
sole-upstream still flags, nested-from-before-constraint still
flags, nested with subcat filter still reaches)
## Numbers
- 696 pytest pass (+8)
- 1672 vitest pass (+7)
- ruff clean
- bundle 306899/307200 (301 bytes headroom)
Pipelines never paid off as a separate module kind, so peel out:
- Engine: drop pipeline_handler, remove "pipeline" from valid module
types in repositories + schema parity.
- Manager: delete Pipelines/PipelineEditor views, PipelineSteps +
PipelineStepPicker components and their tests. Router, sidebar,
topbar, dashboard, TestRunner, ImportExport, resolver and
bundleSelection repointed off the removed routes/types.
- wp_api: drop pipeline branch from modules.py + import_export.py
(and its test).
In its place, add a unified library view:
- src/manager/views/AllItems.vue — modules + bundles in one filter
panel with kind multi-select (OR across the 6 kinds). Reachable
via the Dashboard "View all" link.
- ModuleListView grew the slots/props AllItems needs; BundleAddChild
+ Dashboard + ImportExport updated alongside.
Misc polish along the way: kind-icons / theme tokens / display-prefs
touch-ups, ContextWidget + DebugViewer + AssemblerHelper small fixes,
MockModuleCard + ModulePickerModal tweaks, _shared widget cleanup.