Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5.7 KiB
Filesystem routes, sections, and where chains fit
Status: proposal (researched 2026-08-24, nothing implemented).
What exists today, precisely
- A question's
iddoubles as its URL. Filenames are meaningless:questions/is loaded as a flat directory (both the Gitea loader andquestion_lint --path), every*.yamlbecomes aQuestionkeyed by its own declaredid. - Hierarchy exists only as strings:
action: /proposededges, plus a manualfollowup: trueflag 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→/proposedis a two-step flow flattened into three top-level files.) - Criteria are per-file:
qualifies: <kanidm-group>on a question,requires_groupon 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)intochain_hash, the next page isaction + ?chain=<hash>, and holding a chain is itself a capability (self_transition+ email second factor).chain.rsreserves DAG shape (multiple parents) but nothing produces it yet. Question.routeis 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.yamlnames its directory). An explicitid:still wins so both content repos keep working unchanged; lint warns when it disagrees with the path, and the field can retire later along withroute.action:accepts relative references:action: proposedresolves against the file's directory,action: /subscribedstays absolute. Resolution happens at load time, so validation and the runtime see absolute ids exactly as today. A flow directory becomes self-contained: renamedevelop/and every internal edge moves with it.followup:is inferred:index.yamlfiles are nav pages, non-index files are followups unless they saynav: true. This matches the real content exactly (uhhm's fourfollowup: truefiles 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].yamlserves/review/<key>, with the param available to the page'sResourceSpec.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: <question-ref>(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.