Skip to content

Commit

The plan for Artifacts: Docs mode rebuilt as one mode for Docs, Slides, Design and Dashboards, each private, shared with people and agents, in a space or workspace-wide, edited live together

syntaqxcommitted Parentfbf8e2fBrowse files
1 file+798−00/1 viewed
+798−0
1+# Artifacts mode: plan
2+
3+Status: plan, 2026-10-09. This replaces Docs mode (docs/WORKSPACE.md, "Docs") with **Artifacts**: one mode for four kinds of collaborative work: **Docs, Slides, Design and Dashboards**. Each has private, shared, space and workspace access, and each is edited live by people and agents together.
4+
5+This document is about the product mode. `docs/ARTIFACTS.md` is something else: the research on the Cloudflare Artifacts git store. The two must not share code names (see section 0).
6+
7+---
8+
9+## 0. Naming: keep "Artifacts" the mode apart from Artifacts the git store
10+
11+The word "artifact" already has three meanings in this repository:
12+
13+| Meaning | Where |
14+|---|---|
15+| The Cloudflare Artifacts git store | `services/repos` binding `ARTIFACTS`, `ArtifactsStore`, `ARTIFACTS_NAMESPACES`, docs/ARTIFACTS.md |
16+| Workflow run artifacts | `apps/api/src/artifacts.rs` (`ArtifactsOp`), `crates/contracts` `actions::Artifact`, `apps/web/app/lib/artifacts.ts` (`formatBytes`, `expiresIn`), the `workflow` MCP tool's `*_artifact` actions, REST `/repos/:o/:n/actions/artifacts` |
17+| **The new mode** | this plan |
18+
19+**Decision: user-facing words say "artifact", and code says "folio".**
20+
21+- **Users see "artifact".** That covers UI text, `apps/docs`, URLs (`/<ws>/-/artifacts/...`), the MCP tool name (`artifact`), REST paths (`/workspaces/{ws}/artifacts`), scope names (`artifacts:read`, `artifacts:write`, `artifacts:admin`) and the agents' tool names (`search_artifacts`, ...).
22+- **Code identifiers say "folio".** A folio is one artifact of any kind:
23+- tables `folios`, `folio_grants`, ...
24+- types `Folio`, `FolioKind`, `FolioRole`
25+- the Durable Object `FolioRoom`
26+- events `folio.created`, ...
27+- files `packages/contracts/src/folios.ts`, `crates/contracts/src/folios.rs`, `apps/api/src/folios.rs` (`FoliosOp`, next to the untouched `artifacts.rs`), `apps/web/app/components/folios/*`, `apps/web/app/routes/workspace/folios/*`, `apps/web/app/lib/folios.ts`
28+- "folio" is not used anywhere in the repository today, so grep stays clean.
29+- **The service and its infrastructure keep their names:** `services/docs`, Worker `g1t-docs-service`, D1 `g1t-docs`, R2 `g1t-docs-files`, binding `DOCS`. Renaming them would force new resources for every installation, including self-hosters, and buys nothing. The service header comment and docs/SELF_HOSTING.md say "the docs service hosts folios (Artifacts mode)". The semantic index is new (`g1t-folios`, section 5) because its metadata changes.
30+- The kind of the old Docs pages is `doc` (`FolioKind = "doc" | "slides" | "design" | "dashboard"`). In UI text the tile is "Docs" and a single item is "a doc".
31+- **Rule for reviewers:** a new identifier containing `artifact` outside the git-store and workflow code is a bug.
32+
33+---
34+
35+## 1. What exists today (the base we build on)
36+
37+- **`services/docs`** (TypeScript Worker):
38+- D1 tables: `spaces` (kind workspace/team/private, default_role, agent_mode), `space_members`, `pages` (tree in `parent_id` + `position`, `markdown` rendition), `page_versions` (Yjs state + Markdown), `suggestions`, `templates`, `files`, `pages_fts`, `citations`, `page_changes`, `repo_spaces`, `repo_files`, `doc_chunks(+_fts)`, `doc_embed_usage`, `doc_index_runs`. Migrations are 0001 to 0003.
39+- `src/room.ts`: `PageRoom` is a Durable Object that owns one Yjs document. It speaks y-protocols sync and awareness over hibernating WebSockets, enforces the socket's role, saves through `persist.ts` four seconds after edits and indexes through `indexer.ts` thirty seconds after.
40+- `src/access.ts`: pure role rules (`RANK`, `roleOf`, `readableByAll`, `readableByWorkspace`, `agentAbilities`).
41+- Recall: `recallForAgent` uses Vectorize `g1t-docs` with a `space_id` `$in` filter, then re-checks against D1. Hybrid search uses RRF.
42+- `src/index.ts`: about 2,750 lines with a `/rpc/<method>` switch.
43+- **Web:**
44+- Routes: `apps/web/app/routes/workspace/docs/*`, registered in `apps/web/app/routes.ts` under `-/docs`.
45+- Components: `apps/web/app/components/docs/*` (BlockNote `editor.tsx`, custom `blocks.tsx`, the generic Yjs socket client `provider.ts`, `sidebar.tsx` with a drag tree, `page-parts.tsx` with history and suggestions).
46+- The mode is wired through `ModeKey "docs"` in `apps/web/app/lib/workspace-nav.ts`, the rail in `components/rail.tsx`, `sidebarFor` and `MODE_MENU` in `components/shell.tsx`, and the phone tab bar in `components/mobile.tsx`.
47+- **Agents:**
48+- `services/agents/src/tools.ts` has `DOCS_TOOLS` and `DOCS_WRITE_TOOLS` (`search_docs`, `read_page`, `edit_page`, `create_page`, `stale_pages`, `list_doc_spaces`).
49+- `services/agents/src/ports.ts` holds `docsPorts`.
50+- "Write this up" is in `components/chat/write-up.tsx` and `lib/write-up.ts`.
51+- **External MCP:** `apps/api/src/tools.rs` defines a few resource tools with an `action` field, and the scopes are in `crates/contracts/src/scopes.rs`. There is **no docs tool and no docs scope today**, and apps/api has no `DOCS` binding.
52+- **Events:** `doc.page.*` (`packages/contracts/src/events.ts`, `crates/contracts/src/events.rs` `DOC_PAGE_EVENTS`, `subscribers.rs`).
53+- **Libraries already present:** BlockNote 0.55, yjs, y-protocols, y-prosemirror, lib0, mermaid, katex, shiki, radix-ui, cmdk, lucide.
54+- There is **no chart library**. Charts are hand-rolled SVG (`components/usage.tsx` `UsageChart`, `Sparkline`, `AllowanceRing`; `routes/repo/contributors.tsx`).
55+- Hover hints are `components/ui/hint.tsx`, on the shadcn Tooltip.
56+- Relative time is `TimeAgo` in `components/ui/index.tsx`, and the viewer's zone is `lib/time-zone.ts`.
57+- The people picker is `components/people-picker.tsx`. Tabs are `ui/tab-strip.tsx` and `ui/tabs.tsx`.
58+
59+---
60+
61+## 2. Data model
62+
63+### 2.1 Concepts
64+
65+- **Folio** (shown as an artifact): one item with a `kind`. Every folio has:
66+- one **owner**, always a person. When an agent makes one, the owner is the person it acts for.
67+- a **location**: a **space**, or none, which means it is in the owner's **Private** section.
68+- an optional **parent**. Only a `doc` can have children, so a doc is the page that holds sub-pages. Slides, Design and Dashboards are leaves. There is **no folder kind in v1**, because a doc with children is the folder.
69+- **Space** (a teamspace) keeps the existing `spaces` table and roles:
70+- `workspace`: shown as "Open". Every member gets `default_role`. Members **join** it to see it in their sidebar.
71+- `team`: the team's members get `default_role`.
72+- `private`: shown as "**Members only**", so it can't be confused with the Private section. Only listed members.
73+- **Roles** stay as they are: `view < comment < edit < manage` (`RANK` in access.ts). A folio's owner always has `manage`.
74+- **A person's effective role on a folio** is the highest of:
75+1. owner → `manage`;
76+2. explicit **grants** on the folio or on an ancestor it inherits from, given to a `user:`, `agent:` or `team:` key;
77+3. **space access**, when the folio inherits (`inherit = 1`) up to a folio in a space: the person's space role (`roleOf`);
78+4. **general access** of the folio's access root:
79+ - `workspace`: every member gets `general_role`;
80+ - `link`: a member who has opened the link gets `general_role`.
81+
82+Workspace owners get `manage` on folios in open and team spaces, as today. They get nothing on someone's Private folio or on a Members-only space.
83+- **Restricting.** A doc's child, or a folio in a space, can set `inherit = 0` ("Only people invited"). It then becomes its own **access root**: space access and the parent's grants stop at it.
84+- **"Private" (the lock icon)** is computed, not stored. It means the effective readers are only the owner: no grants, `general_access = 'none'`, and either no space or `inherit = 0`.
85+- **Agents.** An agent's role on a folio is never higher than its asker's. Its grants (`agent:<id>`) make it a participant for notifications and @-mentions. They never widen what it can read for someone who can't read the folio (section 4.3).
86+- **Link access.** "Anyone in the workspace with the link" is not discoverable: it is not in lists or search, and not in recall for a workspace audience. It becomes readable for a person once they open the link, which records a row in `folio_visits`. A **public link** (people outside the workspace) is deferred to Phase 8 and off by default per workspace.
87+- **Favorites, recent, trash, versions and templates** are per folio and work for every kind.
88+
89+### 2.2 D1 schema: `services/docs/migrations/0004_folios.sql`
90+
91+This migration only adds tables. Old tables stay until Phase 7.
92+
93+```sql
94+CREATE TABLE folios (
95+id TEXT PRIMARY KEY, -- fol_…
96+workspace_id TEXT NOT NULL,
97+kind TEXT NOT NULL CHECK (kind IN ('doc','slides','design','dashboard')),
98+title TEXT NOT NULL DEFAULT '',
99+icon TEXT, cover TEXT,
100+owner TEXT NOT NULL, -- user:<id>
101+space_id TEXT REFERENCES spaces (id), -- NULL: owner's Private
102+parent_id TEXT REFERENCES folios (id), -- only a doc may be a parent
103+position REAL NOT NULL,
104+inherit INTEGER NOT NULL DEFAULT 1, -- from parent, or from the space at the top
105+acl_root TEXT NOT NULL, -- nearest self-or-ancestor with inherit=0 or no parent (denormalized)
106+path TEXT NOT NULL, -- '/<root id>/…/<id>/' for subtree updates
107+general_access TEXT NOT NULL DEFAULT 'none' CHECK (general_access IN ('none','workspace','link')),
108+general_role TEXT CHECK (general_role IN ('view','comment','edit')),
109+agent_mode TEXT CHECK (agent_mode IN ('suggest','edit')), -- NULL: the space's, or 'suggest' in Private
110+text TEXT NOT NULL DEFAULT '', -- derived rendition (§3): search, recall, read view, export
111+excerpt TEXT NOT NULL DEFAULT '',
112+preview TEXT, -- small JSON for cards (§6.2), never data values
113+source TEXT, -- e.g. the chat thread link it was written up from
114+mentioned TEXT NOT NULL DEFAULT '[]',
115+created_by TEXT NOT NULL, -- user:/agent:
116+created_at TEXT NOT NULL,
117+updated_by TEXT, updated_at TEXT NOT NULL, -- any change (rename, move, share)
118+edited_by TEXT, edited_at TEXT NOT NULL, -- content changes: "Edited 45m ago"
119+trashed_at TEXT, trashed_by TEXT
120+);
121+CREATE INDEX folios_tree ON folios (workspace_id, space_id, parent_id, position);
122+CREATE INDEX folios_owner ON folios (owner, edited_at) WHERE trashed_at IS NULL;
123+CREATE INDEX folios_recent ON folios (workspace_id, edited_at) WHERE trashed_at IS NULL;
124+CREATE INDEX folios_root ON folios (acl_root);
125+CREATE INDEX folios_path ON folios (path);
126+
127+CREATE TABLE folio_grants ( -- explicit shares, as set
128+folio_id TEXT NOT NULL REFERENCES folios (id) ON DELETE CASCADE,
129+principal TEXT NOT NULL, -- user:/agent:/team:
130+role TEXT NOT NULL CHECK (role IN ('view','comment','edit','manage')),
131+granted_by TEXT NOT NULL, granted_at TEXT NOT NULL,
132+PRIMARY KEY (folio_id, principal)
133+);
134+-- Effective explicit access, materialized per folio for list/search SQL:
135+-- the owner and every grant from the folio up to its acl_root, highest role.
136+-- Rebuilt for a subtree (by `path`) on grant, move, restrict, ownership change.
137+CREATE TABLE folio_access (
138+folio_id TEXT NOT NULL REFERENCES folios (id) ON DELETE CASCADE,
139+principal TEXT NOT NULL,
140+role TEXT NOT NULL,
141+via TEXT NOT NULL, -- the folio whose grant this is (self or ancestor), or 'owner'
142+since TEXT NOT NULL, -- for "Shared with you" ordering
143+PRIMARY KEY (folio_id, principal)
144+);
145+CREATE INDEX folio_access_principal ON folio_access (principal, since);
146+
147+CREATE TABLE folio_visits ( -- recent, and link access once opened
148+folio_id TEXT NOT NULL REFERENCES folios (id) ON DELETE CASCADE,
149+user_id TEXT NOT NULL,
150+first_at TEXT NOT NULL, last_at TEXT NOT NULL,
151+PRIMARY KEY (folio_id, user_id)
152+);
153+CREATE INDEX folio_visits_user ON folio_visits (user_id, last_at);
154+
155+CREATE TABLE folio_favorites (user_id TEXT NOT NULL, folio_id TEXT NOT NULL REFERENCES folios (id) ON DELETE CASCADE,
156+position REAL NOT NULL, created_at TEXT NOT NULL, PRIMARY KEY (user_id, folio_id));
157+
158+CREATE TABLE space_joins ( -- open spaces a member shows in their sidebar
159+space_id TEXT NOT NULL REFERENCES spaces (id) ON DELETE CASCADE, user_id TEXT NOT NULL,
160+position REAL NOT NULL, joined_at TEXT NOT NULL, PRIMARY KEY (space_id, user_id));
161+
162+CREATE TABLE folio_versions (
163+id TEXT PRIMARY KEY, folio_id TEXT NOT NULL REFERENCES folios (id) ON DELETE CASCADE,
164+created_at TEXT NOT NULL,
165+kind TEXT NOT NULL CHECK (kind IN ('created','edit','agent','suggestion','proposal','restore')),
166+authors TEXT NOT NULL DEFAULT '[]', note TEXT,
167+text TEXT NOT NULL,
168+state BLOB, -- Yjs state when ≤ 1.5 MB
169+state_key TEXT -- else in the file store (design decks with many nodes)
170+);
171+CREATE INDEX folio_versions_folio ON folio_versions (folio_id, created_at);
172+
173+-- Doc kind: inline tracked changes, as `suggestions` today.
174+CREATE TABLE folio_suggestions ( …same columns as suggestions, page_id → folio_id, marks_current… );
175+-- Other kinds: an agent's whole change as a Yjs update a person previews and applies.
176+CREATE TABLE folio_proposals (
177+id TEXT PRIMARY KEY, folio_id TEXT NOT NULL REFERENCES folios (id) ON DELETE CASCADE,
178+author TEXT NOT NULL, asked_by TEXT, note TEXT,
179+base_vector BLOB NOT NULL, update_blob BLOB, update_key TEXT,
180+summary TEXT NOT NULL, -- "Adds slides 4–6; rewrites the title slide"
181+status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open','accepted','rejected','stale')),
182+created_at TEXT NOT NULL, decided_by TEXT, decided_at TEXT);
183+
184+CREATE TABLE folio_templates (
185+id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL,
186+kind TEXT NOT NULL CHECK (kind IN ('doc','slides','design','dashboard')),
187+name TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', icon TEXT,
188+body TEXT NOT NULL, -- Markdown (doc, slides) or JSON spec (design, dashboard)
189+created_by TEXT NOT NULL, created_at TEXT NOT NULL);
190+CREATE INDEX folio_templates_ws ON folio_templates (workspace_id, kind);
191+
192+CREATE TABLE folio_files ( …as files, page_id → folio_id… );
193+CREATE TABLE folio_links (from_folio TEXT NOT NULL REFERENCES folios (id) ON DELETE CASCADE, to_folio TEXT NOT NULL, PRIMARY KEY (from_folio, to_folio));
194+CREATE INDEX folio_links_to ON folio_links (to_folio);
195+CREATE TABLE folio_projects (folio_id …, repo TEXT, PRIMARY KEY (folio_id, repo));
196+CREATE TABLE folio_citations ( …as citations, page_id → folio_id… );
197+CREATE TABLE folio_changes ( …as page_changes, page_id → folio_id… );
198+
199+CREATE VIRTUAL TABLE folios_fts USING fts5 (folio_id UNINDEXED, kind UNINDEXED, title, body, tokenize = 'unicode61 remove_diacritics 2');
200+
201+-- The index generalizes: doc_chunks keeps its shape for repo files; folio passages get their own table.
202+CREATE TABLE folio_chunks (
203+id TEXT PRIMARY KEY, -- <folio id>:<seq>
204+workspace_id TEXT NOT NULL, folio_id TEXT NOT NULL, kind TEXT NOT NULL,
205+scope TEXT NOT NULL, -- 'space:<id>' when the folio's access is its space's; else 'folio:<acl_root>'
206+seq INTEGER NOT NULL, heading TEXT, text TEXT NOT NULL, hash TEXT NOT NULL, vector_hash TEXT, updated_at TEXT NOT NULL);
207+CREATE VIRTUAL TABLE folio_chunks_fts USING fts5 (chunk_id UNINDEXED, scope UNINDEXED, folio_id UNINDEXED, heading, text, tokenize = 'unicode61 remove_diacritics 2');
208+```
209+
210+`repo_spaces`, `repo_files`, `repo_files_fts`, the `doc_chunks` rows for repository files, `doc_embed_usage` and `doc_index_runs` are kept: projects' docs stay a read-only source in Artifacts. In Phase 7, migration `0005_drop_pages.sql` drops `pages`, `page_*`, `favorites`, `suggestions`, `templates`, `files`, `pages_fts`, `citations`, `page_changes` and the page rows of `doc_chunks`. It runs only after no deployed code reads them.
211+
212+### 2.3 Access code
213+
214+- **`services/docs/src/access.ts` (pure, tested)** gains:
215+- `effectiveRole(folio, chain, grantsByFolio, spaceRole, person)`
216+- `isPrivate(...)`
217+- `aclRootOf(...)`
218+- `materialize(subtree) → folio_access rows`
219+- `readableByAllFolio(...)` for agent audiences
220+- `generalRoleCap` (general access never gives `manage`)
221+- `canShare(role)`: `manage`, or `edit` when the space allows editors to share. The default is `manage` only.
222+- **Listing SQL ("All").** A folio is listed when any of these holds:
223+1. a `folio_access` row matches one of the viewer's keys (`user:<id>` and `team:<slug>…`);
224+2. its `space_id` is one of the viewer's readable spaces, `inherit = 1` and `acl_root` is the top of the space;
225+3. its access root has `general_access = 'workspace'`;
226+4. its root has `general_access = 'link'` and the viewer has a visit row.
227+
228+Results are paged by `(edited_at, id)`. Each page of rows is re-checked with `effectiveRole` before it is returned (defence in depth).
229+- **Re-materializing.** Subtree changes are bounded at 2,000 folios inline; bigger ones go to a `folios.reacl` queue job on `JOBS`. Open rooms in the subtree get `setRole` so their sockets follow the change (as today).
230+
231+### 2.4 Contracts
232+
233+**`packages/contracts/src/folios.ts`** (new). Wire shapes are snake_case:
234+
235+- `FolioKind`, `FOLIO_KINDS`, `FOLIO_KIND_LABELS` (`Docs`, `Slides`, `Design`, `Dashboard`), `FolioRole` (an alias of `DocRole`, kept so access code is shared), `FolioGeneralAccess`.
236+- `FolioRef { id, kind, title, icon, slug, path }`, where `path` is `/<ws>/-/artifacts/<title-slug>-<id>`.
237+- `Folio`: `FolioRef` plus `space`, `parent_id`, `owner: MemberProfile`, `created_by`, `edited_by`, `edited_at`, `trashed_at`, `viewer_role`, `favorite`, `private`, `shared_count`, `general_access`, `general_role`, `inherited_from: { kind: "space" | "folio"; id; name } | null`, `excerpt`, `preview`, `stale`, `has_children`.
238+- `FolioListQuery { tab: "all" | "yours" | "shared"; kinds?; space_id?; owner?; project?; q?; cursor?; limit? }` and `FolioList { items; next_cursor }`.
239+- `FoliosSidebar { favorites; spaces: (DocSpace & { tree })[]; private_tree; shared; repos }`.
240+- `NewFolio { kind; title?; space_id?; parent_id?; template_id?; content?: FolioContentInput; share_with?: { principal; role }[] }`.
241+- `FolioAccessList` (for the share dialog: rows with their source), `FolioAccessChange`.
242+- `FolioVersion`, `FolioProposal`, `FolioTemplate`.
243+- `FoliosLiveEvent`, generalizing `DocsLiveEvent`.
244+- `FolioPassage { folio: FolioRef | null; repo_file; space_name; heading; text; score; updated_at; stale }`.
245+- `FolioAgentEdit`, discriminated by kind:
246+- `doc`: `{ target: DocEditTarget; markdown }`
247+- `slides`: `{ ops: SlidesOp[] }`
248+- `design`: `{ ops: DesignOp[] }`
249+- `dashboard`: `{ ops: DashboardOp[] }`
250+- `foliosClient(binding)` with the RPC names in section 7.
251+
252+Each kind's own types live in its own file so parallel work doesn't collide: `folios-slides.ts`, `folios-design.ts`, `folios-dashboard.ts`.
253+
254+**`packages/contracts/src/datasets.ts`** (new, mirrored in **`crates/contracts/src/datasets.rs`**) holds the dashboard query layer (section 3.4).
255+
256+**`crates/contracts/src/folios.rs`** holds the subset the Rust API needs (`Folio`, `NewFolio`, `FolioAgentEdit` as `serde_json::Value`, the args structs) and `FOLIO_EVENTS`.
257+
258+**Events** (`events.ts`, `events.rs`, `subscribers.rs`): `folio.created`, `folio.updated` (a version: kind and authors), `folio.trashed`, `folio.restored`, `folio.shared` (who was granted, never content), `folio.stale`. They carry `{ workspaceId, folioId, kind, spaceId | null }` and no title for private folios. They never carry a `repoId`. `doc.page.*` keeps being published by the old code paths until Phase 7.
259+
260+---
261+
262+## 3. Each kind: content model and editor
263+
264+**One room for every kind.** `src/room.ts` becomes `FolioRoom`, which keeps the same socket protocol, hibernation, roles, save alarm and index alarm, with a **kind adapter**:
265+
266+```ts
267+// services/docs/src/kinds/types.ts
268+export interface KindModel {
269+kind: FolioKind;
270+seed(doc: Y.Doc, init: { text?: string; spec?: unknown }): void; // template / agent / blank
271+render(doc: Y.Doc): { text: string; outline: unknown; mentions: string[]; links: string[]; citations: DocCitation[]; preview: unknown };
272+chunks(text: string, title: string): Passage[]; // §5
273+applyAgentEdit(doc: Y.Doc, edit: unknown, origin: Origin): { applied: boolean; summary: string };
274+restore(doc: Y.Doc, old: Y.Doc, origin: Origin): void;
275+validate(update: Y.Doc): string | null; // caps: node counts, sizes
276+}
277+// services/docs/src/kinds/index.ts: { doc, slides, design, dashboard } registry (one line per kind)
278+```
279+
280+`FolioRoom` stores `kind` in `meta` and dispatches to the adapter. `persist.ts` becomes `saveFolio(env, { folio_id, text, preview, ... })`. Wrangler gets a DO migration `{ "tag": "v2", "new_sqlite_classes": ["FolioRoom"] }`. `PageRoom` stays exported until Phase 7 (`deleted_classes`).
281+
282+The web uses one socket client, `components/docs/provider.ts` moved to `components/folios/provider.ts` (`FolioProvider`), at `/<ws>/-/artifacts/live?folio=<id>`. Each kind's editor is registered in `components/folios/kinds.tsx` with `{ icon, label, Editor, Viewer, Thumbnail, mobileEditable }`.
283+
284+### 3.1 Docs (`kind: "doc"`)
285+
286+- **Content:** unchanged. BlockNote on `XmlFragment("document-store")`, comments in the `threads` map. `blocks.ts`, `markdown.ts`, `edits.ts`, `threads.ts` and `citations.ts` move under `src/kinds/doc/` without behaviour changes.
287+- **Editor:** `components/docs/editor.tsx`, `blocks.tsx` and `page-parts.tsx` move to `components/folios/doc/`. Children, backlinks, citations, staleness, suggestions and history all stay.
288+- **New:** sub-page blocks can be any kind (an embedded card linking to a child slides deck or dashboard). This adds a `folio` embed block, generalizing the page card.
289+
290+### 3.2 Slides (`kind: "slides"`)
291+
292+**Yjs model.**
293+- `Y.Map("deck")`: `{ theme, aspect: "16:9" | "4:3", accent }`.
294+- `Y.Array("slides")` of `Y.Map`: `{ id, layout, background, hidden, transition: "none" }`.
295+- Layouts: `title`, `title-body`, `two-column`, `section`, `image`, `quote`, `big-number`, `blank`.
296+- Each slide's regions are root `XmlFragment`s named `slide:<id>:<region>` (`title`, `body`, `left`, `right`, `notes`).
297+- So every region is a **BlockNote editor bound to its own fragment**: BlockNote's collaboration option takes a `fragment`. Mentions, code, math and mermaid come for free.
298+- Each region gets a narrower schema. For example, a title region allows a single heading and inline content only.
299+
300+**Markdown form,** used by agents, templates, export and the rendition:
301+
302+```markdown
303+<!-- slide: title -->
304+# Q4 roadmap
305+Subtitle
306+
307+---
308+<!-- slide: two-column -->
309+## Pricing
310+::: left
311+- …
312+::: right
313+- …
314+Note: speaker notes here
315+```
316+
317+**`src/kinds/slides.ts`** parses and serializes this form, reusing `blocks.ts` `seed` per region. The text rendition is the deck in this form.
318+
319+**Editor:** `components/folios/slides/`.
320+- A filmstrip of thumbnails on the left, drag to reorder. Thumbnails are a read-only render of each slide's regions. Only the current slide mounts live editors, which keeps BlockNote instances to 1–5.
321+- A stage at 16:9 that scales to fit, a layout picker, a notes pane and a theme menu.
322+
323+**Present mode.**
324+- Fullscreen API on the stage, inside the same route (`?present=1`). Arrow keys, space, Esc, `B` for black.
325+- A presenter window (`?present=presenter`) shows notes, the next slide and a timer, kept in sync over `BroadcastChannel`.
326+- "Follow the presenter": the presenter's current slide goes out in **awareness**, and viewers who opt in follow it. No new infrastructure.
327+
328+**Export.** Markdown now. "Print / Save as PDF" through a print stylesheet (`@page` landscape, one slide per page), which works self-hosted with no service. A server-rendered PDF through the og service's `Screenshots` entrypoint (Browser Rendering), behind a `Renderer` adapter, comes in Phase 8.
329+
330+**Agent ops (`SlidesOp`):**
331+- `replace_deck { markdown }`
332+- `insert_slides { after_slide_id | null, markdown }`
333+- `replace_slide { slide_id, markdown }`
334+- `delete_slides { slide_ids }`
335+- `move_slide { slide_id, after_slide_id }`
336+- `set_notes { slide_id, markdown }`
337+- `set_theme { theme, accent }`
338+
339+### 3.3 Design (`kind: "design"`): what is realistic
340+
341+**Scope.** A frame-based layout canvas for mockups, screens, diagrams and one-pagers, which agents can produce structurally. It is not a full vector tool.
342+
343+**Yjs model.**
344+- `Y.Map("canvas")`: `{ background, grid }`.
345+- `Y.Map("nodes")`: `id → Y.Map`.
346+- Node types: `frame`, `rect`, `ellipse`, `line`, `arrow` (with node-bound endpoints), `text` (with `Y.Text` content), `image` (a file key, never inline bytes), `html`, `component` (a definition frame) and `instance` (a `component_id` plus `overrides`).
347+- Common fields: `parent`, `index` (a fractional-index string), `x`, `y`, `w`, `h`, `rotation`, `fill`, `stroke`, `stroke_width`, `radius`, `opacity`, `name`, `locked`, `hidden`.
348+- Frames have `layout: "free" | "row" | "column"` with `gap`, `padding`, `align`. This is simple stack layout computed at render, so **agents don't compute coordinates**.
349+- `html` nodes hold `Y.Text` with agent- or person-authored HTML and CSS. They render in a sandboxed iframe (`sandbox=""`, no scripts) served from the **usercontent origin**, sanitized server-side on save. This is the most agent-friendly way to get rich, realistic designs.
350+- Comments pin to `{ node_id, x, y }` in the `threads` map.
351+
352+**Renderer and editor:** `components/folios/design/`.
353+- React with SVG for shapes and positioned HTML for text.
354+- Pointer tools: select, frame, rect, ellipse, line, arrow, text, image (upload through `folio_files`) and hand.
355+- Pan and zoom, marquee and multi-select, snapping to frame edges and centres, keyboard nudge, and a layers panel with a properties panel.
356+- Make component, and instances with overrides.
357+- Other people's cursors and selections through awareness.
358+- **Export:** a frame as SVG by serializing the DOM, and as PNG through a client canvas.
359+
360+**Library decision: build it on SVG; add no canvas dependency.**
361+- Mature whiteboard libraries either carry licence terms unsuitable for a self-hostable MIT product (a production key or a watermark), or impose a hand-drawn whiteboard look and their own scene model that fights Yjs.
362+- Canvas-2D libraries make text editing and accessibility harder.
363+- An SVG scene graph on Yjs maps fits the stack: `yjs` is already in, and a small `fractional-indexing` helper is about 40 lines.
364+- An MIT whiteboard library could later back a separate "Whiteboard" kind if one is wanted.
365+
366+**Caps** (enforced in `validate`): 5,000 nodes, 200 KB per `html` node, 25 MB total files per folio.
367+
368+**Agent ops (`DesignOp`):**
369+- `upsert_nodes { nodes: NodeSpec[] }`: a nested spec, where children imply `parent` and `index`
370+- `delete_nodes { ids }`
371+- `replace_frame { frame_id, spec }`
372+- `set_html { node_id, html }`
373+- `move { ids, dx, dy }`
374+
375+**Text rendition:** frame names, then text layers in reading order, then the text content of `html` nodes, as headings per frame.
376+
377+### 3.4 Dashboards (`kind: "dashboard"`, beta)
378+
379+**Yjs model.**
380+- `Y.Map("dashboard")`: `{ filters: { range: "7d" | "30d" | "90d" | { from, to }, project?, repos?, team? }, refresh: "manual" | "5m" | "1h" }`.
381+- `Y.Array("tiles")` of `Y.Map`: `{ id, type, title, description, query: DatasetQuery | null, viz: {...}, grid: { x, y, w, h } }`.
382+- Tile types: `stat`, `line`, `area`, `bar`, `stacked_bar`, `table`, `list`, `markdown`.
383+- The grid is 12 columns.
384+- **Only the definition is in Yjs. Data values never are**, nor in versions, `text`, `preview`, the index or templates.
385+
386+**The safe query layer** (`packages/contracts/src/datasets.ts`):
387+
388+```ts
389+export type DatasetId = "issues" | "pull_requests" | "workflow_runs" | "deployments" | "spend" | "agent_sessions";
390+export type DatasetQuery = {
391+dataset: DatasetId;
392+measure: { op: "count" | "sum" | "avg" | "p50" | "p95" | "rate"; field?: string };
393+group_by?: string; // a declared dimension only
394+interval?: "day" | "week" | "month"; // time series
395+filters?: { field: string; op: "eq" | "neq" | "in" | "gte" | "lte"; value: string | number | string[] }[];
396+range?: "7d" | "30d" | "90d" | { from: string; to: string }; // tile's, else the dashboard's
397+limit?: number; // ≤ 100 rows
398+};
399+export type DatasetResult = { columns: { name: string; type: "string" | "number" | "time" | "money" }[]; rows: (string | number | null)[][]; truncated: boolean; as_of: string };
400+export const DATASETS: Record<DatasetId, { service: "work" | "actions" | "deployments" | "billing" | "agents"; dimensions: string[]; measures: string[]; needs: "member" | "billing" }>;
401+```
402+
403+- **Not SQL.** It is a declared catalog of fields, ops and dimensions per dataset. Each **owning service** implements one RPC, `query_dataset(viewer, workspace, query, audience?)`:
404+- `services/work` (Rust): issues and pull requests (opened, closed, merged, cycle time, review time, by repo, label, author kind person or agent).
405+- `services/actions` (Rust): runs (pass rate, duration, by workflow and repo).
406+- `services/deployments` (TS): deployments (frequency, failure rate, time to restore).
407+- `services/billing` (Rust): spend by product, project or person. Needs the billing role.
408+- `services/agents` (TS): sessions, tasks and spend per agent.
409+- Each service **applies its own read rules for the viewer**, using only repositories they can read. It aggregates in its own D1 with a fixed query per measure and dimension, and caps the rows. Validation of `DatasetQuery` against the catalog is shared code in contracts, run in both TS and Rust.
410+- **Execution path:** browser → `/-/artifacts/query?folio=&tile=` → docs service `query_tile`. That checks the viewer can read the folio, reads the tile's query from the room, validates it, fans out to the owning service as the viewer, and caches per `(viewer, query hash)` for the refresh interval behind a `QueryCache` adapter (Cache API on Cloudflare, in-memory or Redis self-hosted).
411+- **Two viewers of the same dashboard can see different numbers.** The tile footer says "Based on what you can see" when the viewer's access is partial: the service returns `partial: true` when the viewer cannot read some repositories.
412+
413+**Charts:** an own SVG chart kit, `components/charts/`, with `LineChart`, `AreaChart`, `BarChart` (stacked and grouped), `StatTile` (with `Sparkline`), `DataTable` and `Legend`.
414+- Extract and generalize `UsageChart`, `Sparkline` and `AllowanceRing` from `components/usage.tsx` so usage pages and dashboards share it.
415+- No new dependency. It is themed with `@g1t/theme` tokens, follows light and dark, and has accessible tables behind every chart ("View as table").
416+- Lazy-loaded with the dashboard editor.
417+
418+**Editor:** `components/folios/dashboard/`.
419+- A grid with pointer drag and resize (no grid library) and a tile inspector.
420+- Query builder: dataset, then measure, group, interval and filters, with live preview.
421+- A dashboard filter bar, a refresh button showing as-of time, and an auto-refresh interval.
422+
423+**Phone:** a single column ordered by `(y, x)`, read-only; editing a tile opens a sheet.
424+
425+**Agent ops (`DashboardOp`):**
426+- `upsert_tile { tile }`
427+- `delete_tiles { ids }`
428+- `set_filters { filters }`
429+- `set_layout { tiles: { id, grid }[] }`
430+
431+Agents read results through the agent tool `query_data` (section 4), never from the folio.
432+
433+**Text rendition:** the title, then each tile's title, description and query described in words ("Pull requests merged per week, by repository, last 90 days"). It never contains values.
434+
435+**Templates:**
436+- "Engineering health": PR cycle time, merged per week, run pass rate, deploy frequency, open issues by label.
437+- "Agent spend and output"
438+- "Workspace spend" (billing role)
439+- "Delivery" (DORA-style)
440+
441+`-/insights`, which is "soon" today, can later open the workspace's pinned dashboard.
442+
443+---
444+
445+## 4. Agents
446+
447+### 4.1 Workspace agents (`services/agents/src/tools.ts`, `ports.ts`)
448+
449+`DOCS_TOOLS` and `DOCS_WRITE_TOOLS` are replaced:
450+
451+| Tool | What |
452+|---|---|
453+| `search_artifacts` | `query`, optional `kind`, `space`, `project`. Hybrid search, audience-narrowed. |
454+| `read_artifact` | id or link. Returns the kind's agent form: doc → Markdown and block ids (as now); slides → deck Markdown with slide ids; design → node spec JSON (frames, then children); dashboard → spec JSON (tiles and queries). Plus `can: { read, suggest, edit }` and `audience_can_read`. |
455+| `create_artifact` | `kind`, `title`, `content` in the kind's agent form or `template`, `where: { space } \| "private" \| "conversation"`, optional `parent`. |
456+| `edit_artifact` | `FolioAgentEdit`, plus `note`, `suggest_only`, `marks_current`. Doc → inline suggestion or edit (as now). Other kinds → a direct edit where allowed, else a **proposal** (`folio_proposals`) a person previews and applies. |
457+| `list_spaces` | Spaces the asker and audience can all read, with abilities. |
458+| `stale_artifacts` | As `stale_pages`. |
459+| `query_data` | Runs a `DatasetQuery`, or a dashboard tile by id, **as the asker narrowed to the audience** (section 4.3). |
460+| `share_artifact` | Grants `view` or `comment` to people **already in the conversation**, only when the asker has `manage`. It can never set general access or grant `edit` or `manage`. For those, the agent posts a card with a "Share" button the person presses. |
461+
462+**Where an agent's new artifact lands:**
463+- **Owner** is the asker. `created_by` is `agent:<id>`, and the agent gets an `edit` grant so it can keep working.
464+- **Location:**
465+- the named space, if the asker can edit there;
466+- "conversation": Private, plus `view` grants to the DM's or private channel's people;
467+- otherwise Private to the asker.
468+
469+In a public channel the default is the workspace's General space if the asker can edit it, else Private, with the reply handled as in section 4.3.
470+
471+### 4.2 External MCP and REST (`apps/api`, Rust)
472+
473+- **Scopes** (`crates/contracts/src/scopes.rs` and the TS mirror `packages/contracts/src/scopes.ts`):
474+- `Resource::Artifacts` with `artifacts:read` (list, get, search, versions, query data), `artifacts:write` (create, update, edit, trash, restore, propose) and `artifacts:admin` (share, change general access, delete forever).
475+- Descriptions go into the scope table.
476+- **Tool:** one MCP tool `artifact` in `apps/api/src/tools.rs`, implemented in `apps/api/src/folios.rs` (`FoliosOp`). Actions:
477+- `list` (tab, kind, space)
478+- `search`
479+- `get` (metadata and the agent form)
480+- `create`
481+- `update` (title, icon, move)
482+- `edit` (`FolioAgentEdit`)
483+- `trash`
484+- `restore`
485+- `versions`
486+- `restore_version`
487+- `access`
488+- `share`
489+- `templates`
490+- `query_data`
491+- `spaces`
492+
493+Its description must say it is not the `workflow` tool's run artifacts.
494+- **REST:**
495+- `GET/POST /workspaces/{ws}/artifacts`
496+- `GET/PATCH/DELETE /workspaces/{ws}/artifacts/{id}`
497+- `GET/PUT /workspaces/{ws}/artifacts/{id}/content`
498+- `GET/PUT /workspaces/{ws}/artifacts/{id}/access`
499+- `GET /workspaces/{ws}/artifacts/{id}/versions`
500+- `POST /workspaces/{ws}/datasets/query`
501+- **apps/api** gets a `DOCS` service binding and a small Rust client for the docs service RPC.
502+- **OpenAPI** (`apps/api/src/openapi.rs`) and `apps/docs/src/data/openapi.json` are regenerated (the test enforces it). Agent tokens (`AgentScope.operations`) can name the new operations.
503+
504+### 4.3 Permissions and leak rules (must have tests)
505+
506+1. **Agent reads.** These use `agentFolios(viewer, audience)`, generalizing `agentSpaces`: **folio-level** effective access for the viewer, intersected with every audience member's.
507+ - `audience: workspace` (a public channel) gives only folios readable through an open space or `general_access = 'workspace'`.
508+ - Link-only and Private folios never qualify for that audience.
509+ - More than 20 people is treated like the workspace audience.
510+2. **Agent replies about a folio the audience can't all read.** The tool result carries `audience_can_read: false`. The prompt rule is: "don't quote it here; say you made or found something and that you've sent the link to <asker> directly". The agents service sends the link to the asker as a DM or ephemeral notice.
511+3. **Chat link unfurls and cards** resolve per viewer. Someone who can't read the folio sees "An artifact you don't have access to" with no title.
512+4. **Mentions inside a folio.** Mentioning someone who can't read it asks the editor "Share with @x?". A notification is sent only to people who can read it. Mentioned agents are notified only through their asker.
513+5. **Backlinks, search and FTS** show only folios the reader can read now.
514+6. **Dashboard values** are computed per viewer. In an agent turn they run as the asker restricted to repositories every audience member can read (the `repoSpacesForAudience` pattern). `spend` and billing datasets are refused unless the audience is just the asker.
515+7. **Workspace memory.** A turn that read a non-workspace-readable folio writes `remember` facts at `person` scope by default (`services/agents/src/memory.ts`).
516+8. **Templates.** "Save as template" from a Private or shared folio confirms "Everyone in the workspace will be able to use this template".
517+9. **Events and the inbox** carry no title or content for folios that aren't workspace-readable.
518+10. **Rooms.** Access changes call `setRole` on every open room in the affected subtree (a queue job for large subtrees). Revoked sockets close with 4403.
519+11. **The vector index** is filtered by `scope` and always re-checked against D1 (section 5).
520+
521+### 4.4 "Write this up" from Chat
522+
523+`components/chat/write-up.tsx` and `lib/write-up.ts` become "Write this up as an artifact". The dialog has:
524+
525+- **Kind:** Auto (default), Doc, Slides, Design or Dashboard.
526+- **Where:** a space the person can edit, **Private (just me)**, or **Shared with this conversation**. The default is "Shared with this conversation" in DMs and private channels, and the General space in public channels.
527+- **Title**, and **Writer** (as now).
528+
529+The posted ask becomes:
530+
531+`@g1t write this thread up as an artifact (<kind or "whichever kind fits best: a doc for decisions and notes, slides to present, a design for screens or layouts, a dashboard to track numbers">) <where> titled "<title>". Link this thread as the source: <link>`
532+
533+The agent calls `create_artifact` with `source` set. `folios.source` shows "From a conversation" on the artifact, linking back for readers of the thread.
534+
535+---
536+
537+## 5. Search and the hybrid index
538+
539+- **Indexer.** `src/indexer.ts` `indexPage` becomes `indexFolio(env, id)`. It reads `folios.text` and `kind` and chunks through `kinds[kind].chunks`:
540+- doc: by heading, as now (`chunks.ts`);
541+- slides: one passage per slide, with heading "Slide 4 · Pricing"; tiny slides join the next;
542+- design: one passage per top-level frame, with heading the frame name;
543+- dashboard: one passage (definition only).
544+
545+The embed cap, the catch-up run and the backfill (`doc_index_runs`, which also covers folios) all stay.
546+- **Vectorize.**
547+- A new index `g1t-folios` (768 dimensions, cosine, binding `FOLIO_VECTORS`) with metadata indexes on `workspace_id`, `scope` and `kind`. Repository files move into it with `scope = repo:<repo space id>`.
548+- Ids are `<folio id>:<seq>` or `rf_…:<seq>`.
549+- **`scope`** is `space:<id>` when the folio's access is exactly its space's (inherit chain to the top, no restriction, no grants). Otherwise it is `folio:<acl_root>`.
550+- Recall builds `allowed` from readable spaces, readable access roots (from `folio_access`, the general access rows and visits) and readable repo spaces. It uses `$in` when there are 40 or fewer keys, else a workspace filter and a post-filter (`vectorQueryPlan` as today).
551+- **Every hit is re-checked** against D1 with `effectiveRole` and the audience before it is returned.
552+- When access changes, a `folios.rescope` job rewrites metadata for the subtree's vectors: `getByIds`, then `upsert` with the same values. No re-embedding.
553+- `g1t-docs` is deleted in Phase 7.
554+- **Adapters.** `Embedder` and `VectorStore` (`src/vectors.ts`) are unchanged; self-hosters swap them. Without them, word recall over `folio_chunks_fts` still works.
555+- **People's search.**
556+- Artifacts home `?q=` is hybrid (RRF, as now), with kind, space, owner and project filters, and shows the matched passage. The separate `/search` page goes away.
557+- Typing in link pickers stays words-only (`folios_fts`).
558+- The command palette (`routes/search-json.ts`) adds an "Artifacts" group from `search_folios` with a limit of 5.
559+- **Recall for agents.** `recall_folios_for_agent` returns `FolioPassage[]`. The agents service (`services/agents/src/recall.ts`) switches to it. The old `recall_for_agent` stays until Phase 7.
560+
561+---
562+
563+## 6. UI
564+
565+### 6.1 Mode wiring
566+
567+- **`lib/workspace-nav.ts`:** `ModeKey "docs"` becomes `"artifacts"`, `PAGE_MODES` maps `artifacts`, and `modeHome` gives `/${slug}/-/artifacts`. Update `workspace-nav.test.ts`.
568+- **`components/rail.tsx`:** `{ key: "artifacts", label: "Artifacts", icon: <Shapes size={19}/> }` in the same slot as Docs.
569+- **`components/shell.tsx`:** `Panel "artifacts"`, `MODE_MENU`, and `sidebarFor` renders `<FoliosSidebar>`.
570+- **`components/mobile.tsx`:** the tab bar and the sheet row.
571+- **`routes.ts`:**
572+- Remove every `-/docs` route. Per the decision, there is no redirect; old links 404 with the normal not-found page.
573+- Add, under `:owner`:
574+
575+```
576+-/artifacts/live routes/workspace/folios/live.ts
577+-/artifacts/api routes/workspace/folios/api.ts (JSON: list, sidebar, access, versions, templates, search, mutations)
578+-/artifacts/query routes/workspace/folios/query.ts (dashboard tile data)
579+-/artifacts/threads/:folio/* routes/workspace/folios/threads.ts
580+-/artifacts/upload routes/workspace/folios/upload.ts
581+-/artifacts/export routes/workspace/folios/export.ts
582+-/artifacts (layout) routes/workspace/folios/layout.tsx
583+index home.tsx ?tab=all|yours|shared &view=list|grid &kind= &space= &owner= &project= &q=
584+new/:kind new.tsx ?space= &parent= &template= (creates, then redirects)
585+templates templates.tsx ?kind=
586+trash trash.tsx
587+stale stale.tsx
588+spaces spaces.tsx (browse and join open spaces)
589+spaces/new space-new.tsx
590+spaces/:space space.tsx
591+spaces/:space/settings space-settings.tsx
592+repo/:repoOwner/:repoName/* repo-file.tsx (projects' docs, read-only, unchanged)
593+:folio folio.tsx (<title-slug>-<id>; dispatches to the kind's editor; ?present=1 for slides)
594+:folio/history history.tsx
595+```
596+
597+Addresses are flat and end in the id, so moving between spaces and Private never breaks a link.
598+
599+### 6.2 Home (`routes/workspace/folios/home.tsx`)
600+
601+- **Header:** "Artifacts", with a search box (`/` focuses it), filters (Kind, Space, Owner, Project) and a **grid/list toggle**. Icon buttons have a `Hint`.
602+- **Tabs:** **All / Yours / Shared with you** (`ui/tab-strip.tsx`), kept in the URL.
603+- **"Make something new" tiles:** Docs, Slides, Design, and Dashboard with a "Beta" badge.
604+- Each tile goes to `new/:kind`, with a "Starting…" busy state and no double submit (as in the latest Docs fix).
605+- A split chevron on each tile opens "From a template…" and "In a space…".
606+- **List view, grouped by day** in the viewer's time zone (`lib/time-zone.ts`): **Today, Yesterday**, then `Oct 7`, with the year when it isn't this year. Each row shows:
607+- the **kind icon**, in the kind's colour on a soft square: Docs `FileText`, Slides `Presentation`, Design `PenTool`, Dashboard `LayoutDashboard`;
608+- the title (or "Untitled");
609+- a **lock icon** with the Hint "Only you can see this" when private, or else up to 3 avatars of who it's shared with;
610+- the space name (muted);
611+- "Edited 45m ago" (`TimeAgo` on `edited_at`), with "by @x" in the Hint;
612+- a **⋯ menu**: Open in new tab, Add to Favorites, Share…, Rename, Duplicate, Move to…, Copy link, Export, Save as template, Move to trash.
613+- **Ordering by tab:**
614+- **Yours:** `owner = me`, by `edited_at`.
615+- **Shared with you:** `folio_access` rows for my keys that aren't mine, by `since` or `edited_at`, whichever is newer. Link-visited folios are included.
616+- **All:** everything readable, by `edited_at`.
617+- **Paging:** cursor paging with "Show more".
618+- **Grid view:** cards with a **client-rendered preview** from `folios.preview` (JSON written at save time):
619+- doc: the first lines;
620+- slides: the first slide's layout and title;
621+- design: the first frame's top 50 simplified nodes;
622+- dashboard: tile boxes with titles and no numbers.
623+
624+No server thumbnails until Phase 8.
625+- **Empty states per tab:** "Nothing shared with you yet. When someone shares an artifact with you, it shows up here."
626+
627+### 6.3 Sidebar (`components/folios/sidebar.tsx`, from `components/docs/sidebar.tsx`)
628+
629+- Search, then **Home**, **New ▾** (the four kinds) and **Templates**.
630+- **Favorites**: draggable.
631+- **Spaces**: joined open spaces, my team spaces and my Members-only spaces, each with its tree. "Browse spaces" and "New space" are in the section's `+`.
632+- **Private**: my tree, with nothing in a space.
633+- **Shared**: the tops of what's shared with me, meaning the highest readable ancestor.
634+- **Possibly out of date** (when there is anything), **Projects' docs** (repo spaces) and **Trash**.
635+- **Tree rows** show the kind icon or emoji, drag to reorder or nest (docs accept children), and have a ⋯ menu. A lock shows on restricted subtrees.
636+- `lib/docs.ts` `buildTree` becomes `lib/folios.ts`.
637+
638+### 6.4 Share dialog (`components/folios/share-dialog.tsx`)
639+
640+Opened from the editor header's **Share** button and from ⋯ → Share….
641+
642+- **Title:** "Share '<title>'".
643+- **Invite row:** a people picker for people, agents and teams (`components/people-picker.tsx`, extended with agents and teams), a role select (Can view / Can comment / Can edit / Full access, from `DOC_ROLE_LABELS`), an optional "Notify" message, and **Invite**.
644+- **"Who has access":**
645+- the owner;
646+- explicit grants, editable;
647+- inherited rows, read-only with "From <space>" or "From <parent>" and a link to manage them there.
648+- **General access:**
649+- in a space: **Everyone in <space>** (inherits) or **Only people invited** (restricts);
650+- then: **Restricted** / **Everyone in <workspace>** (with a role) / **Anyone in the workspace with the link** (with a role);
651+- "Public link: off for this workspace" is shown disabled until Phase 8.
652+- **Agents:** "Agents may: suggest changes / edit directly" (`agent_mode`), defaulting to the space's.
653+- **Footer:** **Copy link**, and Done.
654+- Changing access shows "Updated" inline and the room's live notice refreshes other people's badges.
655+- People without `manage` see the dialog read-only with "Ask <owner> for access", which sends a request notification.
656+
657+**Request access:** a 403 on a folio shows "You need access" with a button that notifies the owner and managers. The owner gets an inbox item with Approve (view / comment / edit) / Deny.
658+
659+### 6.5 Editor shells (`components/folios/shell.tsx` + per kind)
660+
661+**A shared header:**
662+- breadcrumb: space or Private › parent;
663+- icon and title (inline rename);
664+- presence avatars;
665+- the "Offline, changes will sync" status (from the provider);
666+- **Share**, a comments toggle, a history toggle, ⋯ (Favorite, Duplicate, Move, Export, Save as template, Trash);
667+- the kind's own actions: Slides **Present**; Design zoom and Export frame; Dashboard **Refresh**, as-of time and filters.
668+
669+**Shared side panels:** comments, history (versions list, preview, restore) and proposals (preview with accept/reject, for non-doc kinds).
670+
671+**Per kind:** `components/folios/doc/`, `slides/`, `design/`, `dashboard/`. Each is lazy-loaded through `React.lazy` so the home page carries no editor code.
672+
673+### 6.6 Phone
674+
675+- **Home:** list view only, tiles as a horizontal scroller, filters in a sheet, and the sidebar in the mode sheet.
676+- **Docs:** full editing (as today).
677+- **Slides:** view, present (swipe) and edit text on the current slide. No reordering or layout changes beyond a sheet.
678+- **Design:** view, pan, zoom and comment. Editing says "Open on a larger screen to edit the canvas".
679+- **Dashboards:** a single-column read view, refresh, and the filters sheet.
680+
681+---
682+
683+## 7. Docs service RPC (`services/docs`, `foliosClient`)
684+
685+**New file layout**, so `index.ts` stops growing:
686+- `src/folios/service.ts`: the class `Folios`, sharing workspace, people and teams helpers moved from `index.ts` into `src/who.ts`.
687+- `src/folios/rpc.ts`: a method table.
688+- `src/folios/list.ts`, `access-store.ts`, `agents.ts`, `recall.ts`.
689+- `src/datasets/run.ts`: `query_tile`, validation and cache.
690+- `src/kinds/*`.
691+- `index.ts` keeps routing: `/rpc/<method>` checks the folio table first, then the legacy switch.
692+
693+**Methods:**
694+- Lists and navigation: `folio_list`, `folio_sidebar`, `folio`.
695+- Changing folios: `create_folio`, `update_folio`, `move_folio`, `duplicate_folio`, `trash_folio`, `restore_folio`, `delete_folio`, `folio_trash`, `favorite_folio`.
696+- Sharing and spaces: `folio_access`, `set_folio_grant`, `set_folio_general_access`, `request_folio_access`, `join_space`, `leave_space` (plus the existing space methods).
697+- Search, history, templates and export: `search_folios`, `folio_versions`, `folio_version`, `restore_folio_version`, `folio_templates`, `save_folio_template`, `delete_folio_template`, `export_folio`.
698+- Suggestions and proposals: `folio_suggestions`, `decide_folio_suggestion`, `folio_proposals`, `decide_folio_proposal`, `folio_thread`, `folio_threads`.
699+- Dashboards: `query_tile`, `query_dataset_for_agent`.
700+- Agent calls: `folios_for_agent`, `read_folio_for_agent`, `create_folio_as_agent`, `edit_folio_as_agent`, `share_folio_as_agent`, `recall_folios_for_agent`, `stale_folios_for_agent`, `mark_folio_current`, `reindex_folios`.
701+
702+**Other entry points:** `GET /live?folio=` and `PUT /files?folio=`.
703+
704+**Wrangler** (`services/docs/wrangler.jsonc`):
705+- `FolioRoom` (DO migration v2);
706+- `FOLIO_VECTORS` (`g1t-folios`);
707+- service bindings `ACTIONS`, `DEPLOYMENTS`, `BILLING` for dataset fan-out (`WORK` and `AGENTS` already exist);
708+- a cron (`"triggers": { "crons": ["17 3 * * *"] }`) to purge trash older than 30 days.
709+
710+**Datasets fan-out** goes through a `DatasetSource` port per service, so self-hosting needs nothing special.
711+
712+---
713+
714+## 8. Phases
715+
716+Each phase deploys on its own:
717+- Migrations ship in or before the phase that reads them.
718+- Nothing deployed still reads what a phase drops.
719+- Deploy order inside a phase is the docs service, then agents and api, then web.
720+
721+Sizes:
722+- **S** ≈ ≤1 day of one agent
723+- **M** ≈ 2–4 days
724+- **L** ≈ 1–2 weeks
725+- **XL** ≈ 2–4 weeks
726+
727+| # | Phase | Size | Ships | Depends on |
728+|---|---|---|---|---|
729+| 0 | **Contracts and plan** | S | `folios.ts`, `folios-*.ts`, `datasets.ts` (types and validators, with tests); `crates/contracts` `folios.rs`, `datasets.rs`, `FOLIO_EVENTS` (not yet subscribed); `Resource::Artifacts` scopes (not yet used by any op); this doc. No runtime change. | none |
730+| 1 | **Service core + doc kind** | L | Migration `0004_folios.sql`; `FolioRoom` with the kind adapter and `kinds/doc`; access (`effectiveRole`, materialize, with tests); list, sidebar, CRUD, share, trash, versions, templates (doc builtins ported), search, `folio.*` events; indexer to `g1t-folios`; agent RPCs (`*_for_agent`, `recall_folios_for_agent`). Old Docs keeps running on old tables. | 0 |
731+| 2 | **Artifacts mode (web) + agents switch** | L | Rail, mode, shell and mobile wiring; `-/docs` routes deleted; home (tabs, tiles, day-grouped list, grid), sidebar, share dialog, editor shell, doc editor moved, history, trash, templates, spaces pages. **Same release:** `services/agents` tools and recall switched to the folio RPCs (`search_artifacts`, `read_artifact`, `create_artifact` (doc only for now), `edit_artifact`, ...); the write-up dialog with Where (kind fixed to Doc until slides ship); per-viewer chat unfurls for artifact links. Old pages are dropped (see D2). apps/docs: `guides/artifacts.mdx` (overview, spaces, sharing, private), `guides/docs.mdx` removed. Slides, Design and Dashboard tiles show "Coming soon" (disabled with a Hint), not hidden, until their phase. | 1 |
732+| 3 | **External MCP/REST** | M | `artifact` MCP tool, REST routes, `artifacts:*` scopes on ops, OpenAPI and `apps/docs/src/data/openapi.json`, `DOCS` binding in apps/api, guide `guides/bring-your-own-agent.mdx` section, scope table in `guides/authentication.md`. | 1 (parallel with 2) |
733+| 4 | **Slides** | L | `kinds/slides.ts` (Markdown deck parse/serialize, ops, chunks, preview), slides editor, filmstrip, layouts, themes, present and presenter mode, print to PDF, slides templates (5), agent ops, write-up kind, apps/docs `guides/artifacts-slides.md`. | 2 (editor shell registry) |
734+| 5a | **Datasets in owning services** | M each, parallel | `query_dataset` in `services/work` (issues, PRs), `services/actions` (runs), `services/deployments`, `services/billing` (spend), `services/agents` (sessions, spend), each with access tests and a D1 index migration where needed. | 0 |
735+| 5b | **Dashboards** | L | `components/charts/` (extracted from usage.tsx; usage pages switched to it), `kinds/dashboard.ts`, grid editor, query builder, `query_tile` with cache, the `query_data` agent tool, 4 templates, "Beta" badge, apps/docs `guides/artifacts-dashboards.md`. | 2, 5a (each dataset lights up as its service ships) |
736+| 6a | **Design: canvas core** | L | `kinds/design.ts`, SVG renderer, tools, select, snap, layers and properties, frames with stack layout, images, awareness cursors, comments pinned, export SVG/PNG, agent `upsert_nodes`. | 2 |
737+| 6b | **Design: components and HTML frames** | M | Components and instances, `html` nodes in the usercontent sandbox (with server sanitizer), design templates (4), apps/docs `guides/artifacts-design.md`. | 6a |
738+| 7 | **Cleanup** | S | Migration `0005_drop_pages.sql`; DO migration v3 `deleted_classes: ["PageRoom"]`; remove the legacy RPC methods, `DocPage*` contracts, `doc.page.*` from `events.ts`, `events.rs` and `subscribers.rs`, `components/docs/*` and `routes/workspace/docs/*` leftovers; delete Vectorize `g1t-docs`; rewrite docs/WORKSPACE.md "Docs" to point here. Ships only after 2, 3 and the agents switch have been deployed everywhere. | 2, 3 |
739+| 8 | **Later** | — | Public links (usercontent origin, workspace setting, `view` only); server PDF and thumbnails through the `Renderer` adapter (og `Screenshots`); imports (Markdown folders, a deck file); `folio.*` events offered to webhooks; a documenter agent routine on `folio.stale`; dashboard alerts. | — |
740+
741+### Parallel worktrees and file ownership
742+
743+**One worktree each, in parallel:**
744+- **After 0:** `1`, `5a-work`, `5a-actions`, `5a-deployments`, `5a-billing`, `5a-agents`. Each 5a branch owns only its service directory and `crates/contracts/src/datasets.rs` additions. The contracts are settled in 0, so these are read-only for 5a.
745+- **After 1:** `2` and `3`.
746+- **After 2:** `4`, `5b` and `6a`.
747+
748+| Phase | Owns (exclusive) | Touches (append-only, merged by the integrator) |
749+|---|---|---|
750+| 1 | `services/docs/src/folios/**`, `src/kinds/doc/**`, `src/kinds/types.ts`, `src/access.ts`, `src/indexer.ts`, `src/persist.ts`, `src/room.ts`, `migrations/0004_*`, `wrangler.jsonc` | `src/index.ts` (route to the folio table) |
751+| 2 | `apps/web/app/routes/workspace/folios/**`, `components/folios/{shell,sidebar,share-dialog,provider,kinds,doc/**}`, `lib/folios.ts`, `lib/workspace-nav.ts`, `rail.tsx`, `mobile.tsx`, `routes.ts`; `services/agents/src/{tools,ports,recall}.ts`; `components/chat/write-up.tsx`, `lib/write-up.ts`; `apps/docs/.../guides/artifacts.mdx` | `components/shell.tsx` (panel switch) |
752+| 3 | `apps/api/src/folios.rs`, `apps/api/wrangler.jsonc`, `crates/contracts/src/scopes.rs` (Artifacts ops) | `apps/api/src/{tools,rest,openapi,operations}.rs`, `apps/docs/src/data/openapi.json` |
753+| 4 | `services/docs/src/kinds/slides/**`, `src/templates/slides.ts`, `components/folios/slides/**`, `packages/contracts/src/folios-slides.ts`, `guides/artifacts-slides.md` | `kinds/index.ts`, `components/folios/kinds.tsx` (one line each) |
754+| 5b | `components/charts/**`, `components/usage.tsx`, `services/docs/src/kinds/dashboard/**`, `src/datasets/**`, `components/folios/dashboard/**`, `routes/workspace/folios/query.ts`, `folios-dashboard.ts` | the same registries; `services/agents/src/tools.ts` (`query_data`, a small hunk) |
755+| 6a/6b | `services/docs/src/kinds/design/**`, `components/folios/design/**`, `folios-design.ts`, `apps/web/workers/usercontent.ts` (html frames, 6b only) | the same registries |
756+
757+Conflict hotspots are kept to one-line registry appends: `kinds/index.ts`, `components/folios/kinds.tsx`, `packages/contracts/src/index.ts` exports and the apps/docs sidebar config. Never reformat these files in a feature branch.
758+
759+---
760+
761+## 9. Risks and open decisions, each with a recommendation
762+
763+| # | Decision or risk | Recommendation |
764+|---|---|---|
765+| D1 | Code name for the mode | **`folio`** in code; "artifact" only in UI, URLs, MCP, REST and scopes (section 0). Keep `services/docs` and the `g1t-docs*` resources. |
766+| D2 | Carry existing Docs pages over, or drop them? | **Drop** (pre-launch; the user allows a reset). A copy needs every old room's Yjs state read through `PageRoom.state()` for little value. Re-seed demo workspaces from the new templates. If a copy is wanted after all, a one-off owner-run `copy_pages_to_folios` job (pages become `doc` folios in the same spaces, state copied) is an M-sized add-on to Phase 2. |
767+| D3 | `-/docs` URLs | **Removed, no redirect** (the user's instruction). They 404 with the normal page. |
768+| D4 | Addresses | **Flat** `/-/artifacts/<title-slug>-<id>`, so moves never break links. Spaces at `/-/artifacts/spaces/<slug>`. |
769+| D5 | Folders | **No folder kind in v1.** Docs hold children, and only docs. Revisit only if people ask to group decks without a page. |
770+| D6 | Space kinds | Keep `workspace` / `team` / `private` in the database. In the UI: **Open**, **Team**, **Members only**. Open spaces need a **join** (`space_joins`) to show in the sidebar, so it doesn't fill up. |
771+| D7 | Who can share | **`manage` only by default.** A space setting "Editors can share" is offered. Agents can only grant view or comment to people already in the conversation, never general access. |
772+| D8 | Agent changes to non-doc kinds where they can't edit | **Proposals**: a Yjs update against a state vector, previewed in a forked document, applied or rejected as a whole. Doc keeps inline suggestions. |
773+| D9 | Design canvas library | **Build on SVG with Yjs maps**, with `html` frames in the usercontent sandbox for rich agent output. Don't adopt a whiteboard library (licence and fit, section 3.3). Scope it as a layout tool, not a vector editor. |
774+| D10 | Charts | **Own SVG chart kit** extracted from `components/usage.tsx`. No dependency, and the usage pages share it. Revisit only if we need more than about 8 chart types. |
775+| D11 | Dashboard query safety | **A declared dataset catalog, not SQL.** Executed by owning services as the viewer. Values never persisted in the folio, versions, index or previews. Agents are audience-narrowed, and spend is refused in shared audiences. |
776+| D12 | Dashboard numbers differ by viewer | Accepted and labelled: "Based on what you can see" when `partial`. Don't add "run as owner" in v1 (it would leak). |
777+| R1 | Access complexity and performance | Materialized `folio_access` and a denormalized `acl_root`/`path`, with pure `effectiveRole` tests over a matrix of private / space / team / general / link / restrict / nested / owner-workspace-owner. Tree depth capped at 10. Big subtree changes go through the queue. |
778+| R2 | Stale vector metadata after access changes | A `scope` key plus a mandatory D1 re-check of every hit. Rescope jobs only improve recall quality; correctness never depends on them. |
779+| R3 | Yjs document size (design) | Images are files, never inline. Node, html and file caps are enforced in `validate`. Version state over 1.5 MB goes to the file store (`state_key`). Compaction as today. |
780+| R4 | BlockNote instances per deck | Mount live editors only for the current slide; thumbnails are static renders. Fallback: one editor per slide body with regions as blocks. |
781+| R5 | Bundle size | Each kind's editor is lazy-loaded. Home ships no editor code. The chart kit loads only with dashboards. |
782+| R6 | HTML in design frames (XSS) | Server-side sanitizer, served only from the usercontent origin in `sandbox=""` iframes with a strict CSP (no scripts, no forms, images only from the files origin). |
783+| R7 | Self-hosting | DO, R2, D1, Vectorize, Workers AI, the Cache API and Browser Rendering each sit behind an existing or new adapter (`room`, `FileStore`, SQL, `Embedder`, `VectorStore`, `QueryCache`, `Renderer`, `DatasetSource`). docs/SELF_HOSTING.md is updated in Phases 1, 5b and 8. |
784+| R8 | Window where agents and UI disagree during Phase 2 | Ship the docs service first, then agents and web in the same release. Until then the old Docs UI and old tools keep working on the old tables. |
785+| R9 | Rail label length | "Artifacts" is 9 characters, the same as "Workspace", which already fits the 5rem rail. |
786+| R10 | Insights placeholder | Later, point `-/insights` at a workspace-pinned dashboard ("Engineering health" template). Not in scope now. |
787+| R11 | Public links | Phase 8, off by default per workspace, view only, never indexed or recalled, and revocable by rotating the token. |
788+| R12 | Copy rules | No product comparisons in UI or apps/docs. The tile labels are "Docs, Slides, Design, Dashboard". |
789+
790+---
791+
792+### Critical Files for Implementation
793+
794+- `services/docs/src/room.ts`: becomes `FolioRoom` with kind adapters.
795+- `services/docs/src/index.ts` and `src/access.ts`: RPC routing, access rules, agent reads and recall (`agentSpaces`, `recallForAgent`).
796+- `packages/contracts/src/docs.ts`: the source for the new `folios.ts` and `datasets.ts`.
797+- `apps/web/app/routes.ts`, plus `apps/web/app/lib/workspace-nav.ts`, `apps/web/app/components/rail.tsx` and `apps/web/app/components/shell.tsx`: mode wiring.
798+- `services/agents/src/tools.ts` and `apps/api/src/tools.rs`: agent and MCP tools.