Commit Graph
9 Commits
Author SHA1 Message Date
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 ccbf38af99 feat(nodes): add wp_seedlist + split loop config types + wp_ prefix sweep + tooltips
Three coordinated changes to the WP_* node family.

1. New WP_SeedList node
   Emits a list of N derived seeds for a downstream loop (is_output_list).
   Three independent override toggles (override_seed / override_count /
   override_strategy) each mirror the wired Loop's base_seed / count /
   strategy. Connecting loop_config auto-flips override_count +
   override_strategy ON so the natural intent of wiring in a loop gets
   the defaults right; override_seed stays explicit opt-in.
   All emitted seeds are masked to 50 bits via the new MAX_SAFE_SEED
   constant in engine.seed_derive so values sit inside ComfyUI's
   frontend randomize range and copy-paste reproduces identically in a
   standalone sampler.

2. Context Loop type split + downstream Context seed widget lock
   Split WP_CONTEXT_LOOP_CONFIG (wire payload) from a new
   WP_CONTEXT_LOOP_WIDGET (DOM-widget-only TYPE). Loop's own widget
   mounts on the widget type; the wire type has no factory so consumers
   like WP_SeedList render their loop_config socket as a plain input
   instead of being hidden by a fallback-undefined widget.
   New graph walker hasUpstreamLoopOverridingSeed: WP_Context's widget
   glue greys out the local seed stock widget when an upstream Loop has
   override_seed ON, so users read "this value is dead while the Loop
   drives it" without inspecting the Loop config separately.

3. wp_ prefix sweep + tooltips across every WP_* node input
   Custom DOM widget input names now carry the wp_ prefix so canvas
   socket labels read as branded WP widgets: modules -> wp_modules,
   rows -> wp_rows, viewer -> wp_viewer, cleaner -> wp_cleaner,
   context_loop_config -> wp_context_loop_config. Every stock input
   across the node family gets a tooltip describing what it does and
   when to wire it from another node. Tests + comfy_api stub updated.
   CLAUDE.md notes the wp_ convention for future custom widgets.

Bundle size budget bumped 388 -> 395 KB to fit the SeedList Vue chunk
(~4.9 KB net gzip) + override lock UI; entry stays at ~4 KB gzipped.
2026-05-28 12:40:44 +03:00
DumiFlex ed2a1a4d27 fix(engine): dedup never_applied warnings + propagate consumed cross-node
Three bugs surfaced when the user pasted a real WP_Debug snapshot
showing 4 constraint_never_applied warnings for a chain with only 3
constraint instances (2 hair_x_mood siblings + 1 shirt_x_color_compat),
across two sequential WP_Context nodes:

- __wp_consumed_constraints__ wasn't in _CROSS_NODE_INTERNAL_KEYS, so
  a downstream Context inherited the constraint bucket from upstream
  but started with an empty consumed set. End-of-run check then
  re-warned every constraint upstream already consumed. Add it to the
  cross-node-internal list + handle set serialisation in
  WP_Context.execute's defensive-copy loop.

- engine/pipeline.py's end-of-run loop walked every entry in the
  bucket without dedup. Two sibling instances of the same constraint
  library entry share __constraint_module_id__, so the user got the
  same warning twice for one logical constraint. Add a warned_cids
  set so each distinct cid emits at most one warning per run.

- wp_nodes/types.py:build_payload concatenated upstream + this-run
  warnings without dedup. Even with the consumed set now propagating,
  a constraint that legitimately never fires anywhere still gets
  warned once per node along the way. Dedup by (type, module_id) for
  constraint_never_applied at merge time.

Confirmed the engine's first-instance targeting already matches the
user's mental model: when the SOURCE wildcard's own option carries a
nested @{target} ref, that pre-registration roll is skipped and the
constraint claims the NEXT target instance downstream (e.g. @{color}
inside a backdrop wildcard placed after the constraint). Locked in by
test_constraint_targets_next_color_not_source_nested across 5 seeds.

Net: user's 4 warnings collapse to 1 (shirt_x_color_compat, which is
legitimate for any iteration where backdrop's RNG picks a no-@{color}
option). 5 regression tests added; all 1024 pytest + 2381 vitest pass.
2026-05-26 22:24:38 +03:00
DumiFlex 44f71565dc feat(context): route widget seed through effective_chain_seed helper 2026-05-26 17:02:50 +03:00
DumiFlex 41c3b58d26 fix(engine): trace records carry _uid; module_seeds keyed per-instance
Sibling rows in a Context share `module.id` (library uuid) but have
distinct `_uid` values. The pipeline trace emitted only `id` on each
entry, and the context node's `module_seeds` UI payload keyed that
map by `id`, so the per-instance "last seed used" reader collapsed
all siblings onto one value — whichever sibling executed last
overwrote the others. Locking a sibling's seed therefore fell back to
some other sibling's roll rather than its own.

