+8
.vscode/settings.json
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+
}