···3636npx tsc -b # Run TypeScript compiler in build mode
3737```
38383939+### Lexicon Publishing
4040+```bash
4141+/publish-lexicons # Validate and lint lexicons (default)
4242+/publish-lexicons update # Validate, regenerate types, and fix bugs
4343+/publish-lexicons publish # Full flow including publishing to lexicon.garden
4444+```
4545+4646+See [`LEXICON_PUBLISHING.md`](/LEXICON_PUBLISHING.md) for complete documentation on publishing custom AT Protocol lexicons to the lexicon.garden registry.
4747+3948## ⚠️ Code Generator Issues (CRITICAL)
40494150The `npm run gen-api` command generates TypeScript types from lexicon schemas but has **two critical bugs** that will break the build:
+247
LEXICON_PUBLISHING.md
···11+# Publishing Lexicons to Lexicon.garden
22+33+This guide documents the complete process for validating, publishing, and updating AT Protocol lexicons for the Drydown app to the [Lexicon.garden](https://lexicon.garden) registry.
44+55+## Overview
66+77+Drydown uses four custom AT Protocol lexicons:
88+- `social.drydown.house` - Fragrance houses/brands
99+- `social.drydown.fragrance` - Individual fragrances
1010+- `social.drydown.review` - Three-stage fragrance reviews
1111+- `social.drydown.settings` - User preferences for scoring
1212+1313+## Prerequisites
1414+1515+- **goat CLI tool** installed (`brew install bluesky-social/tap/goat` on macOS)
1616+- **Active Bluesky account** with your PDS
1717+- **DNS access** for `drydown.social` domain
1818+- **Node.js** for TypeScript type generation
1919+2020+## Step-by-Step Publishing Process
2121+2222+### 1. Validate Lexicon Schemas
2323+2424+Parse all lexicon files to ensure they're valid:
2525+2626+```bash
2727+goat lex parse src/lexicons/social.drydown.house.json
2828+goat lex parse src/lexicons/social.drydown.fragrance.json
2929+goat lex parse src/lexicons/social.drydown.review.json
3030+goat lex parse src/lexicons/social.drydown.settings.json
3131+```
3232+3333+**Expected output**: `success` for each file
3434+3535+### 2. Lint for Best Practices
3636+3737+Check schemas for quality issues:
3838+3939+```bash
4040+goat lex lint src/lexicons/*.json
4141+```
4242+4343+**Expected output**: 🟢 green checkmark for all files
4444+4545+**Common issues to fix:**
4646+- Invalid record key specifiers: Use `"literal:self"` instead of `"self"`
4747+- Unlimited strings: Add `maxLength` to string fields
4848+- Missing descriptions: Add clear descriptions to all fields
4949+5050+### 3. Regenerate TypeScript Types
5151+5252+After any lexicon changes, regenerate the TypeScript client:
5353+5454+```bash
5555+npm run gen-api
5656+```
5757+5858+Type `y` when prompted to confirm.
5959+6060+### 4. Fix Generator Bugs
6161+6262+The `@atproto/lex-cli` generator has known bugs that must be fixed manually after each run.
6363+6464+**Issue 1: Remove unused imports from type files**
6565+6666+In `src/client/types/social/drydown/*.ts` files, change:
6767+6868+```typescript
6969+// BEFORE (generated, broken)
7070+import { type ValidationResult, BlobRef } from '@atproto/lexicon'
7171+import { CID } from 'multiformats/cid'
7272+import { validate as _validate } from '../../../lexicons'
7373+import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util'
7474+7575+// AFTER (fixed)
7676+import { validate as _validate } from '../../../lexicons'
7777+import { is$typed as _is$typed } from '../../../util'
7878+```
7979+8080+**Issue 2: Fix `src/client/index.ts`**
8181+8282+Add AT Protocol imports and restore bsky schemas:
8383+8484+```typescript
8585+// Add these imports at the top
8686+import {
8787+ ComAtprotoRepoListRecords,
8888+ ComAtprotoRepoGetRecord,
8989+ ComAtprotoRepoCreateRecord,
9090+ ComAtprotoRepoPutRecord,
9191+ ComAtprotoRepoDeleteRecord,
9292+} from '@atproto/api'
9393+import { schemas as bskySchemas } from '@atproto/api'
9494+9595+// Remove this import
9696+// import { CID } from 'multiformats/cid'
9797+9898+// Fix the constructor
9999+constructor(options: FetchHandler | FetchHandlerOptions) {
100100+ super(options, [...schemas, ...bskySchemas]) // NOT just `schemas`
101101+ this.social = new SocialNS(this)
102102+}
103103+```
104104+105105+**Issue 3: Fix `src/client/lexicons.ts`**
106106+107107+Remove unused import:
108108+109109+```typescript
110110+// BEFORE
111111+import { type $Typed, is$typed, maybe$typed } from './util.js'
112112+113113+// AFTER
114114+import { is$typed, maybe$typed } from './util.js'
115115+```
116116+117117+### 5. Verify Build
118118+119119+Ensure TypeScript compilation succeeds:
120120+121121+```bash
122122+npx tsc -b
123123+npm run build
124124+```
125125+126126+Both commands should complete without errors.
127127+128128+### 6. Login to goat (First Time Only)
129129+130130+```bash
131131+goat account login
132132+```
133133+134134+Enter your Bluesky handle (e.g., `username.bsky.social`) and app password.
135135+136136+**To create an app password:**
137137+1. Go to Settings → App Passwords in Bluesky
138138+2. Create a new app password
139139+3. Copy and paste when prompted
140140+141141+### 7. Get Your DID
142142+143143+```bash
144144+goat account status
145145+```
146146+147147+Copy the DID value (starts with `did:plc:...`)
148148+149149+### 8. Configure DNS Authority
150150+151151+To prove you control the `drydown.social` domain, add a DNS TXT record:
152152+153153+**DNS Record Details:**
154154+- **Name/Host**: `_lexicon.drydown.social` (or just `_lexicon` depending on your DNS provider)
155155+- **Type**: `TXT`
156156+- **Value**: `did=YOUR_DID_HERE` (replace with your actual DID from step 7)
157157+- **TTL**: 3600 (or default)
158158+159159+**Example:**
160160+```
161161+_lexicon.drydown.social. IN TXT "did=did:plc:abc123xyz..."
162162+```
163163+164164+### 9. Verify DNS Propagation
165165+166166+Wait 5-10 minutes after adding the DNS record, then verify:
167167+168168+```bash
169169+dig TXT _lexicon.drydown.social
170170+```
171171+172172+**Expected output:** Should show your DID in the TXT record
173173+174174+Alternatively, use an online DNS checker like https://dnschecker.org
175175+176176+### 10. Publish Lexicons
177177+178178+Once DNS is configured and verified:
179179+180180+```bash
181181+goat lex publish src/lexicons
182182+```
183183+184184+This command will:
185185+1. Validate all schemas
186186+2. Check DNS authority records
187187+3. Create lexicon records in your AT Protocol repository
188188+4. Register them with lexicon.garden
189189+190190+**Expected output:** Success messages for each published lexicon
191191+192192+### 11. Verify on Lexicon.garden
193193+194194+Visit https://lexicon.garden and search for `social.drydown`
195195+196196+You should see all four lexicons listed with:
197197+- ✅ Authority badge (DNS verification successful)
198198+- Full documentation
199199+- Relationship graph showing connections between lexicons
200200+201201+## Updating Existing Lexicons
202202+203203+To update lexicons after they've been published:
204204+205205+1. **Make changes** to the JSON schema files in `src/lexicons/`
206206+2. **Validate** with `goat lex parse`
207207+3. **Lint** with `goat lex lint`
208208+4. **Check for breaking changes**: `goat lex breaking src/lexicons`
209209+5. **Regenerate types** with `npm run gen-api`
210210+6. **Fix generator bugs** (see step 4 above)
211211+7. **Verify build** with `npx tsc -b`
212212+8. **Publish updates**: `goat lex publish src/lexicons`
213213+214214+## Common Issues
215215+216216+### "error: expected NSID, got empty string"
217217+218218+This usually means the command syntax is wrong. Use `goat lex parse <file>` not `goat lex validate`.
219219+220220+### "DNS authority check failed"
221221+222222+- Wait longer for DNS propagation (can take up to 24 hours)
223223+- Verify the TXT record is correctly formatted
224224+- Ensure you're using the correct DID
225225+- Check that the record name is exactly `_lexicon.drydown.social`
226226+227227+### "invalid record key specifier: self"
228228+229229+Use `"key": "literal:self"` instead of `"key": "self"` in your lexicon schema.
230230+231231+### TypeScript build fails after regenerating types
232232+233233+Apply the generator bug fixes in step 4. These must be done every time you run `npm run gen-api`.
234234+235235+## Additional Resources
236236+237237+- [AT Protocol Lexicon Specification](https://atproto.com/specs/lexicon)
238238+- [Lexicon.garden Help](https://lexicon.garden/help)
239239+- [goat CLI Documentation](https://github.com/bluesky-social/indigo/tree/main/cmd/goat)
240240+- [Project CLAUDE.md](./CLAUDE.md) - See "Code Generator Issues" section
241241+242242+## Notes
243243+244244+- Lexicon.garden can take up to 24 hours to fully index new schemas due to DNS caching, identity resolution, and indexing delays
245245+- Always test lexicon changes in a development environment before publishing
246246+- Breaking changes to lexicons (removing fields, changing types) can break existing applications
247247+- Keep lexicon files under version control and document all changes
···11+/**
22+ * GENERATED CODE - DO NOT MODIFY
33+ */
44+import { validate as _validate } from '../../../lexicons'
55+import { is$typed as _is$typed } from '../../../util'
66+77+const is$typed = _is$typed,
88+ validate = _validate
99+const id = 'social.drydown.settings'
1010+1111+export interface Main {
1212+ $type: 'social.drydown.settings'
1313+ /** How the user prefers to be noticed (1=skin scent, 5=bold presence) */
1414+ presenceStyle?: number
1515+ /** How important all-day longevity is (1=not important, 5=essential) */
1616+ longevityPriority?: number
1717+ /** Preference for fragrance complexity (1=simple, 5=intricate) */
1818+ complexityPreference?: number
1919+ /** How user evaluates fragrances (1=instinct, 5=analytical) */
2020+ scoringApproach?: number
2121+ /** When viewing others' reviews: show their score or recalculate with your preferences */
2222+ scoreLens?: 'theirs' | 'mine' | (string & {})
2323+ /** Timestamp when settings were first created */
2424+ createdAt: string
2525+ /** Timestamp when settings were last updated */
2626+ updatedAt?: string
2727+ [k: string]: unknown
2828+}
2929+3030+const hashMain = 'main'
3131+3232+export function isMain<V>(v: V) {
3333+ return is$typed(v, id, hashMain)
3434+}
3535+3636+export function validateMain<V>(v: V) {
3737+ return validate<Main & V>(v, id, hashMain, true)
3838+}
3939+4040+export {
4141+ type Main as Record,
4242+ isMain as isRecord,
4343+ validateMain as validateRecord,
4444+}
+7-6
src/components/EditHousePage.tsx
···11import { useState, useEffect } from 'preact/hooks'
22-import { useLocation, Link } from 'wouter'
22+import { useLocation } from 'wouter'
33+import type { OAuthSession } from '@atproto/oauth-client-browser'
34import { AtpBaseClient } from '../client/index'
45import { AppDisclaimer } from './AppDisclaimer'
66+import { Header } from './Header'
57import { Footer } from './Footer'
66-import type { OAuthSession } from '@atproto/oauth-client-browser'
78import { deleteHouse } from '../api/houses'
89import { bulkUpdateFragranceHouse } from '../api/fragrances'
910import { Combobox } from './Combobox'
···1516 handle: string
1617 rkey: string
1718 session: OAuthSession | null
1919+ userProfile?: { displayName?: string; handle: string } | null
2020+ onLogout?: () => void
1821}
19222020-export function EditHousePage({ handle, rkey, session }: EditHousePageProps) {
2323+export function EditHousePage({ handle, rkey, session, userProfile, onLogout }: EditHousePageProps) {
2124 const [, setLocation] = useLocation()
2225 const [houseName, setHouseName] = useState('')
2326 const [isLoading, setIsLoading] = useState(true)
···216219217220 return (
218221 <div className="page-container">
219219- <nav className="main-nav" style={{ marginBottom: '2rem' }}>
220220- <Link href="/" className="nav-logo">Drydown</Link>
221221- </nav>
222222+ <Header session={session} userProfile={userProfile} onLogout={onLogout} />
222223223224 <h2>Edit House</h2>
224225