ADR-0018: Hierarchical filter tree for multi-level resource classes on /reference/

Status: Accepted Date: 2026-05-20

Context

The /reference/ folder page uses FilterableFolderContent (ADR-0015) with flat resource-class pills (CSG, DP, BR…). With CSG ingestion producing 100+ reference notes, class-level filtering is too coarse. Readers need to filter to a Book, a Chapter, or a specific Section.

CSG has a 4-level hierarchy: Book (folder Book01–16) → Chapter (one MD file per chapter) → Section (H1 heading) → Subsection (H2 heading, numbered 1.1.). Future resources like World Scriptures II may share this shape.

Decision

Two-tier filter UI. Class pills remain at the top level. When a hierarchical class is active (e.g. CSG selected), a collapsible tree panel appears below the pills showing only that class’s hierarchy.

Tree data model:

  • Built at Quartz build time from sources: wikilinks across all reference notes.
  • Only cited nodes appear — phantom Books/Chapters with zero references are hidden.
  • Selecting a node is inclusive downward: Book 1 → all refs citing any CSG/Book01/… wikilink; Chapter 1 → all refs citing CSG/Book01/csg-01-01-…; §1.1 → only refs with that exact subsection anchor.
  • Single-select only.

Citation depth: Subsection (H2). CSG citations anchor to the H2 subsection: [[CSG/Book01/csg-01-01-the-original-being-of-god#11-the-incorporeal-god|1.1.-the-incorporeal-god]]. The path encodes Book and Chapter; the anchor encodes Section and Subsection. The extractKey parses both.

CSG resource file frontmatter. Each CSG chapter file gains frontmatter during ingestion:

---
type: resource
class: CSG
book: 1
book-title: "True God"
chapter: 1
chapter-title: "The Original Being of God"
---

Frontmatter is a reader aid and Obsidian query target. Hierarchy structure derives from wikilink paths. Node labels (book-title, chapter-title) come from resource file frontmatter via allFiles — no extra I/O since allFiles is already loaded at build time.

Explicit opt-in in quartz.config.ts. Non-hierarchical classes (DP) keep flat pills unchanged. Each hierarchical class is declared explicitly with pathPattern and levelNames:

hierarchical: {
  CSG: {
    pathPattern: /CSG\/(Book\d+)\/(csg-\d+-\d+-)/,
    levelNames: ["Book", "Chapter"],
  },
  "Believers-Responsibility": {
    pathPattern: /Believers-Responsibility\/([\w-]+)(?:#([\w-]+))?/,
    levelNames: ["Chapter", "Section"],
  },
}

levelNames.length implicitly declares tree depth (1–3 levels). Path separators (/ vs #) are auto-inferred from the cited path — no extra config field. Label lookup order per level: ${levelName.toLowerCase()}-title frontmatter → title frontmatter → slug-to-title-case. Section-level nodes are filter-only — URL deep-links are not supported at section level because # conflicts with the browser URL fragment.

When World Scriptures II or any future hierarchical class is added, one entry is added here. This ADR is the checklist item — see also CONTEXT.md class-addition checklist.

URL deep-links. Tree state encodes as a path-prefix param: selecting Book 1 → ?sources=CSG/Book01; Chapter 1 → ?sources=CSG/Book01/csg-01-01. Filter matches any sources: wikilink that starts with the param value.

Alternatives considered

  • Unified tree for all classes: rejected — DP and BR have no sub-hierarchy; a tree for them would be a flat list with extra chrome.
  • Convention-based auto-detection (nested folder = tree): rejected — fragile; a class with incidental subfolders would grow a spurious tree. Explicit config is self-documenting.
  • Static per-Book pages (/reference/csg/book01/): rejected — multiplies page count, loses the single canonical /reference/ URL, doesn’t compose with the existing class-pill filter.
  • Multi-select tree nodes: rejected — use case doesn’t require it; adds UI complexity for no clear reader benefit.

Consequences

  • (+) Readers can drill from “all CSG refs” down to a specific subsection with no page reload
  • (+) Deep-linkable: any tree state is shareable via URL
  • (+) Generalizes cleanly to World Scriptures II and other multi-level resources via one config entry
  • (+) Non-hierarchical classes (DP, BR) are unaffected
  • (−) FilterableFolderContent requires significant new logic: tree-building pass, collapsible UI, prefix-match filter semantics
  • (−) Every new hierarchical class requires: ADR + CONTEXT.md citation grammar entry + frontmatter schema + quartz.config.ts entry