Three coordinated changes:

1. `engine/pipeline.py` reads `module["_uid"]` (or `getattr` for
   dataclass-shaped modules) alongside `module["id"]` at the start
   of each iteration, and stamps `"_uid"` on every `__wp_trace__`
   append — disabled-skip, coerce-fail, unknown-type-skip,
   handler-error, and ok-completion. Missing `_uid` defaults to ""
   so pre-migration entries are still well-formed.

2. `wp_nodes/context_node.py` builds `module_seeds` keyed by
   `entry["_uid"]` when present, falling back to `entry["id"]` for
   pre-migration entries. The map still travels through the UI
   payload as opaque string-keyed JSON; the widget doesn't care
   what's behind the key.

3. `src/components/context/ContextWidget.vue` calls
   `lastUsedSeedReader(m._uid ?? m.id)` instead of `m.id`. The
   `?? m.id` fallback covers two cases: a workflow saved before the
   `_uid` migration (Vue's ModuleEntry has _uid optional), and an
   engine running pre-this-commit (no `_uid` in trace → key stays
   library-id → fallback hits same key).

Net effect: lock-toggle on sibling A restores A's last roll, not
whatever sibling executed most recently. Previous fixes in this
audit moved the conflict scanner + drag fallbacks onto `_uid`; this
finishes the propagation through the engine layer.
2026-05-16 16:35:28 +03:00
DumiFlex 8626cb1de0 fix(constraints): carry picks + constraints across node boundaries
User's QA debug confirmed: a 3-node chain (hair wildcard in Context A → constraint in Context B → mood wildcard in Context C) produced "long flowing hair" + "melancholic mood" despite a `long → exclude negative` constraint. Mood wildcard ignored the constraint entirely.

Root cause: `strip_internals` filters every `__`-prefixed key at the PIPELINE_CONTEXT socket boundary, including `__wp_picks__` (where the hair wildcard recorded its pick) and `__wp_constraints__` (where the constraint module registered its bucket). Both got dropped between Context A → B and again B → C. By the time C's mood wildcard ran, its ctx was empty of both — so:
  1. The constraint loop in `WildcardHandler.resolve` saw no `__wp_constraints__`, skipped constraint application entirely.
  2. Even if it had seen them, looking up `__wp_picks__["ae07018b"]` for the source pick would've returned None.

Fix — `ContextPayload` gets a third dataclass field, `internals: dict`. `build_payload` extracts a deliberately narrow set of cross-node-internal keys (`__wp_picks__`, `__wp_constraints__`) and emits them on `internals` while still stripping them from `context` (the user-facing payload the assembler consumes). `WP_Context.execute` reads `upstream.internals` and merges the entries back into ctx before running its own pipeline. Defensive shallow-copy so this node's mutations don't corrupt the upstream payload (frozen dataclass shares dict references).

Listed the cross-node key set in a module-level `_CROSS_NODE_INTERNAL_KEYS` constant so future additions are an opt-in choice — every other `__wp_*` key still gets dropped at the boundary by default.

Tests: 4 new cross-node integration cases driving real `WPContext.execute`:
  1. Picks surface on payload.internals + don't leak to context.
  2. Constraints surface on payload.internals + don't leak to context.
  3. Three-node chain matching the QA scenario — long hair only allows positive mood across 20 seeds.
  4. Symmetric short-hair-only-allows-negative-mood case.

472 pytest, 529 vitest, typecheck clean, size budget green.
2026-05-03 03:14:02 +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 9d6047881f feat(wp-nodes): inject __wp_catalog__ from embedded snapshots in WP_Context 2026-04-28 14:17:30 +03:00
DumiFlex d185e20557 feat: polish suite — MVP frontend + subgraph support + DX + a11y
Lands the P4 frontend MVP plus 9 follow-up polish batches (88 commits squashed).

* Pipeline Context, Prompt Assembler, Debug Viewer widgets
* PIPELINE_CONTEXT chain crosses subgraph boundaries
* Conflict badges on SubgraphNodes containing WP nodes
* Empty-state hero, soft-delete + Undo toasts, corrupt-workflow recovery
* Bundled Inter + JetBrains Mono variable fonts, brand identity end-to-end
* Settings panel: Reduce motion + Contrast (a11y) with live updates
* DX: coverage gate, stricter ESLint, pre-commit pnpm size, PR size-delta workflow, contract doc

Closes the polish-brainstorm spec.
2026-04-26 05:39:22 +03:00