Merge pull request 'docs(spec): Corpus Tree — universal directory-tree left pane (Solution Design)' (#28) from docs/corpus-tree-spec into main
This commit is contained in:
@@ -0,0 +1,627 @@
|
|||||||
|
# Solution Design: Corpus Tree — universal directory-tree left pane
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| --- | --- |
|
||||||
|
| **Author(s)** | Ben Stull |
|
||||||
|
| **Reviewers / approvers** | Ben Stull |
|
||||||
|
| **Status** | `draft` |
|
||||||
|
| **Version** | v0.2.0 |
|
||||||
|
| **Source artifacts** | BDD corpus: §§1.9/4 below (this doc) · Prototype: brainstorm mockups, session 0081.0 (`.superpowers/brainstorm/`, not committed) · Reference: current `rfc-app` §7 Catalog + §22 three-tier + `DocsLayout` flyout nav · Supersedes: — |
|
||||||
|
|
||||||
|
**Change log**
|
||||||
|
|
||||||
|
| Date | Version | Change | By |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 2026-06-06 | v0.1.0 | Initial draft (discovery session ohm 0081.0) | Ben Stull |
|
||||||
|
| 2026-06-06 | v0.2.0 | Reworked to the restructured Solution Design standard (two-part front; Pain Points; Business Actors vs Product Personas; renumbered) | Ben Stull |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Business Context
|
||||||
|
|
||||||
|
*The business lens — solution-agnostic throughout. No mechanism is proposed until §2.*
|
||||||
|
|
||||||
|
### 1.1 Executive Summary
|
||||||
|
|
||||||
|
Organizations keep large, living bodies of documentation, and the value in them
|
||||||
|
depends on people being able to find what they need and trust that it is current
|
||||||
|
and collectively maintained. This design targets two outcomes: readers can locate
|
||||||
|
any document by where it sits in a documentation body's own organization, and an
|
||||||
|
organization can bring an existing body of documentation under collaborative,
|
||||||
|
reviewed governance without disrupting how that documentation is already
|
||||||
|
arranged. The benefit accrues to readers (faster, more confident access),
|
||||||
|
contributors (a reviewed way to improve the docs), and the organization (a
|
||||||
|
governed, trustworthy knowledge base).
|
||||||
|
|
||||||
|
### 1.2 Background
|
||||||
|
|
||||||
|
The platform already governs collaboratively-edited documents organized as a
|
||||||
|
single shallow list within a body. Real organizational documentation — handbooks,
|
||||||
|
runbooks, design libraries — is instead deeply structured, and most of it already
|
||||||
|
exists as finished, authoritative content rather than passing through a
|
||||||
|
proposal-to-acceptance flow. There is currently no way to bring such a body onto
|
||||||
|
the platform's governance without flattening its structure, which is why
|
||||||
|
structured documentation bodies stay off-platform today.
|
||||||
|
|
||||||
|
### 1.3 Business Actors / Roles
|
||||||
|
|
||||||
|
Real-world roles, independent of any product.
|
||||||
|
|
||||||
|
| Role | Responsible for (in the business) |
|
||||||
|
| --- | --- |
|
||||||
|
| Reader | Finds and reads documents to do their work; needs current, authoritative content |
|
||||||
|
| Contributor | Proposes new documents and changes to existing ones |
|
||||||
|
| Maintainer | Reviews proposed changes and decides what becomes authoritative |
|
||||||
|
| Documentation steward | Owns an existing body of documentation and decides to bring it under collaborative governance |
|
||||||
|
|
||||||
|
### 1.4 Problem Statement
|
||||||
|
|
||||||
|
A reader cannot navigate a documentation body by its own structure, and an
|
||||||
|
organization cannot place an existing structured body under collaborative
|
||||||
|
governance without reorganizing it. The governance model assumes every document
|
||||||
|
is one entry in a single flat list and passes through a proposal flow — neither
|
||||||
|
of which holds for an established, deeply-organized documentation body whose
|
||||||
|
documents already exist as authoritative content.
|
||||||
|
|
||||||
|
### 1.5 Pain Points
|
||||||
|
|
||||||
|
| # | Pain | Who feels it | Cost / frequency today |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| PP-1 | In a large body presented as one flat list, a reader can't tell where a document sits or browse by area | Reader | Every lookup in a sizeable body; slow, error-prone, gives up |
|
||||||
|
| PP-2 | An existing structured body can't be brought under governance without rearranging its documents into a flat scheme | Documentation steward | Blocks adoption entirely for any living body — a rearrange is a non-starter |
|
||||||
|
| PP-3 | Contributors to an existing body have no governed, reviewed way to propose changes tied to where each document lives | Contributor, Maintainer | Changes happen outside review, or not at all; no shared record of why |
|
||||||
|
| PP-4 | When the documentation is briefly unreachable, the reader is left with nothing to orient by | Reader | Intermittent; erodes trust in the body as a dependable source |
|
||||||
|
|
||||||
|
### 1.6 Targeted Business Outcomes
|
||||||
|
|
||||||
|
| Outcome | Success metric | Baseline → Target | Guardrail (must not regress) | How / when measured |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| Structured documentation bodies adopt the platform's governance | bodies of documentation hosted | 0 → ≥1 | existing bodies' usability unaffected | inspection at first onboarding |
|
||||||
|
| Bringing a body under governance is a decision, not a project | preparatory rearrangement / setup required | a flatten/rework → none | — | inspection at onboarding (PP-2) |
|
||||||
|
| Readers reliably reach documents in a large body | reader reaches the intended document | not feasible for structured bodies → routine | flat-body access unchanged | manual walkthrough (PP-1) |
|
||||||
|
|
||||||
|
> These are threshold/qualitative targets, not funnel metrics: this enables a new
|
||||||
|
> class of hosted documentation rather than tuning a conversion surface.
|
||||||
|
|
||||||
|
### 1.7 Scope (business)
|
||||||
|
|
||||||
|
- **In scope:** readers navigating a body by its own organization; contributors
|
||||||
|
proposing additions and changes anywhere in a body under review; stewards
|
||||||
|
bringing an existing body under governance without rearranging it.
|
||||||
|
- **Out of scope:** authoring tools beyond proposing/reviewing text documents;
|
||||||
|
governance policy changes (who may review/accept) — unchanged.
|
||||||
|
- **Non-goals:** becoming a general file store for non-document assets — the value
|
||||||
|
is governed *documents*, not arbitrary binaries.
|
||||||
|
|
||||||
|
### 1.8 Assumptions · Constraints · Dependencies
|
||||||
|
|
||||||
|
- **Assumptions:** bodies brought under governance are predominantly prose
|
||||||
|
documents (risk: a body that is mostly non-document assets gains little).
|
||||||
|
- **Constraints:** the existing collaborative-governance model (proposal →
|
||||||
|
review → acceptance) is reused, not redefined.
|
||||||
|
- **Dependencies:** the steward can grant the platform access to the existing
|
||||||
|
documentation body.
|
||||||
|
|
||||||
|
### 1.9 Business Use Cases
|
||||||
|
|
||||||
|
Solution-agnostic; no product, no technology.
|
||||||
|
|
||||||
|
**BUC-1 — As a reader, I can find and read a document by where it sits in the body, so that I get authoritative content without knowing an internal name.**
|
||||||
|
|
||||||
|
```gherkin
|
||||||
|
Scenario: BUC-1 — A reader finds and reads a document in a structured body
|
||||||
|
Given a body of documentation organized into sections
|
||||||
|
When a reader looks for a particular document
|
||||||
|
Then they can locate it by where it sits in that organization
|
||||||
|
And read its current content
|
||||||
|
```
|
||||||
|
- **BUC-1 acceptance criteria:** the reader reaches the intended document and
|
||||||
|
reads its current authoritative content.
|
||||||
|
|
||||||
|
**BUC-1a — As a reader, when the body is briefly unavailable, I am still oriented and never hit a dead end.**
|
||||||
|
|
||||||
|
```gherkin
|
||||||
|
Scenario: BUC-1a — The documentation is temporarily unavailable
|
||||||
|
Given the documentation cannot be reached for a moment
|
||||||
|
When a reader tries to access it
|
||||||
|
Then they are still shown what was last known to exist, or told clearly how to try again
|
||||||
|
And are never left at an empty, unexplained dead end
|
||||||
|
```
|
||||||
|
|
||||||
|
**BUC-2 — As a contributor, I can propose a change to a document, so that improvements are reviewed before they become authoritative.**
|
||||||
|
|
||||||
|
```gherkin
|
||||||
|
Scenario: BUC-2 — A contributor proposes a change to a document
|
||||||
|
Given a contributor authorized to change the documentation
|
||||||
|
When they propose a change to an existing document
|
||||||
|
Then the change enters review before it can become authoritative
|
||||||
|
And the document is shown as having a change under review
|
||||||
|
```
|
||||||
|
- **BUC-2 acceptance criteria:** a reviewable proposal exists against that
|
||||||
|
document; until accepted, the authoritative content is unchanged.
|
||||||
|
|
||||||
|
**BUC-2a — As a contributor, when my proposal can't be accepted as placed, I'm told why and nothing is half-done.**
|
||||||
|
|
||||||
|
```gherkin
|
||||||
|
Scenario: BUC-2a — A proposed change cannot be accepted as placed
|
||||||
|
Given a contributor proposing a document
|
||||||
|
When the chosen placement conflicts with an existing document, or is not allowed
|
||||||
|
Then the proposal is refused with a clear, specific reason
|
||||||
|
And nothing is partially recorded
|
||||||
|
```
|
||||||
|
|
||||||
|
**BUC-3 — As a contributor, I can introduce a new document anywhere in the body, so that the body grows where the content belongs.**
|
||||||
|
|
||||||
|
```gherkin
|
||||||
|
Scenario: BUC-3 — A contributor introduces a new document anywhere in the body
|
||||||
|
Given a contributor authorized to add documentation
|
||||||
|
When they propose a new document at a place within the body
|
||||||
|
Then it enters review as a draft in that place
|
||||||
|
And on acceptance it becomes the authoritative document there
|
||||||
|
```
|
||||||
|
- **BUC-3 acceptance criteria:** a draft appears at the chosen place and is
|
||||||
|
reviewable; on acceptance it is the authoritative document there.
|
||||||
|
|
||||||
|
**BUC-4 — As a documentation steward, I can bring an existing body under collaborative governance, so that it gains review and shared maintenance without disruption.**
|
||||||
|
|
||||||
|
```gherkin
|
||||||
|
Scenario: BUC-4 — A steward brings an existing doc body under governance
|
||||||
|
Given an organization with an existing body of documentation
|
||||||
|
When that documentation is brought under collaborative governance
|
||||||
|
Then all of its existing documents are immediately readable as authoritative
|
||||||
|
And none of them had to be rearranged to make that possible
|
||||||
|
```
|
||||||
|
- **BUC-4 acceptance criteria:** every existing document is readable and treated
|
||||||
|
as authoritative, with no rearrangement and no preparatory data entry.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Solution Proposal
|
||||||
|
|
||||||
|
Build software: make the platform's left-pane navigation a **universal
|
||||||
|
directory-tree** that mirrors a content repository's own git directory structure,
|
||||||
|
and address every document by its **path** rather than a flat slug — so a
|
||||||
|
documentation body is navigated by its real structure while keeping the existing
|
||||||
|
propose → review → accept governance on every file. The flat list becomes the
|
||||||
|
degenerate "one folder" case of the tree; a deep body is the general case. A new
|
||||||
|
`CorpusTree` left-pane component replaces the flat §7 Catalog as the *universal*
|
||||||
|
idiom (one navigation model for every project type), with **two display modes** —
|
||||||
|
*structure* (the directory tree) and *flat* (today's ranked list, engaged on
|
||||||
|
search or a non-path sort) — so the four affordances the flat list provides
|
||||||
|
(search, lifecycle state, sort, the contribution surface) all survive.
|
||||||
|
|
||||||
|
An existing body is onboarded as an ordinary registry project pointed at its
|
||||||
|
repository, with **no content migration and no state backfill**: a document
|
||||||
|
present with no lifecycle record is treated as authoritative ("no record =
|
||||||
|
active").
|
||||||
|
|
||||||
|
**Why this approach, over the alternatives:**
|
||||||
|
- *Do nothing / keep flat + rearrange repos* — rejected: PP-2 makes a rearrange a
|
||||||
|
non-starter for a living body.
|
||||||
|
- *A read-only documentation viewer* — rejected: delivers PP-1 but not PP-3
|
||||||
|
(no governed contribution).
|
||||||
|
- *A second, separate tree view beside the flat catalog* — rejected: forks the
|
||||||
|
navigation idiom and doubles the surface; the tree generalizes the flat list
|
||||||
|
rather than sitting beside it.
|
||||||
|
|
||||||
|
**Solution-specific scope / non-goals:**
|
||||||
|
- *In:* path-addressed documents; a directory-tree read surface; the dual-mode
|
||||||
|
pane; lifecycle by path; propose/edit/review at any path; zero-migration
|
||||||
|
onboarding; legacy slug→path URL compatibility.
|
||||||
|
- *Out:* lazy per-folder tree loading (a scale follow-on; v1 serves the full
|
||||||
|
document tree from cache); a rich renderer for non-document files (listed but
|
||||||
|
inert); changes to graduation / integer-ID assignment.
|
||||||
|
- *Non-goals:* merging folders with collections (kept distinct — INV-3); a new
|
||||||
|
project *type* (existing types render via the tree — D5).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Product Personas
|
||||||
|
|
||||||
|
| Product persona | In rfc-app | Maps to business role(s) |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Reader | Browses a project's corpus and opens documents | Reader |
|
||||||
|
| Contributor | Holds collection `can_contribute`; proposes/edits docs at any path | Contributor, Maintainer |
|
||||||
|
| Deployment operator | Registers a content repo as a project and flips the cutover flag | Documentation steward |
|
||||||
|
|
||||||
|
## 4. Product Use Cases
|
||||||
|
|
||||||
|
UX-level; steps are about the Product Personas (§3). Each links to the Business UC
|
||||||
|
it realizes.
|
||||||
|
|
||||||
|
```gherkin
|
||||||
|
Scenario: PUC-1 — Browse the directory tree in structure mode (realizes BUC-1)
|
||||||
|
Given the left pane is in structure mode
|
||||||
|
When the reader opens the project
|
||||||
|
Then folders and files are shown in path order
|
||||||
|
And each markdown file shows its lifecycle-state badge
|
||||||
|
And folders expand and collapse, with expansion persisted across visits
|
||||||
|
```
|
||||||
|
|
||||||
|
```gherkin
|
||||||
|
Scenario: PUC-2 — Search or non-path sort flips to flat mode (realizes BUC-1)
|
||||||
|
Given the pane is in structure mode
|
||||||
|
When the reader types a search query or picks a non-path sort
|
||||||
|
Then folders are hidden and a ranked flat list of matches is shown
|
||||||
|
And clearing the search with a path sort returns the tree with folders restored
|
||||||
|
```
|
||||||
|
|
||||||
|
```gherkin
|
||||||
|
Scenario: PUC-3 — Open a document by its path (realizes BUC-1)
|
||||||
|
When the reader selects a file in the tree
|
||||||
|
Then the main column renders that document
|
||||||
|
And the tree highlights the corresponding node
|
||||||
|
And the document's lifecycle state is visible
|
||||||
|
```
|
||||||
|
|
||||||
|
```gherkin
|
||||||
|
Scenario: PUC-4 — Propose a new doc at a path (realizes BUC-3)
|
||||||
|
Given a contributor with contribute capability
|
||||||
|
When they choose "Propose new doc" and give a path like deploy/runbooks/rollback.md
|
||||||
|
Then intermediate folders are created in the proposal branch
|
||||||
|
And a PR is opened via the bot
|
||||||
|
And a super-draft entry appears at that path
|
||||||
|
```
|
||||||
|
|
||||||
|
```gherkin
|
||||||
|
Scenario: PUC-5 — Propose an edit to an existing doc (realizes BUC-2)
|
||||||
|
Given a contributor viewing an existing document
|
||||||
|
When they propose an edit
|
||||||
|
Then an edit PR is opened against that document's path
|
||||||
|
And the file's node shows the in-review badge with the open PR reference
|
||||||
|
```
|
||||||
|
|
||||||
|
```gherkin
|
||||||
|
Scenario: PUC-6 — Filter and see lifecycle state (realizes BUC-1, BUC-2)
|
||||||
|
Given files exist in active, in-review, and super-draft states
|
||||||
|
When the reader enables the "in review" state filter
|
||||||
|
Then only files with an open edit PR are listed
|
||||||
|
```
|
||||||
|
|
||||||
|
```gherkin
|
||||||
|
Scenario: PUC-7 — Star a doc; starred pins in flat mode (product-only)
|
||||||
|
Given the reader has starred a file
|
||||||
|
When the pane is in flat mode
|
||||||
|
Then the starred file is pinned above the ranked results
|
||||||
|
```
|
||||||
|
|
||||||
|
```gherkin
|
||||||
|
Scenario: PUC-8 — Legacy slug URL redirects to its path (product-only, compat)
|
||||||
|
When the reader navigates to a legacy /p/:pid/c/:cid/e/:slug URL
|
||||||
|
Then they are redirected to the path-addressed URL for that entry
|
||||||
|
And the document renders
|
||||||
|
```
|
||||||
|
|
||||||
|
```gherkin
|
||||||
|
Scenario: PUC-9 — Operator onboards an existing repo (realizes BUC-4)
|
||||||
|
Given an existing doc repo and bot read+write access
|
||||||
|
When the operator registers it as a project's content repo and flips the cutover flag
|
||||||
|
Then every pre-existing file renders as active in the tree
|
||||||
|
And no content was moved and no state was backfilled
|
||||||
|
```
|
||||||
|
|
||||||
|
```gherkin
|
||||||
|
Scenario: PUC-10 — Tree fetch fails (realizes BUC-1a)
|
||||||
|
Given a previously cached tree exists
|
||||||
|
When the tree-listing fetch to gitea fails
|
||||||
|
Then the last good tree is served from cache
|
||||||
|
And when no cache exists, a graceful empty state with a retry action is shown
|
||||||
|
```
|
||||||
|
|
||||||
|
```gherkin
|
||||||
|
Scenario: PUC-11 — Propose at an invalid path (realizes BUC-2a)
|
||||||
|
When a contributor proposes a doc at an existing or out-of-repo path
|
||||||
|
Then the proposal is rejected with a clear validation error
|
||||||
|
And no branch or PR is left behind
|
||||||
|
```
|
||||||
|
|
||||||
|
```gherkin
|
||||||
|
Scenario: PUC-12 — Non-markdown files are listed but inert (product detail)
|
||||||
|
Given the repo contains schemas/app.json
|
||||||
|
When the tree is listed
|
||||||
|
Then schemas/app.json appears in the tree
|
||||||
|
But it has no lifecycle badge and opens no entry view
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. UX Layout
|
||||||
|
|
||||||
|
### 5.1 Left pane — `CorpusTree` (serves PUC-1..PUC-7, PUC-12)
|
||||||
|
|
||||||
|
- **Purpose:** navigate a project's corpus and reach any document; surface
|
||||||
|
lifecycle state and the contribution affordances.
|
||||||
|
- **Layout (top → bottom):**
|
||||||
|
- **Toolbar:** search input; lifecycle state filter-chips; sort selector
|
||||||
|
(path · recent · title · id · state); `+ Propose new doc` action.
|
||||||
|
- **Body — structure mode (default):** indented directory tree; folders with
|
||||||
|
expand/collapse in path order; file rows show a state dot
|
||||||
|
(active / in-review / super-draft) and a star marker; the active file is
|
||||||
|
highlighted. Non-markdown files render dimmed and inert.
|
||||||
|
- **Body — flat mode (search / non-path sort):** folders hidden; a ranked flat
|
||||||
|
list of file rows (path as secondary text); starred pinned to top; pending
|
||||||
|
super-drafts grouped.
|
||||||
|
- **States:** happy: tree/list rendered · empty: "No documents yet" + propose CTA
|
||||||
|
if permitted · loading: skeleton rows · error: stale tree if cached, else an
|
||||||
|
empty state with Retry · permission: propose control hidden without
|
||||||
|
`can_contribute`.
|
||||||
|
- **Notifications:** inline validation error on invalid propose (PUC-11); existing
|
||||||
|
PR/discussion notifications unchanged.
|
||||||
|
|
||||||
|
### 5.2 Main column (serves PUC-3, PUC-5)
|
||||||
|
|
||||||
|
Unchanged — rendered markdown + discussion + PR/contribution affordances —
|
||||||
|
addressed by path instead of slug.
|
||||||
|
|
||||||
|
## 6. Technical Design
|
||||||
|
|
||||||
|
### 6.1 Invariants
|
||||||
|
|
||||||
|
- **INV-1:** An entry is addressed by its repo `path` within a
|
||||||
|
`(project, collection)`. Slug addressing exists only as a legacy redirect.
|
||||||
|
- **INV-2:** A markdown file present on `main` with no explicit lifecycle record
|
||||||
|
is `active`. Onboarding requires no state backfill.
|
||||||
|
- **INV-3:** Folders are organizational only. Access control is the collection's;
|
||||||
|
a folder never carries permissions.
|
||||||
|
- **INV-4:** Lifecycle records are keyed by `(collection, path)` and follow file
|
||||||
|
renames on merge — no orphaned records survive a `git mv`.
|
||||||
|
- **INV-5:** The framework names no deployment. The backing repo is supplied via
|
||||||
|
the registry (`CLAUDE.md` separation-of-concerns).
|
||||||
|
- **INV-6:** Non-markdown files are listed but carry no lifecycle and open no
|
||||||
|
entry view.
|
||||||
|
- **INV-7:** The tree pane never renders blank — stale cache or an explicit
|
||||||
|
empty/retry state on fetch failure.
|
||||||
|
|
||||||
|
### 6.2 High-level architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
CT[CorpusTree pane] -->|GET tree / entry by path| API[API service]
|
||||||
|
CT -->|propose / edit at path| API
|
||||||
|
API --> CACHE[(TTL cache)]
|
||||||
|
CACHE --> GT[Gitea git-trees / raw]
|
||||||
|
API -->|branch / PR writes| BOT[Bot account]
|
||||||
|
BOT --> GT
|
||||||
|
API --> DB[(App DB: lifecycle, stars)]
|
||||||
|
REG[rfc-registry] -->|project to content-repo| API
|
||||||
|
WH[Gitea webhooks] -->|invalidate / reconcile| CACHE
|
||||||
|
```
|
||||||
|
|
||||||
|
- **CorpusTree** — owns left-pane rendering and mode state; owns no source of
|
||||||
|
truth; must never assume a flat namespace.
|
||||||
|
- **API service** — owns the tree-listing and path-addressed entry/contribution
|
||||||
|
endpoints; derives lifecycle state; must never write content except via the bot.
|
||||||
|
- **App DB** — owns lifecycle records, stars, reviewed-marks keyed by
|
||||||
|
`(collection, path)`; system of record for collaboration state.
|
||||||
|
- **Gitea** — system of record for document content and PRs.
|
||||||
|
- **Cache** — holds the materialized tree + per-path metadata; invalidated by the
|
||||||
|
webhook/PR-merge signals used today (§4.1 reconciler pattern).
|
||||||
|
|
||||||
|
### 6.3 Data model & ownership
|
||||||
|
|
||||||
|
| Entity | Owned by | Key fields | System of record |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Document (entry) | Gitea | `(project, collection, path)`, body | Gitea repo `main` |
|
||||||
|
| Lifecycle record | App DB | `(collection, path)`, state, reviewed-by | App DB |
|
||||||
|
| Star | App DB | `(user, collection, path)` | App DB |
|
||||||
|
| Open-PR mapping | Gitea | `path` → PR number | Gitea |
|
||||||
|
| Tree node (materialized) | Cache | `path`, type, `last_commit_at`, state, `open_pr`, `starred_by_me` | derived (cache) |
|
||||||
|
|
||||||
|
### 6.4 Interfaces & contracts
|
||||||
|
|
||||||
|
- **`GET /api/projects/{pid}/collections/{cid}/tree`** — in: pid, cid, viewer ·
|
||||||
|
out: nodes `{path, type, last_commit_at, lifecycle_state, open_pr,
|
||||||
|
starred_by_me}` (full markdown tree) · errors: `404` unknown project/collection;
|
||||||
|
gitea failure → stale cache or `503` with a retriable marker (INV-7).
|
||||||
|
- **Path-addressed entry read** — generalize `…/rfcs/{slug}` to a path
|
||||||
|
(`…/entries/{path}`); out: existing entry payload; errors: `404`.
|
||||||
|
- **Propose-at-path / edit / discussion** — generalize existing contribution/PR/
|
||||||
|
discussion endpoints from `slug` to `path`; propose-new in: target path, body;
|
||||||
|
out: PR ref; errors: `409` path exists, `422` invalid/out-of-repo path
|
||||||
|
(PUC-11), `403` no `can_contribute`.
|
||||||
|
|
||||||
|
### 6.5 Per–Product-Use-Case design
|
||||||
|
|
||||||
|
#### PUC-1 — Browse the tree (realizes BUC-1; honors INV-2, INV-7)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor R as Reader
|
||||||
|
participant CT as CorpusTree
|
||||||
|
participant A as API
|
||||||
|
participant C as Cache
|
||||||
|
participant G as Gitea
|
||||||
|
R->>CT: open project
|
||||||
|
CT->>A: GET …/tree
|
||||||
|
A->>C: get materialized tree
|
||||||
|
alt cache hit
|
||||||
|
C-->>A: tree
|
||||||
|
else miss
|
||||||
|
A->>G: git trees + last-commit
|
||||||
|
G-->>A: entries
|
||||||
|
A->>A: derive state (no record = active, INV-2)
|
||||||
|
A->>C: store
|
||||||
|
end
|
||||||
|
A-->>CT: nodes
|
||||||
|
CT-->>R: structure-mode tree (or stale/empty per INV-7)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Implementation:** materialize the markdown tree once per cache cycle; join
|
||||||
|
per-path lifecycle/star records; "no record = active." Folder expansion is
|
||||||
|
client state (localStorage keyed by project/collection).
|
||||||
|
|
||||||
|
#### PUC-4 — Propose a new doc at a path (realizes BUC-3; honors INV-1, INV-3)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor Cn as Contributor
|
||||||
|
participant CT as CorpusTree
|
||||||
|
participant A as API
|
||||||
|
participant B as Bot
|
||||||
|
participant G as Gitea
|
||||||
|
Cn->>CT: + Propose new doc (path)
|
||||||
|
CT->>A: propose-at-path(path, body)
|
||||||
|
A->>A: validate path (unique, in-repo) else 409/422
|
||||||
|
A->>B: create branch + file (mkdir -p path)
|
||||||
|
B->>G: commit + open PR
|
||||||
|
G-->>A: PR ref
|
||||||
|
A-->>CT: super-draft at path
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Implementation:** reuse the existing bot/branch/PR flow; only the target path
|
||||||
|
and intermediate-folder creation change. PUC-5 (edit), PUC-6 (filter), PUC-8
|
||||||
|
(redirect) reuse existing flows with `slug`→`path` substitution and need no new
|
||||||
|
sequence.
|
||||||
|
|
||||||
|
### 6.6 Non-functional requirements & cross-cutting concerns
|
||||||
|
|
||||||
|
- **Security & privacy:** read follows existing project/collection visibility;
|
||||||
|
propose gated by `can_contribute` (INV-3); all writes via the bot; secrets stay
|
||||||
|
references, never bytes (§6.3-handbook). No new PII.
|
||||||
|
- **Performance & scale:** v1 returns the full markdown tree from the TTL cache;
|
||||||
|
cold build is one git-trees call + a last-commit join. Lazy per-folder loading
|
||||||
|
is the scale follow-on if a body's tree is large enough to hurt cold-build
|
||||||
|
latency.
|
||||||
|
- **Availability & resilience:** stale-cache fallback + empty/retry state
|
||||||
|
(INV-7); no new hard dependency beyond what the corpus already needs.
|
||||||
|
- **Observability:** log tree cold-builds, cache hit/miss, gitea-failure
|
||||||
|
fallbacks; reuse existing health signals.
|
||||||
|
- **Accessibility:** the tree uses ARIA `tree`/`treeitem` roles with keyboard
|
||||||
|
expand/collapse and roving focus; state conveyed by text/label, not color alone.
|
||||||
|
|
||||||
|
### 6.7 Key decisions & alternatives considered
|
||||||
|
|
||||||
|
| Decision | Chosen | Alternatives considered | Why chosen |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| D1 Interaction model | Full governance lifecycle on any file | read-only viewer; read + light comments | operator intent; reuses existing machinery |
|
||||||
|
| D2 Topology | Tree becomes universal | tree as a second view; tree only for a new type | one navigation model; no idiom fork |
|
||||||
|
| D3 Entry identity | Path-addressed | keep slug + side table of paths | path is the natural key for a real body |
|
||||||
|
| D4 Folders vs collections | Distinct | merge folders into collections | collections are access-control, folders are structure |
|
||||||
|
| D5 Project type | No new type | a `tree`/`docs` type | "universal" means all types render via tree |
|
||||||
|
| D6 Build approach | New `CorpusTree`, port flat mode, retire Catalog | evolve Catalog in place; promote DocsLayout flyout | clean dual-mode boundary; safe staged cutover |
|
||||||
|
| D7 Existing files | "No record = active" | backfill a record per file at onboard | zero-migration onboarding |
|
||||||
|
| D8 Affordances | Keep all four via dual mode | drop sort/pending on a tree | operator marked all non-negotiable |
|
||||||
|
| D9 Routing | `e/*` splat + `?pr=` + legacy shim | encode path in slug; new `/f/` segment | least collision; reuses redirect pattern |
|
||||||
|
|
||||||
|
### 6.8 Testing strategy
|
||||||
|
|
||||||
|
Tests are part of each slice (not a follow-up).
|
||||||
|
|
||||||
|
- **Unit (vitest):** `CorpusTree` mode-switching (search→flat, sort→flat,
|
||||||
|
clear→structure), badge rendering, expand/collapse persistence, starred-pin,
|
||||||
|
flat-mode parity with the retired Catalog.
|
||||||
|
- **Backend (DI at boundaries):** tree endpoint state derivation (INV-2), open-PR
|
||||||
|
detection, path-addressed read/contribution, slug→path migration, rename
|
||||||
|
reconciliation (INV-4).
|
||||||
|
- **Two-tier:** Tier-1 local-Docker gitea for branch/PR flows; Tier-2 PPE for the
|
||||||
|
onboard-a-real-repo path (SLICE-4). e2e: browse → open; propose-at-path → PR →
|
||||||
|
merge → state transitions; search flatten; legacy redirect.
|
||||||
|
|
||||||
|
### 6.9 Failure modes, rollback & flags
|
||||||
|
|
||||||
|
- **Failure:** gitea unavailable → **behavior:** stale cache or empty/retry
|
||||||
|
(INV-7) → **rollback:** none needed.
|
||||||
|
- **Failure:** a project regresses on the new pane → **behavior/rollback:** flip
|
||||||
|
the **per-project-type cutover flag** off; the Catalog renders again (not
|
||||||
|
deleted until parity is proven).
|
||||||
|
- **Feature flag:** per-project-type cutover flag, default **off**; deployments
|
||||||
|
enable per project after validating parity.
|
||||||
|
|
||||||
|
## 7. Delivery Plan
|
||||||
|
|
||||||
|
### 7.1 Approach / strategy
|
||||||
|
|
||||||
|
Riskiest-foundation-first: land the path/state model and tree read behind the
|
||||||
|
existing flat UI (no user-visible change), then the dual-mode UI behind a flag,
|
||||||
|
then contribution-at-path, then onboard a real body. Per the handbook execution
|
||||||
|
convention, **each slice is its own coding session** — plan just-in-time →
|
||||||
|
execute → verify → ship → merge + version bump — in dependency order; this design
|
||||||
|
pass happens once and is amended only if a slice proves it wrong. The flag keeps
|
||||||
|
every intermediate state shippable.
|
||||||
|
|
||||||
|
### 7.2 Slicing plan
|
||||||
|
|
||||||
|
#### SLICE-1 — Backend foundation → completes (no user-visible PUC)
|
||||||
|
- **Depends on:** —
|
||||||
|
- **DoD:** path-keyed lifecycle + slug→path migration + "no record = active"
|
||||||
|
(INV-2) + tree endpoint + path-addressed reads; Catalog still renders; backend
|
||||||
|
tests green.
|
||||||
|
|
||||||
|
#### SLICE-2 — `CorpusTree` frontend → completes PUC-1, PUC-2, PUC-3, PUC-6, PUC-7, PUC-8, PUC-10, PUC-12
|
||||||
|
- **Depends on:** SLICE-1
|
||||||
|
- **DoD:** dual-mode pane behind the cutover flag; path routing + legacy shim;
|
||||||
|
flat-mode parity; Catalog retired once a project is flipped; unit + e2e green.
|
||||||
|
|
||||||
|
#### SLICE-3 — Contribution-at-path → completes PUC-4, PUC-5, PUC-11
|
||||||
|
- **Depends on:** SLICE-1
|
||||||
|
- **DoD:** propose/edit/discussion at arbitrary paths; rename reconciliation
|
||||||
|
(INV-4); validation (BUC-2a); tests green.
|
||||||
|
|
||||||
|
#### SLICE-4 — Onboard an existing doc body → completes PUC-9 (BUC-4)
|
||||||
|
- **Depends on:** SLICE-2, SLICE-3
|
||||||
|
- **DoD:** a real external repo registered + bot access + flag flipped; all
|
||||||
|
pre-existing files render active with zero migration; Tier-2 PPE validation.
|
||||||
|
|
||||||
|
### 7.3 Rollout / launch plan
|
||||||
|
|
||||||
|
Pre-v1, single-prod: each slice ships to prod on merge with its CHANGELOG upgrade
|
||||||
|
steps; the per-project-type flag defaults off, so deployments opt in per project.
|
||||||
|
Rollback trigger: flag off → Catalog returns (§6.9).
|
||||||
|
|
||||||
|
### 7.4 Risks & mitigations
|
||||||
|
|
||||||
|
| Risk | Likelihood / impact | Mitigation |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Full-tree fetch slow on a large body | M / M | TTL cache; lazy per-folder loading as a defined follow-on |
|
||||||
|
| Catalog flat-mode parity gaps after cutover | M / H | port logic verbatim; flag-gated per project; keep Catalog until parity proven |
|
||||||
|
| Rename leaves orphan state records | M / M | webhook reconciliation (INV-4) + test |
|
||||||
|
| slug→path migration mis-maps non-`document` entry folders | L / M | resolve entry folder per project type, not hardcoded; migration test |
|
||||||
|
|
||||||
|
## 8. Traceability matrix
|
||||||
|
|
||||||
|
| Pain | Business UC | Product UC | Slice | Tests |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| PP-1 | BUC-1 | PUC-1, PUC-2, PUC-3, PUC-6, PUC-7 | SLICE-2 | `test_tree_*`, `corpustree.*.test` |
|
||||||
|
| PP-4 | BUC-1a | PUC-10 | SLICE-2 | `test_tree_fallback_*` |
|
||||||
|
| PP-3 | BUC-2 | PUC-5, PUC-6 | SLICE-3 | `test_edit_at_path_*` |
|
||||||
|
| PP-3 | BUC-2a | PUC-11 | SLICE-3 | `test_propose_validation_*` |
|
||||||
|
| PP-3 | BUC-3 | PUC-4 | SLICE-3 | `test_propose_at_path_*` |
|
||||||
|
| PP-2 | BUC-4 | PUC-9 | SLICE-4 | e2e `onboard_repo_*` |
|
||||||
|
| PP-1 | (compat) | PUC-8 | SLICE-2 | `test_legacy_redirect_*` |
|
||||||
|
| PP-1 | (detail) | PUC-12 | SLICE-2 | `test_tree_non_markdown_*` |
|
||||||
|
|
||||||
|
## 9. Open Questions & Decisions log
|
||||||
|
|
||||||
|
**Open**
|
||||||
|
|
||||||
|
| # | Question | Owner | Blocks |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| — | none outstanding | — | — |
|
||||||
|
|
||||||
|
**Resolved**
|
||||||
|
|
||||||
|
| # | Decision | Resolution | Date |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| D1 | Interaction model | Full governance lifecycle on any file | 2026-06-06 |
|
||||||
|
| D2 | Topology | Tree becomes the universal left pane | 2026-06-06 |
|
||||||
|
| D3 | Entry identity | Path-addressed; slug is legacy-redirect-only | 2026-06-06 |
|
||||||
|
| D4 | Folders vs collections | Kept distinct | 2026-06-06 |
|
||||||
|
| D5 | Project type | No new type; existing types render via tree | 2026-06-06 |
|
||||||
|
| D6 | Build approach | New `CorpusTree`; port flat mode; retire Catalog | 2026-06-06 |
|
||||||
|
| D7 | Pre-existing files | "No record = active"; zero-migration onboarding | 2026-06-06 |
|
||||||
|
| D8 | Affordances | Keep all four via dual display mode | 2026-06-06 |
|
||||||
|
| D9 | Routing | `e/*` splat + `?pr=` + legacy shim | 2026-06-06 |
|
||||||
|
|
||||||
|
Deferred (not open): lazy per-folder tree loading; raw/preview view for
|
||||||
|
non-markdown files.
|
||||||
|
|
||||||
|
## 10. Glossary & References
|
||||||
|
|
||||||
|
- **Structure mode** — left pane showing the directory tree in path order.
|
||||||
|
- **Flat mode** — left pane showing a ranked flat list (search/non-path sort);
|
||||||
|
the retired Catalog's behavior.
|
||||||
|
- **Entry** — a markdown document, addressed by its repo path within a
|
||||||
|
`(project, collection)`.
|
||||||
|
- **Lifecycle record** — app-DB row keyed by `(collection, path)` carrying
|
||||||
|
non-default state; absence means `active`.
|
||||||
|
- **References:** `SPEC.md` §7 (catalog), §22 (three-tier), §8 (revision), §13
|
||||||
|
(graduation), §20 (versioning); `CLAUDE.md` (separation-of-concerns);
|
||||||
|
handbook `solution-design/GUIDE.md` (this doc's standard).
|
||||||
Reference in New Issue
Block a user