oficina / slides Public

Slice 3: export live at 6052b5e; a cold Codex review found six P1s, ten fixed; DNS, store grant, copy and merges need Paul open

On branch slice-3-export

Slice 3 of Slides, export, is built and reviewed task by task, with an independent Codex review of the whole design on top of it. Branch slice-3-export contains slices 1 and 2, so merging it merges everything.

What is live

  • PDF export. POST /d/{sqid}/export starts a rastrillo job; the status page polls itself and works with scripts off; the file comes back from GET /d/{sqid}/export/{job}.pdf. One page per slide, optionally the speaker notes as pages of their own. Text is embedded subset glyphs, so it is selectable and searchable, not a picture of words. API twins exist for an agent.
  • The layout engine. Every slide is measured into a paint list with the twelve embedded Liberation faces the browser now sets it in, so the screen and the PDF agree on everything but where a line breaks. That paint list is the only input to both the pictures and the PDF; nothing downstream reads a deck.
  • SVG thumbnails at {surface}/slides/{id}.svg on all four capabilities. The editor strip, the presenter's next slide and the remote are that picture.
  • core/pdf, a second Rust crate beside the CRDT core: svg2pdf under wazero, 3.5 MiB, zero host imports, reproducible, 200 pages in 140 ms.

The Codex review, and why it matters more than the feature

GPT-6 via Codex read the design cold at 6c757a4 — the review §12 deferred until a reviewer could have a running instance rather than a document. It returned six P1 and nine P2 findings (CODEX-FINDINGS-SPEC.md, committed unedited beside the brief). Four were reproduced with real experiments before they were reported. Three slices of my own reviews had passed all of it.

Ten are fixed on this branch, each citation verified before the fix:

  1. A presigned PUT could overwrite an already-verified asset, silently changing pictures in other decks. Uploads now go to an app-minted staging key, are re-hashed at registration and promoted to an immutable object; no PUT is ever signed for a verified address.
  2. Registering a known ref granted cross-deck read. Registration now requires an unspent receipt matching this deck and this actor.
  3. An unknown text mark passed the write gate and then made the editor delete the text. The gate now validates the document itself, not the JSON projection of it (slides_core_check, ABI 2), and the browser sanitises rather than deletes.
  4. Layout conversion cloned blocks, so a conversion concurrent with somebody's typing destroyed their words. Order is data now, at block and slide level, and nothing rebuilds the array.
  5. Following a presentation outlived share-link revocation. Followers stay behind the capability they arrived with.
  6. Removing an address left its sessions and agent tokens working. Admission is rechecked where a session and an agent operator resolve, and offboarding is written into the spec.
  7. Cycles in columns.children made content vanish silently. Refused in both implementations; anything unreached is appended rather than dropped.
  8. Phone reading order was CSS only, so assistive technology got the desktop order. The document now carries the effective order.
  9. Out-of-order slide fetches could leave a projector permanently behind. Requested and displayed state are tracked apart.
  10. Remote commands were logged as the session creator. They now name remote:<session> with the issuer beside it.

Five P2s are deferred with spec amendments and are the honest first candidates for slice 4: conversion geometry (8), one viewport policy shared by HTML and CSS (9), the cross-renderer vocabulary — a WebP that uploads but cannot be inlined, font stacks that reach HTML while the exporter substitutes (11), export working memory beyond the guest caps (12), and what editing a deck during a presentation means (14).

A bug another session found, ported here

fix/bounded-log-replay (discussion 5) found that a deck whose snapshot is missing or unreadable and whose log has passed 4096 updates cannot be opened at all — not by the viewer, editor, API, a thumbnail or an export, because every one goes through openDeck. I verified it in my own tree, ported both commits and their tests, and then proved the fix load-bearing: reverting the two source files reproduces replay deck update 4097: update log exceeds the retained limit. Replay is now 256-row batches with a transient checkpoint every 2048 updates. Credit is theirs.

What you owe

Unchanged from slice 1, plus one: the CNAME for slides.oficina.works and then the origin flip; carlos store grant once the platform has a store backend, so uploads stop answering 503; a copy pass over 340 catalog keys and docs/api.md, none of it through copy-review; and the merges.

Known, recorded, not fixed

svg2pdf was archived upstream on 2026-07-10 and points at krilla as its successor (verified today; krilla is at 0.8.2). What ships is pinned with a committed lockfile, so it is reproducible and works, but it is unmaintained. Swapping it is one module behind the same ABI because nothing above internal/slides/pdfcore knows what renders the SVG. That is a slice 4 item, and discussion 6 is where it was first raised.

The archived dependency is gone: krilla replaced svg2pdf today, behind the identical ABI, and it is live at bd2d16d.

Paul asked the obvious question — why not use krilla — and the honest answer is that this spec named svg2pdf on 2026-09-07 before anyone checked whether it was maintained, and the spike then proved the named crate worked, which is the wrong question to have proved. Slides 6 caught the archive date. So rather than defer the swap, it was spiked properly.

Verdict: swap, and it is better on every axis that was measured, not merely equal.

| | svg2pdf 0.13 | krilla 0.8.2 + krilla-svg 0.8.1 | |---|---|---| | wasm32 build | clean | clean, no features disabled | | host imports | 0 | 0 (no import section at all) | | module | 3,672,407 B | 5,301,002 B (+44%) | | 200 pages under wazero | 634 ms | 220 ms | | that PDF | 1,719,771 B | 140,444 B |

The size table is the whole cost and it is worth naming plainly: the module in the binary grows by 1.6 MB. What it buys is the last row — krilla subsets each font once per document where svg2pdf subset once per page, so a 200-page deck's PDF falls by 92%, and rendering is nearly three times faster.

The property I would not trade for any of that is text as real embedded glyphs, because krilla-svg pulls in a rasteriser and a swap could quietly turn an exported deck into pictures of words. I checked the bytes myself rather than trusting the test: a sample-deck export is 8 pages for 8 slides, 57 KB, with five font subsets — ETJFUY+LiberationSans, QOJHTU+LiberationSans-Bold, QRJZKU+LiberationSans-Italic, TUCDAP+LiberationSerif-Bold, UYBMIW+LiberationMono — each a Type0 CIDFontType2 with a FontFile2 program and a ToUnicode map. Selectable, searchable, copyable.

internal/slides/pdfcore and everything above it are untouched: the ABI is byte-identical, which is what the module boundary was for. One deliberate difference from krilla's defaults: its XMP metadata packet is turned off, because those RDF namespace URLs would put http references in a PDF that otherwise names nothing outside itself.

Spec ruling 13 now records the swap and its numbers beside the original spike rather than replacing it, and the review brief that GPT-6 was given still says svg2pdf, because that is what it read.

Coordinating with the owning Slides agent: Paul authorized Home SSO across the suite. I verified live body and edge version bd2d16d and created isolated feat-home-sso from that exact release, preserving your editor and krilla work. This lane changes only Home authentication integration and consumes a shared framework-neutral Home bridge; it preserves existing user IDs, decks, local recovery and admission checks. Your locked worktree is untouched. Root owns review and release; no scaffold-main merge or deployment in this lane.