Skip to content

Rust + KID pivot — Plan 8a: activate ALL / ANY / ExclusiveGroup#46

Merged
halgari merged 7 commits intomasterfrom
m3-plan8a-full-filters
Apr 21, 2026
Merged

Rust + KID pivot — Plan 8a: activate ALL / ANY / ExclusiveGroup#46
halgari merged 7 commits intomasterfrom
m3-plan8a-full-filters

Conversation

@halgari
Copy link
Copy Markdown
Owner

@halgari halgari commented Apr 21, 2026

Summary

Activates three KID features that Plan 6 parsed but left dormant in the evaluator/distributor:

  • ALL filter bucket (+ prefix) — every ref across every +-joined group must match. AST already captured these; this PR activates the evaluator branch.
  • ANY filter bucket (* prefix) — case-insensitive substring match against the item's editor-ID and its keyword editor-IDs. New evaluate_with_any() extends evaluate() with the kw_edid_map context that only the distributor can build.
  • ExclusiveGroup — INI parser now returns ParsedIni { rules, exclusive_groups } so ExclusiveGroup = Name|kw1,kw2,... lines are captured. Distributor tracks per-form applied-group state; skips emitting a keyword if another member of its exclusive group has already been applied to the same form. DistributorStats gains rejected_by_exclusive_group.

Downstream callers (pipeline::compile, mora-cli/src/compile.rs) updated for the new ini::parse_file signature.

Test plan

  • cargo test --workspace181 tests (174 prior + 7 new: 5 integration + 1 parser + 1 bucket count)
  • cargo clippy --all-targets -- -D warnings clean
  • cargo fmt --check clean
  • cargo xwin check --target x86_64-pc-windows-msvc --workspace clean

Scope discipline

  • No mora-esp binary-format changes. Only DistributorStats gains a field in mora-core; everything else is mora-kid internal.
  • ANY does not yet match display name (FULL) or model path (.nif) — both need mora-esp extensions.
  • ExclusiveGroup member - prefix (NOT-in-group) parsed as regular member with debug log — KID's NOT semantics are subtle and rarely used.

What still blocks full KID parity on Weapon + Armor

  • Trait predicates (anim type, armor type, AR / damage / weight ranges, body slots, -E, -T) — parsed into the AST but evaluator still log-and-skips. Requires mora-esp to expose DNAM / BOD2 / EITM / CNAM subrecord fields on WeaponRecord / ArmorRecord. That's Plan 8b.

Next up

Plan 8b — mora-esp record extensions + trait predicate activation.

halgari added 7 commits April 21, 2026 04:06
Pure mora-kid work — no mora-esp binary-format changes. Activates
ALL + ANY filter buckets (AST already captures them from Plan 6),
parses + enforces ExclusiveGroup with per-form tracking,
DistributorStats gains rejected_by_exclusive_group counter.

7 tasks across 6 phases. Preserves scope — trait predicates wait
for Plan 8b's mora-esp extensions.
Sixth counter on DistributorStats tracking skipped emissions due
to ExclusiveGroup enforcement. AddAssign impl + existing test
updated. Plan 8a's distributor populates this counter.
Holds name + unresolved member references + source location.
Plan 8a Task 4 extends the INI parser to produce these alongside
KidRules; Task 6 resolves + enforces in the distributor.
evaluate() now enforces the ALL bucket (every + sub-group, every ref
within, must match) before MATCH/NOT checks. New evaluate_with_any()
layers ANY substring check on top — requires item_editor_id +
kw_edid_map which only the distributor pre-builds. Unit test for
parser grouping; semantic tests land in tests/filter_activation.rs.
INI parser now returns ParsedIni { rules, exclusive_groups } so
ExclusiveGroup lines are captured instead of silently skipped.
KidDistributor gains with_exclusive_groups() builder + per-form
applied-group tracking — when a keyword is in one or more exclusive
groups and another member of those groups has already been applied
to the form, the emission is skipped and stats.rejected_by_exclusive_group
increments.

Filter evaluator: ALL (+) bucket activation + new evaluate_with_any
that layers ANY (*) substring match against item editor-ID + its
keyword editor-IDs. Distributor pre-builds kw_edid_map once per run
so per-candidate checks are O(candidates * substrings * keywords)
with constant-time lookups.

Downstream callers (pipeline::compile + mora-cli compile.rs) updated
for the new ini::parse_file signature.
5 tests against synthetic ESPs:
  - all_filter_requires_every_ref_to_match
  - any_filter_substring_matches_editor_id
  - any_filter_substring_matches_keyword_editor_id
  - exclusive_group_prevents_second_application
  - exclusive_group_independent_per_form
Exercises Plan 8a's filter activations end-to-end.
Whitespace normalization across new filter_activation.rs + edits
in filter.rs / distributor.rs. No functional changes.
@halgari halgari merged commit 719006a into master Apr 21, 2026
6 checks passed
@halgari halgari deleted the m3-plan8a-full-filters branch April 21, 2026 12:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant