···11+# CLAUDE.md
22+33+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
44+55+## Project Overview
66+77+**deckbelcher** is a TanStack Start application with:
88+- React 19 + TanStack Router (file-based routing)
99+- TanStack Query for data fetching
1010+- Tailwind CSS v4 for styling
1111+- TypeSpec/Typelex for lexicon schema generation
1212+- Vitest for testing
1313+- Biome for linting/formatting
1414+1515+## Development Commands
1616+1717+```bash
1818+# Development server (runs on port 3000)
1919+npm run dev
2020+2121+# Build for production
2222+npm run build
2323+2424+# Preview production build
2525+npm run serve
2626+2727+# Testing
2828+npm run test # Run all tests
2929+vitest run <file> # Run specific test file
3030+3131+# Linting & Formatting
3232+npm run lint # Lint code
3333+npm run format # Format code
3434+npm run check # Check both linting and formatting
3535+3636+# Typelex (schema generation)
3737+npm run build:typelex # Compile lexicons from typelex/*.tsp to lexicons/
3838+```
3939+4040+## Architecture
4141+4242+### File-Based Routing
4343+4444+Routes live in `src/routes/` and are managed by TanStack Router:
4545+- `__root.tsx` - Root layout with header, devtools, and HTML shell
4646+- `index.tsx` - Homepage route
4747+- `demo/*` - Demo routes showcasing various TanStack Start features
4848+4949+Route files are auto-generated into `src/routeTree.gen.ts` (excluded from linting).
5050+5151+### Router Setup
5252+5353+Router initialization happens in `src/router.tsx`:
5454+- Integrates TanStack Query via `setupRouterSsrQueryIntegration`
5555+- Wraps router with TanStack Query provider from `src/integrations/tanstack-query/`
5656+- Uses context pattern for dependency injection
5757+5858+### Path Aliases
5959+6060+TypeScript paths are configured with `@/*` alias pointing to `src/*` (tsconfig.json + vite-tsconfig-paths plugin).
6161+6262+### Typelex/Lexicons
6363+6464+- **Source**: `typelex/*.tsp` - TypeSpec definitions for AT Protocol lexicons
6565+- **Generated**: `lexicons/com/deckbelcher/**/*.json` - Compiled lexicon schemas
6666+- Run `npm run build:typelex` after modifying `.tsp` files
6767+- Lexicons follow AT Protocol conventions (used for ATProto/Bluesky integrations)
6868+6969+### Styling
7070+7171+Tailwind CSS v4 is integrated via `@tailwindcss/vite` plugin. Global styles in `src/styles.css`.
7272+7373+### Development Tooling
7474+7575+- **Nix**: flake.nix provides Node.js 22, TypeSpec, and language servers
7676+- **Biome**: Uses tabs for indentation, double quotes, excludes generated files
7777+- **Devtools**: Integrated TanStack Router + Query + React devtools in root layout
7878+7979+## Important Notes
8080+8181+- `src/routeTree.gen.ts` is auto-generated - never edit manually
8282+- Demo files (prefixed with `demo`) are safe to delete
8383+- Biome only lints files in `src/`, `.vscode/`, and root config files
8484+- Router uses "intent" preloading by default
8585+- SSR is configured via TanStack Start plugin
+121
PROJECT.md
···11+# DeckBelcher Project Overview
22+33+## Core Concept
44+55+Magic: The Gathering decklist tool built on AT Protocol, differentiating from Moxfield/Archidekt through genuine social features and ATProto data portability rather than bolted-on comments.
66+77+## Domain & Namespace
88+99+- **Domain**: deckbelcher.com
1010+- **Namespace**: com.deckbelcher.*
1111+1212+## Lexicon Structure
1313+1414+### com.deckbelcher.actor.profile
1515+User profile for DeckBelcher. Fields:
1616+- `displayName` - User's display name
1717+- `description` - Bio/description text
1818+- `pronouns` - User pronouns
1919+- `createdAt` - Account creation timestamp
2020+2121+Future additions may include featured decklist/card.
2222+2323+### com.deckbelcher.list (planned)
2424+Main decklist record:
2525+```typescript
2626+{
2727+ cards: [{
2828+ scryfallId: string, // per-printing ID
2929+ quantity: number,
3030+ section: "mainboard" | "sideboard" | "maybeboard",
3131+ tags: string[] // user metadata like "removal", "wincon"
3232+ }],
3333+ primer: string, // inline, use atproto facets for rich text/card mentions
3434+ tags: string[] // global list tags like "competitive", "budget"
3535+}
3636+```
3737+3838+**Key decisions:**
3939+- Scryfall ID is per-printing (handles multiple printings naturally)
4040+- `section` as structured field, not special tags (easier queries)
4141+- Primer inline in record, NOT separate (avoid complexity, <50kb even with novel)
4242+- Facets over markdown for rich text (can link cards with Scryfall IDs)
4343+- No explicit order field - arrays preserve order, sorts are UI concern
4444+4545+### com.deckbelcher.reply (planned)
4646+Replies to lists/cards/tags:
4747+```typescript
4848+{
4949+ target: {
5050+ type: "card" | "tag" | "list",
5151+ scryfallId?: string, // if type=card
5252+ tagName?: string, // if type=tag
5353+ listUri: string // always include
5454+ },
5555+ text: string,
5656+ // ... standard reply fields
5757+}
5858+```
5959+6060+Replies to removed cards still exist, just surface them lower (below maybeboard). Include CID in target if you want "this was about an old version" support.
6161+6262+### com.deckbelcher.like (planned)
6363+Likes for lists.
6464+6565+## Target Formats
6666+6767+**Priority:**
6868+- Commander (primary)
6969+- Cube (high value - curators love discussing every card choice)
7070+- Pauper/PDH
7171+7272+**Skip:** Standard, Vintage
7373+7474+## Technical Architecture
7575+7676+### Frontend
7777+- Load Scryfall JSON dump (~100MB, gzips well) for instant search/typeahead
7878+- No network latency for adding cards = huge UX win
7979+- Can lazy load by set if needed but probably unnecessary
8080+8181+### Backend
8282+- Constellation indexes social records (likes, replies)
8383+- Use existing Bluesky social graph for follows/discoverability
8484+- Basic PDS for hosting records
8585+8686+### Version History
8787+**SKIP FOR V1:**
8888+- ATProto repos don't keep history by default
8989+- Mutate records in place when edited
9090+- If needed later, Constellation can index CID changes from firehose
9191+- Verdverm pattern ($orig + $hist array) is too complex for v1
9292+- Client-side undo with localStorage if you want it, but not critical
9393+9494+## What Makes This Better Than Moxfield
9595+9696+1. **Reply-to-specific-cards** - Threaded discussions on individual card choices, portable across ATProto
9797+2. **ATProto portability** - Your decklists aren't locked in
9898+3. **Bluesky social graph** - Friends are already there if they're on Bsky
9999+4. **Actually social** - Not afterthought comments
100100+101101+## Target Audience
102102+103103+- Commander/Cube brewers who want feedback
104104+- Content creators doing primers
105105+- Bluesky-native crowd who cares about data ownership
106106+107107+## Explicitly Out of Scope for V1
108108+109109+- Version history / change tracking
110110+- Default view settings for lists
111111+- Auto-tagging via ML (cool idea with Scryfall otags as training data, but ship without it first)
112112+- Undo/redo (maybe client-side later)
113113+114114+## Lexicon Scoping Patterns
115115+116116+Reference from ecosystem research:
117117+- Bluesky uses `app.bsky.actor.profile` with `actor` scope
118118+- Many indie lexicons skip scoping (e.g., `app.popsky.profile`)
119119+- Teal.fm (respected devs) uses `fm.teal.alpha.actor.profile` with `actor` scope
120120+121121+**Decision:** Use `com.deckbelcher.actor.profile` following Bluesky/Teal.fm pattern for consistency.
+68
SCRYFALL.md
···11+# Scryfall Card Model Reference
22+33+## Key Identifiers
44+55+**For DeckBelcher:** Use `id` (Scryfall's UUID) as the canonical identifier per-printing.
66+77+- **`id`**: Unique UUID for each card printing in Scryfall's database
88+ - This is what we store in `com.deckbelcher.list` as `scryfallId`
99+ - Different for each printing/version of the same card
1010+- **`oracle_id`**: Consistent ID across all reprints of the same card
1111+ - Useful for "print-agnostic" searches (e.g., "all Lightning Bolts regardless of printing")
1212+- **`multiverse_ids`**: Gatherer identifiers (can be null)
1313+- **Platform IDs**: `arena_id`, `mtgo_id`, `tcgplayer_id`
1414+1515+## Card Object Structure
1616+1717+### Core Fields
1818+- `name`: Card name
1919+- `mana_cost`: Mana cost string (e.g., `"{2}{U}"`)
2020+- `cmc`: Converted mana cost (numeric)
2121+- `type_line`: Full type line
2222+- `oracle_text`: Rules text
2323+- `keywords`: Array of keyword abilities
2424+2525+### Deckbuilding Fields
2626+- `color_identity`: Array of colors for Commander legality
2727+- `legalities`: Object with format legalities (`"commander": "legal"`, etc.)
2828+- `colors`: Actual card colors
2929+- `power`, `toughness`, `loyalty`: Creature/planeswalker stats
3030+3131+### Multi-Face Cards
3232+- `card_faces`: Array of faces for split/flip/transform/MDFC cards
3333+- Each face has independent attributes (name, mana_cost, type_line, etc.)
3434+- The main object contains shared metadata
3535+3636+## Image & Display Fields
3737+3838+### Images
3939+- `image_uris`: Object with card image URLs at different sizes
4040+ - Keys: `small`, `normal`, `large`, `png`, `art_crop`, `border_crop`
4141+- `image_status`: Quality indicator (`missing`, `placeholder`, `lowres`, `highres_scan`)
4242+- `highres_image`: Boolean for high-res availability
4343+- `illustration_id`: Unique artwork identifier
4444+4545+### Card Appearance
4646+- `layout`: Layout code (e.g., `"normal"`, `"split"`, `"transform"`, `"modal_dfc"`)
4747+- `border_color`: Border type (`"black"`, `"white"`, `"borderless"`, `"silver"`, `"gold"`)
4848+- `frame`: Frame layout version
4949+- `frame_effects`: Array of frame effects (e.g., `"showcase"`, `"extendedart"`)
5050+- `full_art`: Boolean for oversized artwork
5151+- `finishes`: Available finishes array (`["foil", "nonfoil", "etched"]`)
5252+- `promo`: Boolean for promotional prints
5353+- `digital`: Boolean for digital-only cards
5454+5555+### Printing Metadata
5656+- `set`: Set code (e.g., `"2x2"`, `"cmm"`)
5757+- `set_name`: Full set name
5858+- `collector_number`: Print number within set
5959+- `rarity`: `"common"`, `"uncommon"`, `"rare"`, `"mythic"`, `"special"`, `"bonus"`
6060+- `released_at`: Release date (ISO date string)
6161+- `games`: Array of platforms (`["paper", "arena", "mtgo"]`)
6262+6363+## Important Notes
6464+6565+1. **Per-Printing IDs**: Scryfall's `id` is unique to each printing, which matches our lexicon design
6666+2. **Oracle Names**: Use `oracle_id` to group printings of the same card
6767+3. **Multi-Face Complexity**: Check `layout` field and `card_faces` array for split/transform/MDFC cards
6868+4. **Bulk Data**: Scryfall provides bulk JSON downloads (~100MB gzipped) for offline search
···11+{
22+ "lexicon": 1,
33+ "id": "com.deckbelcher.richtext.facet",
44+ "defs": {
55+ "main": {
66+ "type": "object",
77+ "properties": {
88+ "index": {
99+ "type": "ref",
1010+ "ref": "#byteSlice"
1111+ },
1212+ "features": {
1313+ "type": "array",
1414+ "items": {
1515+ "type": "union",
1616+ "refs": [
1717+ "#mention",
1818+ "#link",
1919+ "#tag"
2020+ ]
2121+ }
2222+ }
2323+ },
2424+ "description": "Annotation of a sub-string within rich text.\nExtends Bluesky's facet system to support DeckBelcher-specific features.",
2525+ "required": [
2626+ "index",
2727+ "features"
2828+ ]
2929+ },
3030+ "byteSlice": {
3131+ "type": "object",
3232+ "properties": {
3333+ "byteStart": {
3434+ "type": "integer",
3535+ "minimum": 0
3636+ },
3737+ "byteEnd": {
3838+ "type": "integer",
3939+ "minimum": 0
4040+ }
4141+ },
4242+ "description": "Specifies the sub-string range a facet feature applies to.\nStart index is inclusive, end index is exclusive.\nIndices are zero-indexed, counting bytes of the UTF-8 encoded text.",
4343+ "required": [
4444+ "byteStart",
4545+ "byteEnd"
4646+ ]
4747+ },
4848+ "mention": {
4949+ "type": "object",
5050+ "properties": {
5151+ "did": {
5252+ "type": "string",
5353+ "format": "did"
5454+ }
5555+ },
5656+ "description": "Facet feature for mention of another account.\nThe text is usually a handle, including an `@` prefix, but the facet reference is a DID.",
5757+ "required": [
5858+ "did"
5959+ ]
6060+ },
6161+ "link": {
6262+ "type": "object",
6363+ "properties": {
6464+ "uri": {
6565+ "type": "string",
6666+ "format": "uri"
6767+ }
6868+ },
6969+ "description": "Facet feature for a URL.\nThe text URL may have been simplified or truncated, but the facet reference should be a complete URL.",
7070+ "required": [
7171+ "uri"
7272+ ]
7373+ },
7474+ "tag": {
7575+ "type": "object",
7676+ "properties": {
7777+ "tag": {
7878+ "type": "string",
7979+ "maxLength": 640,
8080+ "maxGraphemes": 64
8181+ }
8282+ },
8383+ "description": "Facet feature for a hashtag.\nThe text usually includes a '#' prefix, but the facet reference should not.",
8484+ "required": [
8585+ "tag"
8686+ ]
8787+ }
8888+ }
8989+}
+58
typelex/deck-list.tsp
···11+import "@typelex/emitter";
22+import "./externals.tsp";
33+import "./richtext-facet.tsp";
44+55+namespace com.deckbelcher.deck.list {
66+ /** A Magic: The Gathering decklist. */
77+ @rec("tid")
88+ model Main {
99+ /** Name of the decklist. */
1010+ @required
1111+ @maxGraphemes(128)
1212+ @maxLength(1280)
1313+ name: string;
1414+1515+ /** Format of the deck (e.g., "commander", "cube", "pauper"). */
1616+ @maxGraphemes(32)
1717+ @maxLength(320)
1818+ format?: string;
1919+2020+ /** Array of cards in the decklist. */
2121+ @required
2222+ cards: Card[];
2323+2424+ /** Deck primer with strategy, combos, and card choices. */
2525+ @maxGraphemes(10000)
2626+ @maxLength(100000)
2727+ primer?: string;
2828+2929+ /** Annotations of text in the primer (mentions, URLs, hashtags, card references, etc). */
3030+ primerFacets?: com.deckbelcher.richtext.facet.Main[];
3131+3232+ /** Timestamp when the decklist was created. */
3333+ @required
3434+ createdAt: datetime;
3535+3636+ /** Timestamp when the decklist was last updated. */
3737+ updatedAt?: datetime;
3838+ }
3939+4040+ /** A card entry in a decklist. */
4141+ model Card {
4242+ /** Scryfall UUID for the specific printing. */
4343+ @required
4444+ scryfallId: string;
4545+4646+ /** Number of copies in the deck. */
4747+ @required
4848+ @minValue(1)
4949+ quantity: integer;
5050+5151+ /** Which section of the deck this card belongs to. Extensible to support format-specific sections. */
5252+ @required
5353+ section: "mainboard" | "sideboard" | "maybeboard" | "commander" | string;
5454+5555+ /** User annotations for this card in this deck (e.g., "removal", "wincon", "ramp"). */
5656+ tags?: string[];
5757+ }
5858+}
+23-3
typelex/main.tsp
···11import "@typelex/emitter";
22import "./externals.tsp";
33+import "./richtext-facet.tsp";
44+import "./deck-list.tsp";
3544-namespace com.deckbelcher.profile {
55- /** My profile. */
66+namespace com.deckbelcher.actor.profile {
77+ /** A DeckBelcher user profile. */
68 @rec("literal:self")
79 model Main {
88- /** Free-form profile description.*/
1010+ /** User's display name. */
1111+ @maxGraphemes(64)
1212+ @maxLength(640)
1313+ displayName?: string;
1414+1515+ /** Free-form profile description. */
916 @maxGraphemes(256)
1717+ @maxLength(2560)
1018 description?: string;
1919+2020+ /** Annotations of text in the profile description (mentions, URLs, hashtags, etc). */
2121+ descriptionFacets?: com.deckbelcher.richtext.facet.Main[];
2222+2323+ /** Free-form pronouns text. */
2424+ @maxGraphemes(20)
2525+ @maxLength(200)
2626+ pronouns?: string;
2727+2828+ /** Timestamp when the profile was created. */
2929+ @required
3030+ createdAt: datetime;
1131 }
1232}
+60
typelex/richtext-facet.tsp
···11+import "@typelex/emitter";
22+import "./externals.tsp";
33+44+namespace com.deckbelcher.richtext.facet {
55+ /**
66+ * Annotation of a sub-string within rich text.
77+ * Extends Bluesky's facet system to support DeckBelcher-specific features.
88+ */
99+ model Main {
1010+ @required
1111+ index: ByteSlice;
1212+1313+ @required
1414+ features: (Mention | Link | Tag | unknown)[];
1515+ }
1616+1717+ /**
1818+ * Specifies the sub-string range a facet feature applies to.
1919+ * Start index is inclusive, end index is exclusive.
2020+ * Indices are zero-indexed, counting bytes of the UTF-8 encoded text.
2121+ */
2222+ model ByteSlice {
2323+ @required
2424+ @minValue(0)
2525+ byteStart: integer;
2626+2727+ @required
2828+ @minValue(0)
2929+ byteEnd: integer;
3030+ }
3131+3232+ /**
3333+ * Facet feature for mention of another account.
3434+ * The text is usually a handle, including an `@` prefix, but the facet reference is a DID.
3535+ */
3636+ model Mention {
3737+ @required
3838+ did: did;
3939+ }
4040+4141+ /**
4242+ * Facet feature for a URL.
4343+ * The text URL may have been simplified or truncated, but the facet reference should be a complete URL.
4444+ */
4545+ model Link {
4646+ @required
4747+ uri: uri;
4848+ }
4949+5050+ /**
5151+ * Facet feature for a hashtag.
5252+ * The text usually includes a '#' prefix, but the facet reference should not.
5353+ */
5454+ model Tag {
5555+ @required
5656+ @maxGraphemes(64)
5757+ @maxLength(640)
5858+ tag: string;
5959+ }
6060+}