# Project Memory

## Test Suite Structure
- `services/Makefile` runs 7 test suites: lut, utils, bot-core, bot (discord), irc, api, wrapped
- Total: ~483 tests (41 lut, 211 utils, 31 bot-core, 67 discord-bot, 33 irc, 65 api, 35 wrapped)
- LUT specs: `josie-health-lut/spec/` (fuzzy matching)
- bot-core specs: `josie-health-bot-core/spec/` (command_parser, api_contract, dto)
- IRC bot specs: `services/irc-bot/spec/` (parity tests)
- wrapped-cr specs: `services/wrapped-cr/spec/` (stats_types deserialization, text report)

## Crystal Testing Patterns
- `RedisClient` cannot be stubbed via inheritance -- its `initialize` opens a Redis connection and all instance vars must be set
- Existing redis_client_spec uses `RedisClient.new rescue nil; next unless client` pattern
- For handler tests that need Redis, test the regex + URI.decode_www_form logic directly instead
- IRC bot's `irc_bot.cr` has top-level boot code (lines 333+), so it cannot be `require`d in specs -- test constants and logic separately

## IRC Bot Architecture (migrated to bot-core)
- IRC bot uses bot-core CommandRouter -- most commands route through `@router.handle()`
- IRC-specific (handled locally): ;id multi-dose+alias, ;high, ;insuf/;subl/;inhaled/;buccal/;transdermal, help, verify
- Bot-core aliases added: hd, rd, list, hha, note, drugs
- `shard.override.yml` points to local paths for dev; `shard.yml` has git deps for CI
- Top-level boot code at end of irc_bot.cr (line 298+) prevents `require` in specs
- verify_irc_auth uses direct HTTP::Client since it's IRC-specific (not in bot-core APIClient)

## Key Architecture Notes
- `josie-health-lut` has route normalization in `drug_lut.cr` line 408 (sniffed, snorted, etc -> insufflated)
- `josie-health-lut/src/fuzzy.cr` has Damerau-Levenshtein fuzzy matching with PREFIX_INDEX for substances
- Fuzzy thresholds: no fuzzy for <=2 chars, max dist 1 for 3-5, max dist 2 for 6-8, max dist 3 for 9+
- `shard.override.yml` in services/core and services/discord-bot point to local LUT/bot-core for dev
- AliasHandler in API (core) uses regex `/\/v1\/aliases\/([^\/]+)\/(.+)/` with `URI.decode_www_form` on captures
- TimezoneHandler reads `request.query_params["tz"]` (not "q")
