20 Commits
Author SHA1 Message Date
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
Claude e00f38b6ff feat(context-loop): sweep mode runs every combination of chosen wildcards
Pick downstream wildcards in the Context Loop's new Sweep panel and the
loop runs the cartesian product of their options, one frame each, up to a
limit. Each frame pins the swept wildcards through the existing pinned
path; 'Hold other picks' keeps unswept modules on the frame-0 seed.
Raises the bundle budget 424 -> 428 KB for the panel.
2026-10-05 19:12:01 +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
DumiFlex 2f08c138ac feat(refs): one pool for the picker, and a debug warning that explains itself
Two halves of the same gap, left over from the nested-ref fix.

**The sub-category picker offered the wrong pool's tags.** Its checkbox list
came from `buildWildcardRefData(catalog)` — a library fetch — while the engine
filters the ref against THIS node's snapshot whenever the uuid is a module
here. On a drifted snapshot the picker listed a tag no option in the running
pool carries: pick it, and the ref resolves to nothing, with the checkbox list
itself being the thing that misled you.

`overlayContextPools` applies the same precedence `resolvePoolFor` already
gives the hover card, so card, picker and engine now describe one pool.
`ContextPool` grew the declared vocabulary (`subCategories`, `tagGroups`,
`hasNull`) the picker needs — distinct from `tagSets`, which is what options
actually carry; a declared tag no option uses is offerable but matches
nothing. `uuidToName` is deliberately NOT overlaid: it holds the variable
binding a chip labels itself with, not a display name.

Only the derivation modal needed it. The constraint modal already builds from
node modules, and the SPA editors have no node, where the overlay is a no-op.

**"Matched no options" told you nothing.** The engine emitted
`ref_subcategory_empty_pool` all along, but `WARNING_LABELS` had no entry, so
Debug rendered the raw slug — and even read, the message could not distinguish
a wrong filter from a right filter aimed at a stale copy. That ambiguity is
exactly what made this bug a hunt.

`_stamp_pool_provenance` records, per catalog entry, whether the pool came
from this node or the library, plus the library's option count when a node
copy has drifted. The warning now reads:

    @{05c54db9:test} matched no options — searched this node's copy
    (1 option); the library now has 2

with `pool_origin`, `pool_total` and the pool's `available_sub_categories` in
`detail` — the tags it DOES offer being the whole explanation. Labels added
for the two nested-ref warnings and `ref_out_of_surface`.

Provenance is runtime-only: `__wp_catalog__` is stripped at the socket and the
workflow-embedded snapshot is built elsewhere, so neither key reaches saved
JSON. The extra reads are one per embedded wildcard on an already-open
connection, and the stamping never raises — diagnostic garnish must not take a
graph run down.
2026-08-02 01:25:28 +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 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 b33afd8797 fix(context): capture the loop's per-frame seeds so the lock pins the previous roll 2026-06-26 06:43:52 +03:00
DumiFlex 91b5f7581a revert(context): drop per-frame seed capture, superseded by double-shift fix 2026-06-26 06:43:50 +03:00
DumiFlex 25388b4650 fix(context): per-frame seed lock captures that frame's own rolled seed 2026-06-26 06:43:48 +03:00
DumiFlex 0f7aaabe3c feat(engine): thread a hold seed and loop index into the pipeline run 2026-06-26 06:43:26 +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 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