Commit Graph
186 Commits
Author SHA1 Message Date
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 63b824e4a1 feat(engine): range count + independent mode + lone-ref pool multi-pick
Resolve the SP2b multi-pick semantics:
- range count drawn in [min,max] (a fixed {N$$} keeps min==max and does NOT
  draw, so its seed sequence is byte-for-byte unchanged);
- independent (~) = N draws WITH replacement, each branch resolved
  independently (a lone ref re-rolls every time);
- unique lone-ref (count>=2, wildcard surface) = N DISTINCT options from the
  ref filtered pool — a per-site SP2a multi-select. count==1 stays on the
  legacy single-pick branch path (seed safe).

Extracts ref_option_pool (catalog lookup + :expr/!null filter + chain
constraints, incl. the strict-unknown raise + warnings) from _resolve_ref so
both the single pick and the lone-ref multi-pick share it. Also closes the
coverage gap: a $var branch inside a multi-block on the combine surface.
2026-06-09 18:08:52 +03:00
DumiFlex 0b36ce892b feat(engine): multi-pick prefix gains N-M range + ~ independent flag
The {N$$sep$$} brace prefix now also accepts a count range ({N-M$$}) and a
~ independent flag ({N~$$} / {N-M~$$}, repeats allowed; absent = unique).
Both tokenizers (Python engine + TS richTokenize) parse it identically
into dp-multi meta {min, max, independent}; legacy `count` is kept (== max)
so existing readers + the resolver behave unchanged for a fixed {N$$}.
Shared syntax-corpus.json locks Py≡TS parity (existing multi_pick entries
gain the new keys; new range/lone-ref cases added).

Resolution of the range/independent semantics lands next (engine resolver);
this commit is tokenizer + parity only — a fixed {N$$} is byte-for-byte
unchanged.
2026-06-09 17:54:48 +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 17a2149fb0 feat: pick_min/pick_max/pick_separator wildcard instance fields 2026-06-08 14:40:40 +03:00
DumiFlex 443db49546 feat(engine): multi-select wildcard binds a list variable 2026-06-08 14:31:26 +03:00
DumiFlex 8eeb0a8032 feat(engine): list-backed variables + var.K accessor 2026-06-08 14:21:40 +03:00
DumiFlex 3ac0afa979 feat(engine): tokenize var.K list accessor (py+ts parity) 2026-06-08 14:12:42 +03:00
DumiFlex e5f54c6ba8 refactor(engine): extract pick_k_unique from multi-pick resolver 2026-06-08 10:13:40 +03:00
DumiFlex 4cda98f227 fix(cascade): discover v2 boolean sub-category filter refs
Sub-category rename/delete discovery keyed on the whole filter body, so
it missed any ref using an SP1 boolean filter. `@{mood:warm or intense}`
indexed as `mood:warm or intense` (TS) / matched by `sub == "warm"` exact
equality (Python), so a rename or delete of `warm` neither updated the
SPA badge nor counted the ref in the server-side preview — even though
the rewrite (engine/cascade/fixers.py) already token-matches and DID
rewrite it on apply. Preview undercounted what apply changed.

Both discovery layers now collect the expression's tags via the shared
parser and index/match per-tag (negated tags count; the `!null` marker is
not a tag):
- TS: new collectTags in subcatExprCascade.ts; reverse-dep-index builds
  one toSubcat entry per tag.
- Python: new collect_tags in fixers.py; scan._scan_subcat tests tag
  membership instead of raw-string equality.
2026-06-08 06:41:15 +03:00
DumiFlex c2d0ef5379 fix(ui): subcategory polish across spa + graph (colours, exclude-null, palette, popover)
Round-2 feedback on the SP1 sub-category UI.

SPA (manager):
- #1/#2 SubcategoryFilterPicker — enlarge the ref-filter popover (340->400px
  min, taller 44px / 14px expression input) and shrink the Delete/Skip/Apply
  buttons (the default .wp-btn size read as oversized in the popover).
- #6 per-option tag picker — selected rows now tint + bold + fill the check
  box (hue-independent) instead of a near-invisible box-only tint.
- #8 group boxes — coloured left accent + coloured header keyed to the axis
  hue, plus a stronger pill tint, so each axis reads as a colour cluster.

