Skip to content

refactor(core): adopt dcc-core-lib via a Foundry adapter (Phases 0-7)#720

Open
cyface wants to merge 232 commits into
mainfrom
refactor/dcc-core-lib-adapter
Open

refactor(core): adopt dcc-core-lib via a Foundry adapter (Phases 0-7)#720
cyface wants to merge 232 commits into
mainfrom
refactor/dcc-core-lib-adapter

Conversation

@cyface

@cyface cyface commented Apr 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

Long-running refactor branch that moves DCC rules logic out of the Foundry system (module/actor.js and friends) into @moonloch/dcc-core-lib, accessed through a thin Foundry-side adapter (module/adapter/*.mjs). Foundry keeps dice evaluation, chat rendering, and hooks; the lib owns rules classification, modifier aggregation, and result shape. This PR now contains the entire Phase 0–7 arc (the original 0–3 scope plus everything that previously "would land on follow-up branches"), squash-merged onto main. See docs/00-progress.md for the authoritative per-session log and docs/dev/ARCHITECTURE_REIMAGINED.md for the target design.

Branch scope: 229 commits / 596 files vs main. Merges main up to v0.67.7.

Phases 0–3 — adapter dispatch for the core roll surfaces

  • Phase 0 — Scaffolding. Vendored dcc-core-lib under module/vendor/dcc-core-lib/ + scripts/sync-core-lib.mjs for version-pinned re-syncs. Adapter seam under module/adapter/ with permanent dispatch-logging (debug.mjs / logDispatch).
  • Phase 1 — Simple rolls. rollAbilityCheck, rollSavingThrow, rollSkillCheck, initiative route through the adapter.
  • Phase 2 — Spell checks. rollSpellCheck routes through calculateSpellCheck for generic / wizard spell-loss / cleric disapproval / patron-bound wizard-elf / spellburn + mercurial flows. Patron taint is now RAW-aligned in the lib (D3a/D3b).
  • Phase 3 — Weapon attacks. rollToHit + damage + crit + fumble route through the adapter for every common path (backstab, deed dice, two-weapon, modifier dialog, NPC adjustments, multi-damage-type + bracket-flavor formulas, magic bonuses, lossless passthrough). dcc.modifyAttackRollTerms bridged into the adapter.

Group B/C/D — extension API, cruft retirement, legacy decommission

  • Group D (sessions 15–25): every _xxxLegacy roll body retired; every public rollXxx dispatcher is now single-path through the adapter, with Playwright regression guards asserting the retired methods are gone from the prototype.
  • Group B: dcc.registerItemSheet / dcc.registerActorSheet / dcc.afterComputeSpellCheck extension hooks.
  • Group C: retired critText/fumbleText shims (C1), pruned pre-V14 migrations + added MINIMUM_SUPPORTED_VERSION guard (C2), closed the halfling i18n-localize anti-pattern with a guard test (C3).

Phases 4–6 — class-registration / extension API

The class-registration API (classMixins / classDefaults / sheetParts / variants) was validated end-to-end by migrating two real sibling content modules onto it — dcc-crawl-classes (PR #40) and mcc-classes (PR #38) — the first homebrew-vertical consumers. The §2.1 schema-slimming question was closed as architecturally bounded (Foundry's static one-schema-per-subtype model blocks full per-class field removal; the lib is the class-clean read-side source of truth) — decision record in docs/dev/SCHEMA_SLIMMING.md.

Phase 7 + Appendix-A — god-object decomposition

Every Appendix-A god-object target had its cohesive non-dispatch groups lifted into focused modules, all behaviour-neutral:

  • config.js 845 → 451 — data tables → module/config/{monster-data,images,dice,active-effect-keys,actor-importer}.mjs.
  • item.js 975 → 339 — DCCItem extends SpellItemMixin(CurrencyItemMixin(ContainerItemMixin(Item))).
  • actor-sheet.js 1890 → ~1040 — AE builders / prepareItems / presentation helpers / drag-drop / drop handlers → module/actor-sheet/*.mjs (free functions; sheets can't mixin #private).
  • actor.js 4574 → 3999 — RollDataMixin(DerivedStatsMixin(ActiveEffectsMixin(Actor))) + spell/skill dispatch mixins; what remains is the adapter dispatch layer that stays co-located with the public rollXxx wrappers.
  • dcc.js 470 → 87 — a pure orchestrator of seven register*() modules (init / ready / scene-control hooks, settings, table loading, chat-and-hook wiring).

main merge (v0.67.7)

Integrates everything that landed on main after the branch's v0.67.2 fork. Most was independently re-implemented in restructured form (resolved take-HEAD); the substantive integrations:

Test coverage (added, not replaced)

  • 1747 Vitest unit/integration tests pass (up from ~700 on the fork point). Adapter specs for every roll surface (adapter-*), plus guard specs (migrations-version-gate-guard, class-dispatch-i18n-guard) and the new spell-result-hook / spell-check-hooks seam coverage.
  • Full Playwright e2e suite (adapter-dispatch, active-effects, data-models, extension-api, sheet-ui) runs green against live v14 Foundry.
  • i18n: npm run compare-lang clean across all 7 languages.

Test plan

  • npm test — 1747 Vitest pass
  • Full browser-tests/e2e/ Playwright suite — green against live v14 Foundry
  • npm run compare-lang — 0 missing / 0 extra across 7 languages
  • Dependent-module smoke: ../../modules/dcc-qol, ../../modules/xcc, ../../mcc-classes, ../../dcc-crawl-classes
  • Squash-merge (the single merge commit on the branch collapses on squash)
  • Release version bump via /release (branch now at main's 0.67.7 base)

🤖 Generated with Claude Code

cyface and others added 30 commits April 17, 2026 16:25
Adopts dcc-core-lib as the pure-functional engine and maps a 7-phase
migration path for thinning the Foundry system into an adapter.
This is the design doc for the refactor/dcc-core-lib-adapter branch.
Sets up the adapter layer that Phases 1–6 will fill in:
- module/adapter/{character-accessors,foundry-roller,foundry-events,foundry-data-loader,chat-renderer}.mjs
  are documented stubs. Each header JSDoc describes where the Foundry↔
  dcc-core-lib boundary lives.
- docs/dev/EXTENSION_API.md catalogs every emitted dcc.* hook and every
  game.dcc.* export, tagging each stable / internal / dead based on
  actual usage by dcc-qol, xcc, mcc-classes, dcc-crawl-classes, and the
  content packs.
- docs/00-progress.md is the running handoff log for this refactor.

No runtime behavior change. dcc-core-lib is linked via npm link for dev;
runtime loading strategy is still an open question (see progress log §Blockers).
Retroactive amendment to the handoff log so future sessions can find the
commit by SHA.
…prep)

Resolves open question #1 in docs/00-progress.md: Foundry has no
bundler and loads module/dcc.js directly, so the lib's built dist/ is
vendored under module/vendor/dcc-core-lib/ and imported via relative
paths. scripts/sync-core-lib.mjs builds the linked source at
\$DCC_CORE_LIB_SRC (default /Users/timwhite/WebstormProjects/dcc-core-lib)
and refreshes the vendor dir, writing a VERSION.json that records the
lib's version, git SHA, and dirty flag. Added module/vendor/** to
standard.ignore so the linter skips vendored output.

Initial sync: @moonloch/dcc-core-lib@0.2.1 (fa908c2, clean tree),
~4.3 MB across 577 files.

Also folds in:
- trailing-newline fixes on the five Phase 0 adapter stubs (standard
  --fix collateral, zero behavior change)
- .nvmrc pinning Node 24 (matches package.json engines)
- docs/01-session-start.md: fresh-session resume prompt, updated now
  that the runtime-loading blocker is resolved

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pulls in the wave-1 modifier redesign (tagged-union RollModifier +
async check pipeline). See dcc-core-lib/docs/MODIFIERS.md for the
design.

This sync unlocks Phase 1 of the adapter refactor — the adapter
layer can now consume rollAbilityCheckAsync, pass an async custom
roller backed by Foundry's Roll.evaluate, and receive structured
modifier breakdowns for chat rendering.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up sync for the f78cbdf hotfix in the lib repo — adds the
async check exports (rollCheckAsync, rollAbilityCheckAsync,
rollSavingThrowAsync, RollCheckOptionsAsync) to checks/index.ts's
public surface. Without this sync, top-level
`import { rollAbilityCheckAsync } from '../vendor/dcc-core-lib/index.js'`
throws "is not a function" at runtime — the function existed in
roll.js but wasn't re-exported.

Version still 0.3.0 (patch-level fix).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migrates DCCActor.rollAbilityCheck end-to-end through the adapter
layer, the first roll type to move in the architecture refactor.

Flow: DCCActor._rollAbilityCheckViaAdapter →
actorToCharacter (character-accessors.mjs) →
rollAbilityCheckAsync (dcc-core-lib 0.3.0) with
createFoundryRoller as the injected async roller →
renderAbilityCheck (chat-renderer.mjs) →
ChatMessage.create.

The public signature of DCCActor.rollAbilityCheck is unchanged, so
dcc-qol and token-action-hud-dcc are unaffected. A dispatcher at the
top of rollAbilityCheck routes to the legacy code path when the
request is one the adapter can't yet handle:

  - options.rollUnder === true (Luck check)
  - options.showModifierDialog === true
  - str/agl with computeCheckPenalty set (CheckPenalty display term)

The legacy method is preserved verbatim as _rollAbilityCheckLegacy
so behavior in those cases is bit-identical to before.

Adapter modules filled:

  - module/adapter/character-accessors.mjs — actorToCharacter builds
    a minimal lib Character from DCCActor. Handles the save-id remap
    (frt/ref/wil ↔ fortitude/reflex/will) and parses Foundry's signed
    save strings ("+2", "-1") into numbers. Reads post-AE values by
    relying on Foundry having already applied Active Effects before
    prepareDerivedData runs.

  - module/adapter/foundry-roller.mjs — createFoundryRoller returns
    an async roller of shape (expression) => Promise<number> suitable
    for rollAbilityCheckAsync. Each invocation creates a Foundry Roll,
    awaits .evaluate(), and stashes the Roll on a shared context so
    chat-renderer can reattach it to the ChatMessage for DSN and
    breakdown rendering.

  - module/adapter/chat-renderer.mjs — renderAbilityCheck preserves
    the flag contract the legacy path emits (dcc.RollType,
    dcc.Ability, dcc.isAbilityCheck, checkPenaltyCouldApply,
    FleetingLuck updates) and adds a structured system.libResult
    payload for downstream consumers.

Tests:

  - New module/__tests__/adapter-ability-check.test.js (3 tests)
    covers the actor → character shape mapping, the adapter round-trip
    for a default Luck check (which takes the adapter path), and a
    rollUnder Luck check (which falls through to legacy). Locks the
    adapter contract before saves/skills/init migrate.

  - Existing module/__tests__/actor.test.js "roll ability check"
    reduced from asserting 3 dccRollCreateRollMock calls to 2 — the
    third (lck, no rollUnder) now adapter-routes and does not invoke
    DCCRoll.createRoll. The rollToMessageMock assertion for that case
    relaxes to expect.objectContaining(...) since the adapter output
    includes an additional system.libResult field.

Docs:

  - docs/00-progress.md gains a 2026-04-18 session entry, marks the
    current phase as "Phase 1 — Adopt the lib for simple rolls",
    documents the lib-side wave-1 work and async siblings, and
    rewrites Next steps to sequence the save/skill/init migrations.
  - docs/01-session-start.md updated so a fresh Claude session
    resumes pointing at the save migration as the next target.

683 unit tests pass across 19 test files. Sync with
@moonloch/dcc-core-lib@0.3.0 (f78cbdf) already landed in the prior
vendor commit.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs surfaced during in-Foundry testing of Phase 1's ability
check adapter path:

1. Display total was wrong on the chat card.

   The previous async roller (createFoundryRoller + rollAbilityCheckAsync)
   gave Foundry only the bare dice expression (e.g. "1d20"), because
   the lib's roller contract passes *per-dice-expression* expressions,
   not the full formula with modifiers. The Foundry Roll's total then
   reflected only the dice (17), but the lib's real total included
   modifiers (18). Foundry's ChatMessage renders the Roll's total, so
   the card showed the wrong number.

   Fix: switch to the two-pass pattern originally discussed in design.
   Pass 1 calls libRollAbilityCheck with { mode: 'formula' } to get the
   full formula string. Pass 2 creates a Foundry Roll from that full
   formula, awaits evaluate() so Foundry computes the correct display
   total, then calls the lib again in evaluate mode with a sync roller
   returning the pre-rolled natural — so the lib classifies against
   the exact dice outcome that will be shown. Neither pass re-rolls
   the dice.

2. Dispatcher missed cmd-click on str/agl.

   actor-sheet.js builds options via `rollModifierDefault ^ (ctrlKey
   || metaKey)` — a bitwise XOR returning 0 or 1, not true/false. The
   dispatcher's `showModifierDialog === true` strict check saw `1` as
   not-true and routed to the adapter, so cmd-click silently stopped
   showing the modifier dialog. Switched to truthy checks.

Also narrowed the str/agl-with-CheckPenalty exclusion to only fire
when the penalty is actually non-zero — a configured penalty of 0 is
an empty display term and safely goes through the adapter.

Moved the structured lib result from system.libResult to
flags['dcc.libResult'] — Foundry's ChatMessage schema drops unknown
keys from system, while flags accept free-form namespaced JSON. This
restores the structured-payload integration point for downstream
consumers and the F12 verification trick in the docs.

Tests updated: actor.test.js's `roll ability check` now expects the
str path to go through the adapter (mock actor's checkPenalty is 0);
adapter-ability-check.test.js reads libResult from flags; 683 tests
green.

Removed the unused foundry-roller.mjs import from actor.js — the
roller file stays in place for future adapter paths that legitimately
want an async roller callback (e.g. saves/skills where the two-pass
model still applies but is the right shape).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds module/__integration__/adapter-ability-check.test.js covering
the Phase 1 adapter pipeline with the real Foundry Roll engine —
catches the display-total regression we hit in manual testing
(Foundry Roll total matching lib result.total after modifiers).

Eleven test cases across two describes:

  - actorToCharacter (integration): verifies the Foundry-shape →
    lib-Character translation without real dice. Runs in every
    environment. 3 tests: ability score mapping, save-id remap
    (frt/ref/wil ↔ fortitude/reflex/will), level + class-id carry.

  - adapter two-pass flow (real Foundry dice): runs only when the
    dice engine is loaded (.foundry-dev/client/dice/ populated via
    `npm run setup:foundry`). 8 tests covering formula shape for
    non-zero and zero ability mods, Roll total = natural + mod across
    40 d20 iterations, result.total == Roll.total across 25
    iterations, critical on natural max, fumble on natural 1, modifier
    breakdown carries origin.category='ability' with applied=true,
    and no level modifier on default ability checks.

ChatMessage rendering is intentionally out of scope — it requires a
full client environment. The mock-based test at
module/__tests__/adapter-ability-check.test.js covers that path.

Uses a tiny makeActor() builder for DCCActor-shaped fixtures without
subclassing Actor; the adapter never touches Foundry Document APIs
so the minimal shape is sufficient.

686 tests pass + 32 skipped (24 pre-existing dice-engine cases +
8 new adapter cases that also require the dice engine).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-sync after the lib was rebased onto remote main. The lib's remote
had a 0.2.2 version bump we didn't have locally; rather than overwrite
it, our breaking change is stacked on top and versioned 0.4.0
(skipping 0.3.0 which only ever existed locally before the rebase).

The actual dist code is unchanged from the previous 0.3.0 sync —
only VERSION.json moves (name/version/commit all update). Adapter
imports continue to work untouched.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrites docs/01-session-start.md to accurately describe the
committed adapter pattern (two-pass sync with Foundry Roll owning
the full formula), not the earlier async-roller design that
silently dropped modifiers from the chat total.

Replaces the generic "continuation" framing with specific save-throw
instructions: the two-pass code shape to copy, why NOT to reach for
rollSavingThrowAsync + createFoundryRoller (with pointer to the
display-total bugfix commit 5a9bae6), how to translate Foundry's
frt/ref/wil save-ids to the lib's fortitude/reflex/will at the
adapter boundary, the expected modifier breakdown (ability-mod +
save-bonus), and the test files to mirror from the ability-check
work.

Also bumps the vendored lib reference to 0.4.0 (up from the
stale 0.3.0 in the prior version) and points at MODIFIERS.md
as required reading.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The prior version mentioned writing integration tests but was vague
about how to run them. Updated to:

- Enumerate the three test kinds required before committing (unit
  mock-based, integration real-dice, existing-test update) with
  specific file paths modeled on the ability-check work.
- Document the three npm scripts (test, test:unit, test:integration)
  and when to use each during iteration vs. pre-commit.
- Explicitly flag that dice-engine-gated cases skip silently unless
  .foundry-dev/client/dice/ is populated, and point at
  `npm run setup:foundry` as the one-time prerequisite.
- Add a note to verify the skip count drops by the number of new
  dice cases before claiming the integration suite is green —
  otherwise the new assertions never ran.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
rollSkillCheck now dispatches through the lib's two-pass rollCheck for
built-in slots and skill items with a die. _resolveSkill shares skill
resolution across paths; _buildSkillDefinition / _buildSkillCheckModifiers
emit the lib SkillDefinition + situational add modifiers (skill value,
level, Mighty Deed, armor check penalty). Carve-outs routed to legacy:
modifier dialog, cleric disapproval, CONFIG.DCC.skillTables entries,
and no-die / description-only skill items.

Bundles the previously uncommitted saving-throw work from session 3:
the save-throw unit + integration tests and the shared logDispatch
helper in module/adapter/debug.mjs that every dispatcher branch uses.

renderSkillCheck preserves the legacy flag contract (dcc.RollType,
dcc.ItemId/dcc.SkillId, dcc.isSkillCheck) plus dcc.libResult and the
skill-description content for item-backed skills.

746 tests pass (+13: 5 unit + 8 dice-gated integration for skills).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Split `DCCActor.getInitiativeRoll` into a dispatcher over
`_getInitiativeRollViaAdapter` (default) and `_getInitiativeRollLegacy`
(invoked when `options.showModifierDialog` is set). The adapter path
asks the lib for a formula via `rollCheck(mode: 'formula')` against a
synthetic `{ id: 'initiative', type: 'check' }` SkillDefinition and
returns `new Roll(formula)`; Foundry's `Combat#rollInitiative`
callback chain, the `core.initiativeRoll` flag, and the
`emoteInitiativeRoll` integration at module/chat.js:492 are all
preserved.

Init has no gameplay crit/fumble in vanilla DCC, so this is single-pass
(formula only) — save / ability / skill migrations remain two-pass
where classification drives gameplay. `system.attributes.init.value`
already bakes in agl mod + otherMod + class level via
`computeInitiative`, so the SkillDefinition omits `roll.ability` and
init.value flows in as one aggregate `add` modifier with origin
`{ category: 'other', id: 'initiative-total' }`. Weapon-die overrides
(two-handed, custom `initiativeDieOverride`) stay Foundry-side; the
`[Two-Handed]` / `[Weapon]` die label is re-injected into the lib's
formula string after the call returns.

Adds `module/__tests__/adapter-initiative.test.js` (6 mock-based
tests) and `module/__integration__/adapter-initiative.test.js` (7
dice-gated tests). Relaxes the two `actor.test.js` tests that
asserted `DCCRoll.createRoll` was called — the adapter path uses
`new Roll(formula)` instead. 759 tests pass (up from 746).

`logDispatch('rollInit', …)` wired in both branches. `debug.mjs` +
every call site gets removed in one cleanup commit at Phase 1 close,
after the skill + init paths are exercised in Foundry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drives each public roll method (rollAbilityCheck, rollSavingThrow,
rollSkillCheck, getInitiativeRoll) in a live Foundry and asserts the
`[DCC adapter] <rollType> → <via adapter|LEGACY path>` console line
from module/adapter/debug.mjs. Covers the adapter path plus every
documented legacy carve-out (rollUnder, showModifierDialog,
useDisapprovalRange + skillTable, description-only skill item, and
the pre-built-Roll short-circuit that should emit no log at all).

Spec is gated on the dispatch logging being present; header notes
that once the Phase 1 cleanup commit strips logDispatch, this file
should either be deleted or retargeted at the `dcc.libResult`
chat-message flags that survive the cleanup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- docs/dev/TESTING.md: new Browser tests (Playwright) section covering
  Node 24, fvtt CLI configure installPath/dataPath, correct world name
  (`v14`, not `automated_testing`), and the "close your manual tab
  first" pitfall.
- browser-tests/e2e/*.spec.js: correct the PREREQUISITES blocks
  (the old `--world=automated_testing` instruction pointed at a world
  that doesn't exist). Now each header points at TESTING.md for the
  full recipe and lists the working TL;DR command.
- docs/00-progress.md + docs/01-session-start.md: reflect Phase 1
  closed, dispatch-logging cleanup commit cancelled (see below),
  and Phase 2 (spell checks) as the next goal.
- module/adapter/debug.mjs JSDoc: rewrite to describe the helper as
  permanent infrastructure rather than a phase-scoped scaffold.

Dispatch-logging decision: `module/adapter/debug.mjs` +
`logDispatch` call sites are kept indefinitely. The Playwright spec
`browser-tests/e2e/phase1-adapter-dispatch.spec.js` asserts on the
`[DCC adapter]` console lines, and `getInitiativeRoll` emits no chat
message that could serve as a substitute assertion target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Makes the prompt paste-and-go for a fresh session: scaffolds
`module/adapter/spell-events.mjs` + `spell-input.mjs`, adds a
dispatcher split on `rollSpellCheck` that migrates only the
generic-cast path while leaving wizard / cleric / patron side
effects on the legacy `processSpellCheck`. XCC's
`game.dcc.processSpellCheck` shim stays intact. Unit + Playwright
test coverage extended in the same session. Explicitly flags the
lib wave-2 modifier migration as a prerequisite for deeper
sessions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Route generic-castingMode spell items (no patron, not Cleric) through
_rollSpellCheckViaAdapter; wizard / cleric / patron-bound / naked paths
stay on the legacy body verbatim, preserving game.dcc.processSpellCheck
and its side-effect orchestration. Adds module/adapter/spell-input.mjs
(buildSpellCastInput with a synthetic generic caster profile),
module/adapter/spell-events.mjs (header-only stub), and a renderSpellCheck
export on module/adapter/chat-renderer.mjs. Unit coverage (4 tests) plus
3 new Playwright cases — 18/18 green against live V14 Foundry, 763/763
Vitest green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…llCheck

Broadens the adapter gate for rollSpellCheck to cover wizard-castingMode
items on non-cleric, non-patron-bound actors. Wizard casts now route
through libCalculateSpellCheck with a real getCasterProfile('wizard')
profile and a single-entry spellbook built from the Foundry item; when
the lib reports spellLost, the onSpellLost event bridge in
spell-events.mjs marks the item system.lost:true via item.update,
replacing the actor.loseSpell(item) side effect in processSpellCheck.

Adapter-side pre-check mirrors DCCItem.rollSpellCheck:260 — warns and
returns early when spellItem.system.lost && automateWizardSpellLoss.
Classes with no lib-side caster profile fall back to the legacy path so
homebrew / spinoff classes still work.

spell-input.mjs: syntheticGenericProfile, buildSpellbookEntry,
buildSpellCheckArgs (returns null on unknown class for legacy fallback).
spell-events.mjs: createSpellEvents({ actor, spellItem }) wires
onSpellLost; sessions 3–5 add disapproval / patron / spellburn /
mercurial callbacks.

Tests: 8 unit tests in adapter-spell-check.test.js (up from 4) — new:
wizard-on-wizard adapter, lost-spell warn+return, wizard-on-patron
legacy, createSpellEvents.onSpellLost bridge. Playwright spec at 19
tests (wizard-mode adapter added). Full suite 767 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ellCheck

Broadens the adapter gate for rollSpellCheck to cover cleric-castingMode
items on non-patron-bound Cleric actors. Cleric casts now route through
libCalculateSpellCheck with a real getCasterProfile('cleric') profile,
character.state.classState.cleric.disapprovalRange seeded from
actor.system.class.disapproval, and a Foundry-loaded disapproval table
adapted to the lib's SimpleTable shape.

When the natural roll triggers disapproval, the lib's
handleClericDisapproval rolls 1d4 × range via the adapter's formula-
dispatching roller (pre-rolled in Foundry), looks up the table entry,
and fires onDisapprovalIncreased; the event bridge updates
actor.system.class.disapproval and posts the DCC.DisapprovalGained
EMOTE chat (replaces actor.applyDisapproval()). The adapter then posts
the disapproval roll chat from result.disapprovalResult via new
renderDisapprovalRoll (replaces actor.rollDisapproval(natural) +
RollTable.draw).

Dispatcher gate now:
  castingMode === 'generic' && !isCleric && !hasPatron → adapter
  castingMode === 'wizard'  && !isCleric && !hasPatron → adapter
  castingMode === 'cleric'  && isCleric  && !hasPatron → adapter
  everything else → legacy

Narrower than the session slice's (castingMode === 'cleric' || isCleric)
gate — wizard-on-cleric and cleric-on-non-cleric would swap the legacy
side-effect set (wizard spell loss vs. disapproval), so those stay on
legacy.

spell-input.mjs: loadDisapprovalTable(actor) async loader + local
toLibSimpleTable helper (extract to shared loader when sessions 4–5
add a second consumer). buildSpellCheckArgs populates cleric
disapprovalRange.

spell-events.mjs: createSpellEvents now returns onDisapprovalIncreased
when an actor is provided. Mirrors legacy applyDisapproval: NPC
early-return, actor.update, DCC.DisapprovalGained EMOTE chat.

chat-renderer.mjs: renderDisapprovalRoll builds a deterministic
${total}d1 Roll so the lib's rolled value renders through Foundry's
normal chat pipeline with the drawn table entry's description.

actor.js: _castViaCalculateSpellCheck loads the disapproval table
when cleric, pre-rolls 1d4 in Foundry when natural ≤ range, uses a
formula-dispatching roller for pass 2, and posts the disapproval
chat after the main spell-check chat.

Tests: adapter-spell-check.test.js now 14 tests (up from 8) — cleric
adapter fires, patron-bound cleric → legacy, cleric item on non-cleric
→ legacy, onDisapprovalIncreased updates actor, NPC bails, no-actor
skips wiring. Playwright phase1-adapter-dispatch.spec.js now 20 tests
(up from 19) — cleric-castingMode → adapter (cleric) assertion. All
773 Vitest tests pass; all 20 Playwright tests pass against live
v14 Foundry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Phase 1 section still described the spec as 18 tests "after the
Phase 2 session 1 extension" — sessions 2 + 3 have pushed it to 20,
all passing against live v14 Foundry as of 2026-04-18.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Patron-bound wizard / elf actors now flow through the adapter for
wizard-castingMode spell checks. `buildSpellCheckArgs` populates
`character.state.classState.<wizard|elf>.patron` so `getPatronId`
resolves; the lib's RAW patron-taint pipeline stays dormant (no
`fumbleTable` plumbed in) and `_runLegacyPatronTaint` ports the
legacy `processSpellCheck:623-660` d100-vs-creeping-chance mechanic
verbatim, preserving the +1% bump on every patron-related cast.
Decision rationale + RAW alignment open question tracked in
docs/00-progress.md.

777 Vitest tests pass (+4 net); 20 Playwright tests pass against
live v14 Foundry (existing wizard+patron case rescoped to assert
adapter dispatch + chance bump).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lculateSpellCheck

Wizard / elf casts now plumb spellburn commitments through the adapter
and pre-roll first-cast mercurial magic adapter-side.

- `buildSpellCheckArgs` forwards `options.spellburn` into
  `input.spellburn`; `buildSpellbookEntry` reads
  `system.mercurialEffect.value` into the lib entry so already-rolled
  effects surface on `result.mercurialEffect`.
- New `onSpellburnApplied` bridge subtracts the burn commitment from
  `system.abilities.<str|agl|sta>.value` (clamped at 1, NPC-aware).
- New `loadMercurialMagicTable()` walks `CONFIG.DCC.mercurialMagicTable`
  (compendium → world fallback) via the shared `foundryTableEntries`
  helper factored out of session-3's `toLibSimpleTable`.
- `_rollMercurialIfNeeded` pre-rolls `1d100` + luckMod × 10 through
  the lib's `rollMercurialMagic`, persists the effect to the Foundry
  item, and attaches it to the in-flight spellbook entry.
- Mercurial display chat renders directly from `result.mercurialEffect`
  via the new `renderMercurialEffect` (mirrors session-3's
  `renderDisapprovalRoll`). Pass-1 events tightened to `{}` — the lib
  fires `onSpellburnApplied` / `onMercurialEffect` unconditionally on
  both passes, so a bridge-based render would double-post.

13 new Vitest tests (790 total, up from 777). 2 new Playwright
dispatch tests (22 total) — all pass against live v14 Foundry.

Open question #6 added: spellburn dialog integration (pre-existing
session-2 regression — adapter bypasses `DCCRoll.createRoll`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Session 5's pre-commit hook `git add .` swept in an ephemeral lock
file from the Claude Code harness. Gitignore the scheduled-task
family so future commits don't pick it up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…atron-taint

Phase 2 close-out session. Both gate items resolved as doc-only decisions;
no runtime behavior changed.

Gate 1 — game.dcc.processSpellCheck consumer audit:
Inventoried 5 call sites (3 DCC-internal + 2 XCC). XCC's sheet calls are
structural peers of DCC's own internal callers, not public-API consumers.
Deprecation / shim / parallel-entry options all rejected. Decision:
processSpellCheck is permanent stable API. Adapter dispatcher routes
happy-paths through _castViaCastSpell / _castViaCalculateSpellCheck;
everything else (result tables, manifestation, forceCrit, naked path,
skill-table spells, XCC sheets) continues through processSpellCheck.
Incremental per-route migration replaces the earlier delete-after-migration
plan.

Gate 2 — Open question #5 (patron-taint RAW alignment):
_runLegacyPatronTaint is permanent adapter infrastructure. Legacy
creeping-chance mechanic preserved verbatim; RAW alignment deferred to
backlog (requires fumble-table effect-tag migration across sibling content
modules + per-patron taint-table resolution).

Updates:
- docs/dev/EXTENSION_API.md — processSpellCheck row + recommendation #5
  reflect permanent-stable designation
- docs/dev/ARCHITECTURE_REIMAGINED.md §7 — Phase 2 marked CLOSED, Phase 3
  marked ACTIVE, "Delete processSpellCheck" goal removed
- docs/00-progress.md — Phase 2 close-out session with inventory + decision
  rationale; question #5 moved to Closed; Next steps rewritten for Phase 3
- docs/01-session-start.md — rewritten as Phase 3 session 1 prompt (attack
  migration scaffold options)
- module/actor.js — _runLegacyPatronTaint JSDoc pinned as permanent

Tests: 790 Vitest pass (unchanged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New `module/adapter/roll-dialog.mjs` with `promptSpellburnCommitment`
bridges the wizard/elf adapter cast path to a DialogV2-based Spellburn
input, fixing the latent regression from Phase 2 session 2 where
wizard adapter casts silently lost the legacy Spellburn UI. The
`rollSpellCheck` dispatcher now awaits the prompt when
`options.showModifierDialog && !options.spellburn && !isNPC`; cancel
aborts, commit threads the `SpellburnCommitment` through to the
existing `onSpellburnApplied` bridge. Closes open question #6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Split DCCActor.rollToHit into dispatcher + _rollToHitLegacy (verbatim)
+ _rollToHitViaAdapter. Gate routes the simplest-weapon happy-path
(no deed die, no backstab, no two-weapon, no showModifierDialog,
non-dice toHit, automateDamageFumblesCrits on) through the adapter;
everything else through legacy.

Adapter preserves dcc.modifyAttackRollTerms + DCCRoll.createRoll for
chat rendering and dcc-qol compatibility, then feeds the natural d20
into the lib's makeAttackRoll so the lib owns classification +
appliedModifiers. Result surfaces as flags['dcc.libResult'] in the
attack chat card — no template changes needed.

New module/adapter/attack-input.mjs (buildAttackInput) + attack-events.mjs
stub. 9 new Vitest cases in adapter-weapon-attack.test.js (803 total,
up from 794). 4 new Playwright cases in phase1-adapter-dispatch.spec.js
(26 total) — all passing against live v14 Foundry (3.0 min run).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Teach `_rollToHitViaAdapter` to reflect `dcc.modifyAttackRollTerms`-
pushed `Modifier` terms into the lib's `bonuses[]`. Snapshots
`terms.length` before the hook, slices additions, translates pure
signed-integer Modifier terms into `RollBonus` entries via a new
`hookTermsToBonuses` helper in `attack-input.mjs`, and surfaces the
per-bonus breakdown as `libResult.bonuses` on the chat flag. dcc-qol's
firing-into-melee and medium-range penalties now participate in the
lib's `totalBonus` / `appliedModifiers` aggregation. In-place dice-
chain mutations of `terms[0].formula` (long-range bump) are not yet
translated — session 4 work.

808 Vitest tests pass (up from 803, +5 translator coverage).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Session 3's hook-translation bridge is closed, so the session-start
prompt now points at session 4 as the active work. Updates line
references in `module/actor.js` (hook fires at 2829, not 2766),
renames "Session 9 slice" → "Session 10 slice", refreshes the
baseline test count (808 Vitest), and swaps the "next slice" lean to
option (a) long-range dice-chain translation — the remaining gap
where dcc-qol mutates `terms[0].formula` in place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cyface and others added 26 commits June 8, 2026 11:34
…y real consumers

The "single-class homebrew vertical" candidate was a synthetic stand-in from before
the real sibling migrations landed. 2026-06-08 audit confirms it's already validated
more thoroughly by real consumers: dcc-crawl-classes ships 9 homebrew classes on base
DCC (Ranger/Paladin/Orc/2 Halfling subclasses/Gnome/Elven Rogue/Dwarven Priest/Bard)
driving registerClassMixin + registerSheetPart + registerClassDefaults +
registerHomebrewClassForProgressionLoad + registerActorSheet + 9 DCCSheet stubs;
mcc-classes exercises the same registries across 7 variant classes. The only
registries no sibling exercises — registerClassStartingItems (built-in dwarf only)
and registerVariant for a base-DCC homebrew (n/a) — already have dedicated coverage
(P5-2, P6-5). Group E is fully done. Docs-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… table

The 32-entry activeEffectKeys table (added in PR #611) had zero runtime consumers —
system OR all four sibling modules — ever. The V14 Active Effects workflow uses
Foundry's native config UI, and the human-facing reference is the independent
docs/user-guide/Active-Effects.md "Common Attribute Keys" section (a curated markdown
list, not derived from this table). The code table only cost a schema-drift sync
burden. Removed module/config/active-effect-keys.mjs + the CONFIG.DCC.activeEffectKeys
re-composition in config.js + the unit guard; the e2e probe flips from
"surface survives extraction" to "surface is removed". Not documented as a stable
surface in EXTENSION_API.md, so no public-API note needed.

Resolves the session-38 latent open item (deprecate/remove the unconsumed table).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The session-41 "dead needsValueRoll/rollValue formula path" finding was an orphaned
FEATURE, not dead code. DCC treasure is often listed as rollable (e.g. a hoard worth
3d100 gp), and the surrounding scaffolding is still formula-aware: the treasure sheet
inputs are type="text" data-dtype="String", every value reader uses parseInt(), the
inventory categorizer guards on needsValueRoll(), and the "Roll Treasure Value" button
+ handler exist. Only a V14 schema tightening had orphaned the feature — the item
value field became an integer CurrencyField and base-item migrateData parseInt()-
destroyed any formula.

Un-broke it:
- New TreasureValueField (StringField per pp/ep/gp/sp/cp) for an item's system.value,
  so a die formula persists; needsValueRoll() flags it and rollValue() resolves it.
- base-item migrateData now String()s legacy integers (no data loss) instead of
  parseInt()-destroying formulas.
- Actor system.currency stays integer CurrencyField — Item Piles / §2.12 ecosystem
  currency-walking is unaffected; only an item's worth is formula-capable.

Resolved values are strings post-restore (the parseInt readers handle them). Tests:
+3 integration (data-models.test.js — formula survives migrateData, legacy int ->
string, TreasureData constructs with a formula intact); the e2e currency probe
rewritten to assert the restored feature end-to-end (a 2d6 hoard persists +
needsValueRoll true + resolves in [2,12] + posts the LootValue card), conversion
assertions moved to strings. 1592 Vitest.

Resolves the session-41 latent open item.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the rollable-treasure-value restore (eefeb2d), found by a full-stack
audit:

1. calculateTreasureWeight (actor-sheet/items.mjs) summed coin denominations with a
   bare `+`. With the value fields now formula-capable StringFields, an unresolved
   coin's "0"+"0"+"12"+"0"+"0" string-concatenated to "001200" -> 1200 -> /coinWeight,
   yielding a wildly wrong encumbrance weight. (Latent before the restore: the
   integer schema kept needsValueRoll always-false, so unresolved coins never reached
   this branch.) Fixed with parseInt per denomination, mirroring the parseInt-guarded
   coin-merge block above it. +1 Vitest regression (string-valued unresolved coin ->
   1.2 lb, not 120).

2. rollValue (item/currency-mixin.mjs) fired this.update(updates) without awaiting,
   so callers (and the e2e probe) could observe the pre-resolution value. Now awaited
   — the method is `async` and callers `await` it, so it should complete its write.

3. e2e treasure probe polls until the formula hoard resolves before asserting (the
   probe's earlier race is what the prior full-suite run caught).

1593 Vitest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audit (2026-06-08) found spell-duel.js had zero dedicated tests despite carrying the
counterspell momentum math + a fragile die-from-table-HTML regex. +15 Vitest
(spell-duel.test.js): getSpellDuelDie (attacker-column regex parse, PD cells, the
[12,28] clamp, table-absent + column-absent null paths); the participant state
machine (addParticipant seed/dup-guard/re-entrancy, removeParticipant, adjustMomentum
floor-of-1 clamp, resetMomentum, isParticipant); and resolveExchange (missing-
participant bail, identical-checks->phlogiston + defender momentum, attacker-win
attackerHigh column selection, the [1,10] momentum-modified-roll clamp). Test-only,
no production change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audit (2026-06-08) found parser.js was never imported by any test. The birth-augur
effect application has four silent guard returns (no luck-mod match, empty augur
name, missing compendium pack, no index match) where a regex regression or a renamed
pack would drop the effect on every imported PC unnoticed. Exposed the helper via a
named export (default export shape unchanged) and added +7 Vitest
(parser-birth-augur.test.js): luck-mod +N/-N parse + effect apply with _id stripped,
the no-trailing-mod path, the empty-name no-op, missing-pack + no-match quiet returns,
and case-insensitive startsWith matching. createActors stays integration-only (heavy
parse->dialog->create orchestration). Test-only + one named export.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ped -> warn)

Audit (2026-06-08): the pure migrationOutcome policy + the per-document transforms
were tested in isolation, but the migrateWorld integration was not — so a bug in the
catch->failures wiring or the version-stamping gate could mark a partially-migrated
world "done" and never retry it. +3 Vitest (migrate-world.test.js): a clean run stamps
systemMigrationVersion + posts the complete toast + returns migrationComplete:true; a
per-doc update rejection is caught and accumulated (the other docs still migrate),
leaves the version UNSTAMPED, fires the DCC.MigrationFailures warn with the count, and
returns migrationComplete:false; multi-doc failures are all counted. Test-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audit (2026-06-08): party-sheet.js had 0 dedicated tests. +9 Vitest
(party-sheet.test.js) for the public instance methods invoked with a faked `this`:
_validateMember (unknown-actor + no-party-in-party rejections, valid accept), the
membership mutators _addMember/_removeMember/_updateMember, and the
_processFormData->_processSubmitData weapon round-trip (active melee/ranged staged into
_pendingMemberUpdates, members with no change skipped, pending updates applied via
_updateMember then cleared) with the parent prototype's super methods stubbed. The
static #roll* action handlers stay e2e-territory (action-map-private thin delegators
to actor.rollXxx, like the actor-sheet handlers). Test-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audit (2026-06-08): both modules were vi.mock-ed everywhere, so only their call sites
were verified. +8 Vitest (table-result.test.js): _adjustCritResult / _adjustFumbleResult
guard branches (no container, missing table-name/NaN roll), the out-of-bounds warn, and
the key dual-write to message content AND system.critResult/critRollTotal (resp.
fumble), incl. the NPC-vs-PC fumble-table routing and the +1/-1 step direction. +7
Vitest (spell-result.test.js): addChatMessage flag construction (SpellCheck/RollType/
ItemId + the crit/fumble Fleeting-Luck branches) and processChatMessage's GM guard +
SpellCheck-only shift-up/down click wiring. DOM stubbed (no jsdom dependency). Test-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nstruction

Audit (2026-06-08). +3 Vitest (item-currency-rollvalue.test.js): rollValue resolves
each denomination + writes system.value.* + posts the LootValue card, rolls a die
formula (2d6) to a concrete total, and the bad-formula catch warns
(DCC.BadValueFormulaWarning) + skips that denomination while still completing. +6
Vitest (roll-modifier-dialog.test.js): getTermByIndex attack-vs-'damage-' routing,
and _constructRoll / _constructDamageFormula terms-array (no-DOM) branch joining the
attack + damage formulas with + and firing each term callback. Exposed
RollModifierDialog via a named export for testing (existing exports unchanged).
Test-only + one named export.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e catch

Final audit (2026-06-08) coverage backfill. +12 Vitest (fleeting-luck.test.js):
updateFlags nat-20->Gain / nat-1->Lose / mid-range no-op / no-dice no-op / pre-set
isNaturalCrit|isFumble, updateFlagsForCrit|Fumble (incl. null-safe), and give/take/
spend balance math with the floor-at-zero clamp on take/spend. +4 Vitest
(actor-init-die.test.js): _initDieAdditiveTerms regex (1d20+1d3 -> +1d3, bare die ->
'', empty/null -> '', flat-mod tail) — the integration test re-implemented the regex
rather than calling it. +1 Vitest (container.test.js): deleteDialog's cascade-delete
catch returns null + logs when parent.deleteEmbeddedDocuments throws. Test-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…on.type

Found by the 2026-06-08 drag/drop audit. In _createDCCWeaponMacro the weapon item is
`data.system.weapon` (the drag payload maps dccData -> system), so the icon-fallback
line's `data.data.weapon.type` was wrong twice: `data.data` is the weapon item itself
(drag-drop sets `data: weapon`), so `data.data.weapon` is undefined and `.type` THROWS
when a default/mystery-man-icon weapon is dragged to the hotbar. Fixed to `weapon.type`.
The existing unit test passed only because it fed a bogus `data: { weapon: { type } }`
shape that doesn't match production — corrected the four cases to the real
`system.weapon` shape (with a `type`) so they exercise the production path.

Also a lint-nit cleanup pass (pre-existing, surfaced by the audit): drop unused globals
from the /* global */ directives in item.js (ui/ChatMessage/CONFIG/CONST),
item/container-mixin.mjs + item/currency-mixin.mjs (Item, JSDoc-only), the unused
ContainerItemMixin import in item-currency-mixin.test.js, and name the Promise
constructor params `resolve` in the extension-api e2e poll helpers. `npx standard` now
fully clean. 1668 Vitest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two behaviour-neutral quick wins (the pre-commit `git add .` hook folds
them into one commit):

Stray debug console.log removal:
- spell-result.js: 7 "Updating spell lastResult"/"Found spell item"
  traces (surrounding lookup logic preserved and slightly tightened)
- item-sheet.js: bare _rollManifestation / _rollMercurialMagic /
  _lookupMercurialMagic breadcrumbs
- actor/roll-data-mixin.mjs: bare console.log(err) in the action-dice
  catch upgraded to console.error with context

Vendor sourcemap strip:
- The vendored dcc-core-lib shipped 274 .map files whose `sources`
  point at the lib's original .ts (not vendored), so Vitest logged
  ~20 "Sourcemap points to missing source files" warnings per run.
  Foundry never reads sourcemaps, so they are dead weight. Removed all
  .map files, stripped the //# sourceMappingURL= comments, and added a
  stripSourcemaps() step to scripts/sync-core-lib.mjs so future syncs
  stay self-contained.

No behaviour change; all 1668 unit tests green and the sourcemap
warnings are gone from the test log.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 7 actor.js shrinkage, continued. Lift the spell-check dispatch
layer (public `rollSpellCheck` + its 9 private dispatchers, ~986 lines)
out of actor.js into module/actor/rolls-spell-mixin.mjs, following the
established mixin pattern (active-effects / derived-stats / roll-data).
DCCActor now composes RollsSpellMixin into its extends chain; every
method stays an instance method with byte-identical behavior and `this`
semantics (this.rollDisapproval / getRollData / classId still resolve up
the prototype chain).

Also extract the shared applyForceCritToFoundryRoll free function (used
by both the skill and spell dispatchers) into module/actor/force-crit.mjs.

actor.js 3999 -> 2982. Behaviour-neutral: all 1668 existing Vitest pass
unchanged (proving transparent composition) + 5 new shape/behaviour
guards in actor-rolls-spell-mixin.test.js.

Note: this intentionally reverses the 2026-06-08 backlog note that no
further actor.js file-shrinkage was warranted — per owner direction the
dispatchers are being extracted for readability; docs updated separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 7 actor.js shrinkage, continued. Lift the weapon attack/damage/
crit/fumble dispatch layer (public `rollWeaponAttack` + `rollCritical`
wrappers plus `_rollWeaponAttackDispatch`, `rollToHit`, `_rollDamage`,
`_buildLibDamageResult`, `_structureDamageInput`, `_rollCritical`,
`_rollFumble`, ~984 lines) out of actor.js into
module/actor/rolls-weapon-mixin.mjs. DCCActor composes RollsWeaponMixin
into its extends chain; behaviour and `this` semantics unchanged.

actor.js 2982 -> 1989. Behaviour-neutral: all existing Vitest pass
unchanged + 2 new shape guards in actor-rolls-weapon-mixin.test.js
(imports the foundry mock first since the mixin destructures TextEditor
at module load).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mixin

Phase 7 actor.js shrinkage, continued. Lift the ability/luck/initiative/
hit-dice/saving-throw dispatch layer (public rollAbilityCheck,
getInitiativeRoll, rollInit, rollHitDice, rollSavingThrow + their
private adapters/dialog helpers, ~777 lines) out of actor.js into
module/actor/rolls-check-mixin.mjs. DCCActor composes RollsCheckMixin
into its extends chain; behaviour and `this` semantics unchanged
(this._stripDieCount resolves to the skill block, always co-composed).

actor.js 1989 -> 1212. Behaviour-neutral: all existing Vitest pass
unchanged + 2 new shape guards in actor-rolls-check-mixin.test.js.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 7 actor.js shrinkage, continued. Lift the skill-check dispatch
layer (public rollSkillCheck + its 8 private resolvers/adapters/builders,
~624 lines) out of actor.js into module/actor/rolls-skill-mixin.mjs.
DCCActor composes RollsSkillMixin into its extends chain; behaviour and
`this` semantics unchanged. _stripDieCount lives here but is also reached
by the check mixin's initiative path via this._stripDieCount (both mixins
are always co-composed on DCCActor).

