Rich Docs
How to read this doc
Start with Context for the three pain points. Skim Architecture overview for the five-layer shape. The User journeys section is the most concrete description of what changes for the PM. Docs browser UX covers the Google-Drive-inspired entry point. Migration plan covers the existing ~102 indocs/ HTML files; End state describes the post-build world. Build safety principles + Implementation phases are the agile sprint plan: worktree-first, feature-flagged, with explicit verification and rollback per sprint. Markdown source: docs/devtasks/rich-docs.md.
Contents
- Context
- Architecture overview
- The registry —
public.indocs - The generalized
/docsskill - Markdown source + request-time rendering
- The Lite Editor
- Access control
- Format catalog
- User journeys
- Docs browser UX (Drive-inspired)
- Migration plan
- End state — what success looks like
- Build safety principles
- Implementation phases (agile sprint plan)
- Open questions
- Build journal
1. Context
Sponic’s primary document workflow is: have a conversation in Claude Code, then ask it to publish a doc to the intranet at in.sponicgardens.com/indocs/. For devtasks specifically this goes through the /devtask skill (which writes four files). For everything else — pitches, recruiting briefs, partner research, runbooks, one-pagers, legal/corp memos, AI host setup — there is no skill. The PM just prompts Claude in conversation: “write me a one-pager at indocs/onepager/foo.html,” and Claude pattern-matches the visual style from a neighboring file and writes the HTML.
The intranet’s indocs/ tree currently holds ~102 HTML files spread across ~20 categories. Roughly 30 are devtasks; the rest are ad-hoc Claude output.
Three pain points have become dominant:
- The feedback loop is too long. The only way to see the rendered doc is to commit and deploy. Small tweaks become 3–5 minute roundtrips.
- Every page looks the same. A slide-deck-style document, a diagram-heavy explainer, and a long dense RFC look identical.
- The docs corpus is hard to navigate. Each category has its own static
index.htmlpage. There’s no unified browser, no global search, no cross-category view, no Drive-style “what did I touch recently / what’s shared with me” entry point. Finding a doc means knowing which category it lives in.
Goal
Four things, all load-bearing:
- Multi-format artifacts behind a single discoverable index — memos as HTML, slide decks as reveal.js, diagrams as SVG, data as sortable tables, freeform sketches as Excalidraw.
- Lite in-browser editing for memo/markdown so small revisions don’t require a Claude Code roundtrip.
- Light-touch multi-format INSIDE a doc. A prose memo can embed a chart, diagram, table, or slide-card inline.
- A Google-Drive-inspired docs browser as the unified entry point. Sidebar nav with Pinned / Recent / Shared / category tree; main pane with grid or list view, global search, sort, filter; right-click context menus; star/pin. Replaces the per-category static index pages.
Explicit non-goal
No inline Claude inside the editor — no “ask Claude to rewrite this paragraph” buttons, no per-doc chat sidebar. For larger changes, open a new CC session.
2. Architecture overview
Five layers, designed independently so they can be built independently:
- The spine — unified docs registry. A single Supabase table for every artifact regardless of format.
- The browser — Drive-inspired docs UI. A single
/indocsroute that queries the registry and renders the Drive-style browser. Replaces all per-categoryindex.htmlpages. - Per-format renderers. Each artifact type has its own renderer; also invokable as inline blocks inside a memo via markdown directives.
- The Lite Editor. An authenticated in-browser editor for markdown-backed formats. Saves via GitHub Contents API.
- Access control. Per-category defaults + per-doc ACL overrides. Admins always have edit access; admins delegate Google-Docs-style.
3. The registry — public.indocs
3.1 Schema sketch
CREATE TABLE public.indocs (
slug TEXT PRIMARY KEY,
type TEXT NOT NULL, -- memo | deck | diagram | table | freeform | custom
category TEXT NOT NULL, -- devtasks | pitch | recruiting | corp | botanics | β¦
title TEXT NOT NULL,
subtitle TEXT,
description TEXT,
tags TEXT[] DEFAULT '{}',
status TEXT,
view_url TEXT NOT NULL,
edit_url TEXT,
download_url TEXT,
source_path TEXT,
owner TEXT,
acl JSONB DEFAULT '{}', -- { "editors": ["[email protected]"], "viewers": "intranet" }
pinned BOOLEAN DEFAULT false,
starred_by TEXT[] DEFAULT '{}', -- emails who have starred (Drive-style)
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
last_edited_by TEXT,
live_status JSONB
);
3.2 Who writes to the registry
/docsskill writes a row on doc creation.- Lite Editor updates
updated_at+last_edited_byon save. - Drive-style browser updates
starred_bywhen a user stars a doc. - Migration script (Phase 1) backfills every existing doc.
4. The generalized /docs skill
/devtask becomes a thin alias for /docs --category devtask --type memo. Invocation shapes:
/docs β interactive: asks for category + type
/docs --category recruiting β memo in recruiting/ (type defaults to memo)
/docs --category devtask --type memo β equivalent to today's /devtask
/docs --category pitch --type deck β reveal.js deck under pitch/
/docs --category corp --type diagram β mermaid/D2 diagram under corp/
/docs --category botanics --type table β sortable xlsx/csv under botanics/
Skill derives slug, picks source path per type, writes source file with frontmatter, inserts registry row with category-default ACL + owner, returns view_url. The four-files pattern of today’s /devtask collapses to two files (source + registry row).
5. Markdown source + request-time rendering
5.1 Decision: request-time rendering
The body of every memo is rendered at request time by a Next.js dynamic route at apps/control/src/app/(public)/indocs/[category]/[slug]/page.tsx. Cloudflare Pages serves the route via OpenNext (already in package.json as @opennextjs/cloudflare).
| Build-time (status quo) | Request-time (decided) | |
|---|---|---|
| Save → live latency | ~1–3 min | seconds |
| Per-request compute cost | none | low (~5–20ms) |
| Cache strategy | CF edge cache forever | CF edge cache + invalidation |
| Auth/personalization possible | no | yes |
5.2 The markdown directive layer
Standard markdown + custom directives: :::callout-green / :::callout-gold / :::callout-warn, :::flow, :::toc, :::mermaid / :::d2, :::chart, :::deck-slide, :::table-from src="...". Implementation: remark-directive + custom plugin.
6. The Lite Editor
- Visit a
type: memodoc signed in → Edit button (if ACL-permitted). - Click → TipTap/Lexical editor loads from markdown.
- Save → Next.js route handler commits via GitHub Contents API.
- Request-time render picks up the new markdown on next load — sub-second.
Commits attribute to sponic-lite-editor[bot] with editor’s name in body. Conflict handling: stale SHA → refresh. Editable in v1: prose, headings, lists, callouts, inline tables, code blocks. Not editable: frontmatter, Build Journal, images.
Anti-goals
No inline Claude, no real-time multi-user collaboration. Last write wins; conflict detection by SHA.
7. Access control
There is no existing in-browser editor in apps/control. But apps/control/public/indocs/auth-guard.js already supports per-page allowlists and role gates (admin/staff/resident). The Lite Editor reuses this auth client.
7.1 Permission model
// apps/control/src/lib/indoc-acl.ts
export const CATEGORY_DEFAULTS = {
devtasks: { edit: "signed-in", view: "intranet" },
recruiting: { edit: "admin+staff", view: "intranet" },
pitch: { edit: "admin", view: "admin+staff" },
corp: { edit: "admin", view: "admin+staff" },
legalcorp: { edit: "admin", view: "admin" },
};
Per-doc acl JSONB overrides:
{
"editors": ["[email protected]", "[email protected]"],
"viewers": "intranet"
}
Per-doc ACL wins if set. Admins always have edit access regardless. Read-side: auth-guard.js. Edit-side: save endpoint checks user email server-side.
8. Format catalog
8.1 Memo (default)
Markdown source → directive layer + page shell, request-time. Edited via Lite Editor.
8.2 Slide deck
Per-slide markdown (--- separator) → reveal.js + Sponic theme. Optional .pptx via pptxgenjs. Edited via Lite Editor.
8.3 Diagram (text-based)
Mermaid or D2 source → SVG. Edited via Lite Editor source-view.
8.4 Data table
xlsx/csv → sortable HTML table + download. Upload-only.
8.5 Freeform diagram (stretch)
Excalidraw JSON → embedded Excalidraw viewer + Edit toggle. Saves via GitHub Contents API.
8.6 Custom (existing hand-designed pages)
Hand-designed HTML (branding.html, ipitch.html, etc.) stays as-is; indexed but editor not exposed.
8.7 Embedded artifacts within memos
A memo can embed any renderer as an inline block:
## System architecture
:::mermaid
graph TD
Registry --> MemoRenderer
:::
The numbers for Q3 are:
:::table-from src="../data/q3-numbers.csv"
Same renderer powers standalone artifact + embed. Lite Editor treats unfamiliar directives as opaque "edit source" widgets; familiar ones get inline visual editing.
9. User journeys
Journey A — Create a memo from CC
- PM has a conversation in CC.
- PM invokes
/docs --category devtask. - Skill writes
docs/devtasks/<slug>.mdand inserts registry row. - Auto-push commits and pushes.
- PM opens
https://in.sponicgardens.com/indocs/devtasks/<slug>— live via request-time renderer.
Journey B — Tweak an existing memo in browser
- PM opens a doc; sees Edit button (ACL-permitted or admin).
- Click → in-place editor loads from markdown.
- PM fixes a typo.
- Click Save → endpoint commits via GitHub Contents API.
- Page reloads → change visible in <1s. No CC session opened.
Journey C — Substantively rework an existing memo
- PM opens a new CC session.
- Claude edits markdown at
docs/<category>/<slug>.md. - Auto-push picks it up; live in seconds.
Journey D — Create a deck/diagram/table from CC
- PM invokes
/docs --category pitch --type deck(or--type diagram,--type table). - Skill writes source file + registry row.
- PM opens rendered URL.
Journey E — Find a doc (Drive-style browser)
- PM opens
https://in.sponicgardens.com/indocs/— the Drive-inspired browser. - Sees: sidebar with Pinned / Recent / Shared with me / My docs / category tree with counts. Main pane in grid view.
- Two paths to a doc: (a) browse via sidebar category click → grid filters → click card; or (b) global search bar → results across all categories → click result.
- Each card shows: type icon, title, description, modified date, owner avatar, share-status icon, star toggle.
- Toolbar: switch to list view for sortable columns; sort dropdown; filter by type/tag/owner.
- Right-click card → context menu (Open, Open in new tab, Edit, Share, Star, Copy link, Move to category).
- Star a doc → appears under Pinned sidebar item.
Journey F — Share/restrict edit access
- Admin opens a doc, clicks Share in the doc header.
- Modal opens: current effective editors listed.
- Admin adds a colleague’s email; "Save permissions" updates registry
aclJSONB. - Colleague sees Edit button and the doc appears under their Shared with me sidebar item.
10. Docs browser UX (Google-Drive-inspired)
The unified browser at /indocs is the single entry point for finding any doc in the corpus. Inspired by Google Drive’s navigation paradigm, adapted to the Sponic visual language.
10.1 Layout
Two-pane (sidebar + main), responsive (sidebar collapses to hamburger on mobile):
10.2 Sidebar — Quick Access
- β
Pinned — docs the user has starred (
starred_bycontains their email). - π Recent — docs sorted by
updated_atdesc, limited to docs they have view access to. - π₯ Shared with me — docs where the user’s email is explicitly in
acl.editors. - π My docs — docs where
ownermatches the user’s email.
10.3 Sidebar — Categories
Collapsible tree. Each node: category name + doc count. Click a category → main pane filters. v1 stays flat with one level.
10.4 Main pane — Grid view (default)
Doc cards with: type icon (memo = page, deck = slides, diagram = node graph, table = grid, freeform = pencil, custom = star), title (2-line truncate), description preview (1-line truncate), owner avatar, modified date, share-status icon (lock = private, people = shared, globe = public), star toggle.
10.5 Main pane — List view
Sortable table: Icon | Title | Type | Category | Modified | Owner | Actions. Column header click toggles sort. Persists to localStorage.
10.6 Toolbar
- View toggle (grid / list) — persists to localStorage.
- Sort dropdown — Modified / Name / Type / Owner / Created.
- Filter dropdown — by type, tag, owner, modified date range.
- + New button — dropdown of types; clicking copies the right
/docs --category <current> --type <X>invocation to clipboard with a “paste this into Claude Code” toast.
10.7 Search
Global search bar (top of main pane). Hits PostgreSQL full-text on title + description + tags + category. Results in a dropdown below the search bar (live, debounced 200ms).
10.8 Right-click context menu
Per card / row: Open, Open in new tab, Edit (if permitted), Share (admin always; editors if per-doc ACL exists), Star/Unstar, Copy link, Move to category (admin only).
10.9 Breadcrumb
Top of main pane. Home, Home βΊ Devtasks, Home βΊ Devtasks βΊ Rich Docs. Each segment clickable. Replaces the current ← All docs link on individual doc pages.
10.10 Per-doc page changes
Breadcrumb at top replaces current ← All docs button. Optional collapsible sidebar showing other docs in same category. Type icon next to title.
10.11 What we adopt from Drive, what we don’t
Adopted: sidebar nav with Pinned/Recent/Shared/category tree; grid vs list view toggle; breadcrumb navigation; right-click context menus; star/pin; global search bar; type icons.
Not adopting (out of scope): thumbnail previews (content renders fast enough); multi-select / batch operations; detailed activity timeline; comments / suggestions (no inline collaboration — anti-goal); file versioning UI (git history is truth); trash/restore (deletes go through git revert); real-time presence indicators.
11. Migration plan
The intranet has ~102 HTML files in indocs/. Three tiers, deliberately decoupled.
11.1 Tier 1 — Registry backfill (Phase 1)
A one-shot Node script (apps/control/scripts/backfill-indocs-registry.mjs) walks all indocs/**/*.html files, extracts metadata, cross-references docs-tab.tsx, upserts into public.indocs with type: "custom" as default.
11.2 Tier 2 — Memo conversion (Phase 5)
Per-doc, Claude-assisted batch conversion. For each: read HTML, extract prose, convert to markdown + directives, write docs/<category>/<slug>.md, update registry to type: memo, delete static HTML.
Sequence:
- First batch: 30 devtasks (already have markdown sources).
- Second batch: high-traffic categories (
sop/,reference/,aihost/). - Third batch: lower-traffic categories (
recruiting/,corp/,botanics/).
11.3 Tier 3 — Leave as custom
Genuinely hand-designed HTML pages stay as type: custom: branding.html, ipitch.html, onepager.html, pressuretest.html, sohocompare.html, onepageroutline.html, polishbiznotes.html, privacy.html.
12. End state — what success looks like
For the PM creating a new doc
- One skill (
/docs) for every category. - One source file per doc.
- No hand-rendered HTML.
For the PM editing an existing doc
- Click Edit on any memo (with permission). Save. Live in <1s. No CC session.
- For bigger reworks: open a CC session against the markdown source.
For the PM finding a doc
- One Drive-style browser at
/indocs/. Sidebar (Pinned / Recent / Shared / categories). Grid or list. Global search. Right-click menus. Star/pin.
For the team’s permissions story
- Sensible category defaults. Per-doc overrides via Share modal. Admins always have full access.
For the multi-format story
- Memos can embed mermaid diagrams, charts, small CSVs, and slide-style cards inline.
- Standalone deck / diagram / table artifacts exist as first-class entries.
Concrete success metrics
A PM can perform a typo fix on a memo in under 30 seconds without ever opening Claude Code. A PM can find any doc in the corpus in under 5 seconds via the Drive-style browser. If both are true, the system worked.
13. Build safety principles
These invariants hold across every phase. If a sprint plan violates one, redesign the sprint before starting.
- Worktree-first. Every phase builds in
.claude/worktrees/rich-docs-pN. No work happens onmain. The worktree’s dev server is the verification surface. Only when the worktree’s verification checklist passes does the work merge to main via/ship. - Feature flags everywhere. Every new surface — registry browser, Drive-style UI, request-time route, Lite Editor button, Share modal, each new artifact type — ships behind a flag in
apps/control/src/lib/feature-flags.ts. Default OFF on first merge to main. Flip on manually after monitoring. Rolling back = flipping a flag, not reverting commits. - Dual-write / parallel-run during cutover. New systems run alongside old until proven equivalent.
/docswrites registry rows alongside files. Request-time route serves alongside static HTML. Drive-style browser renders alongside per-category index pages. Old system only deleted after new one is verified for every doc. - Per-doc opt-in for migrations. Docs migrate to the new system one at a time via a frontmatter flag (
request_time_render: true). A doc that hasn’t opted in keeps serving its static HTML exactly as before. - Reversible until the last step. Most operations reversible (toggle flags, delete registry rows, revert frontmatter). The only irreversible operation is deleting the static HTML file for a migrated doc — and that only happens after its request-time render is verified visual-diff identical against the static HTML for that specific doc.
- Verification in the worktree before merge. Each sprint has an explicit verification checklist. Visual diffs, behavior tests with multiple user roles, and quantitative checks all part of the checklist. No merge without checklist sign-off.
- Production gate after merge. After merging to main, the CF Pages deploy fires (~1-3 min). Production gate criteria specify what must be verified on the live URL before flipping the feature flag on.
- Schema changes are additive only. New columns and tables fine.
DROP COLUMN/DROP TABLEonly after features using them are confirmed dead AND data backed up. - Static HTML is the safety net. Even after migration, the ability to fall back to static HTML serving (via flag flip) remains until Phase 10 ships. The CF Pages static deploy isn’t dismantled — it becomes a fallback path.
14. Implementation phases (agile sprint plan)
Each phase is a sprint built in its own worktree, verified locally, then merged to main behind a feature flag (default off). The flag flips on only after a brief production monitoring window. Use /feature rich-docs to pick up the next un-started phase.
Phase 1 — Registry + Tier 1 backfill
Tasks
- Create migration
infra/migrations/<date>_indocs_registry.sqlwith thepublic.indocsschema. - Apply to a Supabase dev branch first; after verification, apply to PROD.
- Write
apps/control/scripts/backfill-indocs-registry.mjs. - Add
apps/control/src/lib/feature-flags.tswithINDOCS_REGISTRY_BROWSER(default false). - Add minimal route
apps/control/src/app/(public)/indocs/page.tsxthat queries registry but renders today’s UX. No Drive-style upgrades here — Phase 2 handles that. - Replace
docs-tab.tsxDOCSarray with Supabase query (flag-gated).
Verification (worktree dev server)
- Migration applies cleanly on dev Supabase branch.
SELECT count(*) FROM public.indocsreturns 102.- Flag-off:
/indocs/devtasks/looks identical to today. - Flag-on: new route renders all entries; visual diff vs static
index.htmlshows no regression. - DocsTab on
/en/devcontrolshows same docs from either source.
Production gate
- Schema migration applied to PROD Supabase.
- Backfill run against PROD; row count = 102.
- PM previews on live URL with flag off (existing behavior).
- Flip flag on; verify; monitor for 10 min.
Rollback
Flip INDOCS_REGISTRY_BROWSER off → reverts to static index + hard-coded DOCS. Registry rows can stay (harmless).
Phase 2 — Drive-style docs browser
Tasks
Sidebar.tsx(Quick Access + category tree).Toolbar.tsx(view toggle, sort, filter, + New button).DocGrid.tsxandDocList.tsx.DocCard.tsxwith type icons.Breadcrumb.tsx.- Right-click context menu (vanilla; no external lib).
- Global search using PostgreSQL full-text (debounced 200ms).
- Persist view state to localStorage.
- Star/unstar updates
starred_byarray on registry row. - Replace per-doc
← All docswith breadcrumb. - Feature flag:
INDOCS_DRIVE_UX.
Verification (worktree)
- Grid view renders all 102 docs.
- List view sortable on all columns; persists to localStorage.
- Sidebar categories show correct counts; collapsible.
- Search returns results across all categories in <500ms.
- Star/unstar persists; Pinned sidebar item populates.
- Right-click context menu actions all work; Edit visibility correct per ACL.
- Mobile layout: sidebar collapses; usable on phone-sized viewport.
- Breadcrumb navigates correctly.
- + New button copies correct
/docs --category Xinvocation to clipboard.
Production gate
- PM uses the browser for 5 minutes; finds 3 specific docs without prompting.
- No regressions in
DocsTabon/en/devcontrol. - Flag flipped on after monitoring; PM signs off.
Rollback
Flip INDOCS_DRIVE_UX off → falls back to Phase 1’s minimal registry-backed list. Old per-category static pages still served.
Phase 3 — Generalized /docs skill
Tasks
- Create
.claude/skills/docs/SKILL.mdwith category/type-aware instructions. /devtaskbecomes thin alias.- Add
apps/control/src/lib/registry.tswithinsertIndoc()helper (Supabase service-role key from Bitwarden). - Skill writes source + invokes
insertIndoc(). - Per-category defaults documented in skill instructions.
Verification (worktree)
/docs --category devtask --type memoproduces same files as today’s/devtask.- Registry row created in addition to files.
/devtaskalias still works (regression test).- Per-category default ACL applied correctly (check 3 categories).
Production gate
- Test invocation of
/docs --category recruiting --type memoproduces a real doc, visible in registry. - Skill documentation matches actual behavior.
Rollback
Delete skill folder; /devtask unaffected (kept side-by-side).
Phase 4 — Request-time markdown renderer
Tasks
npm install remark remark-directive remark-html unified.- Custom remark plugin for Sponic directives.
apps/control/src/lib/render-memo.ts.apps/control/src/app/(public)/indocs/[category]/[slug]/page.tsxroute.- Frontmatter flag
request_time_render: true(opt-in per doc). - Route only renders if flag true; otherwise falls through to static HTML.
Verification (worktree)
- Opt in one test doc; render via request-time route vs static HTML; visual diff identical (layout, fonts, spacing, callouts, TOC).
- All 7 directive types render correctly.
- Request-time render p95 < 200ms.
- Opted-out doc still served as static HTML; no regression.
Production gate
- PM reviews visual-diff comparison side-by-side.
- Opt in 3 sample docs; verify all render correctly on live URL.
Rollback
Per-doc request_time_render: false reverts that doc to static. Whole feature can be disabled by route check.
Phase 5 — Lite Editor + Tier 2 devtask migration
Tasks
npm install @tiptap/react @tiptap/starter-kit @tiptap/extension-markdown.apps/control/src/components/indocs/LiteEditor.tsx.apps/control/src/app/api/indocs/save/route.ts(GitHub Contents API write).- Generate GitHub App / PAT; store in Bitwarden under DevOps-sponicgarden; document in
infra/runbook.md. - Edit button only for admins (Phase 6 adds ACL).
- Save flow + stale-SHA conflict handling.
- Tier 2 batch 1: migrate 30 devtasks — set
request_time_render: true, delete static.htmlfiles, switch registry rowscustom→memo. - Feature flag:
LITE_EDITOR_ENABLED(admin-only initially).
Verification (worktree)
- Admin user edits a test doc; save succeeds; file updated on GitHub.
- Reload doc; change visible in <2s.
- Two-tab conflict test: edit + save in tab A; try to save in tab B → stale SHA error.
- All 30 devtasks render correctly at request-time route; visual diff no regression.
- Old
.htmlURLs 308-redirect to request-time route.
Production gate
- PM does live edit on a real (non-test) devtask.
- GitHub commit history clean.
- CF Pages doesn’t trigger unnecessary rebuild.
- 30 devtasks remain accessible at existing URLs.
Rollback
Flag off → no Edit button. Static HTML deletion is irreversible — only delete AFTER request-time render verified; if regression appears, restore HTML from git history.
Phase 6 — ACL layer + Share UI
Tasks
apps/control/src/lib/indoc-acl.tswithCATEGORY_DEFAULTS.- Save endpoint enforces ACL server-side.
ShareModal.tsx.- Share button in doc header.
- Continue Tier 2 migration (
sop/,reference/,aihost/). - Feature flag:
INDOCS_ACL_ENABLED.
Verification (worktree)
- 3 test users (admin, staff, demo) try edits across 5 categories — gate behavior matches.
- Share modal: admin adds colleague; colleague sees Edit and can edit.
- Reset to category default works.
- Server-side enforcement: POST as non-editor → 403.
Production gate
- All test cases pass for 3 user roles across 5 categories on live URL.
- Document ACL behavior in
/indocs/sop/.
Rollback
Flag off → ACL JSONB ignored; falls back to Phase 5 default (admins only).
Phase 7 — Diagram artifacts (mermaid / D2)
Tasks
- Vendor mermaid (or wire D2 build step).
- Standalone diagram renderer route for
type: diagram. :::mermaid/:::d2directives in memos (same renderer)./docs --type diagramskill branch.
Verification (worktree)
- Generate a mermaid diagram via
/docs --type diagram; appears in registry browser. - Standalone diagram URL renders SVG correctly.
:::mermaidblock inside memo renders inline.- Lite Editor source-view edits update SVG on save.
Production gate
- One real diagram doc shipped.
Rollback
Remove route + directive registration; existing :::mermaid blocks fall back to rendering as code blocks.
Phase 8 — Slide deck artifacts (reveal.js)
Tasks
- Vendor reveal.js; build Sponic theme.
- Deck renderer route; per-slide markdown source.
/docs --type deckskill branch.- Optional pptx derivative via
pptxgenjs.
Verification (worktree)
- Generate 5-slide deck via
/docs --type deck; renders correctly. - Fullscreen mode works; arrow keys advance.
- Edit a slide’s text via Lite Editor; change visible after reload.
Production gate
- One real deck shipped and presented from the live URL.
Rollback
Remove route; deck source files become inert (still indexed, no renderer).
Phase 9 — Data table artifacts + embedded directives
Tasks
- Server route reads xlsx/csv → sortable HTML table.
- Upload flow in registry admin (drag-drop xlsx).
/docs --type tableskill branch.:::table-from src="..."directive for inline use.:::chartdirective for small bar/line/pie embeds.
Verification (worktree)
- Upload xlsx; appears as
type: table; sortable HTML correct; download works. :::table-from src="..."in a memo renders inline.:::chartdirective renders a chart from inline data.
Production gate
- One real data table shipped.
Rollback
Remove route + directives; uploaded files become inert.
Phase 10 — Freeform diagrams (Excalidraw) — STRETCH
Tasks
- Vendor Excalidraw embed.
type: freeformentries with.excalidrawJSON source.- Read-only viewer by default; Edit toggle for permitted users.
- Save writes JSON via GitHub Contents API.
Verification (worktree)
- Create freeform diagram in-browser; save; reload; persists.
- Permission gating works (read-only for non-editors).
Production gate
- One real freeform diagram shipped.
Rollback
Disable route; JSON files become inert.
15. Open questions
- Markdown directive syntax confirmation.
remark-directive(:::name) vs MDX. Confirm before Phase 4. - Editor lib choice. TipTap (default) vs Lexical. Decide when Phase 5 starts.
- Registry vs git as source of truth for metadata. Proposed: frontmatter wins; registry is a derived index.
- Deck-to-pptx priority. Live reveal.js URL sufficient, or PMs need downloadable
.pptx? - Cache invalidation depth. Phase 4 ships with simple TTL caching. Commit webhook for edge invalidation is a v1.1 question.
- Tier 2 migration ordering after devtasks. By traffic, by category leader, by ease of conversion?
- Drive-style “shared with me” semantics. Show only docs where user is in
acl.editors, or also docs where they’reowner? Probably split into “My docs” and “Shared with me”. - + New button behavior. Phase 2 copies invocation to clipboard. Direct CC integration possible? Defer to feedback.
Build journal
2026-06-01 Β· Haydn Β· phases 5–9 + gallery Β· worktree-rich-docs-p1-71d0
Built the remaining capability in one session, all additive (existing 111 docs + static HTML untouched; Tier-2 migration deferred).
Decisions- Every renderer is client-side (static-export constraint): Phase 7 mermaid (lazy-loaded, Sponic theme); Phase 8 decks (reveal.js, embedded + Present,
/docs-deck); Phase 9 tables/charts (papaparse CSV → sortable table,::table-fromfetch,:::chart→ SVG bars). D2 stays a placeholder. - Lite Editor is a markdown split-editor, not TipTap (directives round-trip as text). Fast-path writes
body_md(instant). The secure git-commit path is theindocs-saveEDGE FUNCTION (no API routes under static export) — NOT deployed; needs aGITHUB_TOKEN+supabase functions deploy. - Share UI writes
acl.editorsclient-side; server enforcement is in the same un-deployed function. - Gallery docs are registry-only rows (no repo files): Memo / Diagram / Deck / Table demos. Reversible.
Tested: mermaid SVG; 5-slide deck + Present; sortable CSV table (numeric) + download; inline chart + CSV; Share writes acl; Lite Editor edit → save (body_md, last_edited_by) → exits showing the change; the Memo Demo renders every format in one doc. tsc clean; image-gen lint OK.
Deferred (creds/approval): deploy indocs-save (GitHub token); Phase 10 Excalidraw; Tier-2 migration of the 111 existing docs.
Next step: end-to-end test the 4 gallery docs + editor + share, then ship Phases 1–9.
2026-06-01 Β· Haydn Β· phase 4 Β· worktree-rich-docs-p1-71d0
Request-time markdown rendering β but client-side, not the SSR the spec assumed. New: render-memo.ts (directive layer), memo-shell.css (namespaced port of the static template), the docs-view viewer route, and migration 20260601_indocs_body_render.sql (additive body_md + request_time_render columns).
- Architecture pivot β client-side render, not SSR (decided with Haydn). The app is a pure static export;
@opennextjs/cloudflareis unwired and incompatible with[email protected]. Switching to SSR would rearchitect the whole prod deploy. Chose: render markdown in the browser at view time, readingbody_mdfrom the registry. Preserves “save → instant view” at near-zero risk. - Viewer is a single query-param route
/<lang>/docs-view?slug=β¦, not a path-based dynamic route (which would needgenerateStaticParamsunderoutput:"export"). Renders any doc instantly, even brand-new ones. Opted-in docs route here; others keep static HTML. - Directive forms: callouts use container
:::name β¦ :::; standalone directives (::table-from,::toc) use the LEAF form::name. An unclosed:::table-fromcontainer swallowed the rest of the doc β fixed and documented. - CSS parity via a namespaced port (
.memo-doc) of the static template, not an iframe β keeps anchor scrolling. Re-addedlist-stylethat Tailwind’s preflight resets. - Mermaid/D2 (Phase 7) and table-from (Phase 9) render as graceful placeholders so opting in never shows a broken block.
Tested: migration applied to PROD; registered a test-render doc with body + opt-in; viewer rendered all 7 directives + a real GFM table + code + anchor ids (dotted “2.1” handled). Render time 13.9 ms (target <200). Visual parity confirmed by screenshot. Opted-out doc shows the static-HTML fallback. tsc clean; no console errors. Deleted the test row + throwaway doc; PROD back to 111.
Blocker (resolved): DDL migration needed a human paste (BW CLI broken on this machine β no Mgmt token). Root-cause fix added to infra/runbook.md: stash SUPABASE_ACCESS_TOKEN in .env.local once and the agent applies future migrations headless via the Mgmt API.
Next step: Ship Phases 1–4 together, or continue to Phase 5 (Lite Editor + GitHub Contents API + Tier 2 devtask migration).
Session status (handoff): Phases 1–4 built, verified, and committed on worktree-rich-docs-p1-71d0 — nothing pushed (kept on the worktree until a deliberate ship). SHAs: 528015882 (P1), e160b5738 (P2), ae39bbab1 (P3), e09b6d053 (P4). PROD public.indocs = 111 rows, schema through Phase 4. Live prod unchanged: v260607.04 10:06a / release 1097. Standing recommendation: add SUPABASE_ACCESS_TOKEN to .env.local for headless future migrations (recipe in infra/runbook.md).
2026-06-01 Β· Haydn Β· phase 3 Β· worktree-rich-docs-p1-71d0
Generalized /devtask into a category/type-aware /docs skill that registers every artifact into public.indocs. New: .claude/skills/docs/SKILL.md, apps/control/src/lib/indoc-acl.ts (CATEGORY_DEFAULTS + defaultAclFor), apps/control/src/lib/registry.ts (insertIndoc() server helper), and apps/control/scripts/insert-indoc.mjs (the CLI the skill calls). /devtask gets an alias note pointing at /docs and instructing it to register the row too.
- The skill writes through a Node CLI, not the TS helper. The skill is Claude in a shell and can’t import
registry.ts; the CLI reads the service-role key from.env.localand upserts.registry.tsstays for the future Lite Editor save endpoint (Phase 5). - Category ACL defaults duplicated in two places —
indoc-acl.ts(typed) and a JS copy ininsert-indoc.mjs(the CLI can’t import TS). Both carry a “keep in sync” header; Phase 6 should add an equality test. Chose duplicating ~7 lines over a build step for one script. - acl.viewers stores only the view level on insert; edit level stays implicit (resolved from category at Phase 6 enforcement). Matches the Phase 1 backfill. Per-doc editor lists appear only when someone is explicitly shared in.
- Registry slug is path-prefixed (
<category>/<slug>); source file isdocs/<category>/<slug>.md. Devtasks keep the historical bare-name HTML view_url. - Dual-write / parallel-run preserved for
--category devtask --type memo: still writes all four legacy files plus the registry row, per Phase 3’s verification bar. Non-devtask categories skip legacy index/docs-tab maintenance — the Phase 2 browser is their index.view_urlstays on the static.htmlshape until Phase 4.
Tested: dry-run shows correct row shape. Real PROD inserts for 3 categories — pitch → admin+staff, recruiting → intranet, legalcorp → admin — all correct. Re-run proved upsert idempotency (count stayed 3). Deleted all 3 _acltest/* rows via service-role key; registry back to exactly 111. tsc --noEmit clean; lint-image-gen.sh OK.
Next step: Ship Phases 1–3 together (flags default off), or continue to Phase 4 (request-time markdown renderer).
2026-05-27 Β· Haydn Β· phase 2 Β· worktree-rich-docs-p1-71d0
Drive-style docs browser built behind INDOCS_DRIVE_UX (layered on Phase 1’s INDOCS_REGISTRY_BROWSER). New shared lib apps/control/src/lib/indocs.ts (IndocRow type, fetchIndocs, toggleStar, plus canEdit/isSharedWith/isOwnedBy/shareStatus and type/category label maps) and components under apps/control/src/components/indocs/: Sidebar, Toolbar, DocCard/DocGrid/DocList, Breadcrumb, vanilla ContextMenu, and the DriveBrowser orchestrator. Phase 1’s minimal list stays as the fallback when the flag is off.
- Global search overrides the sidebar selection. A non-empty query searches the whole corpus (title + description + category + tags), not just the active category — per spec §10.7. Caught and fixed mid-verification after “hermes” in Legalcorp returned 0.
- Client-side search/sort/filter over the already-loaded 111 rows, not PostgreSQL full-text. At this corpus size in-memory filtering is instant and avoids a tsvector migration; revisit if the corpus grows large.
- Quick Access semantics: Pinned =
starred_bycontains email; Recent =updated_atdesc capped at 30; Shared =acl.editorscontains email; My docs =owner == email. - Star = optimistic toggle + read-modify-write on
starred_byviatoggleStar(); RLS allows authenticated UPDATE. - Edit / Share / Move context-menu actions are visibility-correct but stubbed to a toast (Lite Editor → Phase 5, Share → Phase 6). Phase 2 only needs correct visibility per role/ACL.
- localStorage persists view mode + sort key + sort dir (keys
indocs:view/indocs:sortKey/indocs:sortDir).
Tested: worktree dev as [email protected] (actually role: oracle in the DB, despite CLAUDE.md saying demo — so admin-gated menu items correctly showed). Grid 111 cards Β· list view sortable, sort+view persist Β· category filter (Legalcorp = 10) Β· global search (“hermes” → 1 result across categories) Β· star writes to prod starred_by then unstar clears it (both via PostgREST) Β· Pinned populates Β· context menu shows all 7 items for oracle Β· +New copies /docs --category devtasks --type memo Β· breadcrumb navigates Β· mobile (375px) hides sidebar, shows hamburger drawer Β· DocsTab unregressed (15 cards) Β· tsc --noEmit clean Β· no console errors.
Blocker (carried, open): preview_start reads .claude/launch.json from the main checkout not the worktree, so the dev server ran from main and new worktree routes 404’d. Worked around by temporarily patching main’s launch.json to cd into the worktree + exec next dev (reverted before commit). Needs a tooling fix.
Next step: Ship Phases 1+2 together (flags default off), or continue to Phase 3 (generalized /docs skill).
2026-05-27 Β· Haydn Β· phase 1 Β· worktree-rich-docs-p1-71d0
Decisions- Slug = path-relative without .html (e.g.
devtasks/rich-docs). Path-prefixed slug keeps every artifact uniquely keyed and maps to the static URL. - Indexed 111 files, not “102”. The spec undercounted;
findreports 111 including per-categoryindex.html. Kept index pages in the registry because some (HR/offers/*) are content, not nav stubs. - Feature flags via TS module + localStorage, not env vars (Next 16 + Turbopack
output:"export"doesn’t inlineNEXT_PUBLIC_*).window.__ff(name, true)console helper for dev. - Route at /en/docs-browser, not /indocs — avoids collision with the static
public/indocs/*.htmluntil the Phase 4 cutover. - DocsTab registry source filters to tagged rows (curated subset), preserving the 15-entry feel rather than dumping all 111.
Tested: migration applied to prod Supabase; backfill = 111 rows (15 with curated tags). Flag-off shows the legacy hint; flag-on renders 111 docs across 18 categories. DocsTab parity flag on/off. tsc --noEmit clean.
Blocker: BW CLI broken on this machine (Node 25 + BW 2026.3.0 — bw get item re-prompts and dies on non-TTY); migration applied via Supabase SQL editor paste-in. Runbook updated with the gotcha.
Next step: /ship Phase 1 (flag off), then Phase 2 Drive-style browser.