Graph (canvas instance modal):
- #4 exclude-null never persisted — exclude_null was missing from
  RELEVANT_INSTANCE_KEYS (stripped from the preview body + chain hash), from
  INSTANCE_FIELDS_PER_KIND / RESET_FIELDS, and from the engine INSTANCE_SCHEMAS
  (the SP1 chunk documented it but never added the line); added everywhere.
  Toggle now uses native @change on the input instead of a label @click.prevent
  that fought the checkbox own toggle, so the box reflects the new state.
- #5 exclude-null control now hidden entirely when the wildcard has no null
  option (was shown-but-dimmed).
- #7 advanced expression editor gains an insert-at-cursor palette (grouped
  sub-categories + operators), mirroring the SPA ref-filter.
- #9/#6 pool quick-pills — faint axis-hue border so a group reads as a
  cluster even unselected; selected pills get a solid hue fill + bold +
  leading check so on is visible for any hue (incl. the grey ungrouped).

Tests: PoolSection exclude-null (emit/reflect/hidden) + advanced palette
insert; _shell + engine instance-schema parity updated for exclude_null.
2026-06-08 03:18:57 +03:00
DumiFlex a62851256b fix(engine): slugify nested-ref tags in v1->v2 migrate + sp1 e2e smoke
The migrator now slugifies each tag inside a nested @{} ref's filter list (cross-wildcard refs track the target's renamed registry entry — deterministic, same _slug the target applies), not just comma->or + null->!null. Idempotency-safe: the rewrite branch only fires on v1-shaped (comma/null) refs; v2 refs are left verbatim. Mirrored in src/manager/import-export/migrations.ts.

tests/test_sp1_smoke.py: end-to-end SP1 acceptance — a v1 library payload migrates to v2 (registry + option tags slugified, nested ref comma->or + slug + !null, instance category_filter list->expr + exclude_null), the migrated payload validates, and the boolean pool matcher resolves over the new multi-tag option shape.
2026-06-07 05:01:05 +03:00
DumiFlex 11ad8ca4bc feat(cascade): expression-aware subcat rename/delete (engine + ts)
engine/cascade/fixers.py: _rewrite_subcat_ref_in_string + _strip_subcat_ref_in_string now parse a nested ref's :expr segment via subcat_filter, map the tag token (rename, or remove + collapse childless operators), and re-serialize via reads_as — preserving #name + !null. Bare-subcat refs (@{u:cold}) collapse to a bare @{u} on delete; boolean refs (@{u:warm or cold}) collapse to @{u:warm}. Only refs that actually contain the target tag are rewritten (no cosmetic churn).

src/manager/cascade/subcatExprCascade.ts: TS mirror (renameSubcatInExpr / removeSubcatFromExpr via parse + readsAs + mapTags) for the client-side cascade.
2026-06-07 02:33:05 +03:00
DumiFlex 39529a669b feat(engine): migrate library v1->v2 (multi-tag) + bump schema version
engine/migrations/v1_to_v2.py: option sub_category->sub_categories[]; sub-category name slugify (whitespace/grammar chars->_, reserved suffixed, de-duped) cascaded to options + tag_groups; instance category_filter list->boolean-expr string + exclude_null flag; nested @{uuid:a,b,null} refs->@{uuid:a or b!null}. Idempotent. Two entry points share one core: envelope migrate_v1_to_v2 (import/export, registered in _CHAIN) + per-row migrate_module/bundle_v1_to_v2 (lazy/bulk DB path).