actor.js 1212 -> 583 (3999 -> 583 across the five roll-dispatch slices).
What remains is the document lifecycle (prepareBaseData/prepareDerivedData/
_getConfig/levelChange/computeSpeedValue), rollLuckDie, and the damage/
disapproval resolution methods. Behaviour-neutral: all existing Vitest
pass unchanged + 2 new shape guards in actor-rolls-skill-mixin.test.js.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…probe

Reverse the 2026-06-08 "no further actor.js file-shrinkage warranted"
note now that the five roll-dispatch mixins have landed:
- 02-slice-backlog.md: Appendix-A note updated with the extraction summary
- ARCHITECTURE_REIMAGINED.md: Appendix A actor.js target (575 achieved) +
  §8.6 clarification that "stays on DCCActor" is about the public method
  surface, not the source file (wrappers live in their mixin files)
- 00-progress.md: Recent-slices entry for the extraction

Add an e2e probe (adapter-dispatch.spec.js) asserting a live DCCActor
carries every extracted roll dispatcher, so a future mis-wired `extends`
chain localizes to this test instead of failing diffusely across the suite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add @vitest/coverage-v8 and a `test:coverage` script (vitest run
--project unit --coverage) that measures the system runtime source and
enforces ratchet-floor thresholds in vitest.config.js:
statements 60 / branches 60 / functions 63 / lines 60 — set just below
the 2026-06-09 unit baseline (60.5 / 60.65 / 64.22 / 60.95) so coverage
can only hold or climb.

