This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Bun workspace monorepo for a dice rolling ecosystem targeting tabletop RPGs. All packages are TypeScript, published to npm under @randsum.
Core: @randsum/roller — zero-dependency dice engine with built-in notation parsing and validation. Every other package depends on it via workspace:~.
Game packages live in packages/games/ — each wraps roller with game-specific interpretation, accessed via subpath exports:
blades (Blades in the Dark), daggerheart, fifth (D&D 5e), root-rpg, salvageunion, pbta (Powered by the Apocalypse)
Apps: @randsum/cli (published npm CLI), @randsum/discord-bot (private), @randsum/site (Astro docs site, private), @randsum/expo (dice playground — web, iOS, Android, private)
Game packages never depend on each other — only on @randsum/roller.
bun install # Install all dependencies
bun run build # Build all packages (bunup: ESM+CJS+DTS)
bun run test # Run all tests (bun:test, recursive)
bun run lint # ESLint all packages
bun run format # Prettier all packages
bun run typecheck # TypeScript strict check
bun run knip # Find unused files, deps, and exports
bun run check:all # Full CI pipeline (lint, format, typecheck, test, build, size, site)
bun run fix:all # Auto-fix lint + format issues
# Single package
bun run --filter @randsum/roller test # Test one package
bun run --filter @randsum/games build # Build one package
# Single test file
bun test packages/roller/__tests__/roll/roll.test.ts
# Other
bun run size # Bundle size checks (size-limit)
bun run bench # Performance benchmarks (mitata)
bun run site:dev # Astro dev server (localhost:4321)
bun run help # Quick command reference- Strict mode with
isolatedDeclarations,exactOptionalPropertyTypes,noUncheckedIndexedAccess constonly —letis banned by ESLintimport type { X }enforced (consistent-type-imports)- Explicit return types on exported functions
- PascalCase for types/interfaces/enums, UPPER_CASE for enum members
- No
any— useunknownwith type guards - No
as unknown as T— banned by ESLint AST selector prefer-readonlyenabled- No semicolons, single quotes, no trailing commas (Prettier)
- Discriminated unions use
kindortypeas the discriminant field (e.g.,CollectedResultswithkind: 'union' | 'numeric' | 'opaque' | 'result-mapping') - Literal types for API inputs:
roll()accepts plain numbers and notation strings, not branded/opaque types - Error hierarchy:
ValidationErrorandSchemaErrorboth extendRandsumError. Useinstanceof RandsumErrorto catch all RANDSUM errors, or catch them individually for specific handling - Re-export conventions: game subpaths re-export
GameRollResult,RollRecord, andSchemaError. Internal types stay internal. Useexport typefor type-only re-exports
- Framework:
bun:test(import { describe, expect, test } from 'bun:test') - Tests live in
__tests__/directories - Property-based tests use
fast-checkwith.property.test.tssuffix - Stress tests use 9999 iterations for boundary validation
- Seeded random available:
createSeededRandom(42)from test-utils - Coverage target: 80% project, 70% patch (Codecov)
All publishable packages produce ESM only:
dist/index.js(ESM)dist/index.d.ts(TypeScript declarations)- Subpath exports follow the same pattern:
dist/<subpath>.js,dist/<subpath>.d.ts - No
.cjs,.d.cts, ordist/cjs/variants are produced - Bundle size limits enforced: roller 20KB (includes notation), game packages 15KB, salvageunion 35KB
CJS consumers must use a bundler (esbuild, rollup, webpack 5+) that translates ESM to CJS. Direct require() of an @randsum/* package without a bundler is not supported.
Always use bun publish, never npm publish. npm publish does not resolve workspace:~ references — it ships the literal string, which is unresolvable for consumers. bun publish correctly resolves workspace:~ to a real semver range (e.g., ~1.2.3) at pack time.
# From package directory:
bun publish --access public --otp <CODE> # First publish of a scoped package
bun publish --otp <CODE> # Subsequent publishes- OTP is required — the npm account has 2FA set to
auth-and-writes - Publish order:
@randsum/rollerfirst, then dependent packages (@randsum/games,@randsum/cli) - Auth: Bun reads
~/.npmrcvia$XDG_CONFIG_HOME(~/.config/.npmrc), not~/.npmrcdirectly. Both files must stay in sync. Ifbun publishreturns a mysterious 404 on a scoped package, check for a stale token in~/.config/.npmrc
When @randsum/roller receives a minor version bump, dependent packages (game packages) should also receive a corresponding minor version bump to keep the ecosystem in sync. This applies to minor and major releases — patch bumps in core do not require dependents to bump.
Game packages are generated from .randsum.json specs via the codegen pipeline in packages/games/codegen.ts. Each spec defines dice pools, modifiers, outcome tables, and input validation. The generated TypeScript calls roll() from @randsum/roller directly.
roll() throws on invalid input. Wrap calls in try/catch: try { roll(...) } catch (e) { ... }
The RANDSUM_MODIFIERS array in packages/roller/src/modifiers/index.ts is the single source of truth for which modifiers exist and their execution order. Each modifier is a single co-located file in packages/roller/src/modifiers/ that exports both a *Schema (notation pattern, parse/format logic) and a *Modifier (full definition with dice pool behavior).
See https://notation.randsum.dev for the formal specification including faceted classification, conformance levels, and execution pipeline contracts.
roll(20) // Number: 1d20
roll("4d6L") // Notation string
roll({ sides: 6, quantity: 4, modifiers: { drop: { lowest: 1 } } }) // Options object
roll("1d20+5", "2d6") // Multiple arguments combined
roll("d%") // Percentile: 1d100
roll("4dF") // Fate Core: 4 Fate dice (-4 to +4)
roll("dF.2") // Extended Fudge die (-2 to +2)
roll("5d6W") // D6 System wild die
roll("g6") // Geometric die (roll until 1)
roll("3DD6") // Draw die (no replacement)
roll("4d6Lx6") // Repeat operator (6 ability scores)
roll("2d6+3[fire]") // Annotation/label
roll("4d6//2") // Integer division
roll("5d10F{3}") // Count failures <= 3pre-commit (parallel): ESLint --fix, Prettier, typecheck pre-push: build, codegen check, tests, security audit, knip (unused files/deps)
If hooks fail, run bun run fix:all.
Per-package CLAUDE.md files exist in each packages/*/, games/*/, and apps/*/ directory for detailed guidance on each component.
Test failures: Isolate with bun test packages/roller/__tests__/roll/roll.test.ts. Use --bail to stop on the first failure: bun test --bail. Filter by package: bun run --filter @randsum/roller test.
ESLint failures: Common violations: no-let (use const), consistent-type-imports (use import type), prefer-readonly, and the AST selector banning as unknown as T. Auto-fix with bun run fix:all or target lint only: bun run lint -- --fix.
Type errors: Run bun run typecheck. Common strict-mode issues:
isolatedDeclarations— exported functions need explicit return typesexactOptionalPropertyTypes— optional properties cannot be assignedundefinedexplicitly unless the type includes| undefinednoUncheckedIndexedAccess— array/object index access returnsT | undefined, requires narrowing
Bundle size failures: Each publishable package defines size-limit in its own package.json. Check with bun run size or per-package: bun run --filter @randsum/roller size. Common cause: accidentally importing a heavy dependency into a game package (limit: 10KB, salvageunion: 100KB).
Codegen issues: Game packages are generated from .randsum.json specs. Generated files live at packages/games/src/*.generated.ts. Regenerate with bun run --filter @randsum/games gen. Verify generated output matches specs: bun run --filter @randsum/games gen:check.
Hook failures: Pre-commit runs install, lint --fix, format, and typecheck in parallel. Pre-push runs build (priority 1), then test (priority 2), then bun audit --level=high. Recovery: bun run fix:all, then retry. See lefthook.yml for full config.
Full spec: https://notation.randsum.dev (taxonomy, pipeline, conformance, syntax)
Key syntax: NdS (basic), +X/-X (arithmetic), L/H (drop lowest/highest), R{<3} (reroll), ! (explode), !{condition} (conditional explode), U (unique), C{<1,>6} (cap), d% (percentile), dF/dF.2 (Fate/Fudge), W (wild die), F{N} (count failures), //N (integer divide), %N (modulo), gN (geometric die), DDN (draw die), xN (repeat), [text] (annotation)