Register _CHAIN[1] + bump engine CURRENT_SCHEMA_VERSION 1->2. New SQL migration 016 marches stale DB rows 1->2 at boot via run_bulk_lazy + the per-row migrators (014's documented bump pattern). TS mirror (migrations.ts) + client CURRENT bump follow next (no cross-language version assertion couples them). Update version/entity-count assertions in payload-migration, migration-runner, and export-build tests.
2026-06-07 01:40:53 +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 0e1803ec12 feat(syntax): boolean sub-category filter parser, matcher + ts parity
engine/syntax/subcat_filter.py: tokenize->AST parse, matches(), reads_as() normalizer, validate_subcat_name + validate_expression (grammar: not>and>or, comma=OR, n-ary flatten). src/manager/parsing/subcatFilter.ts: behavior-identical TS mirror. Shared fixture routed through validate_expression in both languages so syntax + reserved/unknown errors stay in parity.
2026-06-06 23:22:44 +03:00
DumiFlex bbd1a37bd8 feat(publish): writeback community origin to local row after publish
Publishing a local row to community never linked the row back to the
resulting post, so: no community pill, no update-available tracking,
and the re-publish version-bump flow was unreachable (origin slug was
always null -> publishMode could only ever be 'create').

Adds the writeback path:
- ModuleRepository / BundleRepository.set_community_origin(id, slug,
  version): stamps community_post_slug + community_version_number
  WITHOUT a version bump or fingerprint change -- origin metadata is
  not runtime-affecting, so a published row stays byte-identical to
  what consumers installed.
- wp_api: POST /wp/api/modules/{id}/community-origin + the bundles
  twin. Validates {post_slug: str, version_number: int}.
- api client: modules.setCommunityOrigin + bundles.setCommunityOrigin.
- main.ts host bridge: new optional `markPublished(localId, kind,
  slug, version)` method. Calls the right setCommunityOrigin, then
  refreshes both catalogs so the pill appears without a reload.
  Optional + try/catch-returns-false so a writeback miss never breaks
  the embed's publish-success path. No ABI bump -- adding a method is
  non-breaking; the embed guards on its presence.

Tests: set_community_origin stamps slug+version, does NOT bump
version/fingerprint, raises on missing row.

Pairs with the community embed commit that calls markPublished after
createPost / createVersion.
2026-06-06 06:48:43 +03:00
DumiFlex 395af63b1a feat(engine): content_rating column on library rows (migration 015)
Adds `content_rating TEXT NOT NULL DEFAULT 'safe'` to modules + bundles.
Defaults all pre-015 rows to 'safe'. Repository create/update accept a
new content_rating kwarg with validation against {'safe', 'nsfw'}.
Importer _insert_module / _insert_bundle pass entity.content_rating
through; _update_module / _update_bundle use COALESCE(?, content_rating)
so an install path that omits the key preserves the existing flag.

Why: user wants two NSFW behaviors:
1. Edit page toggle to mark a module/bundle as NSFW locally.
2. Community posts with content_rating='nsfw' auto-stamp NSFW on
   the local row at install time.

This is the engine half of that work -- the schema, the data
plumbing, and validation. The SPA half (IdentityCard toggle + pill
render on ModuleListView + install.ts plumbing) lands next.

Tests cover: migration head bump to 15, column existence on both
tables, safe default backfill, accepts nsfw, rejects invalid, update
toggle round-trip, update without content_rating preserves prior.
2026-06-06 05:22:02 +03:00
DumiFlex 73ca8a3f9a feat(engine): bulk lazy runner — eager iteration of lazy_migrate_row
Implements spec §4 Flow 4: bulk-at-boot is NOT a parallel forward-chain
implementation; it's eager iteration of lazy_migrate_row over every
stale row. Same original-first logic, same re-projection, zero
divergence. Eliminates the transient-wrong window where bulk could
write fabricated defaults over recoverable data in original_payload_json.

run_bulk_lazy returns the failure count; caller surfaces a dashboard
banner when > 0. STALE_PREDICATE catches both 'sv < current' AND
'tolerantly-installed row whose original now outranks the projection'.

Migration 014 invokes run_bulk_lazy at the end of up() — no-op pass
at v1, but validates the wiring is correct. Future bump migrations
(015 = v2, etc) will reuse the same helper.
2026-06-05 14:21:31 +03:00
DumiFlex 07fe3ab064 feat(engine): lazy_migrate_row — the ONE migration runtime
Implements spec §4 Flow 5 in full:
- No-op when row is at CURRENT with no future original
- NULL-drop optimization when row.sv == original.sv == CURRENT
- Recovery exact catch-up: promote original into payload (the §4 bug fix —
  write-back gate is 'base_v < CURRENT OR base IS original', not just
  'base_v < CURRENT'; otherwise exact catch-up silently no-ops)
- Still-drifted re-project: tolerantly strip original down to CURRENT
- Deferred strict-validate failure → tolerant_drift_status='broken'
- Standard forward chain otherwise

original_payload_json is inert storage: two writers only (initial
tolerant install + this NULL-drop). No other path rewrites it.
schema_migrated_at is separate from updated_at (structural migration
is not a user edit).
2026-06-05 13:46:49 +03:00
DumiFlex c46cbbf509 feat(engine): migration 014 - payload schema_version + verbatim-local mirror
Adds the four payload-versioning columns to modules + bundles per
the community-web spec sections 4-5:
- schema_version (NOT NULL DEFAULT 1)
- original_payload_json (NULL - only set during tolerant install)
- tolerant_drift_status ('none' | 'pending' | 'broken')
- schema_migrated_at (separate from updated_at - structural migration
  is not a user edit)

Backfill is implicit via the DEFAULT on the NOT NULL columns.
2026-06-05 12:50:54 +03:00
DumiFlex af314d3abe feat(community): update-action dialog with in-place + as-new + dismiss
Closes the loop on community-tracked rows. The warn-coloured update
pill on ModuleListView is now a button; clicking it opens
CommunityUpdateDialog with three resolutions:

1. Update in place — fetches the post's latest version payload, pre-
   injects a history snapshot of the local row's pre-update state
   into the new payload's `history` sidecar (modules only — bundles
   have no per-row history feature), wraps in the engine envelope,
   then routes through installEnvelope with a forced `replace`
   decision. Engine `_update_module` / `_update_bundle` now rewrite
   community_post_slug + community_version_number when the new
   content carries them, so the row's tracked version bumps to the
   post's latest in one transaction. The user can roll back via the
   editor's existing History panel.

