···8282 /// Create a DeckRecord from an internal Deck model.
8383 pub fn from_deck(deck: &Deck, card_at_uris: Vec<String>) -> Self {
8484 Self {
8585- record_type: "app.malfestio.deck".to_string(),
8585+ record_type: "org.stormlightlabs.malfestio.deck".to_string(),
8686 title: deck.title.clone(),
8787 description: if deck.description.is_empty() { None } else { Some(deck.description.clone()) },
8888 tags: deck.tags.clone(),
···9898 /// Create a CardRecord from an internal Card model.
9999 pub fn from_card(card: &Card, deck_at_uri: &str) -> Self {
100100 Self {
101101- record_type: "app.malfestio.card".to_string(),
101101+ record_type: "org.stormlightlabs.malfestio.card".to_string(),
102102 deck_ref: deck_at_uri.to_string(),
103103 front: card.front.clone(),
104104 back: card.back.clone(),
···117117 /// Create a NoteRecord from an internal Note model.
118118 pub fn from_note(note: &Note) -> Self {
119119 Self {
120120- record_type: "app.malfestio.note".to_string(),
120120+ record_type: "org.stormlightlabs.malfestio.note".to_string(),
121121 title: note.title.clone(),
122122 body: note.body.clone(),
123123 tags: note.tags.clone(),
···143143 let record = DeckRecord::from_deck(deck, card_at_uris);
144144 PreparedRecord {
145145 rkey: generate_tid(),
146146- collection: "app.malfestio.deck".to_string(),
146146+ collection: "org.stormlightlabs.malfestio.deck".to_string(),
147147 record: serde_json::to_value(record).expect("Failed to serialize deck record"),
148148 }
149149}
···153153 let record = CardRecord::from_card(card, deck_at_uri);
154154 PreparedRecord {
155155 rkey: generate_tid(),
156156- collection: "app.malfestio.card".to_string(),
156156+ collection: "org.stormlightlabs.malfestio.card".to_string(),
157157 record: serde_json::to_value(record).expect("Failed to serialize card record"),
158158 }
159159}
···163163 let record = NoteRecord::from_note(note);
164164 PreparedRecord {
165165 rkey: generate_tid(),
166166- collection: "app.malfestio.note".to_string(),
166166+ collection: "org.stormlightlabs.malfestio.note".to_string(),
167167 record: serde_json::to_value(record).expect("Failed to serialize note record"),
168168 }
169169}
···221221 let deck = sample_deck();
222222 let record = DeckRecord::from_deck(&deck, vec![]);
223223224224- assert_eq!(record.record_type, "app.malfestio.deck");
224224+ assert_eq!(record.record_type, "org.stormlightlabs.malfestio.deck");
225225 assert_eq!(record.title, "Test Deck");
226226 assert_eq!(record.description, Some("A test deck".to_string()));
227227 assert_eq!(record.tags.len(), 2);
···230230 #[test]
231231 fn test_deck_record_serialization() {
232232 let deck = sample_deck();
233233- let record = DeckRecord::from_deck(&deck, vec!["at://did:plc:abc/app.malfestio.card/tid1".to_string()]);
233233+ let record = DeckRecord::from_deck(
234234+ &deck,
235235+ vec!["at://did:plc:abc/org.stormlightlabs.malfestio.card/tid1".to_string()],
236236+ );
234237235238 let json = serde_json::to_string(&record).unwrap();
236236- assert!(json.contains("\"$type\":\"app.malfestio.deck\""));
239239+ assert!(json.contains("\"$type\":\"org.stormlightlabs.malfestio.deck\""));
237240 assert!(json.contains("\"title\":\"Test Deck\""));
238241 assert!(json.contains("cardRefs"));
239242 }
···241244 #[test]
242245 fn test_card_record_from_card() {
243246 let card = sample_card();
244244- let deck_uri = "at://did:plc:abc123/app.malfestio.deck/tid123";
247247+ let deck_uri = "at://did:plc:abc123/org.stormlightlabs.malfestio.deck/tid123";
245248 let record = CardRecord::from_card(&card, deck_uri);
246249247247- assert_eq!(record.record_type, "app.malfestio.card");
250250+ assert_eq!(record.record_type, "org.stormlightlabs.malfestio.card");
248251 assert_eq!(record.deck_ref, deck_uri);
249252 assert_eq!(record.front, "What is the capital of France?");
250253 assert_eq!(record.back, "Paris");
···255258 let note = sample_note();
256259 let record = NoteRecord::from_note(¬e);
257260258258- assert_eq!(record.record_type, "app.malfestio.note");
261261+ assert_eq!(record.record_type, "org.stormlightlabs.malfestio.note");
259262 assert_eq!(record.title, "Test Note");
260263 assert_eq!(record.visibility, "public");
261264 }
···265268 let deck = sample_deck();
266269 let prepared = prepare_deck_record(&deck, vec![]);
267270268268- assert_eq!(prepared.collection, "app.malfestio.deck");
271271+ assert_eq!(prepared.collection, "org.stormlightlabs.malfestio.deck");
269272 assert_eq!(prepared.rkey.len(), 13); // TID length
270273 assert!(prepared.record.is_object());
271274 }
···273276 #[test]
274277 fn test_prepare_card_record() {
275278 let card = sample_card();
276276- let prepared = prepare_card_record(&card, "at://did:plc:abc/app.malfestio.deck/tid");
279279+ let prepared = prepare_card_record(&card, "at://did:plc:abc/org.stormlightlabs.malfestio.deck/tid");
277280278278- assert_eq!(prepared.collection, "app.malfestio.card");
281281+ assert_eq!(prepared.collection, "org.stormlightlabs.malfestio.card");
279282 assert_eq!(prepared.rkey.len(), 13);
280283 }
281284···284287 let note = sample_note();
285288 let prepared = prepare_note_record(¬e);
286289287287- assert_eq!(prepared.collection, "app.malfestio.note");
290290+ assert_eq!(prepared.collection, "org.stormlightlabs.malfestio.note");
288291 assert_eq!(prepared.rkey.len(), 13);
289292 }
290293291294 #[test]
292295 fn test_make_at_uri() {
293293- let uri = make_at_uri("did:plc:abc123", "app.malfestio.deck", "3k5abc123");
294294- assert_eq!(uri.to_string(), "at://did:plc:abc123/app.malfestio.deck/3k5abc123");
296296+ let uri = make_at_uri("did:plc:abc123", "org.stormlightlabs.malfestio.deck", "3k5abc123");
297297+ assert_eq!(
298298+ uri.to_string(),
299299+ "at://did:plc:abc123/org.stormlightlabs.malfestio.deck/3k5abc123"
300300+ );
295301 }
296302297303 #[test]
+69
lexicons/README.md
···3232- **Private layer**:
3333 - review schedule, lapses, grades, per-card performance, streaks
34343535+## Publishing Lexicons to AT Protocol Network
3636+3737+### Prerequisites
3838+3939+**Goat CLI**: Install the official AT Protocol CLI tool
4040+4141+```bash
4242+# macOS
4343+brew install goat
4444+```
4545+4646+### Publishing Workflow
4747+4848+1. **Validate schemas locally**:
4949+5050+ ```bash
5151+ goat lexicon lint lexicons/
5252+ ```
5353+5454+2. **Check DNS configuration**:
5555+5656+ ```bash
5757+ goat lexicon check-dns org.stormlightlabs.malfestio.card
5858+ ```
5959+6060+3. **Publish to network**:
6161+6262+ ```bash
6363+ goat lexicon publish lexicons/org/stormlightlabs/malfestio/
6464+ ```
6565+6666+### PDS Validation Modes
6767+6868+AT Protocol PDSs support three lexicon validation modes:
6969+7070+1. **Explicit validation required**: Record must validate against schema; fails if PDS doesn't know the lexicon
7171+ - This is the current mode causing `Lexicon not found` errors
7272+ - Requires publishing lexicons or using optimistic validation
7373+7474+2. **Optimistic validation** (default): Validates if PDS knows the schema, allows creation if unknown
7575+ - Most flexible for custom lexicons during development
7676+ - Set via `validate: undefined` in create/update record calls
7777+7878+3. **Explicit no validation**: Skips validation even if PDS knows the schema
7979+ - Set via `validate: false` in create/update record calls
8080+8181+### Version Updates
8282+8383+When updating lexicon schemas:
8484+8585+1. **Minor Updates** (additive only):
8686+ - Add new optional fields
8787+ - Update descriptions
8888+ - Add new `knownValues` (don't remove old ones)
8989+ - Increment patch version in documentation
9090+9191+2. **Breaking Changes** (avoid if possible):
9292+ - Create new lexicon with new NSID (e.g., `org.stormlightlabs.malfestio.cardV2`)
9393+ - Maintain both versions during migration period
9494+ - Update code to support both old and new schemas
9595+ - Document migration path
9696+9797+3. **Republishing**:
9898+9999+ ```bash
100100+ goat lexicon lint lexicons/
101101+ goat lexicon publish lexicons/org/stormlightlabs/malfestio/
102102+ ```
103103+35104## Evolution Rules
36105371061. **Additive Changes Only**: You can add new optional fields to existing records.