# HANDOFF — Obsidian Sorter

> Written 2026-09-24. Pick this up cold; everything you need is here.

## What this is

An automated system that files notes into Sam's Obsidian vault.

Notes go into `010 inbox/`. A **watcher** proposes a destination by writing it
*into the note's frontmatter*. Sam ticks a checkbox in Obsidian to approve.
The watcher then files it and updates the Table of Contents.

**Nothing is ever filed without approval** — deliberately, so Sam can edit a note
before it moves.

## Where things live

| Thing | Location |
|-------|----------|
| Vault (live) | `.27` → `~/obsidian` — git-synced to Gitea `sam/obsidian-vault` |
| Sorter code (dev) | `.13` → `~/paseo/projects/obsidian-sorter/` |
| Sorter code (deployed) | `.27` → `~/obsidian-sorter/` |
| Watcher service | `.27` → `systemctl --user obsidian-sorter` (enabled + lingering) |
| Command | `.27` → `~/bin/obsidian-sort` |
| Sort log | `.27` → `~/obsidian/010 inbox/_sort log.md` |
| Service log | `.27` → `~/.local/state/obsidian-sorter.log` |
| Design | `~/paseo/projects/obsidian-cleanup-tool/SORTER-PLAN.md` |

## THE TAXONOMY — final, do not "improve" it

**Type mirrors folder.** Nothing is inferred from content.

| Folder | Type |
|--------|------|
| `200 projects/` | `project` |
| `300 areas/` | `area` |
| `400 Personal Family/` | `family` |
| `100 Table of Contents/` | `index` |
| `000 daily/` | `daily` |
| `500 archive/` | `archive` |
| `Clippings/` | `clipping` |
| `010 inbox/` | `note` (unsorted placeholder only) |

**RETIRED — never reintroduce:** `subject`, `domain`, `tool`, `service`,
`container`, `machine`, `storage`, `backup-system`, `network-component`,
`website`, `person`.

Sam's lifecycle model:
```
IDEA (not started)   -> 200 projects/260 Ideas to Build Create Make
BEING BUILT          -> 200 projects/250 Electronics
FINISHED             -> 300 areas/...
```

## The workflow

1. Note lands in `010 inbox/` with `type: note`
2. Watcher (polls every 10s) classifies it and **writes the proposal into the note**:
   ```yaml
   proposed_folder: 200 projects/250 Electronics
   proposed_toc: 100 Table of Contents/Projects.md → Ideas
   proposed_tags: [solar, 12v, bird, bath]
   confidence: 0.69
   approved: false        # a checkbox in Obsidian
   ```
3. Sam ticks `approved` (or edits the proposal first)
4. Watcher files it: move, set type from folder, merge tags, **strip admin
   fields**, update the TOC

## Implementation notes

- **Zero dependencies.** Custom YAML parser (`sorter/frontmatter.py`) — no PyYAML,
  because NixOS has no pip. HTTP via stdlib `urllib`.
- **JEV** (TypeSafe, `POST api.typesafe.ai/v1/systemone`) does semantic
  classification — ~$0.0001/note. Key read from `~/.config/environment.d/*.conf`
  (not in the env for non-login SSH shells).
- **Classifies by FOLDER, not type.** JEV picks the folder; the type follows.
- **Sam's own tags win.** A note tagged `project`/`family` is constrained to that
  bucket — JEV cannot override it (`explicit_bucket_from_tags`).
- **Folder meanings are taught explicitly** in `FOLDER_NOTES` (tier2.py), e.g.
  *"260 Ideas = UNSTARTED; 250 Electronics = ACTIVELY BEING BUILT"*.
  **This is the place to fix any future misfiling.**
- JEV is **probabilistic** (~74% folder accuracy). Tests assert the *bucket*
  strictly, the sub-folder only with `--strict-folders`.
- Files starting with `_` are ignored by the inbox scanner, so `_sort log.md`
  and `_review` files never get sorted.

## Safety guarantees (15/15 tests)

Dry run by default · `Untitled*` never moved · never overwrites an existing
destination · frontmatter merged (user `tags`/`aliases`/`id`/`created` survive) ·
body never rewritten · atomic writes · `.trash/` untouched · idempotent TOC.

## Commands

```bash
# on .27
obsidian-sort              # dry run — show what would happen
obsidian-sort apply        # file the notes (asks to confirm)
obsidian-sort scan         # vault summary
systemctl --user status obsidian-sorter
journalctl --user -u obsidian-sorter -f

# tests (on .13, against the mirror)
cd ~/paseo/projects/obsidian-sorter
python3 tests/run_tests.py
python3 tests/run_tests.py --strict-folders    # may flake (JEV variance)

# refresh the local mirror from the live vault
rsync -a --delete --exclude='.git' --exclude='.trash' --exclude='.venv' \
  --exclude='graphify-out*' --exclude='.obsidian' \
  sam@192.168.20.27:~/obsidian/ ./test-vault/

# deploy code to .27
rsync -a --delete --exclude='test-vault' --exclude='__pycache__' \
  --exclude='.git' --exclude='*.pyc' ./ sam@192.168.20.27:~/obsidian-sorter/
```

## Tag vocabulary

Cleaned 2026-09-24: 214 → 195 unique tags; lowercased everything, merged
`tools`→`tool`, split one malformed comma-joined tag into 9, fixed typos,
removed all retired-type tags. Tool: `tools/normalize_tags.py`.

## Remaining work

- **Phase D** — MCP tool so Pi agents can call the sorter
- **Phase E** — Pi skill ("sort my inbox") + distribute via Gitea to .13/.27/.51
- Add a watcher test to the suite (currently only verified manually)
- Sam is still testing with real notes — **no Pi integration until he's satisfied**
