Skip to content

feat(monk): 2014 Way of Shadow — four-spell 2-ki Shadow Arts, Cloak of Shadows at L11, Opportunist - #1732

Merged
Sandersland merged 1 commit into
stagingfrom
feat/1502-2014-way-of-shadow
Aug 5, 2026
Merged

feat(monk): 2014 Way of Shadow — four-spell 2-ki Shadow Arts, Cloak of Shadows at L11, Opportunist#1732
Sandersland merged 1 commit into
stagingfrom
feat/1502-2014-way-of-shadow

Conversation

@Sandersland

Copy link
Copy Markdown
Owner

Summary

Slice 4/7 of the 2014 Monk epic (#1313), depends on #1500 (merged). Way of Shadow is not in SRD 5.1 — content transcribed from PHB'14 pp.79-80, per the 2026-08-04 execution decision on #1502.

  • New Subclass row "Way of Shadow" (EDITION_2014, slug monk-way-of-shadow) — a distinct slug from Warrior of Shadow (now explicitly retagged EDITION_2024), added to SUBCLASS_SLUGS/SUBCLASS_IDENTITY and monk.ts's subclasses map.
  • shadow-arts.ts (seed): widened ShadowArtSeed with per-row costPoolKey/costBase (the field that genuinely forks between editions); authored the real four-spell menu (Darkness/Darkvision/Pass without Trace/Silence, 2 ki each) plus the retagged 2024 Darkness row. seedShadowArts reads cost fields per-row instead of hardcoding focus/1.
  • monk-features.ts: Warrior of Shadow's 4 rows tagged EDITION_2024; new WAY_OF_SHADOW_RAW (4 EDITION_2014 rows — Shadow Arts L3, Shadow Step L6 with no free unarmed strike, Cloak of Shadows L11 with no resource cost, Opportunist L17).
  • actions.ts: shadowStep/shadowArts/cloakOfShadows (2024) tagged EDITION_2024 explicitly; four new EDITION_2014 rows under the same action keys, disambiguated by edition + grantSubclassSlugs: ["monk-way-of-shadow"], plus a new opportunist key (no 2024 counterpart).
  • shadow-arts.ts (lib): shadowArtEffectSpec's concentration is now name-keyed — every Shadow Art concentrates except "Shadow Arts: Darkvision". activateCloakOfShadows is edition-branched: 2014 costs nothing (AbilityCost {kind:"none"}, not a fake 0-base pool spend), 2024 still costs 3 focus. Gate-rejection error text names the edition-correct subclass/level.
  • subclass-granted-spells.ts: Way of Shadow's own Minor Illusion grant (EDITION_2014, L3, Wisdom).

Rules-accuracy corrections against the issue as filed

Verified against PHB'14 text (web search), not guessed:

  • Cloak of Shadows is PHB'14's 11th-level feature, not 13th. 2014 monk subclasses gate uniformly at 3/6/11/17; no monk subclass feature exists at 13 in any 2014 source. Implemented at L11.
  • Concentration: the issue's brief said "only darkness and silence concentrate." That's incorrect — Pass without Trace is also a concentration spell ("Duration: Concentration, up to 1 hour"); only Darkvision (a flat 8-hour duration, no concentration) is exempt. Implemented as Darkness/Pass without Trace/Silence concentrate, Darkvision does not.

Both corrections are called out prominently for the mandatory Opus rules-accuracy pass.

Test plan

  • TDD: red tests first for the DERIVED_ACTIONS gates (actions.test.ts) — confirmed failing ("expected undefined to be defined") before implementation, green after.
  • Full backend suite green: 364 test files, 4418 tests passing, 14 pre-existing skips (unchanged baseline).
  • npm run typecheck and npm run lint clean across all workspaces (backend, frontend, contracts, shared-types).
  • prisma db seed verified idempotent — ran twice from a fresh worktree DB, second run produced no stale-row drops.
  • assertEverySubclassEditionPopulated passes; scripts/check-catalog-id-edition-guard.sh passes unmodified (symbol-stable allowlist entry).
  • fallow audit --base c610a898 verdict pass — 0 dead code, 0 new complexity findings, 0 new duplication introduced.
  • Fixed the two stale pre-Widen GrantedAbility.name @unique to @@unique([name, edition]) — pre-#1313 migration (migration + 4 seed upserts + 9 test lookups) #1415 comments in granted-ability-fork-reseed.test.ts per the issue's own scope item, and added a new describe block proving the real SHADOW_ARTS catalog (not a fixture) round-trips two seed-equivalent runs (5 rows: 4×2014 + 1×2024, "Shadow Arts: Darkness" once per edition).
  • seed-data.test.ts's SHADOW_ARTS uniqueness test now keys on (name, edition) instead of name alone, matching CHANNEL_DIVINITIES/FEATS' own convention.
  • New DB-integration coverage in shadow-arts-cast.test.ts for the real 2014 mechanics: 2-ki spend regardless of which spell, per-spell concentration (including the Darkvision exception), level gates at 3/6/11/17, free cloakOfShadows, opportunist as a reminder-only reaction, and the edition-scoped picker route.

Closes #1502

🤖 Generated with Claude Code

https://claude.ai/code/session_018rre9Ho8Vx8zNtzzKkpvFn

@claude

claude Bot commented Aug 5, 2026

Copy link
Copy Markdown

Claude finished @Sandersland's task in 4m 59s —— View job


Code Review: feat(monk): 2014 Way of Shadow

  • Gather context and read changed files
  • Review seed data changes (subclasses, shadow-arts, monk-features, subclass-granted-spells)
  • Review lib/classes changes (actions.ts, shadow-arts.ts, monk.ts, subclass-slug.ts)
  • Review test coverage (actions.test.ts, shadow-arts-cast.test.ts, seed tests)
  • Verify rules accuracy corrections called out in PR description
  • Final verdict and summary

Summary

Solid implementation of a genuinely-forked 2014 subclass. The edition-tagging discipline is tight, the two rules-accuracy corrections (Cloak of Shadows at L11 not L13; Pass without Trace concentrates) are verified and correctly applied, and the integration-test coverage for the new mechanics is thorough. Two issues warrant a fix before merge.


Findings

1. Stale comment — shadow-arts-cast.test.ts:712-714 (CLAUDE.md violation)

// No current Shadow Art carries a buff (the 2014 Pass without Trace option
// is retired, #1246) — this pins that the shared row→spec mapping still
// works, since it's reused by Channel Divinity too.

This PR reinstates Pass without Trace as a real 2014 Shadow Art — it is no longer retired. The core claim ("no current Shadow Art carries a buff" in terms of effectKind: "buff") is still correct (Pass without Trace is seeded without an effectKind buff, just as a utility cast), but the parenthetical explanation the 2014 Pass without Trace option is retired, #1246 is now false. CLAUDE.md: "When you edit code, update or delete its comment in the same edit."

Suggested fix: drop the parenthetical or rewrite as: "No current Shadow Art is seeded with effectKind: 'buff' — Pass without Trace is a utility cast without buff metadata."

Fix this →


2. focusSpent audit-trail field mislabels ki as focus — shadow-arts.ts:174

resourceData: { shadowArtId: catalog.id, focusSpent: cost.base },

For a 2014 Way of Shadow monk casting a Shadow Art, cost.base is 2 ki — but the audit event records it as focusSpent: 2. The human-readable summary (produced by castAbilityInTx via applySpendResourceInTx) correctly says "Spent 2 Ki Points", and the frontend doesn't read focusSpent directly, so there's no display bug. However, the data column in CharacterEvent will permanently record the wrong semantic for 2014 characters.

The 2014 describe block (shadow-arts-cast.test.ts:372+) never asserts the focusSpent field on the cast event — only ki.used === 2 — so the bug is also undetected by the current suite.

Simplest fix: focusSpent: cost.kind === "pool" ? cost.base : 0 (the same guard already used on line 215 for Cloak of Shadows) is still wrong for 2014 — the right approach is to rename the field in a way that's edition-agnostic, e.g. resourceSpent: cost.kind === "pool" ? cost.base : 0, or to keep focusSpent and explain the 2024-centric name is intentional and won't change (add a // 2014 ki spends are also recorded under this key comment). Either way a test asserting the value for a 2014 cast should be added.

Fix this →


Minor observations (no change required)

  • shadow-arts.ts:134-136 — the guard if (cost.kind !== "pool") throws "has no focus cost". Unreachable with real data (all Shadow Arts rows use pool costs), but the message would mislead debugging for 2014 ki costs if it ever fired. Consider "has no pool cost".

  • shadowArtEffectSpec unit test gap — the existing unit tests pin that Darkness concentrates and the generic buff shape works, but there's no unit-level test for shadowArtEffectSpec({ name: "Shadow Arts: Darkvision" }) returning concentration: false. The DB integration test at line 464 covers the behaviour end-to-end; a dedicated unit test would directly pin the SHADOW_ARTS_NO_CONCENTRATION predicate without needing a DB. Low priority given the integration coverage.

  • Rules-accuracy corrections are correct — L11 for 2014 Cloak of Shadows (uniform 3/6/11/17 gating), and the concentration set (Darkness/Pass without Trace/Silence concentrate, Darkvision does not) match PHB'14 spell durations. Both corrections to the issue brief are well-reasoned and verified.


Note: I'm unable to submit a formal GitHub PR review (approve/request-changes through the GitHub UI) due to permissions. The findings above represent my review verdict — the stale comment and audit misnaming should be addressed before merge.

…f Shadows at L11, Opportunist (#1502)

Slice 4/7 of the 2014 Monk epic (#1313), depends on #1500 (merged). Way of
Shadow is not in SRD 5.1 — content transcribed from PHB'14 pp.79-80.

- New Subclass row "Way of Shadow" (EDITION_2014, slug monk-way-of-shadow),
  a DISTINCT slug from Warrior of Shadow (now retagged EDITION_2024) — added
  to SUBCLASS_SLUGS/SUBCLASS_IDENTITY and monk.ts's subclasses map.
- shadow-arts.ts (seed): widened ShadowArtSeed with per-row costPoolKey/
  costBase (the field that genuinely forks); authored the real four-spell
  menu (Darkness/Darkvision/Pass without Trace/Silence, 2 ki each, PHB'14
  pp.79-80) plus the retagged EDITION_2024 Darkness row. seedShadowArts
  reads cost fields per-row instead of hardcoding focus/1.
- monk-features.ts: Warrior of Shadow's 4 rows tagged EDITION_2024; new
  WAY_OF_SHADOW_RAW (4 EDITION_2014 rows: Shadow Arts L3, Shadow Step L6 with
  no free unarmed strike, Cloak of Shadows L11 with no resource cost,
  Opportunist L17).
- actions.ts: shadowStep/shadowArts/cloakOfShadows (2024) tagged
  EDITION_2024 explicitly; four new EDITION_2014 rows under the SAME action
  keys (disambiguated by edition + grantSubclassSlugs: ["monk-way-of-shadow"])
  plus a new "opportunist" key (no 2024 counterpart).
- shadow-arts.ts (lib): shadowArtEffectSpec's concentration is now name-keyed
  (every Shadow Art concentrates except "Shadow Arts: Darkvision" — verified
  against real PHB'14 spell durations: Darkness/Pass without Trace/Silence
  are all "Concentration, up to ..."; Darkvision is a flat 8-hour duration).
  activateCloakOfShadows is edition-branched: 2014 costs nothing (AbilityCost
  {kind:"none"}, not a fake 0-base pool spend), 2024 still costs 3 focus.
  Gate-rejection error text now names the edition-correct subclass/level.
- subclass-granted-spells.ts: Way of Shadow's own Minor Illusion grant
  (EDITION_2014, L3, Wisdom), mirroring the 2024 grant.

RULES-ACCURACY CORRECTIONS against the issue as filed (verified against
PHB'14 text, not guessed):
- Cloak of Shadows is PHB'14's 11th-level Monastic Tradition feature, not
  13th — 2014 monk subclasses gate at 3/6/11/17 uniformly; no monk subclass
  feature exists at 13. Implemented at L11.
- Concentration: the issue's brief said "only darkness and silence
  concentrate" — Pass without Trace is also a concentration spell
  ("Duration: Concentration, up to 1 hour"); only Darkvision (8 hours, no
  concentration prefix) is exempt. Implemented as Darkness/Pass without
  Trace/Silence concentrate, Darkvision does not.

TDD: red tests first for the DERIVED_ACTIONS gates (actions.test.ts, would
have failed with "expected undefined to be defined" pre-implementation, and
did), then made green. Backend suite green (364 test files, 4418 tests, 14
pre-existing skips); typecheck and lint clean across all workspaces; `prisma
db seed` verified idempotent (ran twice, second run silent — no stale-row
drops); assertEverySubclassEditionPopulated and
scripts/check-catalog-id-edition-guard.sh both pass; fallow audit verdict
"pass" (0 dead code, 0 new complexity, 0 new duplication).

Fixed the two stale pre-#1415 comments in granted-ability-fork-reseed.test.ts
per the issue's own scope item, and added a new describe block proving the
REAL SHADOW_ARTS catalog (not a fixture) round-trips two seed-equivalent
runs. seed-data.test.ts's SHADOW_ARTS uniqueness test now keys on
(name, edition) instead of name alone, matching CHANNEL_DIVINITIES/FEATS'
own convention.

Closes #1502

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018rre9Ho8Vx8zNtzzKkpvFn
@Sandersland
Sandersland force-pushed the feat/1502-2014-way-of-shadow branch from 2bb5631 to a5ca96c Compare August 5, 2026 02:29
@claude

claude Bot commented Aug 5, 2026

Copy link
Copy Markdown

Claude finished @Sandersland's task in 3m 7s —— View job


Code Review: feat(monk): 2014 Way of Shadow

  • Gather context and read changed files
  • Review seed data changes (subclasses, shadow-arts, monk-features, subclass-granted-spells)
  • Review lib/classes changes (actions.ts, shadow-arts.ts, monk.ts, subclass-slug.ts)
  • Review test coverage (actions.test.ts, shadow-arts-cast.test.ts, seed tests)
  • Verify rules accuracy corrections called out in PR description
  • Final verdict and summary

Summary

This is a re-review following a PR sync. Both major findings from the prior review have been addressed in this update. The implementation is solid and rules-accurate; one minor semantic residue remains.


Prior findings — status

1. Stale comment — shadow-arts-cast.test.ts ✅ Fixed

The comment previously reading "the 2014 Pass without Trace option is retired, #1246" has been rewritten:

// No real Shadow Art carries an effectKind:"buff" row (every 2014/2024
// art is a flat utility cast, #1502) — this pins that the shared
// row→spec mapping still works, since it's reused by Channel Divinity too.

Accurate and no longer self-contradicted by the PR's own additions.

2. focusSpent mislabeling ki in audit events — ✅ Fixed (Shadow Art cast); ⚠️ residue (Cloak of Shadows)

Shadow Art cast (shadow-arts.ts:181): Fixed. The dynamic key [${cost.key}Spent]: cost.base now produces kiSpent: 2 for 2014 casts and focusSpent: 1 for 2024. New integration test pins both:

expect(castEvent?.data).toMatchObject({ kiSpent: 2 });
expect(castEvent?.data).not.toHaveProperty("focusSpent");

Cloak of Shadows activation (shadow-arts.ts:219): Still writes focusSpent: 0 for 2014 monks (cost.kind === "none"0). The value is correct (0 focus was spent), and the test now pins it:

expect(events.some((e) => e.type === "castShadowArt" && e.data?.focusSpent === 0)).toBe(true);

The field name is still focus-centric for an event that has nothing to do with focus; the previous review suggested either renaming to resourceSpent or adding a comment. The author pinned it with a test but didn't document the intentionality. This is a minor semantic issue, not a data-accuracy bug.


New observations

  • WARRIOR_OF_SHADOW_ROWS fixture correctly narrowed to EDITION_2024 — The flatMap over ["EDITION_2014", "EDITION_2024"] is replaced by ["EDITION_2024"], and the new WAY_OF_SHADOW_ROWS (EDITION_2014-only) carries the 2014 side. Both match the seed data; the fixture map update (LITERAL_SUBCLASS_ROWS) correctly adds "way of shadow".

  • EXPECTED_EDITION_TAGGED_FEATURES count advances 12 → 14 correctly — the two new ["monk", "way of shadow", "Martial Arts"] / "Stunning Strike" entries track that Way of Shadow is a distinct subclass context, not a same-slug fork, consistent with how Way of the Open Hand is handled in 2014 Monk (3/7): Way of the Open Hand — Wholeness of Body, Tranquility, 2014 Quivering Palm #1501.

  • MONK_SUBCLASS_GRANT_KEYS in actions.test.tsedition is now required (not optional), and the ki pool is correctly added to the at() call for 2014 subclasses. The "monk-way-of-shadow" entry correctly lists all four action keys including opportunist.

  • Rules-accuracy corrections are verified correct — L11 for 2014 Cloak of Shadows (uniform 3/6/11/17 pattern, per PHB'14 p.80); concentration set (Darkness/Pass without Trace/Silence concentrate, Darkvision does not) matches PHB'14 spell durations exactly. Both corrections to the issue brief are well-grounded.

  • SHADOW_ARTS_NO_CONCENTRATION set is name-keyed, not edition-keyed — clean design. It handles both editions without an edition parameter since only "Shadow Arts: Darkvision" is exempt across all editions and the 2024 row ("Shadow Arts: Darkness") was already always-concentrating.

  • shadow-arts.ts:134-136 misleading error message ("has no focus cost") is still present — still unreachable with real data, still a latent maintenance hazard for 2014 debugging. Unchanged from prior review note.


Verdict

The two actionable findings from the previous review are resolved. The focusSpent: 0 residue on Cloak of Shadows events is a weak semantic issue: the value is correct, the behavior is pinned by a test, and no display code reads it directly. Not a merge blocker.

Ready to merge pending the author's call on the focusSpent: 0 field name.

Note: I'm unable to submit a formal GitHub PR review (approve/request-changes through the GitHub UI) due to permissions. See the FAQ for workarounds.

@Sandersland
Sandersland merged commit 3fa9bd2 into staging Aug 5, 2026
7 checks passed
@Sandersland
Sandersland deleted the feat/1502-2014-way-of-shadow branch August 5, 2026 02:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2014 Monk (4/7): Way of Shadow — the four-spell 2-ki Shadow Arts menu, Cloak of Shadows at 13, Opportunist

1 participant