Changelog
v0.1.2 alpha — July 2026 (in flight)#
Quality pass on the PPTX pipeline, hardening MCP tool schemas, and a long-overdue architectural cleanup: brand themes are now tenant-owned rather than hardcoded into the shipped bundle. Mid-July additions target design-product workloads (per the ScaiWave integration): a real theme API, full-bleed slides, per-element styling, tables on slides, and free-position layout.
Added#
-
REST theme API —
GET /v1/themes(visible set with resolved font/colour schemes inline),GET /v1/themes/{id}(full JSON),PUT /v1/themes/{id}(register/update a tenant brand theme; compile-checked before persisting),DELETE /v1/themes/{id}(tenant themes only). MCP peers:scribe_register_theme/scribe_delete_theme(46 → 48 tools). Brand onboarding is a thin child ofoffice:{"extends": "office", "fonts": {…}, "colors": {…}}— everything else inherits. -
Full-bleed image slides —
add_slideacceptsbackground: {image: <asset URI or https URL>}; the image covers the entire slide with no chrome. The pixel-perfect design-export path: render each designed page to PNG, emit one background-image slide per page. URL backgrounds are fetched server-side with the same SSRF guard as image elements; a failed fetch drops the background (decorative) instead of failing the render. -
Per-element styling —
formatting: {font_family?, font_size_pt?, color?, bold?, italic?, alignment?, spacing_before_pt?, spacing_after_pt?}on headings, paragraphs, and slide bullet lists. Overrides theme defaults per element across docx and pptx. Font families outside the theme are permitted; readers fall back through the theme's fallback chain. -
Tables on slides — the docx
tableelement shape (headers+rows) now works insideslide.elements[], rendered via native pptx tables with theme-coloured header rows. -
Positioned slide elements — any slide element accepts
position: {x, y, w, h}(inches). Positioned elements render at exactly that box and are excluded from the body-zone slot flow — the free-layout escape hatch for design tools. 16:9 canvas is 13.33 × 7.5 in. -
Office-native default theme — new
officebuiltin theme uses Aptos (Microsoft Office 2024+ default) with Calibri fallback and a neutral palette. This is the theme every caller gets whenthemeis omitted. If the reader doesn't have Aptos installed, the deterministic fallback chain lands on Calibri → Segoe UI → Helvetica Neue → Arial → sans-serif — so decks render the way most readers expect. -
PPTX layout engine — slide elements now stack sequentially in a theme-declared body zone with heights proportional to estimated content weight, instead of every element rendering at the same fixed
(x=0.5, y=1.5, w=9, h=5)rectangle.paragraph+bullet_liston the same slide no longer overlap. Themes carry per-masterzonesinpptx.masters[]— override defaults per tenant without touching renderer code. -
paragraphon slides — the docxparagraphelement type is now accepted insideslide.elements[]. The renderer parses embedded multi-line markdown, detects lines starting with-/*/•/—/–as real bullets (<a:buChar>) and1./1)as ordered lists (<a:buAutoNum>), and preserves inline**bold**/*italic*on every line including bulleted ones. Content the LLM used to emit as one wall of text now stacks with real bullet indentation. -
Deck-appropriate slide typography — new optional
pptx.typographyblock on themes overrides docx-derived slide sizes.officeships withslide_title: 36pt / slide_subtitle: 22pt / bullet_body: 18pt— matching Microsoft PowerPoint's stock master defaults. -
Image elements accept
url— as an alternative toasset_ref. When a spec carries{type: image, url: "https://..."}, the worker fetches the URL server-side during finalize, subject to SSRF protection (private/loopback/link-local/metadata IPs blocked, hostname re-checked per redirect hop), a 3-redirect ceiling, 10 s timeout, 20 MB size cap, and an image/* content-type allowlist. Failed fetches surface a labeled placeholder in the render ([image: <alt> — fetch failed: <reason>]) instead of tanking the whole deck. Preferasset_reffor repeatable renders;urlis the "just make it work" convenience path. -
Asset resolver walks the whole spec — pre-fix, it only descended into
spec.body[], silently ignoring everyspec.slides[*].elements[]image reference. Now walks body + slides + sheet charts uniformly. -
Tenant-owned themes — new
tenant_themestable stores per-tenant brand themes.resolve_themechecks tenant themes first, then builtins; a tenant asking for a theme that isn't theirs gets a clean404 theme not found(per §15.3 isolation).scribe_list_themesreturns the visible set with each entry stampedscope: "builtin"orscope: "tenant". -
Template placeholder schemas — template registration and listing now carry
placeholder_schema: machine-readable{name, type, required, example, item_fields?}per variable, derived from the template's grammar (FOR x IN xs→ array with the fields referenced inside the loop;IF cond→ boolean;amount.toFixed(2)→ number). Validate a fill before instantiating. -
Template previews —
POST /v1/templates/{id}/preview(MCP:scribe_preview_template) instantiates the template with identity-fill sample variables (scalars show as«name», arrays get two labelled rows, conditions true) and converts to PDF via Gotenberg. Returns download URLs for both files plus the schema and the exact variables used — a template gallery in one call. PDF conversion is best-effort; the filled office file always returns. -
Font embedding — rendered DOCX and PPTX binaries now embed tenant-uploaded fonts (from
POST /v1/fonts) whose family matches the document theme's heading/body/mono faces. DOCX uses ECMA-376 obfuscated embedding (.odttf+fontKey); PPTX uses.fntdataparts +embeddedFontLst. Readers without the brand font installed now see the real face instead of a fallback. Embedding is best-effort and never fails a render. (WOFF/WOFF2 uploads are converted to TTF at upload time — see below — so every uploaded face is embeddable.) -
Completion webhooks —
finalizeandfrom_templateacceptcallback_url; the worker POSTs the completion payload (same shape as the job result, success or failure) there when the job finishes. Best-effort with one retry, 5 s timeout; private-address URLs are refused (same SSRF policy as image fetching). Replaces polling for event-driven callers. Migration0012adds the column. -
Error-code catalogue — new Error codes reference page: every API and worker code, HTTP mapping, and retriability guidance. Codes are stable; match on
code, not message text. -
Batch patch atomicity documented + pinned — multi-operation
PATCH /v1/documents/{id}has always been all-or-nothing (ops validate against an in-memory copy; the new spec version commits only when every op succeeds). Now documented and regression-tested so it stays that way. -
WOFF/WOFF2 font uploads —
POST /v1/fontsnow accepts WOFF and WOFF2 alongside TTF/OTF and decompresses the web wrapper to raw TTF at upload time (fontTools + brotli). The stored face is embeddable in rendered DOCX/PPTX with no client-side conversion; the original wrapper format is recorded in the font's metadata. Corrupt web fonts are rejected with a cleanFONT_INVALID400. -
formattingon slide titles —slide_titleandslide_subtitleelements now accept the same per-elementformattingoverrides as paragraphs and bullet lists (the renderer always supported it; the validator now agrees). Thanks to Team ScaiWave's verification pass for the catch. -
SDKs
0.1.0-alpha.3— Python (sync+async), TypeScript, and .NET gain:list_themes/get_theme/register_theme/delete_theme,preview_template,callback_urlon finalize, andplaceholder_schemaon template models.
Changed#
scailabsmoved off the builtin bundle — it's now a tenant-registered theme owned by the ScaiLabs tenant. Non-ScaiLabs callers can no longer resolvetheme="scailabs"— the error message names the visible set and is marked non-retriable so LLM tool-calling loops break cleanly. Migration path: no action needed for tenants that never referencedscailabs. ScaiLabs-owned docs continue to render unchanged. Third-party docs currently taggedtheme="scailabs"will 404 on next re-render — set the theme explicitly or omit it to get theofficedefault.officeis now the code default everywheretheme=had a default value:scribe_create,scribe_ingest,scribe_instantiate_template, the corresponding REST routes, template + document services. Callers that already passed a theme explicitly are unaffected.- MCP tool schemas advertise
additionalProperties: false— strict-mode agents (Mistral Large in strict mode, recent Claude tool-calling) refuse to emit fields the schema doesn't declare, so invented arguments like{queries: [...]}onscribe_create_documentno longer make it out of generation. Agents that don't enforce at generation time hit a runtime rejection with a structured, LLM-legible error naming the offending field, listing valid fields, and markedNOT a retriable errorto break retry loops. - Sharpened format-mismatch errors — the previous "op 'add_element' requires a docx document, not 'pptx'" message trapped LLM agents in retry loops because it didn't point at the correct alternatives. Now: names the doc's actual type, lists the correct ops + MCP tools for that type, marks the failure non-retriable. Same treatment for slide-op-on-docx and sheet-op-on-non-xlsx.
- Format-specific tool docstrings — every
scribe_add_*tool now leads with**DOCX only.**/**PPTX only.**/**XLSX only.**and points at the correct alternative family. Well-behaved LLMs pick the right tool at discovery time instead of hitting a 422.
Fixed#
- PPTX bullets rendered as literal dashes in slides that came through the
paragraphelement type. Root cause:bullet: {type: "bullet"}in pptxgenjs is silently no-op (onlytype: "number"is handled). Switched tobullet: truefor default bullet characters — real<a:buChar char="•"/>markers now emit correctly. - Slide element overlap on the
contentmaster when both a subtitle and a body element were present — the body zone started at y=1.5 while the subtitle occupied y=1.3-1.9. Now zones are non-overlapping by construction.
Operator notes#
- Migration required:
alembic upgrade headapplies0011_tenant_themeswhich creates the table + seeds the scailabs theme JSON as owned bytnt_tpljy20lf7svct66. Idempotent to re-run. - Worker restart recommended — the theme registry is loaded via
@lru_cacheat worker startup. Long-running workers from before the migration will hold stale theme state until restarted. - Docs: Themes and the MCP tool catalogue updated.
v0.1.1 alpha — June 2026 (shipped)#
Iterative hardening on top of v0.1.0. No breaking API changes; existing SDKs work unchanged.
Added#
- Native DOCX chart emission — chart parts (
word/charts/chartN.xml) are now hand-emitted via OOXML post-processing for kindsbar,column,line,pie. Charts open in Word and LibreOffice as editable native charts, with series data preserved. Other kinds (area, scatter, sankey, etc.) still go through the ECharts SVG fallback. - Native XLSX chart emission — same coverage matrix; charts anchor to the spec'd cell and reference proper drawing parts.
- Pandoc-merge DOCX ingest — when
pandocis on the worker host, ingest runs pandoc alongside mammoth to detect footnotes, math, citations, definition lists, and line blocks. Findings surface as structured fidelity warnings (INGEST_PANDOC_FOOTNOTE_DETECTEDand friends). No-op when pandoc is absent. - Partner quota rollup —
GET /v1/admin/quotas/partner/{partner_id}?axis=…aggregates usage across every tenant of a partner. Access: superadmin or partner_admin on that specific partner. - Per-user quota rollup —
GET /v1/admin/quotas/tenant/{tenant_id}/users?axis=…breaks down a tenant's usage by acting user. Full coverage forrenders_per_day(windowed) anddocuments_count(via Spec creator);coverage=noneforassets_bytes/templates_count(model has no created_by on those tables yet). - PPTX layout-to-theme-master matching on ingest — slide layouts now recover via OOXML
<p:cSld name>/<p:sldLayout type>against the theme registry instead of always defaulting to"content". - HTML preview for PPTX/XLSX — wraps per-page Gotenberg PNGs into a self-contained HTML preview file.
- OOXML §4.7 breadcrumbs — round-trip equivalence now covers
code_block,callout, andpage_breakon the slow path (when the JSON breadcrumb is absent). - PPTX and XLSX templates (Tier 1) — register a
.pptxor.xlsxwith{{ var }}placeholders, instantiate viaPOST /v1/documents/from_template. Same{{ … }}syntax as DOCX; same dotted-path resolution. Plain substitution only — FOR/IF/JS-expressions are DOCX-only via docx-templates. Missing variables in PPTX/XLSX render as empty + emit aTEMPLATE_VARIABLE_MISSINGwarning instead of erroring (partial fills are common). - Templates admin UI — new
Templatespage in the admin SPA at/admin/templateslists every registered template across the tenants you can see, with per-row expansion showing variable + grammar-token breakdown. Scope: superadmin (all), partner_admin (their partners' tenants), tenant_admin (own tenant only). Backed by the newGET /v1/admin/templatesendpoint. - PPTX + XLSX template Tier 2 — FOR loops + IF conditionals + a safe JS-flavoured expression evaluator now ship in PPTX and XLSX templates. PPTX gets text-frame-level FOR (replicate paragraphs within a shape) AND slide-level FOR (clone whole slides per item, rewiring rels + content-types + presentation.xml). XLSX gets row-level FOR with row renumbering and relative-cell-ref rewriting in formulas. The expression evaluator is a recursive-descent parser (no
eval, no VM) with a per-type method allowlist — supports{{ amount.toFixed(2) }},{{ name.toUpperCase() }}, ternaries, arithmetic, comparisons. Method calls outside the allowlist render empty + emit aTEMPLATE_EXPRESSION_ERRORwarning. - Groups admin surface — new
Groupspage in the admin SPA at/admin/groupsbrowses every ScaiKey group across the tenants you can see. Per-row expansion shows direct + transitive members, parent-group + child-group context. User detail (Users → Roles drawer) now also surfaces the user's group memberships. Backed by three new endpoints:GET /v1/admin/groups,GET /v1/admin/groups/{id},GET /v1/admin/users/{id}/groups. Read-only — group lifecycle stays in ScaiKey; ScaiScribe mirrors via the daily sync. - Per-user job isolation (security) — the non-admin
GET /v1/jobsandGET /v1/jobs/{id}now scope strictly tosubmitted_by == caller. A mortal user in a tenant where colleagues handle sensitive content no longer sees colleagues' job history or result payloads. Migration0010adds thesubmitted_bycolumn onjobs; new jobs stamp it from the auth context at creation. Pre-migration rows (submitted_by IS NULL) are inaccessible via the non-admin endpoints — they only show up in the admin endpoint. Cross-user visibility for operators is unchanged via/v1/admin/jobs. /v1/admin/meenriched — now also returnstenant_name,partner_name, andeffective_roles(a structured view of the caller's superadmin / tenant_admin / partner_admin grants). The admin SPA useseffective_rolesto filter sidebar nav so users don't see menu items for endpoints they can't reach. The backend STILL enforces 403 on every endpoint regardless of what the SPA renders.- Templates upload / download / delete from the admin UI — operators can now register templates on behalf of any tenant in their scope, download the source binary, and soft-delete templates directly from the Templates page. Three new endpoints:
POST /v1/admin/templates(multipart upload withtenant_idform field; counts against the target tenant'stemplates_countquota),GET /v1/admin/templates/{id}/download(presigned URL, ordownload_url: nullfor the file backend),GET /v1/admin/templates/{id}/file(direct stream — the fallback when presigning isn't supported), andDELETE /v1/admin/templates/{id}(soft-delete; binary stays in storage). Authorization: superadmin on any tenant, partner_admin on tenants under their partner, tenant_admin on their own tenant. - SDKs updated to
0.1.0-alpha.2— Python (sync + async), TypeScript, and .NET all gain the seven new endpoints from this iteration: cross-tenant template listing/upload/download/delete, groups listing/detail/user-groups.AdminMenow also carriestenant_name,partner_name, andeffective_roles(structuredis_superadmin/tenant_admin_on/partner_admin_on). The main client getslist_jobs()/listJobs()/ListJobsAsync()(non-admin tenant-scoped, strict per-user isolation) anddelete_template()/deleteTemplate()/DeleteTemplateAsync(). Artifacts live at scailabs.ai/downloads. - MCP — two new
scribe_*tools (44 → 46):scribe_list_jobs(enumerate your pending/recent jobs without remembering IDs) andscribe_delete_template(clean up templates you registered during experimentation). The non-adminDELETE /v1/templates/{id}endpoint backs the new tool — any authenticated user can delete a template in their own tenant.
Changed#
- Chart slow-path recovery and TOC slow-path recovery formally reclassified as fast-path-only by design (the JSON breadcrumb covers ScaiScribe-authored docs; externally-authored docs degrade gracefully with documented fidelity warnings). Not deferrals — closed by design.
Operator notes#
- Run
alembic upgrade headagainst your MariaDB to pick up thequota_periods_usertable (migration0009). - Install
pandoc(apt install pandoc) on the ingest worker hosts to enable the merge audit.
v0.1.0 alpha — June 2026#
First public alpha. Authenticate against ScaiKey, render DOCX / PPTX / XLSX from a JSON spec, ingest existing DOCX, plus a SolidJS admin UI at /admin/.
What works#
- REST API (19 endpoints) — documents, jobs, assets, fonts, templates, health.
- MCP server (42
scribe_*tools) — the primary surface for AI agents. - Three official SDKs:
- Python 0.1.0a1 (
scaiscribeon PyPI) — sync + async clients. - TypeScript 0.1.0-alpha.1 (
@scaiscribe/sdkon npm) — single async client; Node 20+ and browsers. - .NET 0.1.0-alpha.1 (
ScaiLabs.ScaiScribeon NuGet) — .NET 8+.
- Python 0.1.0a1 (
- Admin SPA + sub-client — operator surface for jobs / users / roles / tenants / partners.
- ScaiKey integration — full identity sync, OAuth Authorization Code for the admin UI, OAuth
client_credentialsfor service-to-service. - Themes —
scailabsand per-tenant themes, with single-levelextendsinheritance and a compilation cache. - Templates —
docx-templatesplaceholder fill viafrom_template. - Ingestion — DOCX, PPTX, XLSX with fidelity scoring and structured warnings.
- PDF rendering via Gotenberg.
What works (correcting earlier draft)#
These are shipped and have fixture coverage — they were misclassified as deferred in the initial alpha changelog draft:
- Chart rendering via ECharts SSR — DOCX charts embed as
<w:drawing>SVG images, XLSX charts useexceljs.addImage, PPTX uses nativepptxgenjs.addChart. The "native chart XML" (chart data lives in the workbook) variant for DOCX/XLSX is the remaining v1.x roadmap item. - HTML preview for DOCX — mammoth-based HTML pipeline.
- Per-page screenshots — PDF + pdftoppm.
Known deferrals (v1.x)#
These are honest gaps, not silent omissions — every relevant code path emits a structured warning. Many of these closed in v0.1.1 (see above).
- OCR ingestion — no image-to-text yet.
- ScaiSign / ScaiCMS bridges — integrations come with those products.
Breaking changes#
None — this is the initial release.
Caveats#
- Pin SDK versions during alpha. Minor breaking changes possible through v1.0 GA.
- The spec format is at
schema_version: "0.1". Migration policy starts at v1.0.