Commit Graph
49 Commits
Author SHA1 Message Date
DumiFlex 73a00a3760 fix(context): constraint-reach QA, node codenames, bundle-aware toolbar
QA + polish on the unmerged SP3 constraint-reach work, plus the node-id
redesign and bundle-aware bulk toolbar.

- reach badge reactivity: pairingBadgeEqual compares reach + pairingMapEqual
  compares contributors (context.ts) so a target_select edit re-renders.
- TargetReachSection: neutral section highlight + custom up/down count stepper.
- nested carrier renders ONE badge (constraint-pairs.ts: nested coverage goes
  to viaInbound only, not also contributors).
- instance target_select override reaches the badge: cross-node-pairings.ts
  folds the instance value over payload; the modal collapses to null only when
  it equals the library default (preserves an explicit "all"). Engine
  precedence (instance over payload) locked by tests.
- graph.ts writeBindings honors instance.variable_binding, fixing the duplicate
  shadow ("override") badge + missing-var scan + preview for renamed bindings.
- node identity: fixed, unique, POV-independent adjective-noun codename from the
  litegraph node id (node-codename.ts); read-only neutral chip; dropped the
  editable node_label + A/B/C letters.
- nested-pick label reads "@<target> in <carrier>" (target by display name).
- bulk toolbar: toggle-all flips bundle masters + top-level modules (not bundle
  children); collapse/expand-all flips bundle frames; buttons resized to the
  row-icon size.
2026-06-11 16:38:56 +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
DumiFlex dd785c5ca7 feat(engine): pure combine_constraint_factor + shared constraint corpus 2026-06-11 05:29:40 +03:00
DumiFlex e78592839f feat(spa): "Allow repeats" toggle for the wildcard pool multi-pick
Exposes the pick_independent instance field (engine resolution landed in 33b3600): instance schema + snapshot round-trip, TS instance type + _shell INSTANCE_FIELDS_PER_KIND registry (so reset-overrides + modified-state + the TS/Py parity gate all include it) + preview cache-key, and the canvas PoolSection 'Allow repeats' toggle (shown only while multi-pick is active; off = unique). Instance-only (per-node, canvas runtime), additive + tolerant — no community catalog change.
2026-06-10 00:09:10 +03:00
DumiFlex 33b3600bf8 feat(engine): independent (allow-repeats) multi-pick for the wildcard pool
The pool multi-pick (PICK min-max) now honors instance.pick_independent: when true it draws WITH replacement (repeats allowed) and the count is NOT clamped to the pool size, mirroring the inline ~ flag. Default (unique, pick_k_unique) path is byte-unchanged, so existing locked seeds reproduce. Foundation for the 'Allow repeats' pool toggle; schema (instance + library default) + TS parity + UI follow.
2026-06-09 23:44:25 +03:00
DumiFlex d3881c84a1 fix(snapshot): include SP2a pick fields in instance baseline + snapshot type 2026-06-08 22:11:11 +03:00
DumiFlex ace0486f9d fix(engine): guard junk pick counts + warn on multi-pick constraint source 2026-06-08 21:06:35 +03:00
DumiFlex d41be86fae fix(engine): fold list-valued vars in derivation + converter reads 2026-06-08 20:58:59 +03:00
DumiFlex 443db49546 feat(engine): multi-select wildcard binds a list variable 2026-06-08 14:31:26 +03:00
DumiFlex 13de622c6b feat(engine): multi-tag options + boolean sub-category filters (v2)
Option sub_category -> sub_categories[] (default []). Both pool filters (instance category_filter, nested @{} ref) become boolean expressions (and/or/not/parens; comma=or) matched against an option's tag set via the shared engine/syntax/subcat_filter matcher, applied BEFORE constraints. Exclude-null is a separate flag: instance exclude_null + the trailing !null ref marker, replacing the old 'null' keyword.

