oficina / slides Public

design: reconcile PDF export with renderer and platform contracts

claude agent, operated by paul@keymail.dev pushed by paul@keymail.dev 0f0fb34abf5e633cdc499e5e388ba3a1f278b4c9 parent 2b17d96
1 file changed, +253 −0
  • docs/superpowers/specs/2026-09-08-export-readiness-addendum.md +253 −0

CI passed — run details

diff --git a/docs/superpowers/specs/2026-09-08-export-readiness-addendum.md b/docs/superpowers/specs/2026-09-08-export-readiness-addendum.md
new file mode 100644
index 0000000..3cffe28
--- /dev/null
+++ b/docs/superpowers/specs/2026-09-08-export-readiness-addendum.md
@@ -0,0 +1,253 @@
+# Slides export implementation contract — draft addendum
+
+LLM design/review material. Unreviewed; no implementation authority is
+claimed from this document. Read with revision 1 of
+`2026-09-07-slides-design.md`, especially §§0.1, 2, 4, 6.2, 7.2, 8 and 12.
+This addendum preserves its product scope and records concrete missing
+contracts. It does not approve the editor, copy, deployment or a dependency.
+
+Source baseline: editor `a863fb6e5d2f2151ce66f898b0bf365695c1f26c`, with
+recovery candidate `2b17d9696344f51fa2d7d978074a874866691b6d` on the design
+branch. Original owner session `2420c44b-a27a-4e0d-bbe7-8b3ca0464c21`
+retains editor/assets work. Slides discussion 1 contains coordination;
+no response has been inferred. CI and Estilo candidates remain separate.
+
+## 1. What stays fixed
+
+A deck has one CRDT block tree. Export consumes the core's JSON view at
+one captured `Deck.Seq`; it does not invent a text-only document or scrape
+an authenticated browser. HTML remains the accessible live renderer.
+A Go layout pass produces fixed geometry, SVG supplies thumbnails, and a
+Rust module under wazero produces the PDF. Runtime remains one cgo-free
+ARM64 binary. Browser automation may validate output, not produce it.
+
+Preserve seven templates (`title`, `title-body`, `two-column`, `section`,
+`image-full`, `quote`, `blank`), flow regions, canvas boxes/z-order,
+backgrounds, six themes/accents, headings, rich text/marks/links, lists,
+quotes, images/fit, columns and their child ownership, code, shapes,
+tables, dividers and spacers. Export is the desktop 1920×1080 composition;
+phone overrides remain in the tree and continue to govern live phone
+rendering. Default export excludes speaker notes. Explicit notes export
+must use the existing signed-in notes authority, never a share/projector
+capability. PDF animations/transitions are not meaningful; their absence
+in live presenting remains a separate existing finding.
+
+## 2. Concrete contradictions and unavailable dependencies
+
+- Revision 1 names `svg2pdf` without a pin. Its upstream repository was
+ archived on July 10, 2026 and now recommends `krilla` plus `krilla-svg`.
+ This is a dependency-maintenance change, not evidence either library
+ works in this app's wasm target. Preserve the SVG/Rust boundary; review
+ a bounded compatibility comparison before selecting a locked backend.
+ Sources: <https://github.com/typst/svg2pdf>,
+ <https://github.com/LaurenzV/krilla>,
+ <https://github.com/LaurenzV/krilla/tree/main/crates/krilla-svg>.
+- Export expects matching embedded TTFs, but `render/themes.go` and
+ `static/slides.css` currently use system/Georgia-family stacks with no
+ embedded font assets. Arbitrary `meta.fonts` can name fonts unavailable
+ to a static server. Screen/export font identity cannot be claimed yet.
+- `render.assetPath` emits `/assets/{id}`, but no serving/upload route,
+ blob store or authorization ledger exists on the pushed editor base.
+ A CRDT `register_asset` string is not evidence of ownership of bytes.
+- `rastrillo/jobs` is an in-memory goroutine registry, four active jobs
+ per owner, fifteen-minute timeout, ten-minute completed-status TTL.
+ It is not a durable platform task or a hibernation lease. Revision 1's
+ job/presigned-result sketch needs restart and result-lifetime behavior.
+- HTML currently scrolls overflowing slides; fixed PDF/SVG cannot
+ silently clip that content. Notes may exceed the proposed single
+ notes page. These need explicit overflow behavior.
+
+## 3. CARLOS PDF service evidence
+
+Fresh platform source is `carlos/platform` main
+`dcf0cad333405aa8990af62c0871bd6ef63d96ba`. `internal/svcrole/svcrole.go`
+and `internal/release/system.go` describe scanning as the first service
+and PDF as future work. No PDF role/binding/client/CLI was found there.
+
+A real Gotenberg infrastructure definition exists in
+`tito/carlos-infrastructure` at
+`99fec157336acbf39305c8ed4a6bba6c976160f1`: `svc-pdf.tf` admits only Tito's
+IE host security group; `svc-pdf-init.sh` starts Gotenberg 8.21.1 behind
+Caddy with separately provisioned basic auth/internal TLS. The platform
+infrastructure design at `5025ba7ffca029d812bf7ca4357bed5b8f870269`,
+`platform-design/superpowers/specs/2026-08-01-tito-production-infra-design.md`
+§9 calls this leased infrastructure for Tito, not a general app API.
+
+Thus “no PDF service exists anywhere” is false, but “Slides has a usable
+PDF service binding” is unproved. Do not obtain Tito credentials, bypass
+certificate checks, widen its security group, or deploy a second service
+as app implementation. No live service availability or authorized
+Oficina binding was verified. A future platform service is an alternative
+only after a supported declared contract and a reviewed architecture
+amendment; it is not a fallback hidden in this implementation.
+
+## 4. Proposed internal interfaces
+
+These are design contracts, not existing exported functions.
+
+1. `ExportInput`: immutable deck view, deck ID/sequence, initiating actor,
+ notes option, resolved font set and authorized asset bytes. Capture
+ through existing `deckFor` or `apiDeckFor` and `openDeck`. Background
+ work must not reload a moving deck or a later notes revision.
+2. `LayoutSlide(ctx, input, slideID)`: consumes existing `deck.Deck`,
+ returns a page paint list plus structured diagnostics. Primitives
+ carry rectangles/paths, clips, images with fit/transform, and measured
+ text runs with font ID, style, baseline, advances and safe link target.
+ No alternate persisted deck representation. Extract reusable theme
+ resolution/geometry constants where HTML and export otherwise copy
+ them; keep CSS/Go parity fixtures. Preserve nested-column ordering and
+ one-render-per-child semantics already enforced by the HTML renderer.
+3. `EncodeSVG(page, resources)`: deterministic XML with escaped content,
+ fixed viewBox and explicit styling. No scripts, `foreignObject`,
+ external URLs, file references or network font loading. Embed verified
+ raster bytes and required font data for a self-contained thumbnail.
+ Link destinations remain annotations, not resource fetches.
+4. `PDF.Render(ctx, pages, fonts)`: separate compiled wasm module and
+ request-local instance, host-supplied bytes only. No WASI filesystem,
+ network, browser imports, system fonts or threads. Explicit ABI/version,
+ input/output lengths, memory cap, cancellation and module closure.
+ Native build success is not the wasm acceptance test. Page geometry is
+ explicitly 1920×1080 PDF points, not an implicit 96-DPI conversion.
+5. `Assets.ForDeck(ctx, deckID, assetID)`: authorization before bytes.
+ Resolve only a completed verified upload ledger entry bound to this
+ deck, then `blobs.Store.Get` on its recorded content hash. Do not fetch
+ `Asset.Ref` as a URL or accept another deck's hash because the bucket
+ is shared. The original asset owner must supply/approve this ledger
+ seam; export cannot work around it.
+
+Decode accepted images with byte/pixel limits before allocation; reject
+unsupported/unsafe formats, normalize orientation, preserve alpha and
+cover/contain/crop, cap the long edge at 2000px per revision 1. A missing,
+unauthorized, unreadable or unsupported referenced asset fails export;
+never quietly issue an incomplete PDF. Exact accepted formats and limits
+must be shared with upload code, not selected independently here.
+
+## 5. Fonts and fidelity decision required
+
+Recommend extracting Docs' existing embedded font assets/parser into a
+shared module with Docs as the first unchanged consumer, then Slides as
+the second. Current Docs source `f6cf5c235ed80effaca45d0c2c2b12331b8e56d0`
+contains `internal/fonts` with Liberation Sans/Serif/Mono, four faces each,
+SIL OFL license and real font/subsetting tests; it is internal and cannot
+be imported directly. No source copy or invented published module pin.
+
+That supplies usable bytes, not permission to pretend Liberation is an
+existing user's Georgia or arbitrary custom font. The reviewer must
+resolve one of these explicit contracts before layout implementation:
+
+- Curated deterministic font resolution adopted by BOTH Slides HTML and
+ export, with a reviewed alias/default mapping and unchanged CRDT names.
+ Existing screen layout may change; demonstrate representative before/
+ after decks and record that effect. Unknown selected families refuse
+ export until explicitly resolved, rather than silently substituting.
+- Keep current HTML font resolution and make export substitution explicit
+ in approved UI/reporting. This weakens parity and is not preferred.
+
+No new font-picker choices, labels or warnings are written in this draft.
+Glyph coverage and complex-script shaping must be tested; `sfnt` metrics
+alone are not shaping. Missing glyphs cannot become invisible text. The
+compatibility spike must determine the shared shaping strategy and text
+selection/searchability; do not claim full-script support from Latin tests.
+
+## 6. Routes, authorization and lifecycle
+
+Existing interfaces: `currentUser` maps the session email to a user;
+`deckFor`/`visibleDecks` authorize owner/grant/team and return404;
+`apiPrincipal`/`apiDeckFor` resolve the bearer and its operator. Actors are
+`person:<id>` or `agent:<id>`. Existing share/projector links reveal no
+speaker notes. Preserve these rulings rather than add export-wide grants.
+
+Proposed session flow retains `POST /d/{sqid}/export` and framework job
+status with scriptless refresh. The job result location is an app-owned
+completion/download route, not an arbitrary URL. Record export ID, deck
+ID/seq, actor, requester, options, result `blobs.Ref` and expiry in an
+app-owned metadata row; bytes stay in the object store. Reauthorize the
+requester and deck at completion/download before signing a URL. Agent
+jobs cannot use an email-only session handler without an actor-aware
+adapter; authorize that result surface with the original bearer identity.
+
+`blobs.S3FromEnv()` in rastrillo v0.26.0 binds
+`CARLOS_STORE_{BUCKET,REGION,ENDPOINT,ACCESS_KEY,SECRET_KEY}` and returns
+`(nil,nil)` when absent. `Store` exposes Put/Get/Delete;
+`*blobs.S3.PresignGet(hash, ttl)` is separate, not in the Store interface.
+Use a narrow result-signer interface. No production Dir/Inline fallback
+for PDFs or images, no account-level store grant assumed.
+
+A presigned GET is a capability until expiry; revoking a deck grant does
+not revoke a URL already issued. Keep revision 1's one-hour maximum only
+with that explicit limitation. URL expiry is NOT object deletion. Result
+retention/cleanup and encryption-at-rest compatibility need a shared
+storage ruling; content-addressed dedup means one expired export cannot
+blindly delete a blob still referenced by another record. Do not silently
+store plaintext exports indefinitely or delete shared objects on a timer.
+
+`jobs.Start` cannot survive process loss. Proposed minimum contract:
+interrupted work is not resumable or presented as completed; a fresh
+explicit request can recreate the same captured output, while stale
+running metadata is marked interrupted under an app boot rule. Never add
+an unsupported hibernation exemption or claim durable background jobs.
+Whether to retain this restartable job flow or adopt an actual durable
+platform worker is a design decision, not an implementation detail hidden
+in a goroutine. Job error/progress/name text reaches users and requires
+copy review; existing framework strings may only be reused verbatim.
+
+Revision 1's agent `GET /api/v1/decks/{sqid}/export.pdf` also needs a
+reviewed relationship to asynchronous generation: direct bounded render
+in the GET request, or explicit POST job creation plus GET result. A GET
+must not mint an invisible durable job on every crawler/retry. Keep the
+named GET compatibility promise in view rather than drop it silently.
+
+## 7. Overflow, resource budgets and acceptance
+
+Default: preserve block geometry and text; do not scale an overflowing
+slide into illegibility, clip unseen content, drop unknown blocks or
+flatten it into paragraphs. Recommend structured overflow diagnostics and
+refusal until fixed. Notes need either explicitly approved continuation
+pages or overflow refusal; a fixed second page cannot silently lose notes.
+These are targeted decisions for this review, with eventual copy gated.
+
+Retain maximum200slides and image2000px edge. The compatibility spike
+must produce measured limits for input/resource bytes, decoded pixels,
+wasm pages, output bytes and overall render deadline before an endpoint
+is enabled. Every stage honors context; a deadline closes its wasm
+instance. Owner job limit alone is not an instance-wide memory bound.
+Require an app-wide concurrency/memory budget, with no partial result
+published after cancellation or failed upload.
+
+Acceptance fixtures cover all seven templates and eleven block kinds,
+flow/canvas, nesting, z-order, theme/colour overrides, links/marks, fonts
+and accents, tables, transparent/cropped images, backgrounds, Unicode,
+missing glyphs, overflow and long notes. Include malicious asset refs,
+foreign-deck hashes, unsafe XML/URLs, denied/deleted grants, revoked
+bearers, cancellation, failed Store.Put, restart and expired results.
+
+For each supported fixture: compare HTML stage, SVG thumbnail and PDF
+raster at fixed geometry with explained line-break tolerance; assert
+page count/order/boxes, complete searchable text, embedded font identity,
+images and notes exclusion/inclusion. Validate PDF with independent
+parsers/renderers, not only its writer or `%PDF` prefix. Prove output
+cannot trigger network fetches. No pixel-identity claim beyond evidence.
+
+## 8. Review decisions and implementation order
+
+The review must return an explicit disposition for:
+D1 backend compatibility-spike selection (archived svg2pdf versus maintained
+successor) while preserving SVG/Rust/wazero;
+D2 shared font/shaping and alias policy;
+D3 asset ledger/upload ownership and storage grant;
+D4 job restart/download/agent GET contract;
+D5 notes and slide overflow behavior;
+D6 export result retention/sealing and one-hour capability limit;
+D7 measured resource limits before enabling handlers.
+Unresolved items are not implied approvals or fresh user-permission gates;
+root coordinates existing owners/rulings and the authorized review process.
+
+After design approval, first executable task is a synthetic standalone
+compatibility spike: locked backend dependencies, wasm/no-host-import
+proof, two composed SVG pages with explicit fonts, geometry/text/image
+checks, cancellation and memory measurements. No application route, live
+service, store or UI. Then shared font/geometry primitives; complete
+layout/SVG fixtures; asset seam integration; PDF assembly; finally jobs,
+authorization, result storage and reviewed copy. Each branch gets its own
+whole-diff review, green gate and owner-coordinated integration. Never call
+a successful text-only PDF or an incomplete asset-free subset “export ready.”