2. Install as new entry — installEnvelope with a forced `rename`
   decision (auto-suffix `-v{N}` on the entity name). Keeps the
   original row exactly as it was; the new row also stamps community
   origin so it gets tracked on its own.

3. Dismiss — persists `{post_slug: version}` to localStorage. The
   community update store reads this map on every check; pills clear
   for the dismissed version, but pop again if the post ships a
   newer version (dismissal is per-version, not per-post).

Importer extends `_update_module` + `_update_bundle` to rewrite
community_* columns when the incoming content carries them. Vanilla
Import-tab replaces (no community origin in the entity) keep the
pre-existing UPDATE shape so locally-authored rows don't get their
community_* fields silently nulled by a non-community replace.

ModuleListView mounts the dialog once at the list root and stores
the active update entry locally — pill click hands the entry to the
dialog, dialog drives all three actions through stores + install
pipeline + localStorage, then closes.
2026-06-05 06:49:22 +03:00
DumiFlex 9957702099 feat(community): track install origin + update-available indicator
Library rows installed from the community embed now carry the source
post slug + installed version_number in the engine sqlite. The SPA
uses both surfaces: a "community" pill next to the row name (so users
can tell at a glance which entries were authored locally vs pulled
from the embed), and a warn-coloured "v{N}" pill when the post has
shipped a newer version since install.

Engine:
- Migration 013 adds community_post_slug + community_version_number
  to modules + bundles. Both nullable; locally-authored rows stay
  NULL forever. Implemented as a .py migration with PRAGMA
  table_info guards so rerunning against an already-migrated DB
  (test fixtures rewind migrations to exercise older paths) is a
  clean no-op rather than "duplicate column name".
- _insert_module / _insert_bundle (importer.py) persist the fields
  when the entity dict carries them.
- _row_to_module / _row_to_bundle (repositories.py) surface them on
  every read path. Try/except guard for pre-013 DB shape so a
  connection opened mid-rollout does not crash.

Manager:
- installEnvelope grows optional `origin: { post_slug, version_number }`
  in InstallOptions. When set, stamps every module + bundle in the
  selection before the commit roundtrip.
- Host bridge in main.ts threads origin from `install(envelope, opts)`
  through to installEnvelope. ABI stays at 1 (additive).
- New communityUpdateStore walks the library snapshot, batch-fetches
  each unique community post (deduped, 8-wide concurrency), surfaces
  rows whose installed version is behind the post's
  latest_version_number. Network failures clear the store silently
  so an offline session does not render misleading "v2 available"
  pills.
- AppLayout kicks one check after the initial library fetch
  resolves. No background polling.
- ModuleListView renders two distinct pills per row: a static
  "community" indicator on origin-stamped rows, and a separate
  warn-coloured "v{N}" pill when the update store has an entry.

