v0.1.3 — DOCX/XLSX/PDF report renderers (issue #14)
Completes issue #14 — the three binary report formats — on top of v0.1.2. Reports now render as Markdown (source of truth), HTML, DOCX, XLSX, or PDF via preferences.default_report_format. Each format is a deterministic view over the same provenance-tracked (claims, gaps): the content — every claim, gap, and provenance entry — is identical across all five (SD-06); only the presentation differs. No new security advisories: this is a feature release. 323/323 tests (was 308, +15), typecheck + build green, conformance unaffected (no new egress surface).
What landed
- DOCX / XLSX / PDF renderers (
src/orchestrator/{docx,xlsx,pdf}.ts) — deterministic views over the same(claims, gaps):- DOCX — WordprocessingML package: headings, a clearly-labeled model-narrative Summary, findings grouped by task with inline claim refs, an explicit Gaps section, and a bordered provenance table. XML-escaped; multi-line claim text keeps its line breaks.
- XLSX — SpreadsheetML: one worksheet per section (Summary when present, Findings, Gaps, Provenance), inline strings; the report title is carried.
- PDF — a minimal PDF 1.4: A4 pages, base-14 Helvetica (regular/bold/oblique) with
WinAnsiEncoding, word-wrapped paginated flow (a provenance entry never splits across pages), a page-number footer, and a correct xref table.
- Zero new dependencies. The containers are hand-rolled (the repo keeps the npm install clean — 3 runtime deps): a deterministic STORE ZIP writer (
zip.ts— fixed timestamps + part order, so identical input → byte-identical archive) underpins DOCX/XLSX, plus shared XML escaping (xml.ts). - The binary seam. These are byte artifacts, not text — none has a lossless UTF-8 string form — so the string-based pipeline gained a binary path:
renderReportArtifact(format, input): Buffer(all five) alongside the text-onlyrenderReport;OrchestratorResult/LoopResult/ReportToolResultcarry the artifact in-band asreportArtifactB64(base64).- The File Manager gains a byte-exact
writeBinary(magic-prefix validated: ZIPPKfor docx/xlsx,%PDF-for pdf; journaled as base64 so undo is byte-exact; same confinement/ignore/confirmation gates + audit). A textwrite()to a binary name is refused fail-closed. - Wired through
createOrchestrator, the loop's multi-round consolidation + narrate re-renders (both handle binary), and the plugin'ssandy.report.
- Fail-closed, unchanged law.
REPORT_FORMATSnow lists all five, soloadSandyConfigadmits them; the format check stays fail-closed as defense in depth (a schema/renderer divergence is aConfigError, never a silent Markdown fallback). Provenance/claims are identical across formats (SD-06).
Verification
npm run typecheck && npm test && npm run build→ 323/323 (was 308, +15), green.- The hand-written PDF's structure (xref offsets, startxref, object numbering) was validated directly with a no-dependency check — every xref offset points exactly at its
N 0 objheader, single- and multi-page. - Conformance (in-process + the Docker/Firejail
boundary × modematrix) is unaffected: all rendering and writing is in-process through the existing confined File Manager — no new network/egress surface.