Scoped to the unit project so the gate is deterministic with or without
Foundry (the integration project only adds coverage). The default
`npm test` stays coverage-free (fast pre-commit hook); coverage is
opt-in and is the natural CI gate. Reports → coverage/ (gitignored).
Docs: docs/dev/TESTING.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Investigated item #2 (the "two-pass scaffolding"). It is NOT redundant
double-computation: Foundry rolls the dice once and the lib re-classifies
those same naturals via a sequenced roller; warnIfDivergent (6 sites) is a
single integer comparison that catches silent lib drift. Removing it would
lose a cheap safety net for no real gain.

So rather than remove, document WHEN to: add an exit criterion to the
warnIfDivergent JSDoc (debug.mjs) — delete the calls once dcc-core-lib is
version-pinned and has shipped >=2 consecutive vendor syncs with zero
divergence warnings; the two-pass itself stays (it is the adapter design,
not scaffolding). Bidirectional cross-reference added to
ARCHITECTURE_REIMAGINED.md §8.6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Relocate the Hooks.once('init', …) body out of module/dcc.js into a focused
module/init-hook.mjs, split into named bootstrap steps (registerBuiltInRegistries,
registerDocumentConfig, registerDataModels, assembleGameDccNamespace,
registerSheets, loadSystemTemplates, registerEarlySettings) wired via
registerInitHook(). Matches the established register*() + named-handler
extraction pattern (settings-table-hooks.mjs, chat-and-hook-wiring.mjs).
dcc.js now calls registerInitHook(); foundry.* destructures moved into the
functions that use them (behaviour-neutral — only run during init).

