104 Commits
Author SHA1 Message Date
DumiFlex e41ae4ded0 feat(debug): trace steps say why they came out that way, per kind (#370)
Wildcard steps list their options with weights and why each was cut, the pool chain, locked seed, separator and matched value; fixed values, combines and node templates show what they read; constraint steps link the steps they re-weighted; Model Info, Toggles, Injector and Context Loop rows carry their own facts.
2026-11-12 02:33:22 +01:00
DumiFlex 4005954615 feat(debug): give WP Debug a view of its own for the Loop, Model Info, Toggles and Injector (#360)
Variables grouped into per-node cards with kind squares, module facts, toggle true/false and value + .bool, Injector type chips, Loop frame line; long values fold until Show all; .axis/.bool/tags as kit chips. Trace stays numbered run order. Show node blinks the module or field on the canvas. Grouped right-click menus with a kind header. Loop trace rows, node warning labels and links.
2026-11-12 02:12:23 +01:00
DumiFlex 4ff45d2492 feat(toggles): every toggle can have a Value and a Negative, plus $name.bool (#367)
A toggle with a Value writes it to its variable while on and nothing while off; its Negative goes with the variable. $name.bool reads the switch (true/false) across nodes, in text and in conditions, through __wp_toggle_bools__. Payloads reading $name.bool stamp schema 12; bool is reserved as a tag group.
2026-11-12 02:12:08 +01:00
DumiFlex 6f089755fd feat(wildcards): filter a wildcard's pool by a run-time variable (#309)
* fix(extension): explain an empty wildcard pool when no option has tags

* feat(wildcards): filter a wildcard's pool by a run-time variable

A $name term in a Pool filter or a reference filter reads the variable
when the prompt runs: options tagged with its value stay, options with no
tag in that value's group stay too. An unset variable or a value that is
no tag on the wildcard drops that term and files filter_variable_no_tag.
Constraints still apply on top. Stamps schema 11 (MAX_KNOWN 11).

* test(syntax): cover the filter variable helpers in the syntax suite
2026-11-09 14:11:10 +01:00
Dumitru Picalau 8d0fa337b2 chore: fixes from the 2026-10-21 review (#93)
Shorter release notes, SeedList help, small-model docs; _chat_turn split and pyright in CI; ESLint a11y ratchet and Modal focus trap; tags/models/Image Filter tests; AI LAN warning, web allow expiry, one thinking setting per answer on local models, recommended hints and Reset to recommended (16k context minimum, 32k recommended); Assembler Clear template clears the negative; Vue 3.5.44, SECURITY and Dependabot.
2026-10-22 06:15:13 +02:00
Dumitru Picalau 5056722e1a fix(ai): Claude walkthrough fixes, chat polish and a search results card (#82)
Pack builder: Fix all and updates say why nothing went out while the web search card waits; page follow-up lines are not request words; Only rows narrow only the tag groups they rule on; answers are not request gaps; code-wired story beats skip the lookup lint; left-out rules read plainly and get Fix with AI; a rule overwriting a pick becomes an append; model labels read "For Illustrious"; roomier plan head.

Chat: same-name edits refused with the shown item named; search by category for organizing; empty edit answers carry a note; prompt syntax coloured inside cards; every chat format named in the prompt; pointer captions avoid each other; one shared working line.

Search: what an allowed search found shows as a card in the Pack builder and the Draft with AI panels; Use what fits sends all results for the model to keep what fits, ticking sends only those, None of these sends nothing.
2026-10-21 22:05:41 +02:00
Dumitru Picalau 26df88216f feat(ai): chat round, node negatives, pack builder quality fixes (#69)
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).
2026-10-17 01:23:58 +02:00
Dumitru Picalau df9a19a624 fix: engine rng keying, widget teardown, import validation and connection leak (#72)
Derivation and fixed values modules roll on their own rng stream (seeds through their alternations can resolve differently). DOM widgets tear down when their node is removed (effect scope, cleanups, shared onConnectionsChange wrapper, seed state in WeakMaps, capped topbar rAF loop). /import/commit validates module and bundle child payloads. WP_Context and preview close their library connection; the node path migrates once. Pinned option missing warns. CLAUDE.md says which parts of the schema machine are wired; js budget 473 KB.
2026-10-17 01:07:34 +02:00
Dumitru Picalau 657dc07831 feat(ai): assistant overhaul: tool registry, server-kept chats, search index, security, page tools, web search, saved chats, images, permissions, derivation NOT (schema 10), OpenAI Responses API (#64)
Phases 2 to 8 of the AI assistant overhaul plus fixes from real-model chats: one tool registry with native tool calls, server-kept chat logs with prompt caching, an FTS5 library index with opt-in meaning search, keys bound to their address and a guard on every AI route, page tools and opt-in web search, saved chats with streaming, Try again, a context meter and compaction, chat images, delete/category/tag permissions with Undo, constraint and test cards, card fixes and previews, questions with answers to click, the derivation NOT toggle (schema v10, community catalog 0021), request fixes for the newest Claude models, and OpenAI's Responses API. Review fixes: Try again clears the old turn's fixes and answers, category Undo removes only what it made, a failed delete rolls back in order, not_matches patterns are checked, busy chats refuse a second request, and chat search ignores stored JSON.
2026-10-14 14:56:12 +02:00
Claude 929cd53652 feat: add a model info node and match-variable wildcards for model-aware pipelines
WP Model Info writes $model_family (from the model's config class),
$model_variant (file-name rules) and $model_name into the Context, reading
the checkpoint name by following its MODEL link back to the loader. Graph
walkers, the assembler preview and WP Debug know the new variables.

A wildcard instance can now match a variable instead of rolling, so a
model wildcard becomes a constraint source. The derivation editor gains a
LoRA by model preset. Docs, node help, README and release notes updated;
bundle budget raised to 432 KB.
2026-10-06 03:53:50 +00:00
Claude 417353e92f feat(wildcard): fallback option rolls normally and also stands in
The flagged option now keeps its weight and takes part in normal draws.
It is still taken, without a draw, when constraints or weights leave
nothing to pick. Flagging an option never changes a normal pick, and a
fallback weighted 0 only ever stands in. Editors give it its normal
share and show a fallback chip beside it.
2026-10-02 16:42:36 +00:00
Claude a11ceaac1c feat(wildcard): fallback option used when nothing else is left to pick
A wildcard option can be marked as the fallback. It never takes part in a
normal draw and is used only when constraints (an Only rule included), a
filter or zero weights leave nothing else to pick, instead of resolving to
an empty string. Works for top-level wildcards and nested @{} refs, and
emits an info-level `fallback_used` note in place of
`constraint_excludes_all_options`.

- engine: engine/modules/_fallback.py shared by the handler and
  ref_option_pool; payload validation allows one boolean fallback on a
  non-null option
- schema 9 (additive): FALLBACK_SCHEMA_VERSION stamped in TS and Python,
  MAX_KNOWN_SCHEMA_VERSION 9, strict validator keeps the flag, parity
  fixture regenerated
- library editor: "set as fallback" on an option's probability cell, a
  fallback chip, its weight disabled and excluded from the shares
- canvas: FALLBACK badge on the option row, no share in probabilities
- WP Debug: "Fallback used" label and step fact
- docs, release note, CLAUDE.md
- bundle budget raised 420 -> 424 KB (maintainer approved)
2026-10-02 16:10:31 +00:00
Claude 8d24c8822d feat(derivation): stacked branch cards on the canvas and a Replace negative action
The node modal's Rules section shows each IF / ELIF / ELSE as its own card,
with one row per test and per action and a full-width value field, so long
values wrap instead of being squeezed into a table column. Nested AND / OR
groups read as indented rows under an "ANY of" / "ALL of" header. An empty
field shows the library value (chips included) as a ghost; an overridden one
lights up with the library value underneath and a "Use library" reset.

Derivations get a fifth action mode, `negative_replace` ("Replace negative"):
the value becomes the target variable's only negative, and the variable's own
text is left alone. Schema v8, like "Add to negative".
2026-09-30 16:25:49 +00:00
Claude add6506225 feat(derivation): run several actions per branch and read negatives with $var.neg
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.
2026-09-30 15:33:01 +00:00
Claude 9b567f38d8 fix(canvas): toggle fixed values one at a time and keep wildcard icons purple
Fixed-value rows without ids were all keyed by the same missing id, so
disabling one disabled every row. Rows are now keyed by their id, or by
their position when the id is missing or repeated, in the canvas editor and
the engine alike.

The wildcard type icon took its color from a bare selector that tied with
the shared grey default, which several lazy chunks carry. Whichever chunk
loaded last won, so wildcard icons turned grey after opening a modal or
adding some nodes. A data-kind selector now outranks it.
2026-09-30 14:50:17 +00:00
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
Claude 4d178e2b7e feat(debug): record per-module run detail for WP Debug
Canvas Context runs now set an explain flag. With it, each trace row carries
the module's display name and a handler detail dict: which derivation branch
fired, with every test's result and the value it read; a wildcard's option,
odds, pool and the constraints that re-weighted it; a constraint's reach and
rule counts. Warnings a module raised name it as their owner, and trace rows,
warnings and nested @{} picks are tagged with the Context node's graph id.
WP Debug forwards the nested-pick log, node seeds, internal flags, rolled
axes, constraint hits and multi-pick items.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wwo837fQ1tm6RacaQGYom1
2026-09-29 14:00:48 +00:00
Claude 4449bd9c82 feat(derivation): combine branch tests with AND / OR groups
A derivation branch condition can now be a group ({match: all|any,
conditions: [...]}) of tests and nested groups. The manager editor gets
+ Condition / + Group controls and a clickable AND/OR connector; the
canvas modal shows the whole expression and one value override per test
(override keys "bi" and "bi.k"). The engine gains is_empty and
is_not_empty ops, which the editor already offered.

Payloads using groups or the emptiness ops stamp schema 7
(MAX_KNOWN_SCHEMA_VERSION 6 -> 7, additive, no migrator).

Also fixes the derivation editor rewriting presence ops to equals when a
saved derivation was reopened.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DkgtCKNLjk77oiA5efKPia
2026-09-28 22:15:39 +00:00
Claude 4954b65638 Merge main into the Test Runner branch
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xwq95EvrKu6WgE9D5rahKy
2026-09-26 14:48:37 +00:00
Claude 40a466ec2c feat(manager): show nested wildcard picks and unset reads in the Test Runner
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
2026-09-26 03:59:12 +00:00
Claude 3bce46b28d feat(constraints): add the only rule for linking picks
A constraint rule with mode `only` turns its row into an allow-list: when
the source fires, a target with no rule of its own in that row is excluded.
On an exception it links literal values (maid forces frilled apron); in a
matrix row it links tags. Before, a link needed an exclude rule for every
other target, and a target added later slipped through.

The rule itself weighs 1.0 and targets with any other rule keep it. A matrix
`allow` cell does not count as a rule because the editors show it as
Neutral. The axis-menu probe ignores value-level `only` so a tag link in
the same constraint still pins an accepts roll. Python and TS folds stay in
step through new constraint-corpus cases.

Also reads a legacy `neutral` cell mode override as `allow`: the canvas
matrix stored that value, and the engine rejected it, failing the whole
constraint.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0137To3tFTPKen2wYqEo3wme
2026-09-25 22:17:30 +00:00
Claude aef082964d fix(axes): say when a $var.AXIS read is empty because the pick has no tag
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
2026-09-25 21:25:26 +00:00
Claude 4afbb5a08e fix(axes): keep multi-pick axis reads positional
`$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
2026-09-25 20:18:14 +00:00
DumiFlex 934f7dc92d fix(axes): drive constraints by the rolled tag, fix the assembler read, preserve on import
An accepts axis rolls one tag per pick (read as $var.AXIS), but a constraint
sourced from it folded over the option's whole menu instead of that rolled tag,
so $outfit.SHOES and the constrained $shoes could disagree. The source now
sends only the rolled winner; a target accepts axis OR-folds so a multi-tag
option survives when any alternative is allowed, then its own roll is pinned to
the allowed tag -- $source.AXIS == $target.AXIS under a diagonal. Classify
groups are unchanged (multiply).

The assembler also rendered $var.AXIS empty: __wp_axes__ crosses the socket on
ContextPayload.internals but the node only merged the internal-flag map; it now
merges the whole carve-out so the accessor resolves.

Import: the community validators silently dropped tag_group_kinds (and the
constraint target_select) because the lenient payload object never listed them,
degrading a shared accepts axis to classify. Both are now in the v2/v1
validators + the engine-parity fixtures, with preservation tests. Engine math
mirrored in TS via the shared corpus.
2026-08-20 23:58:44 +03:00
DumiFlex 05d53a0c6c fix(wildcard): an excluded pool emits nothing instead of the first option
_pick_weighted returned options[0] when every weight was zero. That looked
like a working pick - a real option, right shape, right place - and stayed
wrong silently until the list was reordered and the choice changed. It hid
both over-narrowed constraints and a wildcard whose options were all
disabled.

Now returns None, which every caller already turned into an empty binding.
That is what the null option means, so the engine had the vocabulary for
this already.

Applies to all three pools that constraints can zero: the chain handler, the
nested-@{} path in the resolver, and the SPA preview. The inline {a|b}
branch weights are left alone - a user writing {0::a|0::b} is a different
situation and constraints never touch it.

Three tests asserted the old behaviour and are rewritten to state the new
one; the constraint_excludes_all_options warning stays, and now
distinguishes an over-narrowed pool from a legitimate null roll.
2026-08-18 17:38:05 +03:00
DumiFlex f0117d4230 fix(constraints): a multi-tag source no longer excludes its whole target pool
The reported failure: an outfit option tagged sneakers, high_heels and
sandals excluded every shoe in the constrained target, the pool summed to
zero, and _pick_weighted fell through to options[0] forever - looking like a
working pick until the list was reordered.

The kind map is derived from the pick records rather than passed down from a
payload, since a pick only carries an axes entry when its source declared
that group accepts. That keeps combine_constraint_factor pure and makes a
legacy pick with no axes take the unchanged flat-product path.

Also fixes the SPA preview resolver, which mirrors the applier: it dropped
axes when building its per-pick list, so the preview would have shown a
narrower pool than the run actually produced.
2026-08-17 22:58:23 +03:00
DumiFlex 5d910f5f39 feat(derivation): let a condition test a rolled axis
Conditions store condition.var as raw text rather than tokens, so this path
parses the accessor itself via parse_var_reference. _ctx_set is left on
split_var_accessor: an action writing to $outfit.SHOES should still target
the base var rather than pretend to write an axis.

_ctx_has checks the axes bucket rather than ctx, since $outfit.SHOES can
exist while outfit is unbound and vice versa.

Action values needed no work - they already route through resolve_text.
2026-08-17 22:52:52 +03:00
DumiFlex 14edfa3458 feat(syntax): resolve axis reads from the rolled ctx projection
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.
2026-08-17 22:48:53 +03:00
DumiFlex f4444394e5 feat(wildcard): roll one tag per accepts axis at pick time
Two consumers address a pick differently, so each gets the key it addresses
by. The constraint fold needs a tag-to-axis map and binds by module uuid, so
that rides on the pick entry. A $var.SHOES read binds by variable, so the
rolled winner goes to __wp_axes__ keyed by binding - which also means the
read never inherits the uuid-bucket collision.

Rolled at pick time, never on read: that is what makes two modules, or two
nodes, agree. The draw happens AFTER the option draw on the same rng, so an
existing locked_seed still reproduces its option.

Multi-select rolls per pick in pick order, so $outfit.SHOES mirrors the
shape of $outfit.
2026-08-17 20:49:47 +03:00
DumiFlex b6e6f55cce feat(wildcard): validate tag_group_kinds on the payload
A group may declare itself classify (the default, and what every stored
payload means) or accepts. Only an accepts name has to satisfy the
identifier rule, because only an accepts axis is addressable as $var.AXIS;
a classify group keeps whatever name it has today.

Also narrows the constraint weight multiply with isinstance rather than
sentinel identity, which the newly-typed fold made unprovable.
2026-08-17 20:16:41 +03:00
DumiFlex 7cd08f070c feat(constraints): fold accepts-axis tags with max instead of product
An option tagged with several tags from one group means those tags are
alternatives it offers, not statements that are all true at once. The fold
multiplied across them, so any sibling's exclude zeroed the option and a
source carrying three shoe tags excluded every shoe in the target pool.

Tags inside a group the source declared 'accepts' now fold with max.
Everything else still multiplies, and the path is unreachable without
axis_kinds, so a payload predating the feature folds byte-identically -
locked by the corpus case that omits the kind map.

Also replaces the bare object() sentinel with a narrowable class: 'is
EXCLUDE' never narrowed, leaving every 'factor *= r' unprovable.
2026-08-17 20:09:32 +03:00
DumiFlex 39266cb87b fix(constraints): reject two exception rules for the same pair
Reported by the maintainer: the editor accepted the same source/target
combination more than once with contradictory modes and factors. Only one ever
applied. `combine_constraint_factor` files exceptions into a dict keyed by the
pair, so the last one in the list wins and every earlier one is discarded.

Verified against the pure math, and the alarming direction is real:

    [boost x3, reduce x0.5] -> 0.5        not the combined 1.5
    [reduce x0.5, boost x3] -> 3.0        order decides the result
    [boost x3, exclude]     -> EXCLUDE
    [exclude, boost x3]     -> 3.0        the exclusion is thrown away

That last line contradicts the documented model in CLAUDE.md, which says
constraints combine "with `exclude` (factor 0) absorbing". Within duplicate
exceptions it neither combines nor absorbs, and the deciding factor is list
position -- which no surface displays and nothing lets you edit.

Enforced at the AUTHORING boundary, not in `validate_payload`. `resolve` calls
that on every graph run, so a rule there would turn an already-stored payload
into a hard failure mid-execution -- strictly worse than the silent
misbehaviour it replaces, and unreachable for a user who cannot open the editor
without the graph running. Handlers may now expose an optional
`validate_authoring`, called by the module create/update paths and by bundle
child validation; existing rows stay executable.

Engine math is deliberately unchanged. Making duplicates combine would have
been defensible -- it is what the docs describe -- but it silently alters
re-weighting for anyone who already has them, and "you cannot create this
anymore" achieves the same end without touching anyone's output.

Scanned the maintainer's library first: 21 constraints, 0 duplicate pairs, so
nothing existing is affected either way.
2026-08-06 17:27:30 +03:00
DumiFlex 1224d35010 fix(refs): discover @{uuid} refs written outside a wildcard payload
A nested ref in a derivation resolved to empty whenever the wildcard it points
at was not ALSO picked into the same Context node — no matter what the live
library held, and no matter how the ref was filtered.

Root cause is discovery, not resolution, which is why it looked like a
resolution bug. The catalog is synthesised from wildcard modules only, and
`walk_transitive_refs` was seeded from those uuids and followed refs found in
wildcard payloads only. A `@{uuid}` is legal on other surfaces too — most
importantly a derivation rule's action value, and the per-instance
`action_value_overrides` the edit modal writes when a user retargets a branch
on the canvas. Neither was ever scanned.

So such a ref's target reached the catalog only by coincidence: when the user
happened to also have that wildcard picked in the node. Removing it — a
reasonable thing to do once a derivation references it by uuid — dropped it
with nothing to put it back, and the ref became `unknown_ref` → empty string.

`ref_seed_uuids` (engine/modules/snapshot.py) now collects refs from every
module's `payload` AND `instance`, and both catalog builders seed the walk
with them. The instance half matters on its own: a canvas-side retarget lives
there and is not part of the library payload the walker would otherwise see.

Both builders are fixed, not just the graph one. `wp_api/preview.py` had the
same seeding, so the SPA preview resolved against a pool the engine would not
use — that is precisely how "the SPA says 1 of 11 options match, the graph
says 0 of 10" arose. Fixing one and not the other would have left the two
disagreeing in the opposite direction.

Embedded-first precedence is unchanged: a picked module still shadows the
library, so drift detection and workflow reproducibility hold. This only adds
targets that were previously absent entirely. An unresolvable uuid still warns
and resolves to empty rather than raising.
2026-08-01 19:42:27 +03:00
DumiFlex df76abdbb0 fix(engine): derivation hold freezes each branch roll, not the branch choice
Hold was replaying the derivation's frame-0 OUTPUT, which froze WHICH branch
fired -- wrong. A derivation is a transform: the condition should still pick
the branch from the current inputs each iteration; only the random roll
inside a branch should freeze. Now hold resolves with the constant hold seed
keyed PER BRANCH (rule:branch), so each branch's inline {a|b} / @{} value is
frozen to its frame-0 roll independent of which other branches fired -- but a
varying input still switches IF->ELIF and uses THAT branch's frozen value
(== what that branch would have rolled at frame 0).

Drops the now-unused __wp_module_outputs__ capture from pipeline.run (the
freeze-output replay was its only consumer). wildcard / combine /
fixed_values still freeze the whole resolved VALUE via __wp_hold_base_ctx__,
because a constraint can reshape their pool so a frozen seed isn't enough --
a derivation branch's pool is fixed, so per-branch seed freeze suffices.
2026-07-01 03:17:58 +03:00
DumiFlex ff6825cedd feat(engine): derivation honors seed_scope=hold (freeze frame-0 output)
A derivation is a transform that writes a DYNAMIC set of vars (whichever
rules fire), so it can't reuse the single-binding hold path. The pipeline
now captures each module's frame-0 output dict keyed by per-instance uid
during the hold base pass (__wp_module_outputs__); the derivation handler
replays its captured dict verbatim on hold. The captured values are
post-resolve, so inline {a|b} picks AND nested @{} refs freeze too -- and
the output stays frozen even when the derivation's INPUT varies (the same
value-freeze trade-off a constrained wildcard makes).

Adds derivation to HOLD_KINDS + a "Hold across run" toggle on the
derivation RuntimeSection. The row right-click Hold item picks it up for
free via HOLD_KINDS.
2026-07-01 03:17:58 +03:00
DumiFlex 58c5b27922 fix(engine): hold the resolved VALUE across iterations, not just the seed
seed_scope=hold previously swapped in a constant seed and re-picked each
iteration. That failed two ways: (1) a held wildcard whose pool is reshaped by
a constraint (varying source) picks a different option from the same seed; and
(2) under Override seed, the 'hold' seed equalled the per-iteration derived[k]
(effective_chain_seed returns an override verbatim, ignoring loop_index=0), so
hold was a no-op. Nested @{} refs in a held value also re-rolled.

Now hold freezes the frame-0 RESOLVED value. context_node computes a constant
hold seed from the loop's derived-series head; pipeline runs a throwaway base
pass at loop_index=0 when a loop drives + any module holds, and the
wildcard/combine/fixed-values handlers copy their resolved value from that base
context verbatim — skipping the pick AND resolve_text, so a constrained or
@{}-bearing held wildcard stays frozen (green jeans stays green jeans).

Tests: constrained held wildcard + nested-ref held wildcard each stay constant
across iterations while their vary counterparts move.
2026-07-01 03:17:58 +03:00
DumiFlex 15c1ade3ef feat(engine): fixed_values honors hold seed_scope 2026-06-26 06:43:28 +03:00
DumiFlex 8871a2806d feat(engine): combine honors hold seed_scope 2026-06-26 06:43:28 +03:00
DumiFlex 072e4e7836 feat(engine): wildcard honors hold seed_scope, locked_seed still wins 2026-06-26 06:43:27 +03:00
DumiFlex 9071dfba6d fix(engine): bind constraint to its own bundle copy's source pick 2026-06-12 20:23:36 +03:00
DumiFlex febb9f3e70 feat(engine): capture constraint's own bundle_origin on registered meta 2026-06-12 20:09:40 +03:00
DumiFlex d6094ed393 feat(engine): record per-origin source pick view in __wp_picks__ 2026-06-12 20:03:02 +03:00
DumiFlex 96fc26a1ae docs: flag pick-bucket collision + deprecate dead remapBundleUuids 2026-06-12 12:59:19 +03:00
DumiFlex 0e9b5cfbbf refactor(constraint): centralise derivation branch-key format + py/ts corpus parity
Extract branch_carrier_key() (engine) and branchKey() (TS) so the derivation branch-key format (rule_id:bi / rule_id:else) has a single source per language; route all call sites through them.

Add branch_key_cases to tests/fixtures/constraint-corpus.json; the Py runner (test_constraint_corpus.py) and TS runner (constraint-pairs.test.ts) each assert their production helper against the shared fixture key, so the two formats cannot drift independently.

Resolved-output parity is engine-only (TS never resolves derivation refs); A1 already locks it.
2026-06-11 18:03:29 +03:00
DumiFlex 599c3b17f6 feat(engine): stamp derivation carrier (uid, branch_key) for nested reach
_apply_action now accepts carrier_key and stamps (module_uid, branch_key)
on the resolve context before calling resolve_text, enabling SP3 pick-reach
constraints to target wildcards nested inside a specific derivation branch.
Branch call site passes f"{rule_id}:{bi}"; else call site passes
f"{rule_id}:else".
2026-06-11 17:19:28 +03:00
DumiFlex d826c6ba61 fix(ts): pick reach stores bare _uid; partial_reach severity info + stale-comment cleanup 2026-06-11 12:49:23 +03:00
DumiFlex 4165b2e9f4 feat(engine): thread carrier uid + option id for nested-pick constraint reach 2026-06-11 06:42:16 +03:00
DumiFlex be1f765243 feat(engine): constraint reach selector (first/next/all) + multi-constraint combine; retire one-shot
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.
2026-06-11 06:21:57 +03:00
DumiFlex 8c3c9aedc2 feat(engine): validate + register constraint target_select; instance override field 2026-06-11 05:53:44 +03:00
DumiFlex f68d2ece81 feat(engine): per-pick recording + apply constraints via combine_constraint_factor 2026-06-11 05:41:40 +03:00