oficina / slides Public

Slides design spec, revision 1

Hybrid flow/canvas block model, Yjs+yrs collab, HTML renderer with SVG
and Rust PDF export, presentation sessions with a phone remote, agent
drive surface, lone-shape sign-in. Slice 1 is the first live deploy.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01563XGCgxcSZToEXwuTBsKE
claude agent, operated by paul@keymail.dev pushed by paul@keymail.dev a257115593176c9de3da12d2397bec09a122c5ab parent 298e00c
2 files changed, +543 −0
  • .gitignore +5 −0
  • docs/superpowers/specs/2026-09-07-slides-design.md +538 −0
diff --git a/.gitignore b/.gitignore
index 1285380..daee433 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,8 @@
/slides.db
/slides.db-wal
/slides.db-shm
+
+# Session worktrees and sandbox artefacts, never source.
+/.claude/worktrees/
+/.bash_profile
+/.bashrc
diff --git a/docs/superpowers/specs/2026-09-07-slides-design.md b/docs/superpowers/specs/2026-09-07-slides-design.md
new file mode 100644
index 0000000..edd260a
--- /dev/null
+++ b/docs/superpowers/specs/2026-09-07-slides-design.md
@@ -0,0 +1,538 @@
+# 🤖 Slides — design
+
+Design, 2026-09-07. Direction approved by Paul in conversation; this is the
+written form. **Revision 1.** Drafted by the Slides session (Fable 5.1)
+from a brainstorm with Paul the same day. Every ruling in §1 attributed to
+Paul was given in that conversation.
+
+Slides is a presentation editor for the Oficina suite: a deck of slides
+that looks like a Keynote or PowerPoint deck on a desktop and reads like
+a document on a phone, edited by several people at once, presented from a
+laptop and driven from a phone, exported to PDF, and driveable by an agent
+through the same operations a person has. One instance per team, on the
+CARLOS platform, at `slides.oficina.works` for the first team.
+
+Built beside [Docs](https://amadan.net/oficina/docs),
+[Sheets](https://amadan.net/oficina/sheets) and the suite
+[doctrine](https://amadan.net/oficina/suite). Where a decision is already
+made over there, this spec consumes it rather than restating it (§0.2).
+
+---
+
+## 0. The rules this app lives under
+
+### 0.1 The one rule
+
+**A slide is one block tree, rendered many ways.** The tree is the only
+truth: the editor edits it, the viewer renders it, the phone reflows it,
+the PDF prints it, the agent patches it. Nothing that matters to what a
+reader sees lives outside the tree, so there is never a "desktop version"
+and a "mobile version" of a deck, only one deck and a viewport.
+
+Everything below derives from it. The hybrid layout modes (§2.3) are two
+ways of placing the same blocks, not two kinds of slide. The phone
+overrides (§2.5) are entries in the tree, not a second file. The drive
+API (§7) is the inspector's operations with a JSON body.
+
+### 0.2 Consumed suite rulings
+
+This app cites the suite sharing doctrine
+(`oficina/suite`, `docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md`,
+revision 8) by ruling number and never restates it.
+
+| Ruling | Used for |
+|---|---|
+| 1, 5 | One instance per team; the first deploy is the **lone shape** (§6.1). |
+| 4, 12 | Identity and roster belong to the home; until the home exists (it is a skeleton, `oficina/home` `internal/home/app.go`), the lone instance keeps its own allowlist exactly as Docs does. |
+| 6 | No product-wide egress. A PDF leaves as a download the person asked for; nothing is sent anywhere. |
+| 9 | Every write names its real actor. An agent's write names the agent (§7). |
+| 15 | Three rungs (private, granted, team) plus a **read-only link capability**. A share link never edits (§5). |
+
+One ruling made by Paul during this brainstorm is suite-wide and is
+filed as an `oficina/suite` discussion rather than restated here as if it
+were this app's: **agents drive every Oficina app** as their own
+identified principal, `claude~paul` style, on behalf of a person, never
+as the person (2026-09-07). Slides is the first app to build the surface
+(§7).
+
+### 0.3 What this spec does not claim
+
+It does not claim pixel identity between the browser stage and the PDF.
+§4 says where they may differ and by how much. It does not claim the
+suite home exists. It does not claim a native remote exists; §3.4
+designs the phone web page so a native app later speaks the same three
+calls.
+
+---
+
+## 1. Settled decisions (don't relitigate casually)
+
+| # | Ruling | Rejected alternative | Who, when |
+|---|---|---|---|
+| 1 | A slide is a tree of typed blocks; the slide's container has a **layout mode**, `flow` or `canvas`. Same blocks, same editor, two placements. | Blocks only (no free placement; feels like Gamma, not Keynote). Canvas only (unreadable on phones). Two separate slide kinds (two models to maintain). | Paul, 2026-09-07 |
+| 2 | Phones get **reflow** for flow slides and **scale-to-fit** for canvas slides, with a per-slide switch to reflow a canvas slide in reading order. | Hand-authored second layout per slide. | Paul, 2026-09-07 |
+| 3 | Collab core is **Yjs in the browser, yrs under wazero on the server**, Docs' pattern and Docs' live-sync transport (SSE advisory, bytes over POST). Slides is the second implementation of live-sync. | Server-authoritative JSON with last-write-wins (two people in one text block clobber each other). | Paul, 2026-09-07 |
+| 4 | **One Go renderer produces HTML** for the viewer, presenter, remote and the editor's stage. **Export** uses a Go layout engine that emits SVG per slide, and PDF is SVG to PDF through a Rust module under wazero. No Chromium. | Extend Docs' hand-rolled PDF with boxes and images (a second layout engine to keep in step). Headless Chromium (no such platform capability; one static binary). | Paul approved the SVG-and-Rust export path 2026-09-07; the HTML-first split for the live surfaces is the session's refinement, recorded in §4.1. |
+| 5 | Presenting is a **server-side presentation session**: current slide, presenter, pairing code. The phone remote, the presenter view and audience follow all read and write that one record. | Present as a pure viewer with no session (no remote possible). | Paul, 2026-09-07 ("include plans for speaker notes, presentation mode, and a mobile remote control") |
+| 6 | The remote is a **phone web page** first; a native app later speaks the same calls. | Native first. | Paul, 2026-09-07 |
+| 7 | **Every write names its actor**, and an agent is its own principal with an operator, never the person (suite ruling 9 plus Paul's 2026-09-07 ruling). Agent tokens are minted in the app by a signed-in person, named `<agent>~<person>`, stored hashed. | Agents borrowing a person's session. | Paul, 2026-09-07 |
+| 8 | First deploy is the **lone shape**: magic-link sign-in with an address allowlist, exactly Docs' `internal/docs/auth.go` shape. | Waiting for the home. | Session, 2026-09-07, from the survey: no app signs in through the home; the home is a skeleton. |
+| 9 | Themes are a **built-in set of token bundles** over estilo's variables. A deck may override accent colour and the two typefaces. No per-block fonts. | Free font picker per block (the fastest route to an ugly deck). | Paul approved the model 2026-09-07 |
+| 10 | Bytes (images) live in the **platform object store**, referenced by `blobs.Ref`; never on the instance disk, never in a column beyond the 4 KiB inline rule. | SQLite blobs (the instance hibernates and its disk is ephemeral; rastrillo §2). | rastrillo rule, adopted |
+| 11 | Repo `oficina/slides`, module `amadan.net/oficina/slides`, scaffolded by `rastrillo new` v0.26.0 (day theme, topbar shell). | Spanish name (`presentaciones`); the suite names generic apps in English. | Session, by the naming precedent |
+
+---
+
+## 2. The deck model
+
+### 2.1 The Y.Doc
+
+A deck is one Y.Doc. Top-level shared types:
+
+- `meta` — Y.Map: `title`, `theme` (theme id), `accent` (optional hex),
+ `fonts` (optional `{heading, body}`), `aspect` (`16:9` only in v1; the
+ key exists so `4:3` can arrive without a migration).
+- `slides` — Y.Array of Y.Map, one per slide, in deck order.
+- `assets` — Y.Map from asset id to `{ref, name, width, height, alt}`,
+ where `ref` is the object store address (§2.7).
+
+### 2.2 A slide
+
+A slide Y.Map holds:
+
+- `id` — stable, random, 12 lowercase base32 characters, assigned at
+ creation, never reused. The drive API addresses slides by it.
+- `layout` — `flow` or `canvas`.
+- `template` — for flow slides: `title`, `title-body`, `two-column`,
+ `section`, `image-full`, `quote`, `blank`. Ignored on canvas slides.
+- `blocks` — Y.Array of block Y.Maps in reading order (flow) or creation
+ order with a `z` (canvas).
+- `notes` — Y.XmlFragment, speaker notes, the same text schema as §2.6.
+- `background` — `{kind: theme | color | image, value}`.
+- `phone` — `inherit` (default) or `reflow`. Only meaningful on canvas
+ slides.
+- `transition` — `none` or `fade`.
+
+### 2.3 Layout modes
+
+**Flow.** The template names regions (`heading`, `body`, `aside`,
+`media`), each block has a `region` prop, and blocks stack inside their
+region in array order. The renderer places regions with CSS grid, and
+the template's grid collapses to one column at the phone breakpoint, so
+reflow is CSS, not a second layout.
+
+**Canvas.** Each block has `box: {x, y, w, h}` as fractions of the 16:9
+frame (0 to 1, four decimals) and `z`. The renderer positions blocks
+absolutely inside a frame that scales to the viewport width. On a phone
+the frame scales to fit, text and all, unless `phone` is `reflow`, in
+which case the renderer emits the blocks as a flow slide with the
+`blank` template in `z` then top-left reading order.
+
+Switching a slide's mode is an operation, not a conversion the person
+does by hand: flow to canvas assigns each block the box its region gave
+it; canvas to flow drops boxes and assigns regions by position. Both are
+lossy in one direction and say so in the inspector.
+
+### 2.4 Blocks
+
+A block Y.Map holds `id` (same scheme as slides), `type`, `props`
+(Y.Map), and for text-bearing types `text` (Y.XmlFragment). The v1
+vocabulary, the "maximally viable" set:
+
+| Type | Props | Text |
+|---|---|---|
+| `heading` | `level` 1 to 3 | yes |
+| `text` | `size` (`s`, `m`, `l`), `align` | yes |
+| `list` | `style` (`bullet`, `number`), `size` | yes, one paragraph per item |
+| `quote` | `cite` | yes |
+| `image` | `asset`, `fit` (`cover`, `contain`), `alt` (required, may be empty only if `decorative` is true), `radius` | no |
+| `columns` | `count` 2 or 3, `children` (Y.Array of block ids laid out left to right, each a block in this slide's `blocks`) | no |
+| `code` | `language`, `size` | yes, plain, one paragraph per line |
+| `shape` | `kind` (`rect`, `ellipse`, `line`, `arrow`), `fill`, `stroke`, `strokeWidth`, `radius` | optional single-paragraph label |
+| `table` | `rows`, `cols`, `header` (bool), `cells` (Y.Array of Y.XmlFragment, row-major) | per cell |
+| `divider` | none | no |
+| `spacer` | `size` | no |
+
+Every block additionally carries `region` (flow) or `box` and `z`
+(canvas), and an optional `phone` override map (§2.5).
+
+Charts from Sheets, embeds and video are **not in v1** (§9).
+
+### 2.5 Phone overrides
+
+A block's `phone` Y.Map may hold: `hidden` (bool), `order` (integer,
+sorts before array order among siblings), `size` (a size step), and on
+canvas slides `box`. Nothing else. The desktop layout is always the
+base; the phone layer only adds.
+
+The editor's desktop/phone switcher (§3.2) writes to this map when the
+stage is in phone view. The renderer reads it when the viewport is a
+phone, and the print engine never reads it.
+
+### 2.6 Text
+
+Text is a ProseMirror document under a constrained schema: `paragraph`
+only at the block level; marks `bold`, `italic`, `code`, `link`,
+`highlight`. No nested blocks, no headings inside text (a heading is a
+block). Lists are `list` blocks whose paragraphs are items; a nested list
+is a second `list` block with `indent` 1. The same schema drives speaker
+notes and table cells.
+
+### 2.7 Assets
+
+An image is uploaded browser-to-bucket by presigned PUT (rastrillo
+`blobs`), then registered in `assets` with its `blobs.Ref`, dimensions
+and alt text. The server verifies the object exists and is an image
+before registering. Limits: 20 MiB per image, PNG, JPEG, WebP, SVG
+(SVG is sanitised on registration). Deleting the last block that uses an
+asset leaves the asset; a deck-level "unused assets" list lets a person
+remove them. There is no cross-deck asset library in v1.
+
+### 2.8 Fixtures are the contract
+
+The JS side generates Y.Doc updates for a fixed set of decks
+(`fixtures/data/*.json` describing the tree, `fixtures/generate.mjs`
+producing `*.yjs` updates). The Rust core must load each and produce the
+same JSON view (`slides_core_json_read`). Docs' rule, adopted unchanged.
+
+---
+
+## 3. The editor, presenting, and the remote
+
+### 3.1 Editor
+
+A JS bundle built like Docs' (`fixtures/build-bundles.mjs`, esbuild,
+committed hashed output, embedded by `assets.go`), ProseMirror plus yjs,
+no framework. Three panes on a desktop: the **strip** (slide thumbnails,
+drag to reorder, keyboard reorder with a menu equivalent), the **stage**,
+and the **inspector**. On a phone the strip is a horizontal filmstrip
+under the stage and the inspector is a bottom sheet.
+
+The stage shows the server's HTML for the current slide (§4.1) with the
+editor bound to it: each text-bearing block is a ProseMirror view over
+its XmlFragment; canvas blocks get drag, resize handles, snap guides at
+the frame's thirds and other blocks' edges, and z-order from the
+inspector. Flow blocks reorder by drag or by the inspector's move
+buttons.
+
+The inspector is per selection: block props, slide props (layout,
+template, background, transition, phone behaviour), deck props (title,
+theme, accent, fonts). **Every inspector control is one drive operation
+(§7.2)** and the editor calls the same function the API handler calls,
+so a person and an agent can never do different things.
+
+Undo is per person (yjs `UndoManager`, scoped to that client's origin).
+Keyboard: the ordinary text shortcuts, plus `Cmd/Ctrl+Enter` for a new
+slide, `Cmd/Ctrl+D` duplicate slide, `Cmd/Ctrl+Shift+F` present.
+
+### 3.2 The desktop/phone switcher
+
+A three-state control on the stage: **desktop**, **phone**, **both**.
+Phone view renders the stage at 390 CSS pixels wide with the phone
+layer applied. Edits to layout while in phone view go to the `phone`
+override map (§2.5); edits to text go to the text (there is one text).
+A block with an override shows a small badge in both views with "reset
+to desktop" in its inspector. "Both" shows desktop and phone side by
+side, the phone read-only.
+
+### 3.3 Collab
+
+Docs' live-sync spec is adopted as written
+(`oficina/docs`, `docs/superpowers/specs/2026-09-01-live-sync-design.md`
+§3 to §7): `GET /d/{sqid}/stream` SSE emitting `event: update` with a
+seq; bytes by `GET|POST /d/{sqid}/updates`; the cursor+1 rule; broadcast
+on success only; the 15 s ping, 30 s write deadline, 5 minute TTL;
+hibernation-safe because the stream carries no bytes. Slides is the
+second implementation, which is the estilo rule's trigger: whatever
+Docs' hub and stream code turns out to need in both places is extracted
+that week, and the Slides spec records the extraction when it happens.
+
+Presence rides the same stream as a second event type, `presence`,
+carrying `{client, name, colour, slide, block}`; the server holds
+presence in memory only (it is not document state) and drops a client
+after two missed pings. The editor shows avatars on the strip
+thumbnails and a coloured outline on the block another person has
+selected. Docs owns the first build of the presence stack per estilo's
+ownership table; if Docs has not built it when Slides needs it, Slides
+builds it and Docs adopts.
+
+### 3.4 Presenting, speaker notes, the remote
+
+**Presentation session.** A row: `deck`, `presenter` (actor), `slide`
+(index), `blackout` (bool), `started_at`, `ended_at`, `code` (six
+characters, unambiguous alphabet), `remote_token_hash`. Created by
+`POST /d/{sqid}/present`, which returns the session sqid and, once, a
+remote capability URL. Ended explicitly or after 12 hours. One live
+session per deck per presenter.
+
+**Presentation mode.** `GET /p/{session}` is the fullscreen surface:
+the current slide filling the viewport, keyboard (arrows, space, `b`
+for blackout, `n` for notes, `Esc`), swipe, a thin progress bar, and
+the slide's transition. The page subscribes to the session's SSE stream
+(`GET /p/{session}/stream`, `event: slide` with the index and blackout
+state) and applies what it hears, so it is the same page whether the
+presenter's keyboard or the phone moved the deck. It works with scripts
+off as a page per slide with next and previous links (§8).
+
+**Presenter view.** `GET /p/{session}/notes` shows the current slide,
+the next slide, the notes, a clock and elapsed time, and the pairing
+code and its QR. Meant for a second window on a desktop or the
+presenter's phone. Its controls post the same commands as the remote.
+
+**Remote.** `GET /r/{code}` on a phone: enter the code or scan the QR,
+which carries the remote capability URL. The page shows notes, current
+and next slide thumbnails, and **next**, **previous**, **blackout**,
+plus **end**. Commands are `POST /p/{session}/command` with
+`{op: next | prev | goto | blackout | end, slide?}` and the remote
+capability as a bearer (or, scriptless, as a form field). The presenting
+browser applies the resulting `slide` event. Latency target under 300 ms
+on the same continent. The capability lives as long as the session. A
+native remote later speaks exactly `present`, `command` and the
+`stream`; nothing else is needed.
+
+**Audience follow.** A share link (§5) opened while a session is live
+for that deck shows "follow the presenter". Following subscribes the
+viewer to the session stream read-only; the viewer can detach and
+browse, and reattach.
+
+---
+
+## 4. Rendering and export
+
+### 4.1 The HTML renderer
+
+One Go package, `internal/render`, turns a slide (from the Rust core's
+JSON view) into HTML: a `<section slides-slide>` with the template's
+grid or the canvas frame, blocks as semantic elements (`h1` to `h3`,
+`p`, `ul`, `ol`, `blockquote`, `figure`, `pre`, `table`), theme tokens
+as CSS custom properties on the section, and the phone layer as
+`data-phone-*` attributes that the stylesheet honours under the phone
+media query. It serves the viewer, the presenter, the remote thumbnails
+and the editor stage.
+
+The one rule's refinement recorded here: the session had said SVG would
+serve the scriptless viewer. HTML is the better choice for every live
+surface because it reflows, it is accessible without extra work, and it
+is what the editor must bind to. SVG remains for export and thumbnails,
+where a fixed geometry is the point.
+
+### 4.2 The layout engine and SVG
+
+`internal/layout` measures text with the theme's fonts (self-hosted
+TTFs embedded in the binary, metrics via `golang.org/x/image/font/sfnt`)
+and places blocks in a 1920 by 1080 frame, producing a paint list:
+rectangles, images, text runs with font, size, position. `internal/svg`
+serialises a paint list as SVG with text as `<text>` runs and images by
+reference. Thumbnails are the SVG scaled down, served as SVG.
+
+Divergence from the browser is confined to line breaking inside a text
+block and is small when fonts match, the Docs finding. It is not zero
+and this spec does not claim it is.
+
+### 4.3 PDF
+
+`core/` (Rust) gains a second module beside the CRDT: `slides_pdf`,
+wrapping `svg2pdf` with `usvg` and `fontdb` fed the same embedded fonts,
+compiled to wasm32 and run under wazero like the CRDT core. Input: one
+SVG per slide plus the fonts; output: one PDF, 1920 by 1080 points per
+page, with the speaker notes as an optional second page per slide
+("with notes" export). Images are embedded at their stored resolution
+capped at 2000 px on the long edge.
+
+If `svg2pdf` under wasm32 proves unworkable in the plan's spike, the
+fallback recorded now is a Go PDF writer over the paint list directly,
+reusing Docs' font subsetting (`oficina/docs` `internal/pdf`) by
+extraction to a shared module, not by copy.
+
+Export runs as a rastrillo job: `POST /d/{sqid}/export` starts it, the
+status page polls, the result is a presigned GET from the object store
+that expires in one hour. A deck over 200 slides is refused with a
+message.
+
+---
+
+## 5. Sharing and links
+
+Rungs are suite ruling 15's: **private** (the creator), **granted**
+(named people on this instance), **team** (everyone on the instance can
+open it). Every deck starts private. Editing needs sign-in and a rung of
+`granted` or `team`; a `team` deck is editable by every member, which is
+the Docs reading and the one that suits a small team.
+
+A **share link** is a read-only capability, calendar's pattern
+(`oficina/calendar` calendar design §6): `GET /s/{token}` mounted
+outside the session group, only the token's SHA-256 stored, the URL
+shown once at creation, revocable from the deck's share page, with a
+view count. The link opens the viewer (§8) with the deck as it is now,
+not a snapshot. If a presentation session is live, the link offers
+follow (§3.4).
+
+The **present link** is the same capability with `?present=1`, opening
+the fullscreen viewer directly.
+
+An "edit link" does not exist; ruling 15 says an edit capability is a
+sign-in that mints a guest grant, which waits on the home.
+
+---
+
+## 6. Identity and the instance
+
+### 6.1 Lone shape
+
+Sign-in is Docs' `internal/docs/auth.go` adopted whole: rastrillo/auth
+magic links, `SLIDES_ALLOWED_ADDRESSES` gating `POST /signin` in front
+of `Begin` and in `Authorize`, `SLIDES_ORIGIN`, `SLIDES_INSTANCE_KEY`,
+`SLIDES_SMTP_*`, each a startup refusal outside `-env development`. The
+same 303 for permitted and non-permitted addresses. This is the second
+copy of that file, which is the extraction trigger: the plan includes a
+task to propose `auth.go` to rastrillo or estilo as a package.
+
+The person row is `users(id, email, name, colour)` created on first
+sign-in. `sessions.Current(r).Subject` is the email, mapped to the row
+before any scoping, per the rastrillo §5 warning.
+
+### 6.2 Actors
+
+Every write records an **actor**: `person:<user id>` or
+`agent:<agent id>`. Presentation sessions, share links, exports, agent
+tokens and the deck's activity list all carry it. The Y.Doc update log
+carries it too (a column on the updates table, not in the CRDT).
+
+### 6.3 Deploy
+
+One static binary, `CGO_ENABLED=0`, `linux-arm64`, `make release` with
+the version stamped, `carlos deploy --app slides --account oficina`
+with the build probe on `/api/version`. `carlos store create` for the
+object store; `carlos domains attach slides.oficina.works`. Secrets
+`SLIDES_INSTANCE_KEY` and `SLIDES_SMTP_PASS` via `carlos secrets`, the
+rest via `carlos env`, mirroring Docs' set.
+
+---
+
+## 7. The drive surface
+
+### 7.1 Principals
+
+A signed-in person mints an **agent token** from Settings, naming the
+agent (`claude`, `codex`); the app stores `agents(id, name, operator,
+token_hash, created_at, revoked_at)` and shows the token once. The
+agent's principal is `<name>~<operator email>`; every write it makes is
+badged with a robot mark and the operator's name in the activity list,
+following amadan's own convention. A revoked token fails closed. Tokens
+are `Authorization: Bearer` on the API only; the API never reads a
+session cookie and the HTML routes never read a bearer.
+
+### 7.2 Operations
+
+`POST /api/v1/decks/{sqid}/ops` takes a JSON array of operations and
+applies them in order inside one transaction against the Y.Doc through
+the Rust core, producing one update, which is then broadcast exactly as
+a browser's POST would be. Operations, each with the ids of §2:
+
+- deck: `set_meta`
+- slide: `add_slide` (after, layout, template), `remove_slide`,
+ `move_slide`, `set_slide` (props), `set_notes` (text as Markdown
+ subset, converted server-side), `duplicate_slide`
+- block: `add_block` (slide, type, props, text, after or box),
+ `remove_block`, `set_block` (props, box, z, region), `set_text`
+ (Markdown subset), `move_block`, `set_phone` (the §2.5 map)
+- asset: `register_asset` after a presigned upload obtained from
+ `POST /api/v1/decks/{sqid}/uploads`
+
+Reads: `GET /api/v1/decks` (list), `GET /api/v1/decks/{sqid}` (the JSON
+view, with an `ETag` of the seq), `GET /api/v1/decks/{sqid}/slides/{id}.svg`
+(a thumbnail, so an agent can look at what it made),
+`GET /api/v1/decks/{sqid}/export.pdf`. `POST /api/v1/decks` creates.
+
+The editor's inspector calls the same `ops` function in-process through
+the JS side of yjs; the server's `ops` handler calls the Rust core. The
+fixtures (§2.8) include one file per operation so both sides are held
+to the same result.
+
+### 7.3 MCP
+
+An MCP server is a thin adapter over §7.2, deferred until a consumer
+asks for it by name, memoria's rule. The ops are designed so each maps
+to one tool with the same name.
+
+---
+
+## 8. The scriptless floor
+
+With scripts off, a person can: sign in, list decks, create a deck
+(title and theme, from a form), open a deck as a paged viewer (one slide
+per page, next and previous links, notes below when the deck allows),
+present from a share link as the same paged viewer, read the presenter
+notes page, download a PDF, manage sharing, mint and revoke agent
+tokens, and use the remote (each button is a form that POSTs and
+redirects back). They cannot edit slide content; the editor is the one
+surface that needs the bundle, Docs' ruling.
+
+WCAG 2.2 AA with no exemptions, 24 by 24 targets, 320 px reflow (the
+canvas stage may scroll inside its own region in the editor, the estilo
+allowance), colour never alone, every visible string a `rastrillo.T`
+key.
+
+---
+
+## 9. Deliberately not built yet
+
+| Not built | Trigger |
+|---|---|
+| Sign-in through the home; guest grants; edit links | The home's identity layer (home spec §5.3, §9). |
+| Charts from Sheets by S6 | Sheets' S6 server exists and a deck wants a live number. |
+| Video, embeds, audio | A person asks and the platform has a media store policy. |
+| Follow-the-presenter inside Meet | Meet v1 ships. |
+| Native remote | The web remote has been used in anger for a month. |
+| MCP | A consumer names it. |
+| 4:3 and custom aspect | A person asks; the key already exists. |
+| Cross-deck asset library, templates gallery beyond built-ins | Ten decks on one instance. |
+| Comments on slides | Docs' comment thread lands in estilo. |
+| Version history and named snapshots | Docs' snapshots design is adopted rather than reinvented. |
+
+---
+
+## 10. Slices
+
+1. **Live at `slides.oficina.works`:** scaffold, sign-in, deck list,
+ Rust core with the schema and JSON view, HTML renderer for both
+ modes with themes, viewer, share links, presentation sessions with
+ presenter view and phone remote, scriptless floor throughout, the
+ drive API for create and ops, first deploy.
+2. **Editor and collab:** the bundle, live-sync adoption, presence, the
+ switcher, undo, asset upload.
+3. **Export:** layout engine, SVG thumbnails, the PDF module, the export
+ job.
+4. **Extractions:** `auth.go` and the live-sync hub to shared modules;
+ suite discussion outcomes adopted.
+
+Slice 1 is what Paul asked to try first. It is a deck a person or an
+agent can build through the API and present from a phone before the
+editor exists, which is also the order that proves the model before the
+most expensive part is built on it.
+
+---
+
+## 11. Testing
+
+- Rust core: unit tests per operation; fixture replay against the JS
+ generated updates (`fixtures/verify-rust.mjs` pattern from Docs).
+- Renderer: golden HTML per template and per block type, both modes,
+ both viewports; an axe pass in the browser drive.
+- Presentation sessions: a Go test driving two subscribers and a remote
+ command, asserting both see the slide event within the deadline.
+- Share links: token stored hashed, revoked link 404s, live-session
+ follow offered only while live.
+- Drive API: every op has a fixture; a bearer from a revoked agent is
+ refused; the actor column names the agent.
+- Scriptless: the browser drive with JavaScript disabled walks sign-in,
+ create, view, present, remote.
+- Deploy: `/api/version` reports the stamped version on the live host
+ before the deploy is called done.
+
+---
+
+## 12. Review record
+
+Revision 1: unreviewed. The review pass is the first item after slice 1
+ships, so that the reviewer has a running instance and not a document.