- Vitest: module/__tests__/init-hook.test.js (17 cases) covering each step.
- E2E: extension-api.spec.js probe asserting all init-time side effects
  (document classes, V14 AE phases + diceChain change type, data models,
  game.dcc stable surface, sheet registration, early Fleeting Luck setting)
  survive against live Foundry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…k.mjs

Relocate the Hooks.once('ready', …) body plus checkReleaseNotes /
_onShowJournal / _onShowURI out of module/dcc.js into module/ready-hook.mjs,
wired via registerReadyHook(). checkReleaseNotes is exported so its
version-flag branch is unit-testable. Matches the init-hook.mjs extraction
pattern. dcc.js now calls registerReadyHook() and drops the imports only the
ready hook used (migrations, release-notes, key-state, status-icons, settings,
welcomeDialog, table-loading's registerTables/setupCoreBookCompendiumLinks,
foundry-data-loader).

- Vitest: module/__tests__/ready-hook.test.js (13 cases) covering
  checkReleaseNotes branches, the onReady bootstrap steps (settings, KeyState,
  Fleeting Luck / Spell Duel / table / status-icon init), theme body-class
  branches, welcome-dialog gating, loader-failure tolerance, and the
  migration-status threading onto dcc.ready.
- E2E: extension-api.spec.js probe asserting the ready-time side effects
  (world settings, KeyState, theme body classes) survive against live Foundry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…to scene-control-hooks.mjs

