Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| 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 | 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. |