Skip to content

Rust + KID pivot — M3: mora-kid MVP#44

Merged
halgari merged 16 commits intomasterfrom
m3-mora-kid-mvp
Apr 21, 2026
Merged

Rust + KID pivot — M3: mora-kid MVP#44
halgari merged 16 commits intomasterfrom
m3-mora-kid-mvp

Conversation

@halgari
Copy link
Copy Markdown
Owner

@halgari halgari commented Apr 21, 2026

Summary

Delivers mora-kid as a library per
docs/superpowers/plans/2026-04-21-rust-kid-pivot-plan-6-mora-kid.md:

  • Reference enum — KID's 4-way parse dispatch (editor-ID, FormID~Plugin, plugin-only, bare hex), with resolve_form(world) helper
  • KidRule AST — keyword, RecordType, FilterBuckets, Traits, chance, source location
  • Filter bucket dispatch+ ALL, - NOT, * ANY, none MATCH (M3: NOT+MATCH active; ALL/ANY log-and-skip, Plan 7 activates)
  • Weapon + Armor trait parsers — full per-type grammar from KID's WeaponTraits / ArmorTraits
  • INI line parser — comments (; / #), sections, ExclusiveGroup-skip, chance default, NONE sentinel
  • KidDistributor — iterates world.records(WEAP) / world.records(ARMO) (preserving plugin_index for ESL correctness), evaluates filter pipeline, KID-bit-compatible chance roll, emits Patch::AddKeyword
  • pipeline::compile(data_dir, world) — top-level entry that Plan 7's mora-cli will call
  • mora-esp extensions — KYWD record type + EspWorld::keywords() + resolve_keyword_by_editor_id()
  • docs/src/kid-ini-grammar.md — grammar reference derived from KID source analysis

Test plan

  • cargo test --workspace167 total tests pass (126 prior + 41 new across mora-kid)
  • cargo clippy --all-targets -- -D warnings clean
  • cargo fmt --check clean
  • cargo xwin check --target x86_64-pc-windows-msvc --workspace clean
  • End-to-end synthetic integration test: 1-plugin world with KYWD+WEAP+ARMO, 5 distributor scenarios (weapon distribute, armor distribute, unresolved keyword skip, unsupported type skip, chance=0)

Scope discipline

  • Record types at M3: Weapon + Armor only. Other types (RecordType::Other(String)) parsed but distributor logs and skips.
  • Filter buckets at M3: NOT + MATCH active. ALL (+) and ANY (*) parsed into AST but evaluator treats as pass (Plan 7 activates).
  • ExclusiveGroup: parsed-as-key-match and ignored (Plan 7+ implements).
  • Trait predicates: parsed but many require subrecord fields not yet on WeaponRecord/ArmorRecord (anim type, armor type). Those log-and-treat-as-pass — mora-esp extensions in Plan 8 activate.
  • No CLI wire-up. That's Plan 7.

Notable implementation details

  • Distributor uses world.records(sig) + manual parse instead of world.weapons() / world.armors(). The latter drops plugin_index, which is critical for ESL plugins (all share FormID high-byte 0xFE — can't reconstruct plugin_index from FormID alone).
  • Parallel-test tmp-dir race fixed in tests/distribute.rs by giving each test a unique plugin filename and plugins.txt suffix. Subagent caught this during execution.
  • Two subtle pipe-count bugs in the plan's test content were corrected during execution. Armor|||HEAVY,-E (3 pipes) puts HEAVY,-E in the Chance field; correct is Armor||HEAVY,-E (2 pipes).

Next up

Plan 7: mora-cli end-to-end — wire pipeline::compile into the mora compile subcommand, produce the first real mora_patches.bin, stage to Data/SKSE/Plugins/, set up the MoraRuntime integration end-to-end.

halgari added 16 commits April 20, 2026 23:07
Delivers mora-kid as a library: KID INI parser + KidDistributor
producing Vec<Patch> from an EspWorld + rule set. Weapon + Armor
only; NOT + MATCH filter buckets active (ALL/ANY log-and-skip for
Plan 7); trait predicates parsed but subrecord-dependent evaluation
deferred.

16 tasks across 10 phases. Also extends mora-esp with KYWD record
type + EspWorld::keywords() + resolve_keyword_by_editor_id() so
editor-ID references resolve to FormIds.

Uses world.records(sig) directly in the distributor (not
world.weapons()/armors()) to preserve source plugin_index — critical
for ESL-slot plugins which share FormID high byte 0xFE.
- Add KYWD signature constant to signature.rs
- Add keyword submodule to records/mod.rs
- Implement KeywordRecord with EDID parsing in records/keyword.rs

Enables keyword editor-ID resolution for mora-kid rule processing.
keywords() iterator yields (FormId, KeywordRecord) pairs.
resolve_keyword_by_editor_id(edid) scans for a matching editor-ID
(case-insensitive, matches KID iequals). Required by mora-kid for
editor-ID reference resolution.
Modules: distributor, filter, ini, pipeline, reference, rule,
traits_armor, traits_weapon. All stubs typed so cargo check stays
green through Plan 6.
Reference::parse(str) dispatches on content: ~ (FormIdWithPlugin),
.es (PluginName), all-hex (FormIdOnly), else (EditorId).
resolve_form(world) maps to a FormId where possible (PluginName
returns None — plugin filters are non-form refs). 6 unit tests
including the all-hex-edid-vs-formid quirk that matches KID.
3 tests against synthetic KYWD plugin: resolve editor-ID,
resolve FormIdWithPlugin, unknown editor-ID returns None.
RecordType enum distinguishes Weapon/Armor/Other(string).
FilterBuckets carries MATCH/NOT (active) + ALL/ANY (parsed but
unsupported at M3, distributor logs-and-skips). Traits is a small
enum dispatching to per-type trait structs. SourceLocation tracked
for diagnostics.
WeaponAnimType enum (10 weapon types). WeaponTraits struct with
animation types, enchantment flag, template flag, damage range,
weight range. parse_range helper reads D(min max) / W(min max).
7 tests covering empty/NONE/animations/flags/range/mixed/unknown/case.
ArmorType enum (Heavy/Light/Clothing). ArmorTraits struct with
armor types, enchantment flag, template flag, AR range, weight
range, body slots (30..=61). 6 tests.
ini::parse_file / parse_ini_content handle comments, sections,
exclusive-group skip, per-line rule parsing. filter::parse_filter_field
dispatches tokens to MATCH/NOT/ALL/ANY buckets by prefix.
filter::evaluate runs NOT + MATCH (ALL/ANY log-and-skip at M3,
activation in Plan 7). 10+ tests total.
Scans weapons + armors in the world, evaluates each rule's filter
pipeline (NOT + MATCH active; ALL/ANY log-and-skip), runs
KID-compatible deterministic chance roll seeded by
(keyword_editor_id, form_id), emits Patch::AddKeyword.
Unresolved keyword refs / unsupported record types log and skip.
Stats accumulated per rule/candidate.
discover *_KID.ini in data_dir, parse all rules, build
KidDistributor, run against world with KID-compatible chance,
return finalized Vec<Patch>. This is the public entry that Plan 7's
mora-cli will call to produce mora_patches.bin.
One integration test exercising comments, sections, simple/chance/
traits/filters rules, exclusive-group skip, unsupported-type, and
missing-type-skipped-with-warning.
5 tests against synthetic ESP with 1 KYWD + 1 WEAP + 1 ARMO:
  - distributes to weapon
  - distributes to armor
  - unresolved keyword -> rule skipped
  - unsupported record type -> rule skipped
  - chance=0 -> nothing emitted
Exercises full mora-kid pipeline end-to-end with real mora-esp.
Changes:
- Applied cargo clippy --fix for minor lints (mostly iterator-style
  and formatting) across mora-kid.
- Restructured KidDistributor's stats init to use struct-update
  syntax (clippy::field_reassign_with_default).
- cargo fmt canonical whitespace across mora-kid.
@halgari halgari merged commit 0667b33 into master Apr 21, 2026
6 checks passed
@halgari halgari deleted the m3-mora-kid-mvp branch April 21, 2026 05:41
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