Relocate the getSceneControlButtons handler out of module/dcc.js into
module/scene-control-hooks.mjs, wired via registerSceneControlHooks(). Adds the
token-layer Spell Duel (always) + Fleeting Luck (setting-gated) scene-control
buttons. Completes the Phase 7 dcc.js decomposition: dcc.js is now a thin
orchestrator of seven register*() calls with no inline hook bodies.

- Vitest: module/__tests__/scene-control-hooks.test.js (7 cases) covering the
  always-Spell-Duel / setting-gated-Fleeting-Luck branches, both onChange
  delegations, and the setting-read try/catch swallow.
- E2E: extension-api.spec.js probe drives the registered hook via
  Hooks.callAll('getSceneControlButtons', controls) and asserts the tools +
  onChange delegations against live Foundry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… rotate archive

- 00-progress.md: new Recent-slices entry (sessions 52-54, dcc.js 470 → 87),
  Current-phase note that the Appendix-A entry-point split is done; rotate
  sessions 48-49 down into the phase-7 archive to restore the 5-entry cap.
- ARCHITECTURE_REIMAGINED.md: Appendix A dcc.js row + §5.3 line updated to
  "achieved 2026-06-09, 87 lines".
- 01-session-start.md: fresh 2026-06-09 status block; note git's authoritative
  session numbering (52-54 = this batch) vs the older doc-only "session 52" label.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolves 15 conflicted files. The branch forked main at v0.67.2 and
