···11+# Typelex Reference
22+33+Typelex is a TypeSpec-based syntax for authoring AT Protocol Lexicon schemas. It compiles `.tsp` files to Lexicon JSON.
44+55+## Basic Structure
66+77+```typescript
88+import "@typelex/emitter";
99+import "./externals.tsp";
1010+1111+namespace com.example.schema {
1212+ @rec("tid")
1313+ model Main {
1414+ @required
1515+ text: string;
1616+ }
1717+}
1818+```
1919+2020+## Record Types
2121+2222+Use `@rec()` decorator to define record types:
2323+2424+- `@rec("tid")` - Timestamp-based IDs for collections
2525+- `@rec("literal:self")` - Single record per repo (profiles)
2626+- `@rec("any")` - Arbitrary keys
2727+2828+## Property Decorators
2929+3030+### Constraints
3131+- `@required` - Mandatory field
3232+- `@maxGraphemes(n)` - Visual character limit (user-facing)
3333+- `@maxLength(n)` - Byte length limit (typically 10x graphemes for UTF-8)
3434+- `@minValue(n)` / `@maxValue(n)` - Integer bounds
3535+- `@minItems(n)` / `@maxItems(n)` - Array size limits
3636+- `@minLength(n)` - Minimum string/array length
3737+3838+### Special Decorators
3939+- `@inline` - Expand model inline without separate definition
4040+- `@token` - Create empty token models (for marker types)
4141+- `@external` - Declare external namespace stub
4242+4343+## Type System
4444+4545+### Primitives
4646+- `string`, `integer`, `boolean`, `bytes`
4747+- `datetime` - AT Protocol datetime format
4848+- `did`, `handle`, `atUri` - AT Protocol identifiers
4949+- `cid` - Content identifier
5050+- `uri` - Standard URI
5151+5252+### Collections
5353+- Arrays: `string[]`, `Model[]`
5454+- Optional: `field?: type`
5555+5656+### Unions
5757+5858+**Open unions (recommended):**
5959+```typescript
6060+// String with known values
6161+section: "mainboard" | "sideboard" | "maybeboard" | string;
6262+6363+// Model union with extensibility
6464+features: (Mention | Link | Tag | unknown)[];
6565+```
6666+6767+Compiles to `knownValues` in JSON for string unions.
6868+6969+**Closed enums (discouraged):**
7070+Avoid unless absolutely necessary. Use open unions instead.
7171+7272+## External References
7373+7474+To reference external AT Protocol lexicons (like `com.atproto.repo.strongRef`):
7575+7676+1. **Download the external lexicon JSON** to `lexicons/` folder:
7777+ ```bash
7878+ mkdir -p lexicons/com/atproto/repo
7979+ curl -s https://raw.githubusercontent.com/bluesky-social/atproto/main/lexicons/com/atproto/repo/strongRef.json \
8080+ > lexicons/com/atproto/repo/strongRef.json
8181+ ```
8282+8383+2. **Run typelex** - it auto-generates `typelex/externals.tsp` with `@external` stubs:
8484+ ```bash
8585+ npm run build:typelex
8686+ ```
8787+8888+3. **Reference the external type** using the full namespace + `.Main`:
8989+ ```typescript
9090+ namespace com.deckbelcher.social.like {
9191+ model Main {
9292+ @required
9393+ subject: com.atproto.repo.strongRef.Main;
9494+ }
9595+ }
9696+ ```
9797+9898+**Important:** Typelex uses the full lexicon ID as the namespace (e.g., `com.atproto.repo.strongRef`), so you reference it as `com.atproto.repo.strongRef.Main`, not `com.atproto.repo.strongRef`.
9999+100100+### How externals.tsp Works
101101+102102+Starting with typelex v0.3.0+:
103103+- Automatically generates `typelex/externals.tsp` based on JSON files in `lexicons/`
104104+- Only includes external lexicons (not your app's namespace)
105105+- Uses `@external` decorator to skip JSON output for those namespaces
106106+- Must be imported in `typelex/main.tsp` entry point
107107+108108+Generated stub example:
109109+```typescript
110110+@external
111111+namespace com.atproto.repo.strongRef {
112112+ model Main { }
113113+}
114114+```
115115+116116+## File Organization
117117+118118+- Source files: `typelex/*.tsp`
119119+- Output: `lexicons/` directory
120120+- Import pattern: `import "./other-file.tsp"`
121121+- Namespaces determine output structure, not file organization
122122+123123+## Compilation
124124+125125+```bash
126126+npm run build:typelex
127127+# runs: typelex compile com.deckbelcher.*
128128+```
129129+130130+Output is deterministic JSON in `lexicons/` matching namespace structure.
131131+132132+## Common Patterns
133133+134134+### Record with Facets
135135+```typescript
136136+namespace com.example.post {
137137+ @rec("tid")
138138+ model Main {
139139+ @required
140140+ @maxGraphemes(300)
141141+ @maxLength(3000)
142142+ text: string;
143143+144144+ facets?: app.bsky.richtext.facet.Main[];
145145+146146+ @required
147147+ createdAt: datetime;
148148+ }
149149+}
150150+```
151151+152152+### Open String Enum
153153+```typescript
154154+model Card {
155155+ @required
156156+ section: "mainboard" | "sideboard" | "maybeboard" | string;
157157+}
158158+```
159159+160160+Compiles to:
161161+```json
162162+{
163163+ "type": "string",
164164+ "knownValues": ["mainboard", "sideboard", "maybeboard"]
165165+}
166166+```
167167+168168+### Token Markers
169169+```typescript
170170+@token
171171+model ReasonSpam {}
172172+173173+@token
174174+model ReasonViolation {}
175175+176176+model Report {
177177+ @required
178178+ reason: (ReasonSpam | ReasonViolation | unknown);
179179+}
180180+```
181181+182182+## Best Practices
183183+184184+1. **Use open unions** - Add `| unknown` or `| string` for extensibility
185185+2. **Prefer optional fields** - Use `?:` unless truly required
186186+3. **10x rule** - Set `maxLength` ~10x `maxGraphemes` for UTF-8 safety
187187+4. **Semantic namespaces** - Group by domain (`actor`, `deck`, `social`)
188188+5. **Import externals** - Always import `"@typelex/emitter"` and `"./externals.tsp"`
189189+6. **PascalCase models** - Converted to camelCase in JSON output
190190+7. **Main model** - Use `model Main` for primary namespace definition
191191+192192+## Model Naming
193193+194194+- `model Main` → `"main"` def (primary record)
195195+- `model Card` → `"card"` def (nested type)
196196+- Converted to camelCase in output
197197+198198+## Validation
199199+200200+TypeSpec compiler catches:
201201+- Invalid references
202202+- Empty unions
203203+- Mixed union types (literals + models without proper structure)
204204+- Missing required imports
205205+206206+Warnings for documentation issues (unescaped special chars like `@`).
207207+208208+## Resources
209209+210210+- [Typelex Docs](https://tangled.org/@danabra.mov/typelex/blob/main/DOCS.md)
211211+- [AT Protocol Lexicon Guide](https://atproto.com/guides/lexicon)
212212+- [Bluesky Lexicons (reference)](https://github.com/bluesky-social/atproto/tree/main/lexicons)
+13
CLAUDE.md
···7676- **Biome**: Uses tabs for indentation, double quotes, excludes generated files
7777- **Devtools**: Integrated TanStack Router + Query + React devtools in root layout
78787979+## Reference Documentation
8080+8181+Additional reference docs are in `.claude/` - **read and update these when working on relevant topics**:
8282+8383+- **PROJECT.md** - DeckBelcher project overview, lexicon structure, and product decisions
8484+- **SCRYFALL.md** - Scryfall card API reference (IDs, fields, image handling)
8585+- **TYPELEX.md** - Typelex syntax guide (decorators, external refs, patterns)
8686+8787+These contain important context about project decisions, API details, and tooling. Keep them updated as the project evolves.
8888+8989+**When to create new reference docs:** If you're doing significant research, explaining complex topics repeatedly, or the user is spending time teaching you something important—create a new markdown file in `.claude/` to preserve that knowledge for future sessions.
9090+7991## Important Notes
80928193- `src/routeTree.gen.ts` is auto-generated - never edit manually
9494+- `typelex/externals.tsp` is auto-generated from lexicons folder - add external lexicon JSON to trigger regeneration
8295- Demo files (prefixed with `demo`) are safe to delete
8396- Biome only lints files in `src/`, `.vscode/`, and root config files
8497- Router uses "intent" preloading by default
···11+{
22+ "lexicon": 1,
33+ "id": "com.deckbelcher.social.like",
44+ "defs": {
55+ "main": {
66+ "type": "record",
77+ "key": "tid",
88+ "record": {
99+ "type": "object",
1010+ "properties": {
1111+ "subject": {
1212+ "type": "ref",
1313+ "ref": "com.atproto.repo.strongRef",
1414+ "description": "Reference to the content being liked."
1515+ },
1616+ "createdAt": {
1717+ "type": "string",
1818+ "format": "datetime",
1919+ "description": "Timestamp when the like was created."
2020+ }
2121+ },
2222+ "required": [
2323+ "subject",
2424+ "createdAt"
2525+ ]
2626+ },
2727+ "description": "Record declaring a 'like' of a piece of content (decklist, reply, etc)."
2828+ }
2929+ }
3030+}
+6-1
typelex/externals.tsp
···11import "@typelex/emitter";
2233// Generated by typelex from ./lexicons (excluding com.deckbelcher.*)
44-// No external lexicons found
44+// This file is auto-generated. Do not edit manually.
55+66+@external
77+namespace com.atproto.repo.strongRef {
88+ model Main { }
99+}
+1
typelex/main.tsp
···22import "./externals.tsp";
33import "./richtext-facet.tsp";
44import "./deck-list.tsp";
55+import "./social-like.tsp";
5667namespace com.deckbelcher.actor.profile {
78 /** A DeckBelcher user profile. */
+16
typelex/social-like.tsp
···11+import "@typelex/emitter";
22+import "./externals.tsp";
33+44+namespace com.deckbelcher.social.like {
55+ /** Record declaring a 'like' of a piece of content (decklist, reply, etc). */
66+ @rec("tid")
77+ model Main {
88+ /** Reference to the content being liked. */
99+ @required
1010+ subject: com.atproto.repo.strongRef.Main;
1111+1212+ /** Timestamp when the like was created. */
1313+ @required
1414+ createdAt: datetime;
1515+ }
1616+}