From b737e0a7e7ff801792b8e1d378aa4a8c17d869a6 Mon Sep 17 00:00:00 2001 From: Bendik Aagaard Lynghaug Date: Mon, 24 Aug 2026 21:58:40 +0200 Subject: [PATCH] Design doc: filesystem routes, sections, dynamic segments Co-Authored-By: Claude Fable 5 --- docs/design/filesystem-routes.md | 127 +++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 docs/design/filesystem-routes.md diff --git a/docs/design/filesystem-routes.md b/docs/design/filesystem-routes.md new file mode 100644 index 0000000..93bdfb1 --- /dev/null +++ b/docs/design/filesystem-routes.md @@ -0,0 +1,127 @@ +# Filesystem routes, sections, and where chains fit + +Status: proposal (researched 2026-08-24, nothing implemented). + +## What exists today, precisely + +- A question's `id` doubles as its URL. Filenames are meaningless: + `questions/` is loaded as a **flat** directory (both the Gitea + loader and `question_lint --path`), every `*.yaml` becomes a + `Question` keyed by its own declared `id`. +- Hierarchy exists only as strings: `action: /proposed` edges, plus a + manual `followup: true` flag that hides post-submission pages from + the nav. The graph is invisible in a repo listing — you open every + file to learn the flow. (uhhm's actual graph: `/` fans out to three + followups; `/develop` → `/develop-proposal` → `/proposed` is a + two-step flow flattened into three top-level files.) +- Criteria are per-file: `qualifies: ` on a question, + `requires_group` on a resource. Gating a whole area means + repeating the flag in every file of that area. +- Chains are query-string lineage: submitting hashes + `(question, parents, responses, ts)` into `chain_hash`, the next + page is `action + ?chain=`, and holding a chain is itself a + capability (`self_transition` + email second factor). `chain.rs` + reserves DAG shape (multiple parents) but nothing produces it yet. +- `Question.route` is a declared-but-never-read field — a fossil of + this exact idea. + +## The proposal, in three phases + +### Phase 1 — the tree is the router + +Directory structure becomes the URL structure, next-js style: + +``` +questions/ + index.yaml → / + applied.yaml → /applied + develop/ + index.yaml → /develop + proposal.yaml → /develop/proposal + proposed.yaml → /develop/proposed + review/ + index.yaml → /review +``` + +- `id:` becomes optional and **derived from the path** (`index.yaml` + names its directory). An explicit `id:` still wins so both content + repos keep working unchanged; lint warns when it disagrees with the + path, and the field can retire later along with `route`. +- `action:` accepts **relative references**: `action: proposed` + resolves against the file's directory, `action: /subscribed` stays + absolute. Resolution happens at load time, so validation and the + runtime see absolute ids exactly as today. A flow directory becomes + self-contained: rename `develop/` and every internal edge moves + with it. +- **`followup:` is inferred**: `index.yaml` files are nav pages, + non-index files are followups unless they say `nav: true`. This + matches the real content exactly (uhhm's four `followup: true` + files are precisely its non-index leaf pages) and deletes a flag + people must remember. +- Loader: switch from the per-directory contents API to Gitea's git + trees API (`/git/trees/{branch}?recursive=true`) — one request for + the whole tree instead of one per directory, which the flat loader + should be using anyway. + +### Phase 2 — sections: criteria scoped by URL prefix + +A `_section.yaml` in any directory applies to everything beneath it +(underscore = not a page, like next's private folders): + +``` +questions/review/_section.yaml: + qualifies: portal_owners + responsible: { name: Bendik, contact: … } +``` + +This is the URL/criteria interplay actually worth having: **a URL +prefix becomes a trust boundary**. "Everything under /review is +owner-only" is one line in one place, instead of a flag per file that +drifts. Per-question `qualifies` still overrides (tighter or looser — +lint should warn on looser). Sections are also the natural home for +shared `responsible` contacts and, later, per-section branding +accents. + +### Phase 3 — dynamic segments, and chains stay out of the path + +Two criteria axes exist: **who you are** (Kanidm group) and **what +you've done** (holding a chain). Phase 2 scopes the first by prefix; +phase 3 does the same for the second, plus gives records addresses: + +- `[record].yaml` — a dynamic segment, one YAML file rendered per + record: `questions/review/[record].yaml` serves `/review/`, + with the param available to the page's `ResourceSpec.key`. Today a + single record is only reachable through a desk row or a + `?chain=` link; this gives every record a real, gated URL — + linkable from review desks, automations, and n8n notifications. +- `requires_chain: ` (page- or section-level): the page + only renders for a visitor whose chain tip answers the referenced + question. Today's followup pages are soft-hidden (out of nav) but + fully reachable; this makes "you must have come from X" an actual + criterion, declared with a relative ref like actions are. + +**Deliberately not proposed: encoding the chain in the path.** The +page tree is static structure; the chain is runtime lineage and a +bearer capability. Putting it in the path makes it look canonical and +shareable — exactly what a capability URL shouldn't invite — and a +DAG (the reserved multi-parent shape) doesn't linearize into a path +anyway. `?chain=` stays a query parameter: pages keep one canonical +URL, lineage rides along only when it's actually held. + +## Migration + +Phase 1 is fully backward compatible (explicit `id` wins, flat repos +are just trees of depth one). The content repos migrate by `git mv`: +uhhm's develop flow nests under `develop/`, its followups either stay +top-level (`/applied` keeps its URL) or move with their flows if URL +churn is acceptable; redoal's three files are already the tree. Lint +learns the same resolution rules in the same commit, so a bad +reference stays a caught push, never a 404. + +## Order of value + +Phase 1 is cheap and pays immediately (the repo listing becomes the +sitemap). Phase 2 is small and unlocks gated areas properly. Phase 3 +is the real feature work — `[record]` pages change what desks and +automations can link to — and can wait until something concrete needs +it.