ADR-0048: Document Organization — Folder Tree per Scope¶
- Status: accepted
- Date: 2026-07-05
- Deciders: Nicolas Burri
- Refines: ADR-0027 (folder rows are content-plane), ADR-0004 (permission set grows by ORGANIZE_DOCUMENTS); relates to the DECIDED checkpoint document-tree-design.md (T-1a..T-4a)
- Refined by: ADR-0124 (§3's unconditional ORGANIZE_DOCUMENTS gate on folder creation is re-drawn for empty scopes: while a scope holds zero documents, creating the folder skeleton is a setting-up act gated by ACTIVATE_CATALOG_ENTRIES; the first record flips the tree to live and ORGANIZE_DOCUMENTS governs from then on, checked in the write transaction on the V106 pattern)
Context¶
Documents live flat within a scope; users expect a chapter/folder tree (Confluence habit; the default in comparable QMS tools). Nicolas (2026-07-05) wants it before the colleague demo. The non-negotiable: structure must never disturb controlled records.
Decision¶
foldertable, per scope, content-plane RLS (V031):id, scope_id, parent_id (nullable = root child), name, sort_order, created_at; unique(scope_id, parent_id, name);FORCE RLSwith the standard scope predicate. Root is implicit (no row). Max depth 8; parent must be in the same scope; move operations re-validate the ancestor chain (cycle-proof).- Placement per DOCUMENT: nullable
document.folder_id(same migration); all versions share it; new versions/revisions inherit; derive lands at the target scope's root (folders are tenant-local; no cross-scope path mapping). A move is organizational metadata — it never creates a version, never re-freezes content (REQ-DOC-017 untouched). - Permission: new seeded
ORGANIZE_DOCUMENTSgates folder create/rename/reorder/delete and document moves;VIEWsuffices to read the tree. Structure governance is thereby grantable separately from authoring. - Audited, not lifecycle:
FOLDER_CREATED/RENAMED/DELETED,DOCUMENT_MOVEDaudit events (scope-bound, actor-attributed). Folders are deletable only when EMPTY (no documents, no children) — no tree operation cascades onto records. - REST:
GET /api/scopes/{scopeId}/folders(tree with per-folder document counts),POST /api/scopes/{scopeId}/folders(create, parentId+name),PATCH /api/folders/{id}(rename/re-sort/re-parent),DELETE /api/folders/{id}(empty only),PUT /api/documents/{documentRowId}/folder(move,{folderId|null}). Document list/detail DTOs gainfolderIdand a resolvedfolderPath(detail + search hits). - UI: tree panel in the documents view (selected context; "All" groups per scope), folder click filters the list, breadcrumb on the detail page, "Move…" dialog on the document, folder preselect in the create flow, folder-path line on search hits. No drag-and-drop in v1.
Consequences¶
- Separation suite gains the folder case (tree invisible across scopes); folder queries ride the existing RLS session mechanics unchanged.
- The closed permission catalog grows (seeded in V031); role templates in the demo seed grant ORGANIZE_DOCUMENTS to the QM role.
- Demo seed gains an ISO-13485-shaped chapter tree in QMS-OPS (⅘/6/⅞) with the seeded SOPs filed accordingly.
- Requirements: new REQ-DOC rows (tree, audited moves, empty-delete, RLS-scoped folders) enter the v1.6 pending section.
- Deferred by decision: drag-and-drop, recursive delete/re-homing, cross-scope folder operations, label-based virtual views.
Amendment 1 (2026-08-15) — manual order within a folder, and the mode that arms moving (register #78/#100)¶
Two of this ADR's original deferrals come due together, shaped by his rulings:
Drag-and-drop is no longer deferred — it is MODED. The #76 episode (a stale page made always-armed dnd indistinguishable from a broken feature) and his Confluence pain ("you hit the mouse and accidentally move a file and have no clue what happened") decided register
78: browsing arms nothing — no draggable rows, no per-document menus; moving lives in an¶
explicit ARRANGE MODE entered from the new tree-root menu, visibly stateful, disarmed on exit, with every refusal named (cross-scope, own-subtree) and the drop highlight consulting the same validity the drop enforces.
Manual order within a folder (register #100, migration V101). A nullable
folder_position on the document's placement: null everywhere = today's sort untouched; a
manual arrangement assigns 0..n-1 through one complete-list act (PUT
…/folders/{id|root}/document-order, ORGANIZE_DOCUMENTS-gated, one DOCUMENTS_REORDERED
audit event per act — never per row). The reader rule that makes the semantics fall out:
positioned documents first ascending, then null-positioned in the ordinary sort — so an
untouched folder keeps its sort and new arrivals land at the end. Moving into an arranged
folder appends (max+1); moving out clears. Order is scope truth for every reader, like the
structure itself. No backfill, deliberately: freezing an incidental sort into stored truth
would claim an arrangement nobody made.