independently re-implemented several later PRs in restructured (Phase
0–7 decomposed) form, so most conflicts are rewrite-vs-small-edit
artifacts resolved take-HEAD. Substantive integrations:

- #732 spell-check extension seams (the only genuinely-missing logic):
  re-homed main's dcc.js/item.js processSpellCheck edits into their
  decomposed homes —
    * module/spell-check-processor.mjs: suppressPatronTaint opt-out
      flag, forceFumble (ctrl/meta+shift) force block, and the
      dcc.afterSpellCheckResult post-result hook (full payload incl.
      spellburn).
    * module/item/spell-mixin.mjs: spellburnTotal capture in the
      Spellburn term callback + threads forceCrit/forceFumble/
      suppressPatronTaint/spellburn into processSpellCheck.
    * module/actor/force-crit.mjs: extended the shared force-crit
      helper to also force fumbles (covers the naked-actor + skill
      adapter paths symmetrically).
  NOTE: the seams fire for ITEM spell-checks (the path MCC consumes).
  The branch routes NAKED-actor casts through the lib's own patron-taint
  pipeline (onPatronTaint), which does not emit afterSpellCheckResult —
  flagged as a follow-up design decision, not wired here.

- #743 de.json: applied main's German terminology fix (Zauberwürfe /
  Zauberbrand) while keeping the branch's new Spellburn-dialog keys,
  aligning their wording to the fix.