Migration tests updated for the new schema head + bundle/module
column sets.
2026-06-04 17:21:04 +03:00
DumiFlex 738ede31c3 fix(cascade): allow whole-entity delete for fixed_values/combine/derivation/constraint
Orchestrator dispatch table only handled wildcard/category/bundle/option deletes (the kinds that have reverse-refs to fix). Leaf module types fell through to the unsupported (kind, action) error path even though there is no cascade work to do for them — just drop the row + register undo.

Added a fallback branch for (fixed_values | combine | derivation | constraint, delete) that emits empty touched/diff lists; _delete_target was already routing the kind through ModuleRepository correctly via its else-branch, just needed the kind added to the whole-entity-delete guard.

Caveat noted in code: combine.output_var is referenced by name from downstream payloads. A combine delete leaves any consumer with an unresolved variable, surfaced separately by the integrity/broken-refs scan rather than by cascade-apply.
2026-06-04 15:10:16 +03:00
DumiFlex 04d3795103 feat(api): expose db config get/put endpoints with pending-move 2026-06-03 02:16:51 +03:00
DumiFlex 747a84ac16 feat(engine): add user/global/root location path helpers 2026-06-03 02:09:20 +03:00
DumiFlex 52966e69d1 feat(engine): execute pending db moves at plugin load 2026-06-03 01:58:15 +03:00
DumiFlex c03fc070ba feat(engine): honor sidecar preference and add root db location 2026-06-03 01:54:05 +03:00
DumiFlex 069527a066 feat(engine): add sidecar db-config.json reader/writer 2026-06-03 01:45:39 +03:00
DumiFlex 5a62929f91 feat(engine): add run_migrations maintenance op 2026-06-03 00:03:17 +03:00
DumiFlex d39add1a44 feat(engine): add analyze maintenance op 2026-06-02 23:58:55 +03:00
DumiFlex 7f125756a3 feat(engine): add integrity_check maintenance op 2026-06-02 23:55:09 +03:00
DumiFlex 4abe679525 feat(engine): add vacuum maintenance op 2026-06-02 23:50:52 +03:00
DumiFlex 2b5f2b7af7 feat(engine): add gather_info DB introspection 2026-06-02 23:39:56 +03:00
DumiFlex 7e12626248 feat(engine): expose db path resolution source 2026-06-02 23:33:29 +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 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 b7d672396a feat(import-export): add templates bucket to engine exporter + importer 2026-05-28 00:21:51 +03:00
DumiFlex 65080db689 feat(templates): add prompt template table, repository + REST API
Introduces a persisted prompt-template library: migration 012 creates the
templates table (name/description/category_id FK/tags/is_favorite/
template_string), TemplateRepository provides CRUD with partial-update
sentinel semantics, and wp_api/templates.py exposes list/get/create/update/
delete/favorite with FK violations mapped to 400. Bumps the migration-head
version assertions to 12.
2026-05-27 00:25:52 +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 55ec1773b1 fix(engine): carrier-claim fires on null/empty pick + honest warning text
Two follow-ups from the user's screenshot showing a misleading
never_applied warning for @color after backdrop rolled its null option:

- wildcard_handler skipped the carrier-claim on the empty-value early
  return — claim_carrier_constraints sat only after resolve_text, but a
  null/empty pick returns before reaching it. So a backdrop that rolled
  'null' never claimed the constraints it carries, letting @color
  either spill or warn. Extracted the claim into a local _claim_carried
  closure and call it on BOTH the empty-value path AND after
  resolve_text. A carrier that rolls ANY option (including null) now
  claims its carried constraints, matching the user's no-spill choice.
  (Explains the color-vs-mood asymmetry too: mood had a top-level
  instance downstream that caught hair_x_mood, color didn't — so only
  color surfaced the bogus warning.)

- the 'present' never_applied message wrongly prescribed 'move the
  constraint above the target's first appearance' and blamed
  registration order. That advice is wrong when the target's carrier is
  ALSO the constraint's source (the source must stay upstream). Reworded
  to state the observation without the bad fix-it, and dropped the
  ordering-only framing.

Regression test test_carrier_claims_on_null_empty_pick locks in the
null-pick claim across 5 seeds. All 1026 pytest + 2381 vitest pass.
2026-05-26 22:43:29 +03:00