Skip to content

feat: add authoritative skill legality and additive S.D.C.#64

Merged
StreamDemon merged 11 commits into
mainfrom
feat/skill-legality-sdc
Jul 26, 2026
Merged

feat: add authoritative skill legality and additive S.D.C.#64
StreamDemon merged 11 commits into
mainfrom
feat/skill-legality-sdc

Conversation

@StreamDemon

@StreamDemon StreamDemon commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Summary

  • Replace the partial skill catalog and free-text Ley Line Walker restrictions with a complete typed RUE pp. 304–330 catalog and one pure authoritative assembler for O.C.C., O.C.C.-Related, Secondary, and Hand-to-Hand choices.
  • Persist player decisions only; derive O.C.C./category bonuses, prerequisites, weighted costs, repeat limits/specializations, and fixed overrides in shared rules code used by preview, backend validation, and saved sheets.
  • Derive Physical S.D.C. from the p. 287 2D6+12 base plus every applicable O.C.C./Physical-skill formula, expose a sourced contribution breakdown, and roll the same formulas server-side.
  • Update the builder, review, dossier, backend storage, tests, and rule-source documentation while preserving readable/usable legacy flattened characters.

Why

Issue #61 is the rules foundation required before the next O.C.C. can land. Previously, legality lived partly in prose, flattened bonuses were client-authored, prerequisites could dangle silently, Secondary Skills borrowed incomplete selection behavior, and S.D.C. always used the bare base formula.

Source authority

The implementation was checked against rendered Rifts Ultimate Edition pages:

  • p. 287 — Physical S.D.C. base and cumulative contributions
  • pp. 299–300 — O.C.C. training/prerequisites, related-skill bonuses, and the independent Secondary pool
  • pp. 304–330 — complete skill descriptions, categories, prerequisites, percentages, repeat mechanics, and Physical-skill S.D.C. formulas
  • pp. 322–323 — Zoology and Appraise Goods second-selection specialization bonuses

docs/rules/PAGE_MAP.md records the corrected page coverage.

