learn and share notes on atproto (wip) 🦉 malfestio.stormlightlabs.org/
readability solid axum atproto srs

docs: define AT Protocol lexicons for core data types

* add data model mapping and publishing pipeline documentation

* configure dprint.

+8
.vscode/settings.json
··· 1 + { 2 + "[json]": { "editor.defaultFormatter": "dprint.dprint", "editor.formatOnSave": true }, 3 + "[jsonc]": { "editor.defaultFormatter": "dprint.dprint", "editor.formatOnSave": true }, 4 + "[typescript]": { "editor.defaultFormatter": "dprint.dprint", "editor.formatOnSave": true }, 5 + "[typescriptreact]": { "editor.defaultFormatter": "dprint.dprint", "editor.formatOnSave": true }, 6 + "[javascript]": { "editor.defaultFormatter": "dprint.dprint", "editor.formatOnSave": true }, 7 + "[javascriptreact]": { "editor.defaultFormatter": "dprint.dprint", "editor.formatOnSave": true } 8 + }
+34
docs/data-model-mapping.md
··· 1 + # Data Model Mapping 2 + 3 + This document maps the public AT Protocol Lexicon records to our internal SQL database schema. 4 + 5 + ## Principles 6 + 7 + 1. **Separation of Concerns** 8 + We store "my view" of the world (private state) separate from "the network's view" (public records). 9 + 2. **Hydration** 10 + We allow hydrating public records into local DB rows for efficient query/indexing, but the source of truth for the record itself is the signed commit in the repository. 11 + 3. **Private State** 12 + User progress, scheduling, and local-only drafts live ONLY in the internal DB. 13 + 14 + ## Mapping Table 15 + 16 + | Public Lexicon | Internal DB Table(s) | Notes | 17 + | :----------------------------- | :------------------- | :------------------------------------------------------------- | 18 + | `app.malfestio.deck` | `decks` | Public metadata. | 19 + | `app.malfestio.card` | `cards` | Content. | 20 + | `app.malfestio.note` | `notes` | Standalone notes. | 21 + | `app.malfestio.source.*` | `sources` | Metadata for articles/lectures. | 22 + | `app.malfestio.collection` | `collections` | | 23 + | `app.malfestio.thread.comment` | `comments` | | 24 + | _(None)_ | `reviews` | **Private**. Logs of every review event. | 25 + | _(None)_ | `study_progress` | **Private**. Current SRS state for a card (box/interval/ease). | 26 + | _(None)_ | `user_settings` | **Private**. Daily goals, UI references. | 27 + | _(None)_ | `drafts` | **Private**. Content being authored before publishing. | 28 + 29 + ## Sync Strategy 30 + 31 + - **Publishing**: 32 + Write to `drafts` -> User clicks "Publish" -> Sign record -> Push to PDS -> Move `drafts` content to `decks`/`cards` tables (or mark as synced). 33 + - **Consuming**: 34 + Pull from PDS (firehose or direct sync) -> Validate signature -> Upsert into local tables.
+38
docs/publish-pipeline.md
··· 1 + # Publish Pipeline Spec 2 + 3 + This document defines the lifecycle of content from draft to published record. 4 + 5 + ## Lifecycle States 6 + 7 + 1. **Draft** (Local Only) 8 + - Stored in local SQL/captured in UI state. 9 + - Not visible to PDS or other users. 10 + - Mutable without restriction. 11 + - IDs are local UUIDs or temporary placeholders. 12 + 13 + 2. **Published** (Public / Unlisted / Shared) 14 + - Signed and committed to the AT Protocol repository. 15 + - Assigned a permanent `at://` URI. 16 + - stored in Lexicon-compliant format. 17 + - **Edits**: Append a new commit replacing the record. History is technically preserved in the repo log but UI typically shows latest. 18 + 19 + 3. **Deprecated / Tombstoned** 20 + - User "deletes" the content. 21 + - **Action**: We replace the record with a minimal "tombstone" or actually delete the record from the repo (RepoOp `delete`). 22 + - *Note*: Aggregators may still have cached copies. 23 + 24 + ## Protocol Flow 25 + 26 + 1. **Auth**: User logs in via OAuth (or app app-password initially). 27 + 2. **Format**: App converts internal `Draft` model -> `Lexicon` JSON. 28 + 3. **Sign & Commit**: 29 + - App constructs a repository operation (create/update). 30 + - Sends to PDS (Personal Data Server). 31 + 4. **Confirm**: PDS confirms commit CID. 32 + 5. **Index**: App updates local distinct "published" view to match confirmed state. 33 + 34 + ## Versioning Content 35 + 36 + - No git-like branching for content *history* in the MVP. 37 + - "Edit" = "Overwrite". 38 + - Collaborative editing (forking) = "Copy & Publish New".
+2 -32
docs/todo.md
··· 37 37 38 38 ## Roadmap Milestones 39 39 40 - ### Milestone A - Product Spec + IA + UX Flows 41 - 42 - #### Deliverables 43 - 44 - - Core user journeys (5): 45 - - Import source -> generate notes/cards -> publish deck 46 - - Daily study -> review queue -> grade -> progress view 47 - - Follow curator -> discover deck -> fork -> contribute improvements 48 - - Discuss a card/deck -> moderation/report flow 49 - - Lecture workflow -> outline -> timestamps -> linked cards 50 - - Information architecture + navigation map 51 - - "Share vs private" rules doc (what becomes public records; what never does) 52 - 53 - #### Acceptance 54 - 55 - - Every screen maps to a backend capability + a data model entity. 56 - 57 - ### Milestone B - Lexicon Design Kit + Data Model Mapping 40 + - **(Done) Milestone A**: Defined core user journeys, information architecture, and privacy rules for the platform. 58 41 59 - #### Deliverables 60 - 61 - - Lexicon repo folder with: 62 - - record schemas for note/card/deck/article/lecture/collection/comment 63 - - schema evolution notes (what can change, what cannot) 64 - - Mapping doc: 65 - - Public record (lexicon) <-> internal DB row(s) 66 - - Minimal "publish pipeline" spec (draft->published->deprecated) 67 - 68 - #### Acceptance 69 - 70 - - You can create a deck and serialize it into a stable record shape. 71 - - Follow Lexicon rules; prefer additive evolution. 72 - - Review Bluesky "custom schemas" patterns for compatibility expectations. 42 + - **(Done) Milestone B**: Designed AT Protocol Lexicons for all core types and documented data model mapping + publishing pipeline. 73 43 74 44 ### Milestone C - Foundations: Repo, CI, Axum API Skeleton, Solid Shell 75 45
+6
dprint.json
··· 1 + { 2 + "typescript": { "preferSingleLine": true, "jsx.bracketPosition": "sameLine" }, 3 + "json": { "preferSingleLine": true, "lineWidth": 121, "indentWidth": 2 }, 4 + "excludes": ["**/node_modules"], 5 + "plugins": ["https://plugins.dprint.dev/typescript-0.95.8.wasm", "https://plugins.dprint.dev/json-0.20.0.wasm"] 6 + }
+11
lexicons/README.md
··· 1 + # Lexicon Schemas 2 + 3 + This directory contains the Lexicon definitions for the malfestio's public records. 4 + 5 + ## Evolution Rules 6 + 7 + 1. **Additive Changes Only**: You can add new optional fields to existing records. 8 + 2. **No Renaming**: Do not rename fields. 9 + If a semantic change is needed, add a new field and deprecate the old one. 10 + 3. **No Type Changes**: Once published, a field's type is fixed. 11 + 4. **Version by Copying**: If a breaking change is absolutely required, create a new Lexicon with a new major version or a new name (e.g., `app.malfestio.noteV2`).
+59
lexicons/app/malfestio/card.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.malfestio.card", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A flashcard for spaced repetition study.", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["deckRef", "front", "back", "createdAt"], 12 + "properties": { 13 + "deckRef": { 14 + "type": "string", 15 + "format": "at-uri", 16 + "description": "Reference to the deck this card belongs to." 17 + }, 18 + "front": { 19 + "type": "string", 20 + "format": "markdown", 21 + "maxLength": 10000, 22 + "description": "Content on the front of the card." 23 + }, 24 + "back": { 25 + "type": "string", 26 + "format": "markdown", 27 + "maxLength": 10000, 28 + "description": "Content on the back of the card." 29 + }, 30 + "cardType": { 31 + "type": "string", 32 + "knownValues": ["basic", "cloze"], 33 + "default": "basic", 34 + "description": "Type of the card (e.g., basic or cloze deletion)." 35 + }, 36 + "hints": { 37 + "type": "array", 38 + "items": { "type": "string" }, 39 + "description": "Optional hints to display before revealing the answer." 40 + }, 41 + "media": { 42 + "type": "array", 43 + "items": { 44 + "type": "object", 45 + "required": ["uri", "kind"], 46 + "properties": { 47 + "uri": { "type": "string", "format": "uri" }, 48 + "kind": { "type": "string", "knownValues": ["image", "audio"] }, 49 + "alt": { "type": "string" } 50 + } 51 + }, 52 + "description": "Multimedia attachments for the card." 53 + }, 54 + "createdAt": { "type": "string", "format": "datetime" } 55 + } 56 + } 57 + } 58 + } 59 + }
+34
lexicons/app/malfestio/collection.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.malfestio.collection", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A curated collection or learning path.", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["title", "createdAt"], 12 + "properties": { 13 + "title": { "type": "string", "maxLength": 300 }, 14 + "description": { "type": "string", "maxLength": 3000 }, 15 + "items": { 16 + "type": "array", 17 + "items": { 18 + "type": "object", 19 + "required": ["type", "ref"], 20 + "properties": { 21 + "type": { "type": "string", "knownValues": ["deck", "note", "article", "lecture"] }, 22 + "ref": { "type": "string", "format": "at-uri" }, 23 + "note": { "type": "string", "description": "Curator's note about this item." } 24 + } 25 + }, 26 + "description": "Ordered items in the collection." 27 + }, 28 + "tags": { "type": "array", "items": { "type": "string" }, "maxLength": 64 }, 29 + "createdAt": { "type": "string", "format": "datetime" } 30 + } 31 + } 32 + } 33 + } 34 + }
+32
lexicons/app/malfestio/deck.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.malfestio.deck", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A collection of flashcards and sources.", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["title", "createdAt"], 12 + "properties": { 13 + "title": { "type": "string", "maxLength": 300, "description": "Title of the deck." }, 14 + "description": { "type": "string", "maxLength": 3000, "description": "Description of the deck context." }, 15 + "tags": { "type": "array", "items": { "type": "string" }, "maxLength": 64 }, 16 + "cardRefs": { 17 + "type": "array", 18 + "items": { "type": "string", "format": "at-uri" }, 19 + "description": "Ordered list of references to cards in this deck." 20 + }, 21 + "sourceRefs": { 22 + "type": "array", 23 + "items": { "type": "string", "format": "at-uri" }, 24 + "description": "References to source materials (articles, lectures) used in this deck." 25 + }, 26 + "license": { "type": "string", "description": "License for the deck content." }, 27 + "createdAt": { "type": "string", "format": "datetime" } 28 + } 29 + } 30 + } 31 + } 32 + }
+51
lexicons/app/malfestio/note.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.malfestio.note", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A standalone note containing rich text, tags, and links.", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["title", "body", "createdAt"], 12 + "properties": { 13 + "title": { "type": "string", "maxLength": 300, "description": "Title of the note." }, 14 + "body": { 15 + "type": "string", 16 + "format": "markdown", 17 + "maxLength": 100000, 18 + "description": "The body content of the note in Markdown format." 19 + }, 20 + "tags": { 21 + "type": "array", 22 + "items": { "type": "string" }, 23 + "maxLength": 64, 24 + "description": "Tags associated with the note." 25 + }, 26 + "links": { 27 + "type": "array", 28 + "items": { 29 + "type": "object", 30 + "required": ["uri"], 31 + "properties": { 32 + "uri": { "type": "string", "format": "uri" }, 33 + "title": { "type": "string" }, 34 + "type": { "type": "string", "description": "Type hint for the linked resource." } 35 + } 36 + }, 37 + "description": "External or internal links referenced in the note." 38 + }, 39 + "createdAt": { "type": "string", "format": "datetime", "description": "Timestamp of creation." }, 40 + "updatedAt": { "type": "string", "format": "datetime", "description": "Timestamp of last update." }, 41 + "visibility": { 42 + "type": "string", 43 + "knownValues": ["private", "unlisted", "public"], 44 + "default": "private", 45 + "description": "Visibility setting for the note." 46 + } 47 + } 48 + } 49 + } 50 + } 51 + }
+43
lexicons/app/malfestio/source/article.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.malfestio.source.article", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A reference to an article used as source material.", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["url", "title", "createdAt"], 12 + "properties": { 13 + "url": { "type": "string", "format": "uri", "description": "URL of the article." }, 14 + "title": { "type": "string", "description": "Title of the article." }, 15 + "author": { "type": "string", "description": "Author of the article." }, 16 + "publishedAt": { 17 + "type": "string", 18 + "format": "datetime", 19 + "description": "Original publication date of the article." 20 + }, 21 + "extractedTextRef": { 22 + "type": "string", 23 + "format": "at-uri", 24 + "description": "Optional reference to a blob or record containing the extracted text." 25 + }, 26 + "highlights": { 27 + "type": "array", 28 + "items": { 29 + "type": "object", 30 + "properties": { 31 + "quote": { "type": "string", "maxLength": 5000 }, 32 + "start": { "type": "integer", "description": "Character start offset." }, 33 + "end": { "type": "integer", "description": "Character end offset." } 34 + } 35 + }, 36 + "description": "User highlights from the article." 37 + }, 38 + "createdAt": { "type": "string", "format": "datetime" } 39 + } 40 + } 41 + } 42 + } 43 + }
+38
lexicons/app/malfestio/source/lecture.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.malfestio.source.lecture", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A reference to a lecture or video used as source material.", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["url", "title", "createdAt"], 12 + "properties": { 13 + "url": { "type": "string", "format": "uri", "description": "URL of the lecture (e.g., YouTube link)." }, 14 + "title": { "type": "string", "description": "Title of the lecture." }, 15 + "creator": { "type": "string", "description": "Creator or channel name." }, 16 + "timestamps": { 17 + "type": "array", 18 + "items": { 19 + "type": "object", 20 + "required": ["t", "label"], 21 + "properties": { 22 + "t": { "type": "integer", "description": "Time in seconds." }, 23 + "label": { "type": "string", "description": "Description of the timestamp." }, 24 + "noteRef": { 25 + "type": "string", 26 + "format": "at-uri", 27 + "description": "Optional reference to a note taken at this timestamp." 28 + } 29 + } 30 + }, 31 + "description": "Important timestamps or chapters in the lecture." 32 + }, 33 + "createdAt": { "type": "string", "format": "datetime" } 34 + } 35 + } 36 + } 37 + } 38 + }
+21
lexicons/app/malfestio/thread/comment.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.malfestio.thread.comment", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A comment on a deck, card, or note.", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["subjectRef", "body", "createdAt"], 12 + "properties": { 13 + "subjectRef": { "type": "string", "format": "at-uri", "description": "The root subject being commented on." }, 14 + "replyTo": { "type": "string", "format": "at-uri", "description": "The parent comment if this is a reply." }, 15 + "body": { "type": "string", "format": "markdown", "maxLength": 5000, "description": "The comment text." }, 16 + "createdAt": { "type": "string", "format": "datetime" } 17 + } 18 + } 19 + } 20 + } 21 + }