4-segment ref grammar @{uuid#name:expr!null} single-sourced across tokenize.py/scan.py/pipeline.py/_constraints.py (! excluded from name/expr captures); tokenizer REF meta now carries filter_expr + exclude_null instead of a comma-split list. scan.py findall unpack sites updated for the 4th group.

validate_payload: per-option sub_categories[] must be in the registry; optional tag_groups (UI-only axes, members in-registry, <=1 group); registry names validated via subcat_filter.validate_subcat_name (rejects whitespace, boolean/ref chars, reserved words incl. null). wp_api/_validators delegates to the same function.

Constraints stay alive via a primary-tag stopgap: _apply_constraint_to_options + _record_pick use sub_categories[0] as the matrix key; true multi-tag matrix matching is SP3. Cascade fix_subcat_delete/rename + orchestrator scrub the per-option list; instance_schemas category_filter -> string. Existing engine tests migrated to the v2 shape; added tests for the ref regex, payload validation, and multi-tag pool filtering.
2026-06-07 00:43:55 +03:00
DumiFlex 789a9aedb8 feat(wildcards): require option.id (8-hex) in payload validation 2026-05-24 06:03:55 +03:00
DumiFlex 6a0c0bf97b refactor: remove pipelines feature; add unified AllItems library view
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.
2026-05-17 20:50:26 +03:00
DumiFlex ef2535548f feat(constraint): instance-only matrix cells — empty-cell overrides become new entries at resolve
The previous commit let users cycle empty matrix cells in the modal but
the engine only iterated `payload.matrix` — overrides for keys absent
from the library were silently ignored. Cycling produced dead UI: state
saved, engine ignored, runtime identical.

Engine extension: after the library-merge pass, do a second pass over
`cell_mode_overrides` ∪ `cell_factor_overrides` minus `covered_keys`.
Each leftover key becomes a new matrix entry with implicit baseline
mode "allow" + factor 1.0; whichever side(s) the user set populate the
rest. Disabled set still wins — entries in `disabled_matrix_cells` skip
both passes.

Added `decode_key` helper alongside `encode_key` for the second-pass
src/tgt extraction. Round-trip + malformed-input rejection covered by
new key tests. Malformed override keys in the resolve path are skipped
silently rather than crashing the run (defensive — instance JSON could
have rotted across schema migrations).

Updated the existing "unknown key silently ignored" test to assert the
new behavior (entry created from override). Added focused tests for:
factor-only override defaults to allow mode, disabled set still skips
empty-cell overrides, malformed key in override map doesn't crash.
2026-05-10 14:50:18 +03:00
DumiFlex 297c4e173d feat(engine): constraint handler honors cell + exception overrides + extras 2026-05-10 06:26:17 +03:00
DumiFlex 949d4dd531 feat(engine): derivation honors branch disable + value overrides + rule reorder + locked_seed 2026-05-10 03:45:11 +03:00
DumiFlex 4bb232efd1 feat(engine): add exists/not_exists/is_set/is_unset ops to derivation handler 2026-05-09 13:24:40 +03:00
DumiFlex a555cc98a9 fix(combine): honor instance.template_override + variable_binding in graph/conflicts/engine 2026-05-09 10:19:30 +03:00
DumiFlex e5eef0a4c5 test(fixed_values): cover lock seed determinism + ref-out-of-surface + mixed-token render 2026-05-08 19:37:15 +03:00
DumiFlex 6cfeec8cef feat(fixed_values): engine resolves inline syntax per value + locked_seed
Each emitted value now passes through resolve_text with
surface="fixed_values" so {a|b|c} alternations + {N\$\$sep\$\$...} repeats
+ escapes resolve. \$var reads + @{uuid} refs are gated off (lenient:
warn + render literal) — fixed_values is a binding PRODUCER, not a
consumer.

Seed lock: instance.locked_seed pins {a|b|c} resolution per instance
via the shared engine.modules._seed.derive_module_rng helper, keyed by
ctx["__wp_module_id__"] when present (else "fixed_values" literal).

Backward compat: ctx=None / minimal-dict callers skip the
syntax-resolution path via _ctx_supports_resolve guard.

5 new tests: alternation per value, locked_seed determinism, escape
renders literal \$, \$var warns + literal, values_overrides precedence.
2026-05-08 18:33:05 +03:00
DumiFlex 1ccad6a349 feat(combine): engine reads instance.template_override + locked_seed
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.
2026-05-08 18:26:21 +03:00
DumiFlex a8b8a6dfbd feat(engine): gate VAR token by surface — combine/derivation/assembler only
Wildcard + fixed_values surfaces are binding PRODUCERS, not consumers
— they don't read upstream \$var. Combine + derivation + assembler do
(template interpolation is their reason to exist).

Adds VarOutOfSurfaceError to engine.syntax.types + extends SurfaceKind
literal with "fixed_values". _resolve_var checks surface membership
in _VAR_SURFACES_ALLOWED frozenset; out-of-surface refs warn + render
literal \$name in lenient mode (existing default), raise in strict
mode (used by tests + dev mode).

Lenient strategy means workflows saved before this cycle keep
resolving — the warning surfaces in toast / conflict scanner without
crashing the queue.

Test fixture adjustments document the new contract:
- tests/syntax/test_resolve.py default surface flips wildcard -> combine
  (these tests cover VAR resolution mechanics, not surface gating)
- tests/syntax/test_determinism.py corpus single_var + var_in_text
  cases use surface=combine
- tests/engine/modules/test_wildcard_handler.py:
    * test_resolve_var_substitution_in_option renamed to
      test_resolve_var_in_option_renders_literal_warns + asserts new
      lenient path
    * test_resolve_pinned_mode_resolves_inline_syntax swaps \$color for
      a {red|red} alternation that DOES resolve on wildcard surface

Surface gating tests live in tests/engine/syntax/test_resolve_var_surface.py
covering all 5 surfaces + lenient vs strict modes + internal-key skip.
2026-05-08 18:22:48 +03:00
DumiFlex fdf3d03235 refactor(engine): lift _derive_module_rng to engine/modules/_seed.py
Pure refactor — wildcard_handler.py imports from new shared module
instead of defining inline. Combine + fixed_values cycles will reuse
the same helper for their seed-lock support.

Identical SHA-256 derivation, same call sites. All 210 engine tests
pass after the lift.
2026-05-08 18:15:57 +03:00
DumiFlex 3247fb9705 fix(instance): legacy exception shape + uniform None defaults from review
Two CRITICAL/IMPORTANT findings from final review:

1. DisabledExceptionsSection + prune.ts read only Tier 2 `source_value`/
   `target_value` while engine accepts both Tier 2 and legacy
   `source`/`target` (constraint_handler.py:215). UI on a legacy-
   shaped payload would emit encodeKey([undefined,undefined]) and
   silently drop saved disable selections + falsely flag valid keys
   as stale on refresh. Mirror the engine fallback chain in TS.

2. _fresh_instance() defaulted variable_binding to '' and internal to
   False. The frontend modified-state predicate is value != null, so
   any non-None sentinel falsely lights up the orange dot, forces the
   modal to default to Instance tab, and tags the section as modified
   on a pristine snapshot. Engine readers already coerce None at the
   read site (wildcard_handler.py:216 falsy-or fallback, pipeline.py:160
   bool() coercion), so we keep a single contract: None everywhere =
   no override.

Also: prune.ts now uses encodeKey() helper instead of raw
JSON.stringify for consistency with the cross-language contract.
2026-05-07 15:49:17 +03:00
DumiFlex dbfb7979bb feat(api): add PUT /wp/api/modules/<id>/payload for save-to-library round-trip
adds wp_api/library_save.py with a single route handler that validates the
incoming payload via the module type's registered handler, persists it through
ModuleRepository.update, and returns {"ok": true, "new_hash": "<sha256>"}.
covered by 5 new async tests in tests/wp_api/test_library_save_endpoint.py.

also fixes pre-existing ruff E501 violations in test_derivation_handler.py
and test_instance_schema_parity.py that were blocking the pre-commit hook.
2026-05-07 13:42:46 +03:00
DumiFlex 2a3ba907cb feat(engine): tier 2 disabled_rule_ids filtering in derivation handler 2026-05-07 13:25:29 +03:00
DumiFlex 22bed80e37 feat(engine): tier 2 disable filtering in constraint handler 2026-05-07 13:20:52 +03:00
DumiFlex 7a8c216e12 feat(engine): tier 2 instance defaults in _fresh_instance 2026-05-07 12:15:35 +03:00
DumiFlex 487873822e fix: surface silent-failure paths flagged by audit
Code review (silent-failure-hunter + feature-dev:code-reviewer) flagged seven paths where the engine or scanner silently swallowed problems instead of warning. Fixed the high-confidence ones:

1. `engine/modules/wildcard_handler.py:_apply_constraint_to_options` — `mode: "allow"` with `factor != 1` silently ignored the factor (SPA editors expose the factor field for every mode, so users naturally try `allow + factor=0.5` expecting weight reduction). Added a `constraint_factor_ignored_on_allow` warning surfaced via the existing `__wp_warnings__` channel. Defensive `unknown_constraint_mode` path also added — currently dead code (validator rejects typo'd modes upfront) but cheap belt-and-suspenders for any future path that bypasses validate.

2. Constraint excludes-all detection — when the matrix excluded every option in the target's pool the post-application total weight was 0 and `_pick_weighted` fell back silently to `options[0]`. The user got the same first option forever with no signal that their constraint over-narrowed. Added a `constraint_excludes_all_options` warning fired when constraints applied AND total weight collapsed to 0.

3. `engine/modules/constraint_handler.py:_ctx_set_constraint` — both the setter API and dict-fallback paths swallowed `Exception` silently, leaving the constraint un-registered with zero signal. Added a `constraint_register_failed` error-severity warning so a fundamental wiring break shows up in WP_Debug instead of being mysterious.

4. `engine/modules/fixed_values_handler.py` — non-list `instance.values_overrides` (string/dict from old SPA save or hand-edited JSON) silently fell back to library payload, dropping the user's edits without explanation. Added a `fixed_values_overrides_malformed` warning naming the wrong type so the fallback is debuggable.

5. `src/extension/preview-resolver.ts` — failure ledger was a `Set<string>` that made every failure permanent for the session. A single page-load 5xx flake left a uuid stuck rendering as `$ae07018b` until full reload. Replaced with `Map<string, {at, permanent}>` plus a 30s retry TTL: 404 stays permanent (server confirms the uuid doesn't exist), every other status retries after the TTL. Network/JSON errors are also retryable now and log once via the same `window.__wp_walker_log__` flag the walker uses.

6. `src/extension/conflicts.ts` — `localWildcardIndex` only tracked ENABLED wildcards. A constraint pointing at a disabled local wildcard would falsely flag as "constraint_source_missing" / "constraint_target_missing" because the disabled module was invisible to the index. Track both enabled and disabled — disabled-state is a separate concern from "does the uuid live in this node."

Tests: 3 new constraint-integration cases (factor-ignored-on-allow, excludes-all, plus the existing positive-path checks unchanged), 2 new fixed_values cases (malformed warning fires, no-warning when overrides absent).

468 pytest, 523 vitest, typecheck clean, entry 2.71 KB / total 186 KB gzipped.

Audit found other items still outstanding (sameAsLibrary index-strict compare, multi-constraint composition surprise, shadows+duplicate dual-emit policy, ctx as growing god-object) — deferring those as design questions rather than reflexive code changes.
2026-05-03 01:54:04 +03:00
DumiFlex 4f827c7517 feat(constraints): static ordering checks + relax factor>=0 validator
QA reported two ordering gaps in the constraint module — the second one surfaced as a runtime ValueError that killed the whole constraint at validate time:

1. Static ordering: a constraint card stayed silent when its source/target wildcards were missing or in the wrong execution order. Runtime emitted `unknown_constraint_source` once it hit a missing source, but the user only saw it in WP_Debug after queuing, and the more nuanced cases (target before constraint, target in upstream Context) had no signal at all.

2. Strict factor > 0: SPA-saved payloads occasionally surface zero-factor cells (the editor exposes them as a legitimate "exclude this pair" form). The handler rejected them with "factor must be a positive number," which made the entire constraint fail validation and skip its bucket registration.

Static ordering — four new conflict types in `extension/conflicts.ts`:

  - `constraint_source_after_self`     — source in same node at index >= constraint
  - `constraint_source_missing`        — source uuid not in this node nor in upstream
  - `constraint_target_before_self`    — target in same node at index <= constraint
  - `constraint_target_in_upstream`    — target lives in an upstream Context (already picked)

`scanConflicts` accepts a new third arg `upstreamWildcardUuids: string[]` so it can tell "source upstream = good" apart from "source nowhere = bad." The check fires alongside the existing missing/dup/shadow logic; multiple problems on one constraint surface as separate dots in the tooltip. Target NOT in same node and NOT in upstream is intentionally NOT flagged — could still be a valid downstream Context, and the static scanner has no downstream walker to differentiate from a typo. Runtime still catches that case.

Walker plumbing — `extension/graph.ts` adds `collectUpstreamWildcardUuids(rootGraph, node)` returning the set of every wildcard module's id reachable upstream (skipping muted/bypassed nodes, mirroring the existing var walker). Wired through `widgets/context.ts` → `ContextWidget.vue` prop → scanner.

`labelFor` extended for the four new types so card tooltip + subgraph badge agree on wording: "source after constraint", "source missing", "target before constraint", "target already picked upstream".

Validator relax — `engine/modules/constraint_handler.py:_validate_cell` and the exception-list check now allow factor=0 (effectively "exclude") and only reject negative factors. The legitimate SPA-saved zero-factor payload now passes validation; the constraint registers normally and the wildcard handler sees its bucket. Updated the matching test from "rejects zero factor" to "accepts zero factor" with an inline comment naming the regression.

Tests:
  - 8 new scanner integration cases — clean chain, source-after-self, source-missing, target-before-self, target-in-upstream, target-unfindable-no-warning, disabled-skip, dual-warning composition.
  - 3 new walker cases — uuid collection across upstream Contexts, mute/bypass exclusion, no-upstream-link.
  - Updated constraint handler test for the new factor=0 acceptance.

Totals: 523 vitest, 464 pytest, typecheck clean, entry 2.71 KB / total 185 KB gzipped.
2026-05-03 01:29:33 +03:00
DumiFlex da03077e62 feat(fixed_values): two-tier library/instance model with reset-to-library
Fixed_values now mirrors the wildcard pattern: library-picked modules treat `payload.values` as the immutable library snapshot, and user edits land in `instance.values_overrides` instead of mutating the snapshot in place. Aligns the kind with how wildcards already work — "modified" becomes a real card-level signal, "reset to library" becomes a one-click affordance, and library drift detection no longer collides with local edits.

Engine (`fixed_values_handler.py`): two-tier read. Overrides win when present and non-empty; otherwise the library `payload.values` resolves as before. Defensive against malformed (non-list) overrides — defaults to library payload.

Modal save: branches on `payload_hash` presence. Library-tracked modules write entries to `instance.values_overrides`; library `payload.values` is never touched. Inline-created modules (no `payload_hash`) keep editing `payload.values` directly — original behaviour, no library anchor to preserve. Save also includes a soft-reset path: when entries exactly match library values, the override is deleted instead of stored, so the modified dot clears automatically.

Modal UI: when a library-tracked fixed_values has overrides set, a "reset" bulk-button appears next to "add". Clicking reverts entries to `payload.values` and drops the override field. The user still has to Save to commit, preserving the cancel-discards contract.

ContextWidget `isModified`: extends to fixed_values via `instance.values_overrides` non-empty check. Inline-created modules never light up — they have no library anchor to diverge from. Existing wildcard branches unchanged.

Frontend mirrors of the engine read (`extension/graph.ts:writeBindings`, `extension/conflicts.ts:writesOf`): both consult overrides first, fall back to entries + payload.values. Keeps the assembler preview and the conflict scanner consistent with the runtime resolver.

Migration: workflows saved before this change have edits stored in `payload.values` for library-tracked fixed_values. Engine still reads payload.values when no override is set, so they continue to resolve correctly. The modified dot only fires for new edits via the new modal save path — existing pre-migration content shows as "in library" until next opened + saved. Acceptable cost; the alternative would require a server round-trip to compare against live library hashes.

Tests: 6 new engine handler cases (override path, empty fallback, add/drop entries, malformed override defense), 6 new modal save/reset cases (library-tracked write to overrides, no-edit clean save, inline payload write, reset button visibility + click, both negative cases). 512/514 vitest, 457/461 pytest.
2026-05-02 17:15:01 +03:00
DumiFlex f53eb7ee5a fix: align conflict labels + drop _constraints leak + restore modal leave anim
Three QA-driven fixes from the post-5.5.6 canvas pass:

1. Conflict label drift. Card tooltip said "overrides upstream / duplicate / missing variable" while the subgraph badge said "shadows / duplicate / missing" — same conflict types, different words. Hoisted a single `labelFor(type)` helper into `extension/conflicts.ts` and routed both consumers through it. Wording is now uniform: "overrides upstream / duplicate / missing".

2. Constraint metadata leak. `ConstraintHandler.resolve` wrote its bucket to `ctx["_constraints"]` (single-underscore key). `engine.context.strip_internals` only filters double-underscore (`__`) keys, so the bucket leaked to the public socket payload — the assembler's autocomplete listed `$_constraints` as a pickable variable even though no module had bound it. Renamed the key to `__wp_constraints__` so the standard internals-strip drops it. Added a regression test that runs the handler then asserts strip_internals removes it.

3. Modal entry-row leave animation never fired. The original CSS had only `wp-medit-list-enter-*` classes; deleting an entry just blanked the row instantly because `*-leave-active` / `*-leave-to` were undefined. Added a 120ms fade + 8px slide on leave with `position: absolute` so the row exits without blocking the FLIP-move on surviving siblings. Matches the new card-style entry container — vanish-without-feedback felt broken in the redesigned look.
2026-05-02 16:32:45 +03:00
DumiFlex cda46b23c3 feat(engine): warn on missing $var so combine templates surface gaps
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
2026-04-30 14:31:03 +03:00
DumiFlex 6921606570 feat(context): module picker + lock/internal + card indicators (#8)
Phase 5.5.4 (SPA library picker for ContextWidget) plus QA-driven polish.

* New ModulePickerModal with search, kind tabs, single + multi-pick, and
  compact filter UX (favorites/hide-added inline; category + tag chips
  behind popover with active-count badge).
* ModuleEditModal kind-aware body — wildcard option editor with
  enable/disable, weight overrides, mode picker (random/subset/pinned),
  sub-category multi-select.
* Lock + internal per-instance overrides ported and adapted: locked_seed
  derives a per-wildcard rng via sha256(seed:binding); internal hides
  bindings from the public PIPELINE_CONTEXT payload.
* Card indicators: modified dot (option-pool overrides), missing-from-
  library dot (polled hash check), in-card lock + internal toggles.
* RNG-faithful assembler preview via new /wp/api/preview/resolve; header
  shows "PREVIEW · 42". UNRESOLVED chips clickable to strip $var.
* Bulk collapse/expand toggle, focus toast w/ pulsing border overlay
  that tracks node bounds, friendly @name substitution in modal.
* Backend: new /wp/api/modules/import-from-workflow endpoint preserves
  uuid; engine emits per-module effective seeds via WP_Context UI
  payload for authoritative lock-toggle defaults.
2026-04-30 00:53:13 +03:00
DumiFlex d6e546880e test(engine): pin no-DB-fallback contract on get_module 2026-04-28 13:08:31 +03:00
DumiFlex fef8cc5fdc refactor(snapshot): drop walker fallback regex; tests use valid 8-hex 2026-04-28 13:01:06 +03:00
DumiFlex e9e710c295 feat(snapshot): walk_transitive_refs lazy walker (spec §2.10) 2026-04-28 12:52:18 +03:00
DumiFlex bf61ef38b4 feat(snapshot): add canonical SnapshotEntry TypedDict (spec §2.4) 2026-04-28 12:40:15 +03:00
DumiFlex 5da2aa5c40 refactor(engine): combine/derivation/constraint handlers use resolve_text
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.
2026-04-28 06:52:43 +03:00
DumiFlex 657fe57890 refactor(engine): wildcard handler delegates to resolve_text 2026-04-28 06:39:39 +03:00
DumiFlex 2d89a7391d feat(engine): scaffold combine/derivation/constraint/pipeline module types 2026-04-27 03:54:13 +03:00
DumiFlex f0be23f0a8 feat(engine): fixedvalueshandler + route legacy dispatch through it 2026-04-26 16:30:49 +03:00
DumiFlex 30bf13efc0 feat(engine): wildcardhandler with weighted rng and @{ref} resolution 2026-04-26 16:24:54 +03:00
DumiFlex ca35257e92 fix(engine): explicit type guard in resolve_module for malformed snapshots 2026-04-26 16:20:52 +03:00
DumiFlex 43c6a3db91 feat(engine): module dispatcher + modulehandler abc 2026-04-26 16:17:57 +03:00
DumiFlex 6b13e7d455 feat(engine): snapshot freezing + payload hash + legacy coercion 2026-04-26 16:12:17 +03:00