- #742 NPC skill action die, #736 token.* AE overrides, #739 Speed
  Modifier + base-speed seeding migration: already present on the
  branch in decomposed homes; verified, conflicts resolved take-HEAD.
  Adapted main's #742 createRoll-based test to the branch's adapter
  architecture (_resolveSkill + _buildSkillCheckRollTerms).

- #733/#738 SCSS (multidigit AC/HP, portrait letterbox, party-name):
  already present verbatim in the branch's styles/_*.scss partials;
  dcc.scss kept as the branch's partial-manifest; dcc.css regenerated.

- lang ordering, speed-compute.test.js add/add: unioned.
- package.json/system.json/latest.json/version.txt: took main's 0.67.7.

1738 Vitest green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aint on adapter spell paths

Completes the #732 spell-check-seam integration for the branch's adapter
architecture. The item path (DCCItem.rollSpellCheck → game.dcc.processSpellCheck)
already fires the seam after the merge re-homing; this extends it to the
adapter cast paths that bypass processSpellCheck:

- New shared emitter module/actor/spell-result-hook.mjs
  (emitAfterSpellCheckResult + sumSpellburn). Normalizes the lib
  SpellCheckResult to the processSpellCheck payload contract so one
  listener works for both paths. `result` and `patronTaint` are null on
  the adapter path (the lib classifies tiers internally and routes patron
  taint via the onPatronTaint event); listeners key off `naturalRoll`.

