73a00a3760d31cbfd4e397cdd2cf60aef01ee3fb
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d826c6ba61 | fix(ts): pick reach stores bare _uid; partial_reach severity info + stale-comment cleanup | ||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
e3d5c28612 |
feat(engine): carrier-claim failsafe + cause-aware never_applied warning
Two changes driven by the user's real chain (shirt option carries
@{color}; backdrop carries @{color}+@{mood}; constraints sit between
them):
Carrier-claim failsafe (no spill past a carrier)
A constraint is now positionally claimed by the FIRST downstream
wildcard that could host its target — a direct instance OR a
nested-ref carrier — matching what the pair badge already shows.
After a wildcard resolves, claim_carrier_constraints consumes any
unconsumed constraint whose target the wildcard carries (an
@{target} ref in ANY of its options), provided the source is
already picked. So when backdrop rolls a no-ref option, it still
claims the constraint instead of letting it spill onto a later
top-level target instance. No-op when the chosen option DID resolve
the ref (nested path already consumed it) or the source isn't
picked yet.
Cause-aware never_applied warning
The end-of-run warning now classifies WHY a constraint didn't fire:
_target_present_in_chain scans the module list + catalog for a
direct instance or a carrier of the target. Present → 'target
exists in the chain but wasn't claimed (rolled before this
constraint registered, or in a separate branch)'. Absent → 'no
instance or nested-ref carrier found (check the target uuid)'.
detail.target_present is stamped so the DebugViewer can later pick
a distinct icon without re-parsing the message.
Net for the user: the failsafe means a rolling carrier always claims
its constraints, so the confusing per-seed never_applied warnings for
backdrop disappear entirely; the only warnings left are genuine
absent-target / ordering cases, each with actionable wording.
5 regression tests added (carrier claim + no spill across 20 seeds,
source-not-ready no-op, present-vs-absent message). All 1029 pytest
+ 2381 vitest pass.
|
||
|
|
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. |
||
|
|
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)
|