Skip to main content
suchi ships with Johnny.Decimal as the default filing system — a schema-level taxonomy the app validates, routes on, browses by number, and (in the enterprise track) hangs retention policies off.

The opinion

JD is on by default. First boot loads a generic starter tree with zero configuration. Every document has a JD category from the moment it’s ingested. URLs, search, and default storage paths are JD-shaped out of the box. Flat mode exists as an explicit opt-out (taxonomy: flat in settings). Crucially, flat mode is a degenerate JD tree, not a second code path: the internal representation is always jd_areas + jd_categories, flat mode just hides the tree in the UI and collapses everything to a single inbox category. One code path, one opinion, a respectful escape hatch. You can always come back to change this. Swap presets after ingesting docs by adding "refile": true to the preset-apply call — existing docs park on the new inbox and a refile sweep re-runs rules and re-renders the symlinks under the new tree. Without refile, the swap is refused (with a 409) as a safety.

The starter tree

Auto-loaded on first boot. Delete and replace freely — suchi jd import (arrives post-Phase-1) will pipe a fresh YAML in.
the default

Invariants

  • Every document has a jd_category_id — schema NOT NULL.
  • The inbox category (system=1) is undeletable via API/UI.
  • settings.jd_inbox_category_id always points at a live system category. If pruned, suchi repairs the pointer at boot and logs jd.inbox.repaired.
  • Category codes are unique. Ranges nest exactly: category 22 must live inside an area 20..29.

Over the JSON API

Every non-browser consumer (mobile app, MCP tool, agent, SPA) that needs to render a filing chip or drive a picker uses two surfaces:
  • GET /api/jd/categories/ — enumerate the tree. Query params: ?q=<prefix> (case-insensitive code-or-name match, ?q=22 and ?q=tax both hit 22 Tax), ?area=<code> (scope to one area’s 10-slot range). Each row carries area_code + area_name denormalized, so a picker can render the grouped tree in a single request. See api#get-apijdcategories.
  • GET /api/documents/{id} — the JD fields on the response are denormalized: jd_category_id, jd_category_code, jd_category_name, jd_area_name. Enough to render “22 Tax · Money” without a second call. Set the category by PATCHing back with {"jd_category_id": <n>}.
jd_area_code isn’t included on either surface — the category’s own code encodes it (22 → 20–29), and duplicating invites divergence.

What the inbox is for

Ingest paths never silently mis-file. When a classifier (or the importer, without a matching --map-jd rule) can’t confidently pick a category, the document lands in inbox with a needs-review tag. It’s browsable, searchable, and bulk-reassignable. Nothing is ever “unclassified” in a way that hides it.