# Changelog

All notable changes to this project will be documented in this file. The format is loosely based on Keep a Changelog, adapted for the project's modular cog architecture.

## [Unreleased]

### Cog Architecture Migration (Waves 1-6, 2026-05-15)

Architectural shift: every functional concern moved from feature folders into a flat `src/cogs/` directory. The old layout (`src/parser/`, `src/matcher/`, `src/builder/`, `src/api/`, `src/import/`, `src/ui/`, `src/util/`) is gone. After Wave 6, `src/` contains only `main.js` (the Foundry entrypoint declared in `module.json`'s `esmodules` array), the `cogs/` directory with 35 cogs, and the `schema-snapshots/` data directory. Every cog carries a banner-comment header (`COG: <name>.js - <one-line description>`) and is auto-discovered by `tools/regenerate-cog-index.mjs`, which writes the `_index.js` registry. The architecture mirrors how the Abyssal Engine Discord bot and webapp organize feature code; the user's request was "everything needs to be self contained in smaller files, no monolithic scripts."

The migration shipped in six waves so each layer could be tested in isolation:

- **Wave 1 (v0.5.0 / 0.5.1)** moved `src/parser/` (798 LOC across 13 files) to 14 cogs (`parser-portfolio`, `parser-actor`, `parser-items`, `parser-actor-type`, `parser-compset-registry`, plus 9 per-compset normalizers). The pilot cog `system-detection.js` was promoted earlier in v0.5.0 to deduplicate the SF2e class slug set that previously lived in two parser files (the duplication was the root cause of a Soldier mis-detection bug fixed in v0.4.6).
- **Wave 2 (v0.5.3)** moved `src/matcher/` (798 LOC across 6 files) to 6 cogs (`matcher-core`, `matcher-slug-table`, `matcher-overrides`, `matcher-fuzzy`, `matcher-foundry-resolver`, `matcher-mock-resolver`).
- **Wave 3 (v0.5.4)** moved `src/builder/` (627 LOC across 5 files) to 5 cogs (`builder-actor-pf2e`, `builder-actor-sf2e`, `builder-attachments`, `builder-hp`, `builder-items`). The SF2e cog re-imports 11 names from the PF2e cog (5 constants + 6 helpers); the SF2e implementation is intentionally a thin overlay on the PF2e chassis, not a fork.
- **Wave 4 (v0.5.5, batched)** moved `src/api/` (266 LOC across 2 files) to 2 cogs (`api-hlo-client`, `api-token-cache`).
- **Wave 5 (v0.5.5, batched)** moved `src/import/` (235 LOC across 3 files) to 3 cogs (`import-conversion-journal`, `import-extract-overrides`, `import-find-existing-actor`).
- **Wave 6 (v0.5.5, batched)** moved `src/ui/` and `src/util/` (870 LOC across 4 files) to 4 cogs (`ui-import-dialog`, `ui-settings`, `util-version`, `util-logger`). `src/main.js` stayed at its current path because Foundry's loader expects the entrypoint at the path declared in `module.json`'s `esmodules` array.

All consumer paths repointed in lockstep with each wave: 4 source files (`main.js` and the dialog) plus the test scripts under `tools/` (`parse-noxora.mjs`, `parse-pf2e-fighter.mjs`, `sanity-check.mjs`, `extract-overrides.mjs`). The three test scripts pass after every wave (`npm run sanity`, `npm run parse:noxora`, `npm run parse:fighter`). Internal cog-to-cog imports use the sibling-cog form (`./parser-X.js`, `./matcher-Y.js`) so the directory remains flat and the regenerator stays simple.

### Documentation refresh (2026-05-15)

`README.md`, `docs/CONTRIBUTING.md`, and `docs/HANDOFF_v1_20260512.md` updated to reflect the cog architecture: the directory tree in HANDOFF section 8 lists the 35 cogs by name, the contributor walkthrough for adding a new compset handler now points at `src/cogs/parser-compset-registry.js` and `src/cogs/matcher-slug-table.js`, and the README architecture paragraph describes the cog grouping. The deploy-script table in README dropped the removed `npm run deploy:rsync` entry and added `npm run sync:v14` and `npm run cogs:regen`.

### Added (Phase 0, 2026-05-12)

Initial repository scaffolding and module skeleton.

- Project identity files: `README.md`, `LICENSE` (MIT), `.gitignore` (covers Node, Foundry packaging, secrets, live-token fixtures).
- Handoff documentation under `docs/`, capturing the research conversation, HLO Public API contract, HLO export format, architectural decisions, and the phased roadmap. Currently at revision 1.0.1, which folds in Foundry Hub market-positioning recon, the HeroVau.lt strip decision, the shinra.cc canonical URL scheme, and brand asset selection.
- Foundry module manifest `module.json` declaring V13 compatibility, PF2e and SF2e (assumed id `sf2e`, pending verification) system relationships, the Abyssal Engine brand art, and shinra.cc canonical URLs for project landing, manifest, download, readme, changelog, and license.
- Source tree skeleton under `src/`, with stub files for the planned cogs: `api/`, `parser/` (with `compsets/`), `matcher/`, `builder/`, `ui/`, `util/`. Each stub carries the version header and a TODO describing the file's role in its phase.
- Brand art copied from the Abyssal Engine asset library: production purple icon and Discord bot banner (the `_Prod` variants, deliberately not the `_DEV` crimson ones).
- Empty `data/test-fixtures/` directory reserved for the Noxora reference character (Phase 1 test fixture, to be added when the JSON is migrated from the research conversation).

### Added (Phase 1 wave 1, 2026-05-12)

End-to-end pipeline from pasted HLO JSON to a buildable Foundry actor skeleton, validated against the Noxora fixture in Node before wiring the UI.

- Parser layer in `src/parser/`: top-level `parse-portfolio.js` walks the HLO export shape (`portfolio`, `metadata`, `actors`), `parse-actor.js` normalizes per-actor `gameValues` and infers `pf2e` vs `sf2e` from class trait IDs, and `parse-items.js` recursively walks nested items (weapon ammo, armor upgrades) producing a flat IR plus a parent/child `itemTree`. Every item gets a universal `slug` derived from its HLO ID, plus compset-specific normalization via the dispatcher.
- Compset handlers in `src/parser/compsets/`: `abilities.js` (AbilScore, Save, Skill, ArmorClass, Movement, Reserves, Derived) computes modifiers and proficiency ranks; `ancestry.js` (Ancestry, Heritage, Language) extracts trait-based slugs; `class.js` (Class, Deity) detects SF2e via class slug.
- Matcher layer in `src/matcher/`: `slug-table.js` declares the compset-to-pack mapping and generates ordered slug candidates (ID, SF-suffix-stripped, name-derived, trait-derived); `overrides.js` carries the curated landmines from zarmstrong's changelog (Champion subclasses, Alchemist research fields, Rogue rackets, Investigator methodologies, known feat and spell typo families); `fuzzy.js` implements Levenshtein-based similarity with name normalization; `matcher.js` orchestrates the hybrid strategy. The pluggable resolver pattern lets the same matcher run inside Foundry (`foundry-resolver.js` wrapping `game.packs`) or out (`mock-resolver.js` with a seeded index for Node).
- Builder layer in `src/builder/`: `build-items.js` emits compendium-reference stubs from match records; `build-actor-pf2e.js` assembles a Foundry actor skeleton with abilities, saves, skills, languages, attributes, details (ancestry, heritage, class, deity), and the embedded item stubs. The skeleton is the wire format the import dialog passes to the resolver step.
- UI layer in `src/ui/`: `import-dialog.js` is the ApplicationV2 paste-JSON dialog. On Import click it runs the parser, the matcher (against `makeFoundryResolver()`), the builder, then walks the skeleton replacing each reference with an actual compendium document via `pack.getDocument()` before calling `Actor.create()`. A conversion log surfaces matches, misses, and warnings to the user.
- `src/main.js` updated to register the renderActorSheet hook that injects an "Import from HLO" button onto character sheets for `pf2e` and `sf2e`.
- `styles/import-dialog.css` carries the Abyssal Engine purple palette for the dialog chrome and header button.
- `templates/import-dialog.hbs` is the Handlebars partial powering the dialog body (tab bar with token/upload modes greyed out for Phase 4/5, textarea, conversion log, footer buttons).
- Development tooling: `package.json` declares `type: module` and an `npm run parse:noxora` script; `tools/parse-noxora.mjs` runs the parser, matcher, and builder against the Noxora fixture and dumps the full pipeline output for verification.
- Research artifact: `research/sf2e-system-v14-dev.json` captures the live SF2e Foundry system manifest (id confirmed as `sf2e`, Foundry V14.360+, the full compendium pack list used by the matcher).
- `module.json` compatibility broadened to `minimum: 13`, `verified: 14.361`, `maximum: 14` to support PF2e on V13 and SF2e on V14.
- README "Why this exists" section now in Steven's voice.
- HANDOFF bug-tracker URL marked permanent (GitHub Issues; the shinra.cc bug-tracker subpage is no longer a planned replacement).

### Added (Phase 2, 2026-05-12)

Compset handlers for every HLO compset Noxora carries (Feat, Action, Ability, Spell, Weapon, NaturalWep, WeaponTrait, Ammunition, Armor, ArmorTrait, Upgrade, GearBasic, Trait, plus the Phase 3 Augmentation handler). Each handler exposes a `normalize(raw)` that surfaces the compset-specific fields the matcher and builder need (action cost, prerequisites, weapon reach, armor AC contribution, period/recharge tracking).

The dispatcher registry in `src/parser/compsets/index.js` now points every known HLO compset at its dedicated handler instead of the Phase 1 passthrough fallback.

### Added (Phase 4, 2026-05-12)

Direct HLO Public API integration as a first-class import mode.

- `src/api/hlo-client.js` implements `acquireAccessToken` and `getCharacter` against `https://api.herolab.online/v1/`. Handles the `wait`/poll cycle with a 20-iteration safety cap, distinguishes CORS rejections from HTTP errors via the typed `HLOApiError`, and respects rate-limit signals.
- `src/api/access-token-cache.js` is a module-scoped in-memory cache (50 minute TTL) so a single user does not re-exchange their JWT on every import.
- `src/ui/settings.js` registers two Foundry settings: a per-client user token store and a per-world fuzzy match threshold (default 0.85, configurable in module settings).
- The dialog template (`templates/import-dialog.hbs`) gains an Element Token tab with two inputs (user token password field, element token text field with maxlength 14). The Paste JSON tab remains as the air-gapped fallback. The Upload File tab remains a Phase 5 placeholder.
- The dialog logic (`src/ui/import-dialog.js`) gains a `_fetchViaApi` path that exchanges the JWT, fetches the character, then feeds the same parser pipeline used by paste mode.
- `src/main.js` registers settings on `init`.

### Refactor (Phase 1.1, 2026-05-12)

`build-actor-pf2e.js` no longer emits inline `system.details.{ancestry,heritage,class,deity}` blocks. PF2e and SF2e store these as embedded items and derive sheet display from them; the matched compendium references on the actor's `items` array are the authoritative source. The Node test harness was updated to follow the corrected shape.

### Added (Phase 5, 2026-05-12)

Upload-JSON mode wired as a third input tab. The dialog now offers all three intended input paths: element token (default, API-driven), pasted JSON, and uploaded JSON file. The upload path reads the file locally via `File.text()` and pipes through the same parser pipeline used by the other two modes.

A test plan covering Phases 1 through 5 lives at `docs/PHASE_1_TO_5_TEST_PLAN.md`. The Node test harness handles the out-of-Foundry baseline; the doc walks through the in-Foundry verification steps for module loading, settings registration, button injection, all three import modes, the conversion log behavior, and failure-mode tests for empty input, malformed JSON, and bad tokens.

### Added (Phase 3 and matcher polish, 2026-05-12)

Item 1, dedicated SF2e actor builder. `src/builder/build-actor-sf2e.js` composes the PF2e shared core (abilities, saves, skills, lores, languages, attributes, details) and layers SF2e-specific behavior on top: augmentation and upgrade flagging so the SF2e sheet can group them, focus pool placeholder for Mystic/Witchwarper/Technomancer characters even when HLO did not surface a Reserves entry. The import dialog now branches on `actor.inferredSystem` and dispatches to the right builder.

Item 2, attachment handling for nested items. `src/builder/build-attachments.js` walks the parser's itemTree and produces an AttachmentPlan: a typed list of operations classifying each nested item as `ammo`, `weapon-trait`, `armor-trait`, `weapon-upgrade`, `armor-upgrade`, or `augmentation`. The import dialog's `resolveSkeletonForCreate` step applies the plan after compendium resolution: ammo wires `system.selectedAmmoId` on the parent weapon, redundant traits are dropped (the compendium parent already carries them) with their data preserved in the parent's flags for diagnostics, upgrades stay as items with an `attachedTo` flag pointing to their parent. Noxora's 19 nested items are now classified and routed: 2 ammo, 10 weapon-trait, 5 armor-trait, 1 weapon-upgrade, 1 armor-upgrade.

Item 3, expanded matcher overrides plus matcher polish. The overrides table is now documented inline with a "how to add new entries" comment block and sectioned by content area; SF2e subclass placeholders are flagged for the next pass once we have real HLO IDs to anchor them. Two matcher robustness wins: `slug-table.js` now generates singular/plural variants of each candidate slug ("known weakness" auto-tries "known weaknesses" without needing a curated override), and `fuzzy.js` combines Levenshtein similarity with Jaccard token overlap so word-reorder cases ("Bipod, tactical" vs "Tactical Bipod") surface that pure edit distance would miss.

Item 4, Lore subskill handling. `build-actor-pf2e.js` now detects skill names ending in " Lore" and routes them to embedded items of type `lore` with `system.proficient.value` instead of emitting them as invalid entries on `system.skills`. Noxora's "Xovaikain Lore" (rank 1) now appears correctly as a Lore item.

### Added (wave 5, 2026-05-12)

HP baseline calculation, re-import detection, and the PF2e branch test fixture.

- `src/builder/build-hp.js` carries lookup tables for class and ancestry HP across PF2e (Player Core 1, Player Core 2, Remaster) and SF2e (Operative, Mystic, Solarian, Envoy, Technomancer, Witchwarper, plus SF2e ancestries). `computeBaselineHp(actorIR)` produces `ancestryHp + (classHp + conMod) * level` and returns null when either lookup is missing. The PF2e and SF2e builders wire this into `system.attributes.hp = { value, max }` so the sheet renders sensible numbers at create time instead of 0/0 while Foundry auto-calc catches up.
- `src/import/find-existing-actor.js` scans the world for any actor previously imported from the same HLO portfolio (matched on `flags.hlo-importer-abyssal-engine.hloCharId` plus `hloKey`). The import dialog now uses this to detect re-imports and update the existing actor in place rather than silently producing duplicates. The update is intentionally non-destructive: existing embedded items are preserved and new ones are added via `createEmbeddedDocuments`, with a loud warning in the conversion log telling the user to resolve duplicates manually.
- The parser now threads `portfolio.charId` through `parseActor` so the actor IR carries `portfolioCharId`, used by the re-import detector and stamped into the actor flag.
- `data/test-fixtures/synthetic-pf2e-fighter.json` is a hand-rolled PF2e Dwarf Fighter export shaped to exercise the pf2e builder branch end-to-end. The Node harness `tools/parse-pf2e-fighter.mjs` runs it through the full pipeline; the SF2e Noxora harness still validates the sf2e branch. New npm scripts: `parse:fighter` for the PF2e branch and `parse:all` for both.
- The mock resolver gained pf2e pack entries (ancestries, heritages, classes, deities, feats, equipment) so the synthetic fixture lands clean matches.
- The build-actor-pf2e helpers (ABILITY_KEY, SAVE_KEY, PROFICIENCY_RANKS, STAT_BEARING_COMPSETS, SF2E_FOCUS_POOL_CLASSES, buildDetails, buildAbilities, buildAttributes, buildSaves, buildSkills, buildLanguages, buildLoreItems, rankFor, slugifySkillName, isLoreSkillName) are now exported so build-actor-sf2e.js composes the shared core rather than duplicating it.

### Added (wave 6, 2026-05-12)

Conversion-log persistence, NPC type detection, and a sanity-check harness.

- `src/import/conversion-journal.js` builds an HTML rendering of the conversion log (errors, warnings, info entries grouped by level with counts) and creates a JournalEntry capturing the import for later review. A new `Save conversion log as Journal Entry` client setting (default on) toggles this behavior. Each entry carries flags pointing back at the imported actor and the source HLO charId so users can correlate imports months later.
- `src/parser/detect-actor-type.js` runs a heuristic against the parsed IR (player field presence, Class compset presence, background text presence, ability-score count, natural-weapons-only signal) and emits a confidence-scored "character" or "npc" decision. The PF2e and SF2e builders now use `actorIR.actorType` instead of hardcoding `"character"`, and the actor flags carry the full decision (detected type, confidence, list of reason strings) so users can see why the call was made. Confidence below 0.6 produces an `ambiguous-actor-type` warning surfaced in the conversion log.
- `tools/sanity-check.mjs` verifies invariants that would otherwise drift silently: every compset in `COMPSET_TO_PACKS` has a handler in the registry, every override key parses as `compset:slugOrName`, every override slug is lowercase kebab-case and every pack reference is a real Foundry pack name, slug derivation is idempotent for representative inputs. ~100 individual checks run in well under a second. New npm scripts: `npm run sanity` (runs only the sanity check) and `npm test` (runs sanity check plus both parser fixtures).

### Added (wave 7, 2026-05-12)

Contributor guide, in-dialog log counts, and a Test API Connection diagnostic.

- `docs/CONTRIBUTING.md` covers local development setup, every npm script, the coding conventions (version headers, no em-dashes, prose over bullets, modular cog architecture, banned phrases), and the practical workflows for adding a new compset handler, adding a matcher override, and adding ancestry or class HP table entries. The em-dash audit example uses a printf-escape so the file itself does not violate the rule it teaches.
- The dialog's conversion log section now reads "Conversion log (N info, M warn, K error)" in the header so users see the shape of the result before scrolling.
- A `Test API Connection` button on the Element Token tab exchanges the user token for an access token without fetching a character. The token cache is invalidated before the test so a previously-cached access token cannot mask a revoked or rotated user token. Separates "is my JWT still valid" from "does this particular import work" for cleaner debugging.

### Added (wave 8, 2026-05-12)

Release packaging, manifest validation, and README quickstart.

- `tools/check-module-json.mjs` validates the manifest against required Foundry fields and project conventions (id matches directory, version is semver-shaped, compat minimum is V13 or newer, shinra.cc URLs for url/manifest/download/readme/changelog, GitHub Issues for bugs, both pf2e and sf2e in relationships.systems, MIT license, Steven as authors[0]). 23 checks pass against the current manifest. New npm scripts: `check:manifest` (manifest only) and the test pipeline now runs it before the parser fixtures.
- `tools/release.mjs` bundles the module into `releases/hlo-importer-abyssal-engine-X.Y.Z.zip` ready for upload to `shinra.cc/hlo-importer-abyssal-engine/releases/latest.zip`. Uses `execFileSync` rather than `exec` to avoid shell-injection concerns, takes its file list from a fixed constant in the script. Requires the system `zip` command on PATH (Linux: `apt install zip`, macOS: built-in, WSL: same as Linux). New npm script: `npm run release`.
- README gains a Quick start section walking developers through clone, npm test, and module-data-folder symlink, plus a table of every available npm script. Architecture summary updated to reference `Actor.create` and `Actor.update` (the V13+ pattern) rather than `Actor.importFromJSON` (the legacy zarmstrong pattern).
- `.gitignore` excludes the `releases/` directory so build artifacts do not contaminate the repo.

### Added (wave 9, 2026-05-12)

Minion-aware actor-type detection, JournalEntry-fed override extractor, and the shinra.cc project subpages.

`src/parser/detect-actor-type.js` now classifies actors as `character`, `npc`, or `familiar` and tags non-character actors with a `minionSubtype` of `familiar`, `animal-companion`, `eidolon`, or `drone`. The detection runs minion signals first (name patterns like "Familiar of X", "Animal Companion of X", "Eidolon", "Drone", plus HLO trait IDs and class slug heuristics) and falls back to the wave-6 PC-vs-NPC heuristic. Both builders use `actorIR.actorType` and surface the full decision in actor flags so users can see why the call was made. Confidence below 0.6 produces an `ambiguous-actor-type` warning.

`src/import/extract-overrides.js` scans the dialog's structured log records (or the rendered HTML of a saved conversion JournalEntry) for "Unmatched" warnings and produces ready-to-paste entries for `src/matcher/overrides.js`. Each stub is keyed correctly and points at the suggested pack with a `PUT-FOUNDRY-SLUG-HERE` placeholder for the user to fill from the Foundry compendium browser. Two access paths: the `tools/extract-overrides.mjs` CLI reads stdin (text or HTML) and prints stubs to stdout; the import dialog has a "Copy override stubs" button next to the log header that copies the same output to the clipboard. New npm script: `extract:overrides`.

`shinra-cc-site/hlo-importer-abyssal-engine/` carries the static site pages for the project subsection of shinra.cc, ready for upload to Bluehost. Built on the same ZeroFour-template + dark-mode CSS as the Abyssal Engine legal page, with cyan `#00bcd4` accent for site consistency and the purple Abyssal Engine banner art for brand identity. Pages: `index.html` (project landing with install instructions and the manifest URL), `readme.html` (mirror of the GitHub README), `changelog.html` (per-wave summary tied to commit SHAs), `license.html` (MIT plus upstream acknowledgement), `legal.html` (trademark attribution for LWD, Paizo, Foundry). Assets include `banner.png` and `icon.png` copied from the module's brand art. `releases/` is reserved for the .zip output of `npm run release`. The directory's `README.md` documents the upload workflow.

### Fixed (wave 10, 2026-05-12)

Two production-blocking bugs caught by verifying against a real Foundry V13.351 install with PF2e 7.12.2 and SF2e 0.0.11.

`module.json` compatibility was declaring V14 verified and V14 maximum, but the actual current stable PF2e (7.12.2) and SF2e (0.0.11) both target Foundry V13.348 to V13.351, not V14. Production users on V13 would have seen "module is for a newer Foundry version" warnings; users on V14 would have tried to load a module against systems that do not support V14. Corrected to minimum 13, verified 13.351, maximum 13. The manifest validator gained a `maximum is at least V13` check. (V14 work is planned by the maintainer but not the current target.)

`COMPSET_TO_PACKS` and `OVERRIDES` were using SF2e-style kebab-case pack names (`feats`, `actions`, `spells`, `equipment`) for both systems. PF2e actually uses legacy logical names in its system.json: `feats-srd`, `actionspf2e`, `spells-srd`, `equipment-srd`, `classfeatures` (no hyphen), `ancestryfeatures` (no hyphen), `conditionitems`. Every PF2e feat, spell, equipment, action lookup would have silently missed in production because `game.packs.get("pf2e.feats")` returns undefined while `game.packs.get("pf2e.feats-srd")` is the real pack. Restructured `COMPSET_TO_PACKS` to be a per-compset object keyed by system id; added `packsFor(compset, systemId)` helper; updated the matcher to use it; updated the mock resolver, overrides, and sanity check to use the correct PF2e pack names. Verified live: Bren the Dwarf Fighter now routes to `pf2e.feats-srd`, `pf2e.equipment-srd`, etc.; Noxora the Operative continues routing to `sf2e.feats`, `sf2e.equipment`.

### Added (wave 10, 2026-05-12)

`.gitignore` now excludes the local Foundry VTT portable install (`data/FoundryVTT-WindowsPortable-*/`, `data/FoundryVTT-*/`) so Steven can keep a reference install under `data/` for offline schema verification without contaminating the repo. The install is multi-gigabyte and never goes to GitHub.

### Added (wave 11, 2026-05-12)

Foundry V14 + Anachronism cross-system content support.

`module.json` compat broadened to `minimum: 13, verified: 14.361, maximum: 14`. Verified against a live V14.361 portable install with PF2e 8.1.2 and SF2e 1.1.2. The V13 PF2e 7.12.2 and SF2e 0.0.11 line continues to be the minimum, since pack-name conventions and actor field paths are identical between V13 and V14. SF2e took a 0.0.x to 1.x version jump alongside V14 but the schema is stable.

`COMPSET_TO_PACKS` now includes Anachronism-module fallback packs for every routing entry. The Anachronism modules ship official Paizo content packaged for the opposite system: `pf2e-anachronism` (v2.1.0) carries PF2e content for SF2e games, and `sf2e-anachronism` (v2.1.0) carries SF2e content for PF2e games. Both use clean kebab-case pack naming (`actions`, `feats`, `classes`, `equipment`, etc.). The matcher tries the active system's native packs first and falls back to the Anachronism packs for cross-system content. When an Anachronism module is not installed, `game.packs.get()` returns undefined and the matcher silently moves on, so adding the fallbacks is harmless when the modules are absent.

`src/matcher/slug-table.js` gained `qualifyPackId(systemId, packBase)` which detects already-namespaced entries (those containing a `.`, like `pf2e-anachronism.feats`) and uses them verbatim, while bare entries (`feats-srd`, `feats`) get the active system id prepended. `src/matcher/matcher.js` was updated to call it instead of building the qualified id inline.

`tools/sanity-check.mjs` now includes a cross-system matcher proof: a PF2e world correctly matches Operative via `sf2e-anachronism.classes`, an SF2e world matches Wizard via `pf2e-anachronism.classes`, and native content still hits the system's own packs without falling through. Plus invariant checks on `qualifyPackId` and `packsFor` so future refactors that drop the dot-detection get caught.

The schema verification doc gains the V14 line and a dedicated Anachronism section.

### Added (wave 12, 2026-05-12)

`tools/deploy-shinra.sh` orchestrates the Bluehost deploy: gates on `npm test`, stages `module.json` (and optionally a fresh release zip) into `shinra-cc-site/hlo-importer-abyssal-engine/`, then either rsyncs to Bluehost (when `SHINRA_DEPLOY_TARGET` is set and `--rsync` is passed) or prints three concrete upload-command options (rsync, sftp, scripted re-run). Wired into npm as `deploy`, `deploy:full`, and `deploy:rsync`. The `shinra-cc-site/README.md` deployment workflow now describes both the automated and manual paths.

`.env.example` is a committed template documenting `SHINRA_DEPLOY_TARGET` and `SHINRA_DEPLOY_SSH_KEY`. Steven copies it to `.env.local` (gitignored) and fills in his Bluehost target once; the deploy script auto-sources `.env.local` on every run so future deploys do not need re-exporting. Direct `export` in the shell still takes precedence if needed.

### Outstanding

- Live Foundry verification of all flows on both V13 and V14.
- Real SF2e subclass override entries once HLO's actual IDs surface during testing.
- Upload `module.json` to Bluehost (the wave-12 staging put it in the bundle; WinSCP it to `public_html/hlo-importer-abyssal-engine/module.json`).
