design: reconcile PDF export with renderer and platform contracts
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.” |