- Fired from all three adapter terminals in rolls-spell-mixin.mjs:
  _castNakedViaAdapter (naked casts — the primary gap), _castViaCastSpell
  (generic), and _castViaCalculateSpellCheck (wizard/cleric/elf).

- suppressPatronTaint honored in _castViaCalculateSpellCheck by clearing
  input.isPatronSpell, which collapses the patron-taint table load + d100/d6
  pre-rolls so the lib's RAW creeping-chance runs no taint sub-roll —
  mirroring the legacy `!suppressPatronTaint` guard. (Naked/generic paths
  carry no patron taint, so the flag is moot there.)

Tests: new module/__tests__/spell-result-hook.test.js (payload contract +
sumSpellburn); extends adapter-spell-check.test.js with end-to-end
assertions that the naked path fires the hook, surfaces spellburn, and that
suppressPatronTaint:true skips the patronTaintChance bump. 1747 Vitest green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cyface cyface changed the title refactor(core): adopt dcc-core-lib via a Foundry adapter (Phases 0-3 + cruft) refactor(core): adopt dcc-core-lib via a Foundry adapter (Phases 0-7) Jun 10, 2026
cyface and others added 3 commits June 10, 2026 17:28
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-core-lib-adapter

Conflict/adaptation notes:
- system.json: kept refactor-branch 0.70.0 version + pre-release manifest URLs
- .gitignore: dropped duplicate coverage/test-results entries (branch already had them)
- chat.test.js: unavailable-table test adapted to this branch's crit-lookup-hint
  contract (main shows DCC.CritTableUnavailable; this branch shows a manual
  lookup hint instead)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…/ PR #720 endgame

Brings the handoff docs current through 2026-06-12:

- 00-progress.md: rotate sessions 50-51 + the unnumbered latent-items
  slice to the phase-7 archive (relabeled — git's authoritative 52-54 =
  the dcc.js batch); add Recent-slices entries for the 2026-06-09
  coverage backfill + tooling batch (previously unrecorded), the
  2026-06-10 #732 seam completion + v0.67.7 merge + 0.70.0 pin, and the
  2026-06-12 v0.67.8 merge (#744 crit fix). Current phase rewritten:
  refactor COMPLETE, branch in pre-merge holding, 1752 Vitest /
  205 E2E clean. Next steps replaced with the sequenced PR #720
  endgame (review pass -> pre-release -> sibling PRs -> squash-merge).
- 01-session-start.md: new 2026-06-12 status block; fix the nvm
  instruction (this machine uses fnm, Node 24 default).
- 02-slice-backlog.md: active queue marked DRAINED with pointer to the
  endgame list.
- TESTING.md: e2e recipe no longer says 'nvm use 24' (no nvm here).
- phase-7.md: receive the three rotated entries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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