Commit Graph
18 Commits
Author SHA1 Message Date
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 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 d41be86fae fix(engine): fold list-valued vars in derivation + converter reads 2026-06-08 20:58:59 +03:00
DumiFlex 8eeb0a8032 feat(engine): list-backed variables + var.K accessor 2026-06-08 14:21:40 +03:00
DumiFlex e5f54c6ba8 refactor(engine): extract pick_k_unique from multi-pick resolver 2026-06-08 10:13:40 +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 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 1e3e0a5929 fix(syntax): pass cached name through unknown_ref warning detail + message 2026-05-26 19:45:06 +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 b950b8fd82 feat(engine,scanner): nested-ref constraints + @{uuid:subcat} filter + smarter target classification
Three improvements aimed at making constraints actually useful in
composed pipelines, with the conflict scanner keeping up.

## Phase A — nested constraints (engine)

Pre-2026-05 constraints only fired when the target wildcard ran as a
top-level chain module. Wildcards reached through `@{uuid}` refs from
inside another wildcard's option value silently bypassed every rule.
Composition (outfit-set wildcards, palette reuse) was a constraint
blind spot.

- New `engine/modules/_constraints.py` holds the shared application
  logic (loop constraints by target uuid, look up source pick, apply
  matrix/exception cells, emit unknown_constraint_source +
  constraint_excludes_all_options warnings)
- `wildcard_handler.resolve` now delegates to the helper instead of
  inlining the loop
- `engine/syntax/resolve._resolve_ref` calls the same helper after
  fetching the catalog wildcard but before rolling — nested target
  wildcards now get constrained identically to chain-level ones
- `ResolveContext` exposes `get_constraints()` + `get_picks()` so the
  resolver has the back-channel it needs without depending on the
  full pipeline ctx dict

## Phase B — `@{uuid:subcat[,subcat]}` filter

New optional ref syntax. `@{color:warm}` resolves the catalog wildcard
`color` but restricts its option pool to entries whose `sub_category`
is in the filter list. Same semantics as chain-level
`instance.category_filter`, but scoped per call site so one library
wildcard stays reusable.

- Python tokenizer regex extended to capture optional `:subcat,subcat`
  group; `_resolve_ref` filters options pre-pick + pre-constraint
- TS `richTokenize.ts` mirrors the grammar so previews + RichTextInput
  recognise the new form
- `extension/graph.ts:WC_REF_RE` + `preview-tokens.ts:REF_RE` updated
  to skip the filter when extracting uuids (uuid is still group 1)
- Empty pool after filter → empty string output + new
  `ref_subcategory_empty_pool` warning so the user sees an
  unsatisfiable filter instead of falling back silently

## Phase C — scanner reach-set + classification refinement

Constraint scanner pre-fix:
- flagged `constraint_target_in_upstream` whenever target had ANY
  upstream instance, even if a downstream / local-after / nested-after
  instance was also present
- flagged `constraint_target_missing` for targets reachable only
  through nested `@{}` refs

Both false-positives. Post-fix scanner computes
`localNestedReachAfter[i]` — transitive `@{}` walk through the local
catalog starting from modules at positions ≥ i — and uses it to:

- skip `target_in_upstream` when a bindable instance exists elsewhere
  (downstream, local-after-self, nested-after)
- skip `target_missing` when target is reachable via nested ref from
  any later chain wildcard
- still flag both when no bindable instance exists (sole-upstream,
  truly-missing both remain caught)

## Tests

- 8 new Python tests under `TestNestedConstraints` +
  `TestNestedSubcategoryFilter` (constraint applies through nested,
  excludes-all warning, source-not-yet-picked warning, subcat narrows,
  multi-subcat union, empty pool warning, empty filter no-op,
  subcat+constraint composition)
- 7 new TS tests under `scanConflicts — constraint nested-reach
  classification` (nested reachable doesn't flag missing, transitive
  deep reach, upstream+downstream silent, upstream+local-after silent,
  sole-upstream still flags, nested-from-before-constraint still
  flags, nested with subcat filter still reaches)

## Numbers

- 696 pytest pass (+8)
- 1672 vitest pass (+7)
- ruff clean
- bundle 306899/307200 (301 bytes headroom)
2026-05-19 21:40:08 +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 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 bd2cfe1635 feat(test-runner): lazy catalog — resolve nested @{uuid} refs
Wire walk_transitive_refs into _build_ctx_with_catalog so the SPA Test
Runner populates __wp_catalog__ from the live DB before resolution. Also
fixes _resolve_ref to support SnapshotEntry payload.options shape alongside
the legacy flat options shape, and updates db_session to accept an injected
connection from request.app["wp_db"] for test isolation.
2026-04-28 13:23:03 +03:00
DumiFlex 465f0e8a9a feat(syntax): _resolve_multi_pick — weighted without-replacement + sep join 2026-04-28 05:47:35 +03:00
DumiFlex 126b53738b feat(syntax): _resolve_inline_pick — uniform random branch + recurse 2026-04-28 05:39:27 +03:00
DumiFlex f67a70b55e feat(syntax): _resolve_ref with surface/depth/cycle checks + weighted pick 2026-04-28 05:25:09 +03:00
DumiFlex 4485fb34e5 feat(syntax): resolve_text dispatch loop with text/escape/var resolution 2026-04-28 05:13:35 +03:00