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