17 Commits
Author SHA1 Message Date
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 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
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 febb9f3e70 feat(engine): capture constraint's own bundle_origin on registered meta 2026-06-12 20:09:40 +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 8c3c9aedc2 feat(engine): validate + register constraint target_select; instance override field 2026-06-11 05:53:44 +03:00
DumiFlex f8ac25f443 chore: pre-merge polish — kind-aware chips, injector + debug improvements, engine fixes
Omnibus polish landing the remaining unstaged work on the branch:

frontend
  - kind-aware RefChip — @{uuid} refs targeting non-wildcard library
    modules (constraint, combine, derivation, fixed_values, bundle) now
    render with the matching kind color + PrimeIcon instead of falling
    through as unresolved wildcards. cascade/resolveChip exposes
    resolveModuleChip; preview-resolver caches snap.type as kind;
    RichTextPreview accepts a uuid-to-kind map; RefChip honours the
    new moduleKind prop.
  - DebugViewer derives uuid-to-kind from trace entries so engine
    warnings (e.g. constraint_never_applied carrying @{constraint_id})
    render the constraint id as a colored constraint chip.
  - Injector node refactor: two-tier socket (template_subject /
    subject row pairs) + general-template row model; binding modal
    handles general rows; row reorder + cap; rows collapsed by
    default; sockets gain tooltips.
  - SPA misc: useStarterSet rebuilds cascade reverse-dep index on
    catalog refresh so just-created starters surface in cascade
    impact dialogs; tokens / resolveTokens / EmptyState height /
    AppLayout sidebar count live-refresh / ModuleListView mid-cols.

engine
  - constraint_handler: 3 dedup bugs in carrier-claim + classify the
    never_applied warning.
  - pipeline: detect via-nested constraint carriers; carrier-claim
    failsafe + clearer never_applied warning text.
  - syntax/resolve: nested @{uuid#name} optional segment recognised.

tests
  - DebugViewer / Injector / RefChip / resolveChip / preview-resolver
    test suites extended for the new behaviour.
  - Engine + integration tests adjusted for constraint dedup + new
    carrier path + nested-ref parsing.
2026-05-28 12:53:23 +03:00
DumiFlex aa6d26a306 fix(engine): per-instance constraint consume + one-claim-per-target
The consumed-set keyed on __constraint_module_id__ = the LIBRARY uuid,
so two instances of the same constraint library entry collapsed into a
single one-shot. CLAUDE.md is explicit: 'each constraint module is a
one-shot ... author multiple constraint modules to affect multiple
target instances.' Two authored modules must be independent.

The carrier-claim failsafe made the collision visible: a carrier
(backdrop) claiming the shared cid spent the WHOLE hair_x_mood family,
so a downstream direct mood instance — which the pair badge assigns to
hair_x_mood #2 — then rolled unconstrained.

Changes:
- pipeline.py stashes __wp_current_module_uid__ (the per-instance _uid)
  alongside the existing __wp_current_module_id__ (library uuid), and
  pops both after each module. The wildcard handler still matches the
  library id against constraint targets; only the constraint handler's
  consume key switches.
- constraint_handler keys __constraint_module_id__ on the uid, falling
  back to the library id when _uid is absent (legacy / hand-built test
  modules — preserves their behaviour).
- claim_carrier_constraints now claims at most ONE constraint per
  carried target (first unconsumed), so a multi-ref carrier can't
  swallow an entire same-target family — it consumes one per target,
  exactly like apply_constraints_for_target fires one per call.

Regression test reproduces the user's exact shape (two hair_x_mood with
the same library id + distinct _uid): backdrop claims instance A,
top-level mood claims instance B, mood ends up constrained. All 1027
pytest + 2381 vitest pass.
2026-05-26 22:49:27 +03:00
DumiFlex 3bd71588b3 fix(constraint): empty source/target valid for null-option exceptions 2026-05-26 19:40:05 +03:00
DumiFlex 43f9b9807c feat(engine): first-instance constraints + null wildcard option + inverted null filter
- Constraint first-instance one-shot: each constraint claims first
  downstream target wildcard instance then is consumed. Engine tracks
  via ctx __wp_consumed_constraints__ across pipeline + nested ref
  resolves. Emits constraint_never_applied (info) for unused at end.
- Null wildcard option: is_null flag opts into no-output slot that
  survives category filter + matrix constraint. Reserved sub_category
  name guard forbids null as real sub-cat.
- Inverted nested-ref null filter (2026-05-25): null keyword in filter
  list now EXCLUDES wildcard null option. Default keeps null in pool
  alongside listed sub-cats.
2026-05-25 00:07:30 +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 22bed80e37 feat(engine): tier 2 disable filtering in constraint handler 2026-05-07 13:20:52 +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 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 2d89a7391d feat(engine): scaffold combine/derivation/constraint/pipeline module types 2026-04-27 03:54:13 +03:00