diff --git a/docs/archify/obsidian-sorter.architecture.json b/docs/archify/obsidian-sorter.architecture.json
new file mode 100644
index 0000000..9d99866
--- /dev/null
+++ b/docs/archify/obsidian-sorter.architecture.json
@@ -0,0 +1,289 @@
+{
+ "schema_version": 1,
+ "diagram_type": "architecture",
+ "meta": {
+ "title": "Obsidian Sorter \u2014 Note Filing Pipeline",
+ "output": "docs/obsidian-sorter-map.html",
+ "visual_preset": "blueprint",
+ "animation": "trace",
+ "quality_profile": "showcase",
+ "views": [
+ {
+ "id": "intake",
+ "label": "Intake",
+ "focus": [
+ "author",
+ "inbox",
+ "watcher"
+ ],
+ "note": "A note lands in the inbox and the watcher picks it up."
+ },
+ {
+ "id": "classification",
+ "label": "Classification",
+ "focus": [
+ "watcher",
+ "rules",
+ "jev",
+ "proposal"
+ ],
+ "note": "Rules decide first; anything unclear escalates to the JEV model."
+ },
+ {
+ "id": "approval",
+ "label": "Human approval",
+ "focus": [
+ "proposal",
+ "obsidian",
+ "writer"
+ ],
+ "note": "Nothing moves until the owner ticks the checkbox."
+ },
+ {
+ "id": "filing",
+ "label": "Filing",
+ "focus": [
+ "writer",
+ "folder",
+ "toc"
+ ],
+ "note": "The note moves to its folder and the index is updated."
+ }
+ ]
+ },
+ "components": [
+ {
+ "id": "author",
+ "type": "external",
+ "label": "Sam",
+ "sublabel": "writes a note",
+ "pos": [
+ 40,
+ 340
+ ],
+ "size": [
+ 120,
+ 60
+ ]
+ },
+ {
+ "id": "inbox",
+ "type": "database",
+ "label": "010 inbox/",
+ "sublabel": "unsorted notes",
+ "pos": [
+ 220,
+ 340
+ ],
+ "size": [
+ 130,
+ 60
+ ]
+ },
+ {
+ "id": "watcher",
+ "type": "backend",
+ "label": "Watcher",
+ "sublabel": "systemd \u00b7 10s poll",
+ "pos": [
+ 410,
+ 340
+ ],
+ "size": [
+ 140,
+ 62
+ ],
+ "tag": "watch.py"
+ },
+ {
+ "id": "rules",
+ "type": "backend",
+ "label": "Tier 1 \u2014 Rules",
+ "sublabel": "folder + keyword hints",
+ "pos": [
+ 620,
+ 170
+ ],
+ "size": [
+ 160,
+ 62
+ ],
+ "tag": "classify.py"
+ },
+ {
+ "id": "jev",
+ "type": "cloud",
+ "label": "TypeSafe JEV",
+ "sublabel": "bucket \u00b7 folder \u00b7 tags",
+ "pos": [
+ 620,
+ 500
+ ],
+ "size": [
+ 160,
+ 62
+ ],
+ "tag": "remote API"
+ },
+ {
+ "id": "proposal",
+ "type": "database",
+ "label": "Proposal",
+ "sublabel": "written into the note",
+ "pos": [
+ 850,
+ 340
+ ],
+ "size": [
+ 150,
+ 62
+ ],
+ "tag": "frontmatter"
+ },
+ {
+ "id": "obsidian",
+ "type": "external",
+ "label": "Obsidian",
+ "sublabel": "owner ticks approved",
+ "pos": [
+ 1060,
+ 340
+ ],
+ "size": [
+ 150,
+ 62
+ ],
+ "tag": "human gate"
+ },
+ {
+ "id": "writer",
+ "type": "backend",
+ "label": "Writer",
+ "sublabel": "atomic move",
+ "pos": [
+ 1270,
+ 340
+ ],
+ "size": [
+ 150,
+ 62
+ ],
+ "tag": "writer.py"
+ },
+ {
+ "id": "folder",
+ "type": "database",
+ "label": "Destination",
+ "sublabel": "type follows folder",
+ "pos": [
+ 1490,
+ 220
+ ],
+ "size": [
+ 150,
+ 62
+ ]
+ },
+ {
+ "id": "toc",
+ "type": "database",
+ "label": "Table of Contents",
+ "sublabel": "index updated",
+ "pos": [
+ 1490,
+ 460
+ ],
+ "size": [
+ 150,
+ 62
+ ]
+ }
+ ],
+ "boundaries": [
+ {
+ "kind": "region",
+ "label": "sorter code \u2014 zero third-party dependencies",
+ "wraps": [
+ "watcher",
+ "rules",
+ "writer"
+ ],
+ "pad": 26
+ }
+ ],
+ "connections": [
+ {
+ "id": "save",
+ "from": "author",
+ "to": "inbox",
+ "label": "save",
+ "labelDy": -22
+ },
+ {
+ "id": "poll",
+ "from": "inbox",
+ "to": "watcher",
+ "label": "poll",
+ "labelDy": -22
+ },
+ {
+ "id": "classify",
+ "from": "watcher",
+ "to": "rules",
+ "label": "classify",
+ "labelDx": -30
+ },
+ {
+ "id": "confident",
+ "from": "rules",
+ "to": "proposal",
+ "label": "confident",
+ "labelDx": 20
+ },
+ {
+ "id": "unsure",
+ "from": "rules",
+ "to": "jev",
+ "label": "unsure",
+ "labelAt": [
+ 700,
+ 250
+ ]
+ },
+ {
+ "id": "verdict",
+ "from": "jev",
+ "to": "proposal",
+ "label": "verdict + tags",
+ "labelDx": 20
+ },
+ {
+ "id": "review",
+ "from": "proposal",
+ "to": "obsidian",
+ "label": "review or edit folder",
+ "labelDy": -24
+ },
+ {
+ "id": "approved",
+ "from": "obsidian",
+ "to": "writer",
+ "label": "approved",
+ "labelDy": -22
+ },
+ {
+ "id": "file",
+ "from": "writer",
+ "to": "folder",
+ "label": "file",
+ "labelDx": 24
+ },
+ {
+ "id": "index",
+ "from": "writer",
+ "to": "toc",
+ "label": "index",
+ "labelDx": 24
+ }
+ ]
+}
\ No newline at end of file
diff --git a/docs/diagrams/obsidian-sorter-flow.png b/docs/diagrams/obsidian-sorter-flow.png
new file mode 100644
index 0000000..a044b34
Binary files /dev/null and b/docs/diagrams/obsidian-sorter-flow.png differ
diff --git a/docs/obsidian-sorter-flow.mmd b/docs/obsidian-sorter-flow.mmd
new file mode 100644
index 0000000..1f5a89f
--- /dev/null
+++ b/docs/obsidian-sorter-flow.mmd
@@ -0,0 +1,16 @@
+flowchart LR
+ A["Sam writes a note"] --> B["010 inbox/"]
+ B --> C{"Watcher
systemd user service
polls every 10s"}
+ C --> D["Tier 1 - rules
folder + keyword hints
user tags win"]
+ D -->|"confident"| F["Proposal"]
+ D -->|"unsure"| E["Tier 2 - TypeSafe JEV
bucket + folder + tags"]
+ E --> F
+ F --> G["Proposal written
into the note frontmatter"]
+ G --> H["Sam reviews in Obsidian"]
+ H -->|"ticks approved"| I["Writer"]
+ H -->|"edits folder first"| G
+ I --> J["Destination folder"]
+ I --> K["Table of Contents"]
+
+ click C "https://maps.lab.audasmedia.com.au/obsidian-sorter/docs/obsidian-sorter-map.html" "View Archify Map"
+ click E "https://docs.typesafe.ai" "TypeSafe JEV documentation"
diff --git a/docs/obsidian-sorter-map.html b/docs/obsidian-sorter-map.html
new file mode 100644
index 0000000..197886e
--- /dev/null
+++ b/docs/obsidian-sorter-map.html
@@ -0,0 +1,13678 @@
+
+
+
Inspecting compiled semantics
+No matching nodes
+ +Choose up to two semantic kinds. One reveals its real traffic; two compare only direct authored relationships.
+ +Choose a kind to inspect its nodes and touching relationships.
+