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— schemaNOT NULL. - The inbox category (
system=1) is undeletable via API/UI. settings.jd_inbox_category_idalways points at a live system category. If pruned, suchi repairs the pointer at boot and logsjd.inbox.repaired.- Category codes are unique. Ranges nest exactly: category
22must live inside an area20..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=22and?q=taxboth hit22 Tax),?area=<code>(scope to one area’s 10-slot range). Each row carriesarea_code+area_namedenormalized, so a picker can render the grouped tree in a single request. Seeapi#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.