diff --git a/README.md b/README.md index dd49ded..a0709b9 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,33 @@ Consumers today: `battle-focus` (encounter + journal + summary) and (dnd5e rolls, PF2e, etc.) lives in separate adapter repos that declare Foundry + system version ranges they support. +## v0.4.0 — Foundry v14 only + +v0.3.0 renamed the module id (hax-hooks-lib → foundry-hooks-lib) and +shipped the Playwright test against a live Foundry v14. v0.4.0 narrows +the scope to **Foundry v14 only** — the v0.2.0-era anti-corruption +layer that absorbed both v13 and v14 hook renames is reduced to +v14-only normalization. Specifically: + +- The `renderChatInput` registered-hooks entry no longer subscribes + to the v13 `renderChatLog` name. +- The `combatRound` arg-normalization no longer detects the v13 + round-num position; v14's `updateOptions.round` is the only path. +- `compatibility.minimum` in `module.json` is now `14` (was `13`). +- The smoke test dropped 8 v13-only assertions (E.2's + `renderChatLog is in installed`, E.3's "both produce" check, E.4's + v13 round-shape). 546/546 remaining assertions pass. + +`tests/PLAN.md` documents the v14-only scope at the top of the file +and in Section E. + +**Migration note for v13 consumers:** upgrade to Foundry v14. The +contract still documents the v13 → v14 hook name mapping in §9 for +historical reference, but the library no longer subscribes to v13 +names. + +--- + ## v0.3.0 — module id renamed (hax-hooks-lib → foundry-hooks-lib) v0.2.0 is a complete rewrite. v0.1.0 shipped as a curated-event catalog diff --git a/docs/HOOK_CONTRACT.md b/docs/HOOK_CONTRACT.md index 882c202..6e42caa 100644 --- a/docs/HOOK_CONTRACT.md +++ b/docs/HOOK_CONTRACT.md @@ -1,8 +1,13 @@ -# HOOK_CONTRACT.md — `hooks-lib` v0.2.0 +# HOOK_CONTRACT.md — `hooks-lib` v0.3.0 -**Status:** Proposed spec. Subject to revision until v0.2.0 ships. +**Status:** Implemented spec for v0.3.0. **Audience:** hooks-lib authors + consumer authors + system adapter authors. -**Stability:** v0.2.0 ships as the contract; breaking changes require a major bump. +**Stability:** v0.3.0 ships as the contract; breaking changes require a major bump. + +**Scope:** Foundry v14 only. v0.2.0's anti-corruption layer absorbed +v13/v14 hook renames; v0.3.0 narrows to v14. See §9 for the v14 +mapping (the v13 column is kept for historical context — those +renames are no longer subscribed to). --- @@ -374,20 +379,26 @@ return matters. The dispatch mode is part of the hook's entry in §6. ## 9. Anti-corruption: hook rename mapping Foundry renames hooks between versions. The library absorbs the -churn so consumers don't have to. Example mappings (v0.2.0): +churn so consumers don't have to. **v0.3.0 is Foundry v14 only.** -| Foundry v14 hook | Foundry v13 hook | Library `envelope.hook` | -|------------------|------------------|-------------------------| -| `renderChatInput` | `renderChatLog` | `renderChatInput` | -| `updateCombat` (active→inactive) | `combatEnd` (sometimes) | `combatInactive` | -| `dnd5e.rollAttackV2` | `dnd5e.rollAttack` (v1) | `dnd5e.rollAttackV2` (only v2 supported in v0.2.0) | +v0.2.0 supported both v13 and v14 by subscribing to both names and +normalizing the envelope. The current v0.3.0 only subscribes to +v14 names; the v13 column below is historical (those entries are +no longer in the registered hook set, and firing them produces no +envelope). -The library internally subscribes to BOTH the v13 and v14 hook -names where applicable, normalizing the envelope `hook` field to the -modern name. Consumers see one stable name regardless of Foundry -version. +| Foundry v14 hook | Foundry v13 hook (historical) | Library `envelope.hook` | +|------------------|--------------------------------|-------------------------| +| `renderChatInput` | `renderChatLog` | `renderChatInput` | +| `updateCombat` (active→inactive) | `combatEnd` (sometimes) | `combatEnd` | +| `dnd5e.rollAttackV2` | `dnd5e.rollAttack` (v1) | `dnd5e.rollAttackV2` (only v2 supported) | -For each mapped pair, the library's tests assert the normalization. +For v14, the library subscribes to the v14 name and emits it +verbatim. The envelope.hook name IS the v14 name. Consumers see +one stable name. + +If a future v14 micro-release renames a hook again, add the mapping +back here and re-introduce the dual-subscription pattern. --- @@ -397,14 +408,13 @@ Foundry's hook arities shift between versions. The library passes args verbatim, but for hooks where arity is unstable, the library normalizes by padding/truncating to a documented shape. -Example (illustrative — exact normalization is implementation-defined): +Example (Foundry v14; v0.3.0 is v14 only): ```js -// Foundry v13: combatRound(combat, updateData, roundNum) // Foundry v14: combatRound(combat, updateData, updateOptions) // Library normalizes to: -envelope.args = [combat, updateData, roundNum ?? updateOptions?.round ?? 1, updateOptions] +envelope.args = [combat, updateData, updateOptions?.round ?? 1, updateOptions] ``` This normalization is **the only** consumer-facing change vs raw @@ -412,7 +422,7 @@ Foundry. Consumers can rely on `args[N]` being the documented field even if Foundry shifts it. The exact normalized shapes are listed in `docs/HOOK_ARG_SHAPES.md` -(separate file because it's long). v0.2.0 documents the shape for +(separate file because it's long). v0.3.0 documents the shape for every hook in §6. --- diff --git a/foundry-hooks-lib-0.1.0.zip b/foundry-hooks-lib-0.1.0.zip deleted file mode 100644 index d53d48d..0000000 Binary files a/foundry-hooks-lib-0.1.0.zip and /dev/null differ diff --git a/foundry-hooks-lib-0.2.0.zip b/foundry-hooks-lib-0.2.0.zip deleted file mode 100644 index 784931e..0000000 Binary files a/foundry-hooks-lib-0.2.0.zip and /dev/null differ diff --git a/foundry-hooks-lib-0.3.0.zip b/foundry-hooks-lib-0.3.0.zip deleted file mode 100644 index 645559f..0000000 Binary files a/foundry-hooks-lib-0.3.0.zip and /dev/null differ diff --git a/module.json b/module.json index 2d0bc26..e53b4dc 100644 --- a/module.json +++ b/module.json @@ -1,8 +1,8 @@ { "id": "foundry-hooks-lib", "title": "Foundry Hooks Lib", - "description": "Foundry VTT module: generic Foundry hook facade. Subscribes to every relevant Foundry hook (combat, document CRUD, canvas/UI, dnd5e v2 rolls), emits a uniform {ts, hook, args} envelope. No domain interpretation — consumers query the stream. See docs/HOOK_CONTRACT.md.", - "version": "0.3.0", + "description": "Foundry VTT v14-only module: generic Foundry hook facade. Subscribes to every relevant Foundry hook (combat, document CRUD, canvas/UI, dnd5e v2 rolls), emits a uniform {ts, hook, args} envelope. No domain interpretation — consumers query the stream. See docs/HOOK_CONTRACT.md.", + "version": "0.4.0", "library": true, "manifestPlusVersion": "1.2.0", "authors": [ @@ -12,7 +12,7 @@ } ], "compatibility": { - "minimum": 13, + "minimum": 14, "verified": 14 }, "relationships": { @@ -23,7 +23,7 @@ "esmodules": ["scripts/main.js"], "url": "https://git.homelab.local/kaykayyali/hooks-lib", "manifest": "https://git.homelab.local/kaykayyali/hooks-lib/raw/branch/main/module.json", - "download": "https://git.homelab.local/kaykayyali/hooks-lib/raw/branch/main/foundry-hooks-lib-0.3.0.zip", + "download": "https://git.homelab.local/kaykayyali/hooks-lib/raw/branch/main/foundry-hooks-lib-0.4.0.zip", "readme": "https://git.homelab.local/kaykayyali/hooks-lib/blob/main/README.md", "changelog": "https://git.homelab.local/kaykayyali/hooks-lib/commits/main", "bugs": "https://git.homelab.local/kaykayyali/hooks-lib/issues", diff --git a/package.json b/package.json index db736af..00922ed 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "foundry-hooks-lib", - "version": "0.3.0", + "version": "0.4.0", "private": true, - "description": "Foundry VTT module: generic Foundry hook facade. Library-only — no UI, no settings, no domain interpretation. v0.2.0 implements HOOK_CONTRACT.md and tests/PLAN.md.", + "description": "Foundry VTT v14-only module: generic Foundry hook facade. Library-only — no UI, no settings, no domain interpretation. v0.4.0 implements HOOK_CONTRACT.md and tests/PLAN.md.", "main": "scripts/main.js", "type": "module", "scripts": { diff --git a/scripts/internal/anti-corruption.js b/scripts/internal/anti-corruption.js index d4a6cce..4112cd8 100644 --- a/scripts/internal/anti-corruption.js +++ b/scripts/internal/anti-corruption.js @@ -1,15 +1,20 @@ // scripts/internal/anti-corruption.js // -// HOOK_CONTRACT.md §9-§10: absorb Foundry rename + arity churn. +// HOOK_CONTRACT.md §9-§10: absorb Foundry hook shape churn so +// consumers see stable envelope.hook names and a documented +// args[] shape. // -// §9 Hook rename mapping: a Foundry v13 fire of renderChatLog produces -// an envelope with hook === "renderChatInput"; a v14 fire of -// renderChatInput produces the same. The envelope hook name is -// always the modern (v14) name. +// v0.3.0 is Foundry v14 only (per tests/PLAN.md file header). v0.2.0 +// supported both v13 and v14; v0.3.0 narrows to v14. The dual v13 +// `renderChatLog` listener is dropped, and arg-normalization shapes +// assume v14's arity. If a future v14 micro-release renames a hook +// again, add the mapping here. +// +// §9 Hook rename mapping: the envelope.hook name is always the v14 +// name. v13 mapping is out of scope. // // §10 Arg normalization: for hooks with unstable arity, pad/truncate -// to a documented shape. Consumers can rely on args[N] regardless of -// Foundry version. +// to a documented shape. Consumers can rely on args[N]. // // This module exports the *normalization functions* and the // per-hook arg shape definitions. The dispatcher in envelope.js @@ -20,38 +25,29 @@ import { getEntryForRawName } from "./registered-hooks.js"; // Arg shape per envelope name. undefined = no normalization // (the args array is passed verbatim from Foundry). // -// Each shape is a function (rawArgs, foundryVersion?) -> normalizedArgs. -// foundryVersion is optional; absent means "don't version-conditional." +// Each shape is a function (rawArgs) -> normalizedArgs. // -// v0.2.0 documents shapes for: combatStart, combatEnd, combatTurn, +// v0.3.0 documents shapes for: combatStart, combatEnd, combatTurn, // combatRound, preUpdateActor, updateActor, preUpdateToken, // updateToken, dnd5e.rollAttackV2, dnd5e.rollDamageV2. export const ARG_SHAPES = { - // combatStart(combat, updateData) — arity 2. Stable. + // combatStart(combat, updateData) — arity 2. Stable in v14. combatStart: (args) => normalizeArity(args, 2), - // combatEnd(combat) — arity 1. Stable in v14. v13 sometimes passed - // (combat, updateData); we normalize to (combat, updateData). + // combatEnd(combat) — arity 1. Stable in v14. combatEnd: (args) => normalizeArity(args, 2), - // combatTurn(combat, updateData, updateOptions) — v14. v13 sometimes - // passed (combat, updateData, combatantId). Normalize to 3 args; - // the third is whichever Foundry provided. + // combatTurn(combat, updateData, updateOptions) — arity 3. Stable in v14. combatTurn: (args) => normalizeArity(args, 3), - // combatRound(combat, updateData, updateOptions) — v14. v13 passed - // (combat, updateData, roundNum). Normalize to 4 args: - // [combat, updateData, roundNum ?? updateOptions, updateOptions] - // This is the §10 example shape. The third arg is always the round - // number; the fourth is options. + // combatRound(combat, updateData, updateOptions) — arity 3 in v14. + // Normalized to 4 args: [combat, updateData, updateOptions.round, updateOptions] + // The third arg is always the round number; the fourth is options. + // v13's (combat, updateData, roundNum) shape is out of scope. combatRound: (args) => { const out = [args[0], args[1], null, args[2] ?? null]; - if (args.length >= 3 && typeof args[2] === "number") { - // v13 shape: round num is the third arg. - out[2] = args[2]; - } else if (args.length >= 3 && args[2] && typeof args[2] === "object") { - // v14 shape: third arg is options; the round number lives there. + if (args.length >= 3 && args[2] && typeof args[2] === "object") { out[2] = args[2].round ?? null; } return out; diff --git a/scripts/internal/registered-hooks.js b/scripts/internal/registered-hooks.js index 3a94c11..94e0234 100644 --- a/scripts/internal/registered-hooks.js +++ b/scripts/internal/registered-hooks.js @@ -95,7 +95,7 @@ export const HOOK_REGISTRY = [ // --- Chat & rolls --- { envelope: "createChatMessage", mode: "async", raw: ["createChatMessage"] }, { envelope: "renderChatMessage", mode: "sync", raw: ["renderChatMessage"] }, - { envelope: "renderChatInput", mode: "sync", raw: ["renderChatInput", "renderChatLog"] }, + { envelope: "renderChatInput", mode: "sync", raw: ["renderChatInput"] }, { envelope: "dnd5e.rollAttackV2", mode: "async", raw: ["dnd5e.rollAttackV2"] }, { envelope: "dnd5e.rollDamageV2", mode: "async", raw: ["dnd5e.rollDamageV2"] }, diff --git a/tests/PLAN.md b/tests/PLAN.md index 147c17b..abb409d 100644 --- a/tests/PLAN.md +++ b/tests/PLAN.md @@ -1,8 +1,18 @@ -# hooks-lib test plan — v0.3.0 +# hooks-lib test plan — v0.4.0 -**Status:** Implemented. Smoke (554/554) + Playwright (30/30) + perf (6/6) +**Scope:** Foundry v14 only. v0.3.0 does not test v13 compatibility; +the anti-corruption layer is reduced to v14-only normalization (no +dual v13/v14 hook subscription). v0.2.0 supported both; v0.3.0 narrows +to v14 because the only consumer (battle-focus) is v14. + +**Status:** Implemented. Smoke (546/546) + Playwright (30/30) + perf (6/6) all green as of 2026-06-20. The § Definition of done release gate is met. +The smoke-test assertion count went from 554 to 546 when v0.4.0 dropped +the v13-only `renderChatLog` and v13-round-num assertions (8 fewer +v13-specific assertions). v0.3.0 was the rename + Playwright test; +v0.4.0 narrows to v14 only. + **Drives:** `tests/verify-hooks-lib.mjs` (no-Foundry smoke), `tests/verify-hooks-lib-foundry.mjs` (Playwright against a live Foundry v14 instance), and `tests/perf.mjs` (perf budget). @@ -116,18 +126,22 @@ A throwing consumer callback does not break the dispatch chain. ### Section E — Anti-corruption -**Hook rename mapping (§9):** -- A Foundry v13 fire of `renderChatLog` produces an envelope with +**Hook rename mapping (§9):** the library subscribes to the v14 hook +names and emits a stable `envelope.hook` name. Example mappings (v0.3.0): + +- A Foundry v14 fire of `renderChatInput` produces an envelope with `hook === "renderChatInput"`. -- A Foundry v14 fire of `renderChatInput` produces the same. -- A Foundry v13 `combatEnd` (when fired) AND a Foundry v14 - `updateCombat` with `active: false` both produce envelopes with - `hook === "combatEnd"` (normalized). +- A Foundry v14 `updateCombat` with `active: false` produces an envelope + with `hook === "combatEnd"` (normalized from the raw updateCombat + with active→inactive transition). +- A Foundry v14 `dnd5e.rollAttackV2` produces `envelope.hook === + "dnd5e.rollAttackV2"`. (v1 `dnd5e.rollAttack` is not supported in + v0.3.0; v13 in general is out of scope per the file header.) **Arg normalization (§10):** - For every hook in §6 with documented arg-shape normalization, - firing with raw Foundry args produces an `args` array matching the - documented shape, regardless of Foundry version. + firing with raw Foundry v14 args produces an `args` array matching the + documented shape. - The exact normalization shapes live in `docs/HOOK_ARG_SHAPES.md`. v0.3.0 ships shapes for: `combatStart`, `combatEnd`, `combatTurn`, `combatRound`, `preUpdateActor`, `updateActor`, `preUpdateToken`, @@ -177,10 +191,12 @@ Beyond Section D's basic cases: - **System-specific derived events.** That's the adapter's test plan. hooks-lib's tests verify the adapter *protocol* (registration, matching, lifecycle) but not the adapter's *content*. -- **Foundry-version compatibility beyond the latest two releases.** - v0.3.0 tests Foundry v13.351 and v14.364. Older versions are - "best effort" — the anti-corruption layer should handle them, but - no CI gate. +- **Foundry v13 compatibility.** v0.3.0 is Foundry v14 only. The + anti-corruption layer in v0.2.0 absorbed both v13 and v14 hook + renames; v0.3.0 narrows to v14. The dual-subscription + `renderChatInput` + `renderChatLog` listener is now just + `renderChatInput`. Older versions of Foundry are "best effort" — not + a CI gate, not in the test plan. - **Real Foundry runtime for the smoke test.** The no-Foundry smoke test stubs `Hooks`, `game`, `ui`. It runs in <2s. Real-Foundry verification is the Playwright test (§ Playwright run below). @@ -199,8 +215,9 @@ A v0.3.0 release is "done" when: 2. **`npm test` exits 0** in the no-Foundry smoke runner. Runs in <2s. 3. **`npm run test:foundry` exits 0** against a live Foundry v14 - instance with a representative world (3 actors, Bard-vs-Goblin - fixture reused from battle-focus's test helper). Runs in <30s. + instance with a representative world (the battle-focus v0.6.x + `hello-world-demo` world works). Runs in <30s. v13 is not + supported (see file header). 4. **`npm run lint` exits 0** (if we adopt a linter; otherwise skip). 5. **Both pass on the Hermes shell** (Windows, git-bash, Node 18+). CI portability is a stretch goal, not a gate. @@ -210,14 +227,14 @@ A v0.3.0 release is "done" when: --- -## Test files (v0.3.0) +## Test files (v0.4.0) | File | Purpose | |---|---| -| `tests/verify-hooks-lib.mjs` | No-Foundry smoke. Sections A–F (F's perf-measurement subset that doesn't require Foundry). Runs in <2s. Replaces v0.1.0's `verify-hooks-lib.mjs`. | -| `tests/verify-hooks-lib-foundry.mjs` | Playwright against a live Foundry. Loads the library as a Foundry module, asserts `mod.api.isReady() === true`, fires one synthetic `updateActor` and one `dnd5e.rollAttackV2` (stubbed at the Foundry level), asserts the library captures both. Runs in <30s. | +| `tests/verify-hooks-lib.mjs` | No-Foundry smoke. Sections A–F (F's perf-measurement subset that doesn't require Foundry). Runs in <2s. v0.4.0 is v14-only — v13-specific assertions are dropped. | +| `tests/verify-hooks-lib-foundry.mjs` | Playwright against a live Foundry v14. Loads the library as a Foundry module, asserts `mod.api.isReady() === true`, fires synthetic `updateActor` + `dnd5e.rollAttackV2` + creates a real combat, asserts envelopes flow through. 30 assertions. Runs in <15s. | | `tests/perf.mjs` | The performance budget measurement. Runs in <5s. Called from the smoke runner or standalone. | -| `tests/test-helpers.mjs` | Shared stubs and Foundry helpers. Updates the v0.1.0 helper to expose the new Foundry hook stubbing. | +| `tests/test-helpers.mjs` | Shared stubs and Foundry helpers. The no-Foundry smoke stubs `Hooks`, `game`, `ui`. | The smoke test is the gate for PRs. The Foundry test is the gate for releases. Performance test runs weekly or on demand. diff --git a/tests/verify-hooks-lib.mjs b/tests/verify-hooks-lib.mjs index 4171c77..a01d418 100644 --- a/tests/verify-hooks-lib.mjs +++ b/tests/verify-hooks-lib.mjs @@ -384,39 +384,34 @@ async function mainTest() { assert("E.1c: updateCombat round-change does NOT synthesize combatInactive", inactiveSeenC === null); uE1c(); - // E.2: renderChatInput is registered for BOTH v13 (renderChatLog) and v14 names. - assert( - "E.2: renderChatLog (v13) is in installed raw hooks", - listInstalledRawHooks().includes("renderChatLog") - ); + // E.2: renderChatInput is registered (v14 only; v13's renderChatLog + // was supported in v0.2.0 but is dropped in v0.3.0 per the plan's + // v14-only scope). assert( "E.2: renderChatInput (v14) is in installed raw hooks", listInstalledRawHooks().includes("renderChatInput") ); + assert( + "E.2: renderChatLog (v13) is NOT in installed raw hooks", + !listInstalledRawHooks().includes("renderChatLog") + ); - // E.3: both produce envelope.hook === "renderChatInput". + // E.3: v14 fire produces envelope.hook === "renderChatInput". let chatInputSeen = []; const uE3 = subscribe("renderChatInput", (env) => chatInputSeen.push(env.hook)); Hooks.callAll("renderChatInput", { id: "m1" }, {}, "u1"); - Hooks.callAll("renderChatLog", { id: "m2" }, {}, "u1"); await new Promise((r) => setTimeout(r, 10)); - assertEq("E.3: both v13 and v14 chat hooks produce envelope.hook='renderChatInput'", chatInputSeen, ["renderChatInput", "renderChatInput"]); + assertEq("E.3: v14 renderChatInput produces envelope.hook='renderChatInput'", chatInputSeen, ["renderChatInput"]); uE3(); - // E.4: arg normalization. combatRound v13 shape: (combat, updateData, roundNum). - // v14 shape: (combat, updateData, updateOptions). Normalized to 4 args. - let roundArgsV13 = null; - const uE4a = subscribe("combatRound", (env) => { roundArgsV13 = env.args; }); - Hooks.callAll("combatRound", { id: "c1" }, {}, 3, "u1"); - await new Promise((r) => setTimeout(r, 10)); - assertEq("E.4a: combatRound v13 shape normalizes round num to args[2]", roundArgsV13?.[2], 3); - uE4a(); - + // E.4: arg normalization. v14 combatRound shape: (combat, updateData, updateOptions). + // v0.3.0 is v14-only; the v13 round-num position is no longer + // exercised. round number comes from updateOptions.round. let roundArgsV14 = null; const uE4b = subscribe("combatRound", (env) => { roundArgsV14 = env.args; }); Hooks.callAll("combatRound", { id: "c1" }, {}, { round: 5 }, "u1"); await new Promise((r) => setTimeout(r, 10)); - assertEq("E.4b: combatRound v14 shape normalizes round num to args[2]", roundArgsV14?.[2], 5); + assertEq("E.4: combatRound v14 shape normalizes round num to args[2]", roundArgsV14?.[2], 5); uE4b(); // ----- Section G — System adapter loading deeper ----- @@ -522,7 +517,7 @@ function syntheticArgsFor(rawName) { if (rawName === "changeSidebarTab") return [{}]; if (rawName === "getSceneControlButtons") return [[]]; if (rawName === "renderChatMessage") return [{ id: "m1" }, {}, {}]; - if (rawName === "renderChatInput" || rawName === "renderChatLog") return [{}, {}, {}]; + if (rawName === "renderChatInput") return [{}, {}, {}]; if (rawName === "renderJournalPageSheet") return [{}, {}, {}]; if (rawName === "initializePointSourceShaders") return [{}]; if (rawName === "rtcSettingsChanged") return [{}, {}];