docs: add subdirectory CLAUDE.md files for key domain modules
Provide non-obvious business rules and constraints for economy, inventory, quest, moderation, trade, and trivia modules to reduce context-gathering overhead for AI tools. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
11
shared/modules/economy/CLAUDE.md
Normal file
11
shared/modules/economy/CLAUDE.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Economy Module
|
||||
|
||||
- All currency values are `bigint`. Never use `Number()` for arithmetic on balances -- use BigInt literals (e.g., `0n`, `500n`) and `sql` template expressions for DB updates.
|
||||
- `modifyUserBalance` is the canonical way to change a user's balance. It checks for insufficient funds on negative amounts, logs a transaction record, and emits `BALANCE_CHANGED` for quest progression. Bypass it only if you have a very good reason.
|
||||
- Daily rewards reset at **UTC midnight**, not 24h from last claim. The cooldown `expiresAt` is set to the next UTC 00:00:00. Streak breaks if the user misses an entire 24h window after the cooldown expired.
|
||||
- Daily reward is capped at `MAX_DAILY_REWARD = 500n` regardless of streak/weekly bonus.
|
||||
- The streak has a grace period: if a user's timer record is missing (e.g., DB migration), the code allows one "free" increment to avoid unfair resets.
|
||||
- Weekly bonus triggers every 7th consecutive day (streak % 7 === 0).
|
||||
- **Exam system**: a weekly check-in that rewards users based on XP gained since their last exam. The reward uses scaled BigInt arithmetic (`* 10000 / 10000n`) to avoid floating-point precision loss. Exams are locked to a specific day of the week set at registration time. Missing your exam day means zero reward -- there is no retroactive claim.
|
||||
- Lootdrops use **in-memory state** (`Map`s for channel activity and cooldowns). This state is lost on restart. The DB stores only spawned/claimed drops. Claiming uses an atomic `UPDATE ... WHERE claimedBy IS NULL` to prevent race conditions.
|
||||
- Lootdrops expire after 10 minutes and are cleaned up by a 60-second interval.
|
||||
11
shared/modules/inventory/CLAUDE.md
Normal file
11
shared/modules/inventory/CLAUDE.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Inventory Module
|
||||
|
||||
- Inventory has two hard limits from config: **max slots** (distinct item types) and **max stack size** (quantity per item). Both are enforced in `addItem` and will throw `UserError` if exceeded.
|
||||
- When quantity reaches 0, the inventory row is **deleted** (not kept with quantity 0). This means slot count = row count.
|
||||
- `buyItem` delegates balance deduction to `economyService.modifyUserBalance` within the same transaction to ensure atomicity. Never deduct balance directly when purchasing.
|
||||
- Item usage is driven by a JSON `usageData` field on the item record. Items without `usageData.effects` cannot be used. The `consume` flag in `usageData` controls whether the item is removed after use.
|
||||
- **Effect system**: effects are validated at runtime via Zod (`EffectPayloadSchema`) before execution. The registry maps effect type strings to handler functions. Adding a new effect type requires: (1) add to `EffectType` enum in constants, (2) add Zod schema variant in `effect.types.ts`, (3) add handler in `effect.handlers.ts`, (4) register in `effect.registry.ts`.
|
||||
- `XP_BOOST` and `TEMP_ROLE` effects use `userTimers` with upsert -- activating while already active **replaces** the timer (does not stack or extend).
|
||||
- `TEMP_ROLE` only records the timer in DB; actual Discord role assignment must happen in the bot command layer.
|
||||
- `LOOTBOX` effect uses weighted random selection. Weights are relative, not percentages. A `NOTHING` loot type is valid and intentional.
|
||||
- The `getAutocompleteItems` method filters to only show items that have usable effects, so non-usable items won't appear in the `/use` autocomplete.
|
||||
10
shared/modules/moderation/CLAUDE.md
Normal file
10
shared/modules/moderation/CLAUDE.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Moderation Module
|
||||
|
||||
- Case IDs are sequential strings formatted as `CASE-XXXX` (zero-padded to 4 digits). Generated by querying the latest case and incrementing. Not a DB sequence -- concurrent inserts could theoretically collide, but in practice moderation actions are low-frequency.
|
||||
- Only `WARN` type cases are created with `active: true`. All other case types (TIMEOUT, BAN, KICK, NOTE) default to `active: false`. The `active` flag is specifically for tracking unresolved warnings.
|
||||
- `issueWarning` has two side effects beyond creating the case:
|
||||
- **DM notification**: sends the user a warning embed via DM. Fails silently if the user has DMs disabled. Controlled by `config.dmOnWarn` (defaults to true if unset).
|
||||
- **Auto-timeout**: if active warning count >= `autoTimeoutThreshold`, the user is automatically timed out for 24 hours and a separate `TIMEOUT` case is created with `moderatorId: "0"` (system). The timeout target (Discord GuildMember) is passed in from the command layer.
|
||||
- `clearCase` sets `active: false` and records who cleared it and why. It works on any case type, not just warnings.
|
||||
- The service does **not** perform Discord actions (kick, ban, timeout) directly -- it only manages database records. The bot command layer is responsible for calling Discord APIs and then recording cases here. The one exception is auto-timeout in `issueWarning`, where the Discord member object is passed in.
|
||||
- `searchCases` supports pagination via `limit`/`offset` (default limit 50).
|
||||
10
shared/modules/quest/CLAUDE.md
Normal file
10
shared/modules/quest/CLAUDE.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Quest Module
|
||||
|
||||
- Quests are **event-driven**. The `handleEvent` method is called by system event listeners (not by commands directly). It matches events by exact name or prefix (e.g., trigger `ITEM_COLLECT` matches event `ITEM_COLLECT:101`), enabling both generic and specific quest triggers.
|
||||
- Max active quests is controlled by `gameSettingsService`, not hardcoded. Default is 3.
|
||||
- `assignQuest` uses `onConflictDoNothing` -- re-assigning an already-assigned quest silently no-ops. This is intentional to avoid duplicate quest entries.
|
||||
- Quest progress is a simple integer counter. The `weight` parameter in `handleEvent` allows a single event to advance progress by more than 1 (useful for bulk actions).
|
||||
- Quest completion is **automatic**: when progress >= target during `handleEvent`, `completeQuest` is called within the same transaction. There is no manual "turn in" step.
|
||||
- Rewards (xp and balance) are distributed via `economyService` and `levelingService` inside the completion transaction. The `QUEST.COMPLETED` event is emitted with `systemEvents.emit` (fire-and-forget, not async) for bot-layer notifications.
|
||||
- `requirements` and `rewards` are stored as JSON columns. Always expect `{ target: number }` for requirements and `{ xp?: number, balance?: number }` for rewards.
|
||||
- Completed quests are never deleted -- they stay in `userQuests` with a `completedAt` timestamp. `getAvailableQuests` excludes any quest the user has ever been assigned (completed or not).
|
||||
9
shared/modules/trade/CLAUDE.md
Normal file
9
shared/modules/trade/CLAUDE.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Trade Module
|
||||
|
||||
- Trade sessions are stored **in-memory only** (a `Map` keyed by thread ID). Sessions are lost on restart. There is no persistence or recovery mechanism.
|
||||
- The trade uses a **two-phase lock** pattern: both users must `toggleLock` (accept) before `executeTrade` can proceed. Any offer modification (add/remove item, change money) automatically **unlocks both users**, forcing re-confirmation. This prevents bait-and-switch.
|
||||
- `executeTrade` wraps both directions of transfer in a single DB transaction. If any part fails (e.g., insufficient funds, inventory full), the entire trade rolls back.
|
||||
- Money and item transfers go through `economyService.modifyUserBalance` and `inventoryService.addItem`/`removeItem`, which means all their validation (balance checks, stack limits, slot limits) and side effects (transaction logging, quest events) apply.
|
||||
- Item transactions are logged separately in the `itemTransactions` table (distinct from currency `transactions`), with `TRADE_IN`/`TRADE_OUT` types.
|
||||
- The trade types are defined in `bot/modules/trade/trade.types.ts` but the service lives in `shared/modules/trade/`. The import uses `@/modules/trade/trade.types` (bot alias). This cross-boundary import works because both run in the same process.
|
||||
- `_sessions` is exposed on the service object for testing purposes only.
|
||||
11
shared/modules/trivia/CLAUDE.md
Normal file
11
shared/modules/trivia/CLAUDE.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Trivia Module
|
||||
|
||||
- Trivia is an **economic sink**: the entry fee is deducted immediately when starting, before the question is fetched. If the API call fails after payment, the user loses the fee. This is by design (prevents free retries).
|
||||
- Questions come from the **OpenTDB API** with base64 encoding to avoid HTML entity issues. The service decodes all fields from base64 before returning.
|
||||
- Sessions are in-memory (`Map` keyed by `userId_timestamp`). Lost on restart. Expired sessions are cleaned up every 30 seconds.
|
||||
- The cooldown is set **at session start**, not on answer submission. This means a user is on cooldown even if they never answer.
|
||||
- Answer correctness (`isCorrect`) is determined by the **caller** (interaction handler), not the service. The `submitAnswer` method trusts the `isCorrect` boolean. The session stores `correctIndex` for the UI layer to compare.
|
||||
- Reward calculation: `potentialReward = entryFee * rewardMultiplier`. The multiplier comes from config. Wrong answers get 0 (the entry fee is already gone).
|
||||
- Unlike most services, `TriviaService` is a **class instance** (not a plain object). This is because it needs constructor logic for the cleanup interval. The singleton is exported as `triviaService`.
|
||||
- The reward payment in `submitAnswer` reads the current balance and sets it directly (not using `sql` addition). This is a potential race condition under extreme concurrency but acceptable given the per-user cooldown.
|
||||
- Session is deleted before processing the reward to prevent double-submit, even if the reward transaction fails.
|
||||
Reference in New Issue
Block a user