Compatibility and boundaries

  • Existing flattened characters remain readable and can reroll legacy base-only S.D.C.; narrow mutations preserve them.
  • New/full writes require authoritative selections and reject client-authored bonus metadata or unknown choice slots.
  • Coalition Grunt content/registry (Coalition Grunt O.C.C.: second playable class foundation #57), additional species, loadouts, and unrelated O.C.C. breadth remain out of scope.

Verification

  • vp check packages/rules and 396 rules tests
  • vp check packages/backend and 134 backend tests
  • vp check apps/web and 63 web tests
  • root vp check and 593 root tests
  • git diff --check
  • live browser acceptance: incomplete selections could not advance; a legal Ley Line Walker was created, rolled through the expanded sourced S.D.C. path, saved, reloaded, and retained repeatable labels and derived bonuses with no console errors
  • independent code review completed; all findings were fixed and re-reviewed

Closes #61
Closes #22
Closes #25
Closes #26


Summary by cubic

Adds a complete, typed skill catalog with an authoritative assembler for O.C.C., O.C.C.-Related, Secondary, and Hand-to-Hand selections, replacing client-authored bonuses with source-validated decisions. Physical S.D.C. is now additive (p.287 base plus all O.C.C./Physical-skill contributions) with a source breakdown and server-side rolls.

  • New Features

    • Full RUE pp.304–330 skill catalog and policies, with prerequisites, costs, repeats, and category legality derived in @riftforge/rules.
    • One assembler used by preview, backend validation, and saved sheets; builder persists decisions only and updates repeatable labels on input without gaps; category routes can be persisted.
    • Additive Physical S.D.C. range and roll from base + skill/O.C.C. formulas; dossier shows a sourced breakdown.
  • Migration

    • New/edited characters must send skillSelections (occChoices, related, secondary, optional hthId); do not send legacy skills or hthType.
    • Backend rejects client-authored bonus metadata; legacy flattened characters remain readable.

Closes #61
Closes #22
Closes #25
Closes #26

Written for commit 2d3076e. Summary will update on new commits.

Review in cubic

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change introduces typed skill catalogs and O.C.C. legality, authoritative skill-selection payloads, shared skill assembly, additive sourced Physical S.D.C. derivation, backend validation and rolling, and builder support for repeatable labeled selections and S.D.C. previews.

Changes

Skill legality and catalog

Layer / File(s) Summary
Typed skill catalog and policy validation
.codex/superpowers/plans/..., packages/rules/src/schema/skills.ts, packages/rules/src/engine/skills.ts, packages/rules/src/content/skills/skills.json, packages/rules/tests/skills.test.ts, docs/rules/PAGE_MAP.md
Skill categories, prerequisites, secondary-skill policies, repeat-selection metadata, source references, and Physical S.D.C. formulas are typed and validated against the expanded catalog.
Typed O.C.C. legality and skill assembly
packages/rules/src/schema/occ.ts, packages/rules/src/content/occ/ley-line-walker.json, packages/rules/src/engine/occ.ts, packages/rules/src/engine/builder.ts, packages/rules/tests/*
O.C.C. grants and related-skill rules use structured policies, while assembly validates choices, costs, prerequisites, labels, categories, and selection limits.

Derived S.D.C. and persistence

Layer / File(s) Summary
Authoritative derivation and additive S.D.C.
packages/rules/src/schema/character.ts, packages/rules/src/engine/character.ts, packages/rules/src/engine/combat.ts, packages/rules/tests/character.test.ts, packages/rules/tests/combat.test.ts
Authoritative selections drive effective skills and H.T.H. training; Physical S.D.C. combines base, O.C.C., and skill formulas with sourced breakdowns and range validation.
Backend write and roll authority
packages/backend/convex/*, packages/backend/tests/*
New writes require typed selections and reject legacy or tampered skill metadata; legacy records remain readable and S.D.C. rolls use derived formulas.

Builder integration

Layer / File(s) Summary
Builder selection contract and preview
apps/web/src/builder/*, apps/web/src/components/sheet-view.tsx, apps/web/tests/*, README.md
The builder supports repeatable labeled picks, category-aware spending, selection-only payloads, shared preview assembly, and rendered Physical S.D.C. sources.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Builder
  participant RulesAssembler
  participant ConvexBackend
  participant CharacterSheet
  participant SdcRoller
  Builder->>RulesAssembler: Submit authoritative skill selections
  RulesAssembler-->>Builder: Preview assembled skills and S.D.C. breakdown
  Builder->>ConvexBackend: Persist selections
  ConvexBackend->>RulesAssembler: Validate and derive character
  RulesAssembler-->>ConvexBackend: Effective skills and S.D.C. formulas
  ConvexBackend->>SdcRoller: Roll all derived S.D.C. components
  SdcRoller-->>ConvexBackend: Persist final S.D.C. total
  ConvexBackend->>CharacterSheet: Return derived sheet
Loading

Possibly related issues

  • #22: The catalog now validates dangling prerequisite references and includes the missing typed skill data.
  • #25: Physical S.D.C. now accumulates base, O.C.C., and skill contributions with sourced formulas and server-side rolling.
  • #26: Typed O.C.C. policies and assembly now validate related-skill legality, costs, minimums, and prerequisites.
  • #28: The catalog expansion adds source/page-backed skill coverage and corresponding rules-layer tests.

Possibly related PRs

  • StreamDemon/RiftForge#63: Both changes extend deriveSheet validation and character derivation at the same rules-engine entry point.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes cover typed skill policies, prerequisite validation, additive S.D.C., and legacy compatibility required by #61, #22, #25, and #26.
Out of Scope Changes check ✅ Passed The added docs, tests, and plan file all support the stated skill legality and S.D.C. foundation work; no clear unrelated changes stand out.
Title check ✅ Passed The title clearly summarizes the main change: authoritative skill legality plus additive S.D.C.
Description check ✅ Passed The description directly matches the changeset, covering typed skill legality, shared assembly, additive S.D.C., and legacy compatibility.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@StreamDemon
StreamDemon marked this pull request as ready for review July 26, 2026 12:04

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (7)
packages/backend/convex/schema.ts (1)

5-24: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Skill-category enum duplicated between rules and backend.

This 18-literal union re-encodes skillCategorySchema from packages/rules/src/schema/skills.ts verbatim. Since packages/backend already depends on @riftforge/rules for getOcc/deriveSheet, consider exporting the raw category string list from the rules package and building both the Zod enum and this Convex validator from it, so a future category addition/rename can't silently drift between the two packages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/backend/convex/schema.ts` around lines 5 - 24, Remove the duplicated
category literals from skillCategoryValidator and centralize the raw category
list in the rules package alongside skillCategorySchema. Export that shared
list, build the rules Zod enum and backend Convex validator from it, and update
package exports/imports so both consumers use the same source.
packages/rules/src/engine/combat.ts (1)

71-79: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider making rng required now that the signature is being rewritten. The Math.random default lets a caller obtain a non-deterministic roll from the pure rules engine; the backend already passes its own source. Making the parameter explicit removes that footgun (recognising this matches the existing rollDice/rollHitPoints convention, so it may be a deliberate house style).

As per coding guidelines, "Engine functions must return raw book amounts and remain pure and deterministic; dice rolls and elapsed time must be supplied as inputs."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/rules/src/engine/combat.ts` around lines 71 - 79, Make the rng
parameter required in rollPhysicalSdc instead of defaulting to Math.random,
matching the explicit-rng convention used by rollDice and rollHitPoints. Update
all callers to pass their random source so the rules engine remains
deterministic and returns raw book amounts.

Source: Coding guidelines

packages/rules/src/engine/skills.ts (1)

111-113: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Indexes are built twice at load. buildSkillCatalog already calls buildSkillIndexes(catalog.skills) internally (Line 76); returning them (or exposing an internal helper) would avoid the second full pass and keep a single source of truth for the id/name maps.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/rules/src/engine/skills.ts` around lines 111 - 113, Update
buildSkillCatalog and the skillCatalog initialization so the indexes produced
during catalog construction are returned or exposed and reused for skillById and
skillByName. Remove the second buildSkillIndexes(skillCatalog.skills) call,
keeping a single source of truth for both maps.
packages/rules/src/engine/builder.ts (1)

166-166: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Type constraints[].fromCategory as SkillCategory to drop the cast. occRelatedSkillsSchema already narrows it to the enum, so the as SkillCategory at Line 405 is only needed because the plan interface widens it back to string. Narrowing the interface also makes perCategory lookups checked.

♻️ Proposed change
-  constraints: { fromCategory: string; min: number }[];
+  constraints: { fromCategory: SkillCategory; min: number }[];
-    const category = constraint.fromCategory as SkillCategory;
-    const have = perCategory[category] ?? 0;
+    const have = perCategory[constraint.fromCategory] ?? 0;

Also applies to: 404-406

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/rules/src/engine/builder.ts` at line 166, Update the plan
interface’s constraints type so fromCategory uses SkillCategory instead of
string, matching occRelatedSkillsSchema. Then remove the unnecessary as
SkillCategory cast around the perCategory lookup near the related skill-building
logic, preserving typed key checking.
packages/rules/tests/builder.test.ts (1)

504-508: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Order-sensitive assertion. The expected array encodes the recursion/Set insertion order of addOccPrerequisites; a harmless reordering there would fail this test for the wrong reason. Prefer asserting membership (e.g. expect(new Set(ids)).toEqual(new Set([...])) or toContain per id) unless the ordering is itself a contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/rules/tests/builder.test.ts` around lines 504 - 508, Update the
assertion for assembled.skills in the addOccPrerequisites test to verify skill
membership without depending on insertion order. Compare Sets or assert each
expected skillId is contained, while preserving validation that all expected
skills are present.
packages/rules/src/engine/occ.ts (1)

190-209: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Unbounded backtracking is fine today but has no guard as content grows. Branching is candidates.length per slot and depth is the sum of all constraint.min values, so a future O.C.C. with several large "any" categories and higher minimums makes this exponential at module load. A memo on (slotIndex, spent, usedBySkill-signature) or a simple visited-state cap would keep the fail-fast cheap.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/rules/src/engine/occ.ts` around lines 190 - 209, Bound the recursive
search in canAssignMinimums with a visited-state memo or state-count cap keyed
by slotIndex, spent, and the usedBySkill allocation signature. Ensure repeated
states are skipped or the search fails fast once the cap is reached, while
preserving successful minimum assignment behavior and the existing error when no
assignment is possible.
packages/rules/src/schema/skills.ts (1)

63-64: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Consider rejecting a category that is repeated in additionalCategories. A duplicate heading would double-count the skill in skillsForPolicy consumers that concatenate per-category results (e.g. relatedSkillPlan variants). A refinement here keeps the contradiction out at content-load time.

♻️ Suggested refinement
   .superRefine((skill, ctx) => {
+    const extra = skill.additionalCategories ?? [];
+    if (extra.includes(skill.category) || new Set(extra).size !== extra.length) {
+      ctx.addIssue({
+        code: "custom",
+        path: ["additionalCategories"],
+        message: "additionalCategories must be distinct and exclude the primary category.",
+      });
+    }
     if (

Also applies to: 90-106

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/rules/src/schema/skills.ts` around lines 63 - 64, Update the schema
containing category and additionalCategories to reject entries in
additionalCategories that duplicate the primary category, using a Zod refinement
or equivalent validation. Preserve the existing optional-array behavior and
ensure duplicate headings are rejected during content loading, including the
corresponding schema definition around the additional category fields.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/builder/steps/occ-skills.tsx`:
- Around line 141-146: The choose-N label editor in the TextInput within the
slot rendering flow should not remove a pick on every keystroke when the label
is temporarily empty. Update the onInput/setLabel interaction to retain the
current slot while editing, and only remove or commit an empty label on
blur/commit (or otherwise defer empty-value handling), preserving later pick
positions.

In `@apps/web/src/builder/steps/related-skills.tsx`:
- Around line 111-132: Replace the referentially keyed For rendering in the
selected specialization rows with index-keyed rendering using Index, so label
edits do not remount the active TextInput. Preserve the existing pick label
binding, onLabel(pickIndex, ...), and onRemoveRepeat behavior while deriving
each row from its stable selection index; verify typing keeps focus and the
caret in the browser.

In `@apps/web/src/components/sheet-view.tsx`:
- Around line 456-464: Replace the hardcoded “RUE” prefix in the StatRow
rendering at apps/web/src/components/sheet-view.tsx#L456-L464 with
contribution.source.book, preserving the page citation format. Update the
corresponding source-string assertion at
apps/web/tests/character-sheet.test.ts#L46-L51 to expect the dynamic book label.

In `@docs/rules/PAGE_MAP.md`:
- Around line 42-44: The category index table in PAGE_MAP.md is missing the
catalog headings Cowboy, Horsemanship, Military, and Pilot Related. Update the
page-anchored list to include each heading with its corresponding page ranges
from skills.json, while preserving the existing ordering and formatting.

In `@packages/rules/src/engine/builder.ts`:
- Around line 420-432: Update the skill-pick validation and assembly flow around
assembleSkills() so hand-to-hand-basic cannot be selected from the Physical
secondary or related pools unless its hthType value is also propagated. Prefer
excluding hand-to-hand-basic from those eligible pools, preserving existing
handling for all other skills; otherwise wire its selection through to hthType
consistently.

---

Nitpick comments:
In `@packages/backend/convex/schema.ts`:
- Around line 5-24: Remove the duplicated category literals from
skillCategoryValidator and centralize the raw category list in the rules package
alongside skillCategorySchema. Export that shared list, build the rules Zod enum
and backend Convex validator from it, and update package exports/imports so both
consumers use the same source.

In `@packages/rules/src/engine/builder.ts`:
- Line 166: Update the plan interface’s constraints type so fromCategory uses
SkillCategory instead of string, matching occRelatedSkillsSchema. Then remove
the unnecessary as SkillCategory cast around the perCategory lookup near the
related skill-building logic, preserving typed key checking.

In `@packages/rules/src/engine/combat.ts`:
- Around line 71-79: Make the rng parameter required in rollPhysicalSdc instead
of defaulting to Math.random, matching the explicit-rng convention used by
rollDice and rollHitPoints. Update all callers to pass their random source so
the rules engine remains deterministic and returns raw book amounts.

In `@packages/rules/src/engine/occ.ts`:
- Around line 190-209: Bound the recursive search in canAssignMinimums with a
visited-state memo or state-count cap keyed by slotIndex, spent, and the
usedBySkill allocation signature. Ensure repeated states are skipped or the
search fails fast once the cap is reached, while preserving successful minimum
assignment behavior and the existing error when no assignment is possible.

In `@packages/rules/src/engine/skills.ts`:
- Around line 111-113: Update buildSkillCatalog and the skillCatalog
initialization so the indexes produced during catalog construction are returned
or exposed and reused for skillById and skillByName. Remove the second
buildSkillIndexes(skillCatalog.skills) call, keeping a single source of truth
for both maps.

In `@packages/rules/src/schema/skills.ts`:
- Around line 63-64: Update the schema containing category and
additionalCategories to reject entries in additionalCategories that duplicate
the primary category, using a Zod refinement or equivalent validation. Preserve
the existing optional-array behavior and ensure duplicate headings are rejected
during content loading, including the corresponding schema definition around the
additional category fields.

In `@packages/rules/tests/builder.test.ts`:
- Around line 504-508: Update the assertion for assembled.skills in the
addOccPrerequisites test to verify skill membership without depending on
insertion order. Compare Sets or assert each expected skillId is contained,
while preserving validation that all expected skills are present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 890d8d9b-45d0-41b8-9572-669ac2227729

📥 Commits

Reviewing files that changed from the base of the PR and between 25b9231 and 1524d5d.

📒 Files selected for processing (29)
  • .codex/superpowers/plans/2026-07-26-skill-legality-sdc-foundation.md
  • README.md
  • apps/web/src/builder/steps/occ-skills.tsx
  • apps/web/src/builder/steps/related-skills.tsx
  • apps/web/src/builder/store.ts
  • apps/web/src/components/sheet-view.tsx
  • apps/web/tests/builder.test.ts
  • apps/web/tests/character-sheet.test.ts
  • docs/rules/PAGE_MAP.md
  • packages/backend/convex/characters.ts
  • packages/backend/convex/schema.ts
  • packages/backend/tests/characters.test.ts
  • packages/backend/tests/combat.test.ts
  • packages/backend/tests/healing-cast.test.ts
  • packages/rules/src/content/occ/ley-line-walker.json
  • packages/rules/src/content/skills/skills.json
  • packages/rules/src/engine/builder.ts
  • packages/rules/src/engine/character.ts
  • packages/rules/src/engine/combat.ts
  • packages/rules/src/engine/occ.ts
  • packages/rules/src/engine/skills.ts
  • packages/rules/src/schema/character.ts
  • packages/rules/src/schema/occ.ts
  • packages/rules/src/schema/skills.ts
  • packages/rules/tests/builder.test.ts
  • packages/rules/tests/character.test.ts
  • packages/rules/tests/combat.test.ts
  • packages/rules/tests/occ.test.ts
  • packages/rules/tests/skills.test.ts

Comment thread apps/web/src/builder/steps/occ-skills.tsx
Comment thread apps/web/src/builder/steps/related-skills.tsx Outdated
Comment thread apps/web/src/components/sheet-view.tsx
Comment thread docs/rules/PAGE_MAP.md Outdated
Comment thread packages/rules/src/engine/builder.ts
@StreamDemon
StreamDemon merged commit 0a8c7a1 into main Jul 26, 2026
3 checks passed
@StreamDemon
StreamDemon deleted the feat/skill-legality-sdc branch July 26, 2026 13:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant