Claude Skill

documents

Generate, inspect, validate, and fix PDF, Word (.docx), Excel (.xlsx), and PowerPoint (.pptx) documents: turn structured content into render-ready artifacts, verify structural and output quality before delivery, and repair broken files. Use when a task involves creating, editing,

LLM Mart · 0 points · 11 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download magnus919-agent-skills-documents-addad86.zip · 34 KB
Part of magnus919/agent-skills — 145 skills

Install

skills CLI npx skills add https://github.com/magnus919/agent-skills/tree/main/documents
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install magnus919-agent-skills@llmmart
Git git clone https://github.com/magnus919/agent-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole magnus919/agent-skills collection as a plugin from our marketplace. Git is the plain clone.

README

Documents — PDF, Word, Excel & PowerPoint Skill

One skill that lets your agent generate, inspect, validate, and fix PDF, Word (.docx), Excel (.xlsx), and PowerPoint (.pptx) files — with a shared workflow, per-format references, and a validation script that verifies output quality before anything ships.

Why Install This Skill

Document output is one of the most common things people ask agents to produce, yet it is easy to get subtly wrong: files that open in one viewer but corrupt in another, spreadsheets with broken cell references, decks with missing slide relationships. This skill packages the full generation-to-delivery loop so your agent produces files that are structurally sound and actually render.

After installing, your agent can turn a markdown brief into a formatted PDF, build a Word report with proper headings and tables, generate a spreadsheet from CSV data, assemble a slide deck from an outline — and then run the included validation script on every artifact to prove it is well-formed before you ever open it. Because the four formats share one workflow, one skill covers them all; you do not need four overlapping skills with four sets of instructions to maintain.

What You Get

Directory Purpose
SKILL.md Shared six-step workflow (scope → content model → template → render → validate → deliver) with per-format load-on-demand
references/pdf.md PDF generation, tooling, and validation specifics
references/word.md Word (.docx) package layout, generation, and validation specifics
references/excel.md Excel (.xlsx) workbook structure, generation, and validation specifics
references/powerpoint.md PowerPoint (.pptx) deck structure, generation, and validation specifics
references/output-quality.md Cross-format output-quality checklist for all four formats
scripts/validate-documents.py Stdlib-only validation script: structural sanity + optional render check, with --json output and graceful degradation when no renderer is installed
templates/pdf-template.md Fillable generation template for PDF (print-ready HTML/CSS or LaTeX)
templates/word-template.md Fillable generation template for Word documents
templates/excel-template.md Fillable generation template for Excel workbooks
templates/powerpoint-template.md Fillable generation template for PowerPoint decks
fixtures/ One small valid sample per format, used to smoke-test the validation script

Quick Start

# Validate a finished artifact (human report)
python3 scripts/validate-documents.py report.pdf

# Validate with a render check and machine-readable output
python3 scripts/validate-documents.py --render-check --json report.pdf data.xlsx deck.pptx

# Smoke-test the script against the bundled per-format fixtures
python3 scripts/validate-documents.py --json fixtures/sample.pdf fixtures/sample.docx fixtures/sample.xlsx fixtures/sample.pptx

The render check uses pdftoppm (poppler-utils) for PDF and LibreOffice for Office formats when they are installed. When neither is present, validation still performs full structural checks and reports the render check as unavailable instead of failing — no renderer required to use the skill.

Triggers

Load this skill when the user mentions any of:

  • Generating documents: "create a PDF report", "make a Word document", "turn this CSV into a spreadsheet", "build a slide deck"
  • Editing documents: "update the docx", "change the Excel file", "fix this presentation"
  • Extracting from documents: "read the text from this PDF", "pull the table out of this xlsx"
  • Converting: "docx to PDF", "export this data as an Excel file"
  • Validating: "check that this document is valid", "why won't this file open", "verify the output before sending"

Do not load for ebooks (use the epub skill), image/video/media production, or data pipeline work (use data-engineering).

Requirements

  • Python 3.8+ — the validation script uses only the standard library.
  • Optional renderers (only for the render-check step): pdftoppm/mutool/ghostscript for PDF, libreoffice/soffice for Office formats. Generation libraries such as python-docx, openpyxl, or python-pptx are optional per format and documented in the references.

Skill manifest

Documents — PDF, Word, Excel & PowerPoint Skill

One skill for the four most common document formats. All four share a single agent workflow — structured content in, render-ready, validated artifact out — so they live in ONE family skill with per-format references, following the epub precedent. Load the shared workflow below, then pull the per-format reference for the format you are actually touching.

Format Extension Reference (load on demand)
PDF .pdf references/pdf.md
Word .docx references/word.md
Excel .xlsx references/excel.md
PowerPoint .pptx references/powerpoint.md
All formats — references/output-quality.md

Generation templates for each format live in templates/, and the validation script with per-format fixtures lives in scripts/.

When to use

Load this skill when the task involves any of the four formats:

  • Generate: build a report, memo, spreadsheet, or deck from structured content (markdown, JSON, data tables, outlines).
  • Edit: modify an existing document's content, layout, or metadata in place.
  • Extract: pull text, tables, or structure out of an existing file.
  • Convert: move content between formats or from a data source into a document.
  • Validate: check that a produced artifact is structurally sound and will render correctly before it is delivered.

When not to use

  • Ebooks and EPUB — use the epub skill; it owns the EPUB container, reading order, and package validation.
  • Images, video, and other media — this skill covers document formats only; route media production to the appropriate media skills.
  • Code and API documentation sites — use the technical-documentation and documentation-site conventions, not office documents.
  • Data pipelines — moving or transforming raw data belongs to data-engineering; Excel here is a deliverable format, not a data store.
  • Office documents to Markdown — converting an existing office document (docx, xlsx, pptx, pdf, odt, rtf, epub, csv) to GitHub-Flavored Markdown belongs to the anydoc skill; this skill owns generation, editing, and validation, not document-to-markdown extraction.

The Shared Workflow

Every document task follows the same six steps, regardless of format. Deep format-specific detail is deferred to the per-format reference — read it at the step where it matters.

1. Scope

Pin down what the document is for before touching a file:

  • Audience and purpose — who reads it and what decision it supports.
  • Format — PDF (fixed layout, print, archival), Word (editable prose, review), Excel (data, calculations), PowerPoint (presentation).
  • Boundaries — page/slide count, size limits, brand or style constraints.
  • Source of truth — the structured content the document is generated from (markdown, JSON, CSV, outline), so the artifact is reproducible.

2. Content model

Represent the document's content as structured data before rendering:

  • A title, sections/headings, body text, and metadata for prose documents.
  • A table model (headers, rows, column types) for spreadsheets.
  • A slide outline (title + bullets per slide, speaker notes) for decks.
  • Keep content and layout separate: content in the model, layout in the template. This is what makes regeneration cheap.

3. Template

Choose the generation template for the target format from templates/:

Fill the [fill: ...] markers in the template with content from the content model. Templates are the contract between content and layout — changing the template is how you change appearance without touching content.

4. Render

Produce the artifact file:

  • PDF — render the template to PDF (print CSS in a browser or engine, or a LaTeX toolchain). See references/pdf.md for tooling.
  • Word / Excel / PowerPoint — write the OOXML package directly (stdlib zipfile + XML for small artifacts) or with the conventional library for the format (python-docx, openpyxl, python-pptx). See the per-format reference for the exact package layout to produce.

5. Validate

Never deliver unvalidated output. Run the validation script:

python3 scripts/validate-documents.py --render-check --json report.pdf brief.docx data.xlsx deck.pptx

The script performs structural sanity (container signatures, required parts, XML well-formedness) and, when a renderer is installed, a render check (actually renders the file). When no renderer is present it reports unavailable instead of failing — validation never hard-requires a renderer. See references/output-quality.md for the full output-quality checklist, and the fixture files in fixtures/ (one per format) to smoke-test the script itself:

python3 scripts/validate-documents.py --json fixtures/sample.pdf fixtures/sample.docx fixtures/sample.xlsx fixtures/sample.pptx

6. Deliver

Hand off the artifact with its provenance:

  • The source content model (so it can be regenerated).
  • The template version used.
  • The validation result (structure passed; render checked or unavailable).
  • Any known deviations (fonts substituted, images downscaled, layout drift).

Exit conditions

The task is complete when the artifact exists, passes structural validation (and the render check when a renderer is available), and the content matches the agreed scope. Stop after delivering the validated artifact with its provenance; do not keep iterating on layout without a new scope instruction.

Scripts

All scripts live in scripts/ relative to this skill's directory and follow cli-builder conventions: --json for machine output, non-interactive, errors to stderr. Run with --help for full flag details.

validate-documents.py — Structural Sanity + Render Check

python3 scripts/validate-documents.py report.pdf            # human report
python3 scripts/validate-documents.py --json report.pdf    # machine report
python3 scripts/validate-documents.py --render-check --json report.pdf data.xlsx deck.pptx

Behavior:

  • Structural sanity per format: PDF header/EOF/page objects; OOXML ZIP container, [Content_Types].xml, required parts, XML well-formedness. Legacy .doc/.xls/.ppt files are recognized via OLE2 magic bytes.
  • Render check (--render-check): renders PDF via pdftoppm/mutool/gs and Office formats via LibreOffice. Reports unavailable — exit 0 — when no renderer is installed (graceful degradation, never a crash).
  • Exit codes: 0 all pass (or render check unavailable); 1 a file fails structure or rendering; 2 usage/I/O error.
  • JSON output: top-level status (ok / fail / unavailable / error) with per-file checks and render results.

Related skills

  • epub — ebook container skill; the sibling family-skill precedent for this format family.
  • data-engineering — data pipelines and transformation; Excel is a deliverable format here, not a data store.
  • cli-builder — the CLI conventions the validation script follows (--json, non-interactive, exit codes).
Files (agent-skills)
  • evals
    • evals.json 8.6 KB
      {
        "schema_version": 1,
        "skill_name": "documents",
        "evals": [
          {
            "id": "pdf-report-generation",
            "prompt": "Generate a one-page PDF report from this markdown: '# Q3 Revenue Summary', '## Highlights', '- Revenue grew 12% quarter over quarter', '- Two new enterprise customers closed', '## Outlook', 'Flat growth expected in Q4 due to seasonality.' The report needs a title, a highlights section, and an outlook section, and it must open reliably in any PDF reader.",
            "expected_output": "A PDF file (report.pdf) produced from the markdown content model through the shared workflow: scope, content model, template, render, validate, deliver. The artifact starts with a %PDF- header, contains page objects, and ends with the %%EOF trailer, so it opens in any reader. Text remains selectable (a real text stream, not a rasterized image). Before delivery the agent runs scripts/validate-documents.py --render-check --json report.pdf, confirms exit 0 with status ok (or render check unavailable when no renderer is installed), and records the validation result with the artifact.",
            "assertions": [
              "The output is a real PDF artifact with a %PDF- header, page objects, and %%EOF trailer",
              "The content model (title, highlights, outlook) maps one-to-one onto the rendered document",
              "The text is selectable rather than rasterized",
              "The validation script is run and its result (ok, or unavailable render check) is reported with the artifact",
              "Delivery records the content source and template used so the report can be regenerated"
            ]
          },
          {
            "id": "word-memo-generation",
            "prompt": "Turn these meeting notes into a Word (.docx) memo with a title, two sections with real heading styles, and a short paragraph in each: Title: 'Product Sync 2026-07-14'. Section 'Decisions' with 'The team adopted the quarterly roadmap and moved the mobile launch to September.' Section 'Action items' with 'Anika to draft the pricing page copy by Friday. Dev to wire the new payment provider by end of month.'",
            "expected_output": "A .docx file (memo.docx) that is a valid OOXML package: ZIP container with [Content_Types].xml and word/document.xml, and the XML well-formed. Headings use heading styles (w:pStyle referencing a style), not manually bolded runs, so the navigation pane and TOC work. Body text lives in w:t runs. The agent runs scripts/validate-documents.py --json memo.docx, confirms exit 0 and status ok, and reports the structure check before delivering.",
            "assertions": [
              "The memo is a .docx OOXML package with [Content_Types].xml and word/document.xml present",
              "Headings use heading styles so the document is navigable, not ad-hoc formatting",
              "Body text is stored in w:t runs",
              "The validation script exits 0 with status ok on the produced file",
              "The delivery notes the styles used and confirms no placeholder text remains"
            ]
          },
          {
            "id": "excel-sheet-generation",
            "prompt": "Build an Excel (.xlsx) workbook from this CSV: Quarter,Revenue\\nQ1,100\\nQ2,150\\nQ3,120. Add a header row, a Total row that sums the Revenue column, and number formatting for the revenue cells. Save as revenue.xlsx.",
            "expected_output": "An .xlsx workbook (revenue.xlsx) with xl/workbook.xml and at least one xl/worksheets/sheet1.xml part, well-formed XML inside a valid ZIP container. The header row holds Quarter and Revenue labels, the three data rows hold the CSV values as real cell values, and the Total row uses a formula (SUM) that also carries a cached value so viewers that do not recalculate still show 370. Revenue cells use a number format. The agent validates with scripts/validate-documents.py --json revenue.xlsx (exit 0, status ok) and spot-checks a cell value against the source CSV before delivery.",
            "assertions": [
              "The workbook is a valid ZIP container with xl/workbook.xml and worksheet parts",
              "The header row, data rows, and Total row are real cells with the CSV values",
              "The Total row uses a SUM formula with a cached result (370)",
              "Revenue cells carry an explicit number format",
              "The validation script exits 0 with status ok and a cell value is spot-checked against the source"
            ]
          },
          {
            "id": "powerpoint-deck-generation",
            "prompt": "Create a 3-slide PowerPoint (.pptx) deck from this outline: Slide 1 title 'Q3 Review', subtitle 'Finance team'. Slide 2 'Revenue' with bullets 'Up 12% QoQ', 'Two new logos'. Slide 3 'Next quarter' with bullets 'Flat growth expected', 'Focus on retention'. Add speaker notes to each content slide and save as q3-review.pptx.",
            "expected_output": "A .pptx deck (q3-review.pptx) with ppt/presentation.xml and three ppt/slides/slideN.xml parts inside a valid ZIP container, all XML well-formed. Every slide ID in the presentation's sldIdLst resolves through presentation.xml.rels to a real slide part, so the deck opens without repair prompts. Slide titles and bullets are text in a:p/a:r/a:t elements within each slide's shape tree, and speaker notes exist for the content slides. The agent validates with scripts/validate-documents.py --json q3-review.pptx (exit 0, status ok) and confirms the slide count matches the outline.",
            "assertions": [
              "The deck is a valid ZIP container with ppt/presentation.xml and three slide parts",
              "Every slide ID in sldIdLst resolves to a real slide part through the relationships file",
              "Titles and bullets are present as slide text elements and match the outline",
              "Speaker notes are included for the content slides",
              "The validation script exits 0 with status ok and the slide count matches the outline"
            ]
          },
          {
            "id": "multi-format-validation-gate",
            "prompt": "A colleague generated four files for a client delivery: report.pdf, brief.docx, data.xlsx, and deck.pptx. Before sending them out, verify every file is structurally sound and will render, and explain what the validation result means including what happens when no renderer is installed on the machine.",
            "expected_output": "A validation pass over all four formats using scripts/validate-documents.py --render-check --json with the four files: report.pdf, brief.docx, data.xlsx, deck.pptx. The agent interprets the JSON report: status ok means every file passed structural checks (PDF header/EOF/page objects; OOXML ZIP container, content types, required parts, XML well-formedness); per-file render results are ok when a renderer produced output, and unavailable when no renderer is installed for that format, which is reported honestly rather than treated as a defect. Any file that fails structure or rendering is flagged with its failed checks and the agent fixes or rebuilds it before delivery. The explanation notes exit codes 0 (pass or render unavailable), 1 (failure), 2 (usage error).",
            "assertions": [
              "All four files (pdf, docx, xlsx, pptx) are validated in one pass with the shared script",
              "The report distinguishes structural status (ok/fail) from render status (ok/unavailable)",
              "A missing renderer is reported as unavailable, not as a document failure",
              "Failed checks are identified per file and the artifact is fixed or rebuilt before delivery",
              "Exit codes are explained: 0 pass, 1 failure, 2 usage error"
            ]
          },
          {
            "id": "docx-text-extraction",
            "prompt": "A teammate has a .docx file with a draft policy and wants the full text pulled out as clean plain text, plus a check that the document structure (headings and paragraphs) is intact. Extract the text and report on the document's structural health.",
            "expected_output": "An extraction pass over the .docx that reads word/document.xml, pulls w:t text in document order (headings and paragraphs preserved in reading order), and returns clean plain text. The same pass validates the document structurally with scripts/validate-documents.py: the ZIP container is a real archive, [Content_Types].xml is present, word/document.xml is well-formed XML, and heading styles are present so the outline is intact. The report states the text extraction result and the structural health verdict (ok, or the specific failed checks if the file is corrupt).",
            "assertions": [
              "Text is extracted from word/document.xml in document order",
              "Headings and paragraphs are preserved in the extracted plain text",
              "The structural validation runs and reports the ZIP, content-types, and document.xml checks",
              "The report separates extraction success from structural health",
              "Corrupt files are diagnosed with their specific failed checks rather than a generic error"
            ]
          }
        ]
      }
      
  • fixtures
    • sample.docx 972 B · in bundle
    • sample.pdf 600 B · in bundle
    • sample.pptx 1.7 KB · in bundle
    • sample.xlsx 1.6 KB · in bundle
  • references
    • excel.md 3.9 KB
      # Excel (.xlsx) — Generation & Validation Reference
      
      > **Last Updated:** 2026-08-03
      
      Load this reference when the target format is **Excel** — generating an .xlsx
      workbook, modifying one, or validating a spreadsheet artifact. It complements
      the shared workflow in `SKILL.md`; this file is the Excel-specific detail for
      steps 3-5 (template, render, validate).
      
      ## XLSX fundamentals
      
      An .xlsx file is an **OPC ZIP archive**:
      
      - **`[Content_Types].xml`** — content types for workbook and worksheet parts.
      - **`_rels/.rels`** — package relationships; points at `xl/workbook.xml`.
      - **`xl/workbook.xml`** — sheet list (`<sheets><sheet name=... sheetId=...
        r:id=.../>`); the workbook-level relationships file
        `xl/_rels/workbook.xml.rels` maps each `r:id` to a worksheet part.
      - **`xl/worksheets/sheetN.xml`** — cell data: `<sheetData>` with `<row>` and
        `<c r="A1">` cells. Cells hold values in `<v>` (numeric) or inline strings
        via `<is><t>`; shared strings live in `xl/sharedStrings.xml` and are
        referenced by index.
      - **`xl/styles.xml`** — number formats, fonts, fills, column widths.
      - **`xl/calcChain.xml`** and formula cells — `<f>` elements hold formulas;
        the `<v>` element holds the **cached** result.
      
      The validation script checks the ZIP container, `[Content_Types].xml`,
      `xl/workbook.xml`, at least one `xl/worksheets/sheetN.xml`, and their XML
      well-formedness.
      
      ## Generation paths
      
      ### openpyxl (recommended)
      
      `pip install openpyxl`, then build from the data model:
      
      ```python
      from openpyxl import Workbook
      wb = Workbook()
      ws = wb.active
      ws.title = "Revenue"
      ws.append(["Quarter", "Revenue"])   # row 1
      ws.append(["Q1", 100])              # row 2
      wb.save("data.xlsx")
      ```
      
      Write headers first, then data rows; let the library handle shared strings and
      styles. For large datasets, consider `write_only` mode to keep memory flat.
      
      ### Raw OPC construction (small, dependency-free artifacts)
      
      For tiny workbooks, write the OOXML package directly with stdlib `zipfile` +
      XML: `[Content_Types].xml`, `_rels/.rels`, `xl/workbook.xml`,
      `xl/_rels/workbook.xml.rels`, and `xl/worksheets/sheet1.xml`. This is what the
      bundled fixture `fixtures/sample.xlsx` does. Keep the
      `xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"` namespace.
      
      ### CSV is not Excel
      
      When the consumer only needs tabular data and never needs formatting,
      formulas, or multiple sheets, a CSV is simpler and more robust than xlsx. Use
      xlsx when the artifact itself is the deliverable.
      
      ## Validation specifics
      
      ```bash
      python3 scripts/validate-documents.py --json data.xlsx
      python3 scripts/validate-documents.py --render-check --json data.xlsx
      ```
      
      Structural checks the script runs for XLSX:
      
      - **ZIP container** — `PK..` magic; the file is a real archive.
      - **Content types** — `[Content_Types].xml` present.
      - **Workbook part** — `xl/workbook.xml` present and well-formed XML.
      - **Worksheets** — at least one `xl/worksheets/sheetN.xml` present.
      - **Text content** — informational check for cell markers.
      
      The render check converts the workbook to PDF via LibreOffice and reports
      `unavailable` when LibreOffice is not installed.
      
      ## Output-quality checklist for Excel
      
      Before delivery, verify:
      
      - **Headers on row 1** — a clear header row with column meaning, so the sheet
        is self-describing.
      - **Values, not just formulas** — every `<f>` formula cell has a cached `<v>`
        result; viewers that do not recalculate show the cached value.
      - **Number formats are right** — dates and currencies use the intended number
        format instead of raw serial numbers where users will be confused.
      - **No broken references** — no `#REF!`/`#VALUE!` errors in cached values.
      - **Column widths readable** — data is not clipped in the default view.
      - **Sheet names meaningful** — default `Sheet1` is a smell for a deliverable.
      
      See [references/output-quality.md](output-quality.md) for the cross-format
      version of this checklist.
      
    • output-quality.md 4.5 KB
      # Output-Quality Validation — All Four Formats
      
      > **Last Updated:** 2026-08-03
      
      Load this reference when you need to **verify a produced document before
      delivery**, regardless of format. It is step 5 (validate) of the shared
      workflow in `SKILL.md`, expanded into a checklist that applies to PDF, Word,
      Excel, and PowerPoint alike, plus the exact behavior of the validation script.
      
      ## Why validate at all
      
      The most common document defects are structural, not typographical: a file
      that opens in one application and corrupts in another, a spreadsheet whose
      cells are invisible to formulas, a deck whose slides do not render. Structural
      validation catches these before delivery, cheaply and deterministically, and a
      render check catches the next class (layout, fonts, overflow) whenever a
      renderer is available.
      
      ## Two layers of checking
      
      ### 1. Structural sanity (always available, stdlib only)
      
      The validation script checks properties that any reader depends on:
      
      | Format | Checks |
      |--------|--------|
      | PDF | `%PDF-` header, `%%EOF` trailer, at least one page object |
      | Word (.docx) | ZIP container, `[Content_Types].xml`, `word/document.xml` well-formed |
      | Excel (.xlsx) | ZIP container, `[Content_Types].xml`, `xl/workbook.xml` + worksheet parts well-formed |
      | PowerPoint (.pptx) | ZIP container, `[Content_Types].xml`, `ppt/presentation.xml` + slide parts well-formed |
      
      These checks need no third-party packages, so validation always runs.
      
      ### 2. Render check (best effort, graceful)
      
      When an external renderer is installed, the script additionally **renders the
      file** — `pdftoppm`/`mutool`/`gs` rasterize PDF page 1; LibreOffice converts
      Office formats to PDF. When no renderer is available for a format, the render
      check reports `unavailable` in the JSON and exits 0: a missing renderer is an
      environment fact, not a document defect. Never block delivery on a render
      check you could not run; say so in the provenance instead.
      
      ## Running the validation script
      
      ```bash
      python3 scripts/validate-documents.py report.pdf brief.docx data.xlsx deck.pptx
      python3 scripts/validate-documents.py --render-check --json report.pdf data.xlsx
      ```
      
      Exit codes:
      
      - **0** — every file passes structural validation (and any attempted render
        succeeded, or no renderer was available).
      - **1** — at least one file fails structure or an attempted render failed.
      - **2** — usage/I/O error (missing or unreadable path).
      
      JSON report (`--json`): top-level `status` is `ok`, `fail`, `unavailable`, or
      `error`; each file carries per-check results and a `render` object. The
      `summary` block gives the pass/fail/skip/error counts.
      
      ## Content-completeness checks
      
      Structure passing does not mean the content is right. Before delivery, confirm
      the artifact matches the agreed scope:
      
      - **Expected sections present** — the scope's headings/slides/sheets all exist.
      - **No placeholder remnants** — no unresolved `[fill: ...]` markers, `TODO`
        text, or lorem ipsum from the template.
      - **Data intact** — for Excel, spot-check cell values against the source data;
        for prose, check a few paragraphs verbatim.
      - **Metadata set** — title/author where the reader displays it.
      
      ## Visual and layout quality
      
      Renderer-dependent, so verify when a renderer is available or by opening the
      artifact:
      
      - **No overflow** — text fits boxes/cells/slides; no clipped content.
      - **No missing glyphs** — fonts embedded (PDF) or available (Office); no
        `tofu` boxes.
      - **Consistent styling** — headings use styles; decks use one layout family.
      - **Page/slide count matches the scope** — no blank trailing pages/slides.
      
      ## Accessibility basics
      
      Accessibility is part of output quality, not a separate concern:
      
      - **PDF** — tagged PDFs with real text (not scans) and a reading order.
      - **Word** — heading styles (they become the navigation/outline), alt text on
        images, real tables with header rows.
      - **Excel** — header rows on row 1, no blank separator rows inside tables,
        meaningful sheet names.
      - **PowerPoint** — alt text on images, sensible shape reading order, notes.
      
      ## The deliverable gate
      
      A document is ready to deliver when:
      
      1. `scripts/validate-documents.py` exits 0 on it (structure passed).
      2. The render check is `ok` (renders) **or** honestly reported as
         `unavailable` (no renderer present).
      3. Content-completeness and visual checks pass against the scope.
      4. The provenance records the content model, template version, and validation
         result, so the artifact can be regenerated and re-verified.
      
      Anything less is a known deviation — record it rather than hiding it.
      
    • pdf.md 4.9 KB
      # PDF — Generation & Validation Reference
      
      > **Last Updated:** 2026-08-03
      
      Load this reference when the target format is **PDF** — generating a
      fixed-layout document, converting content to PDF, or validating a PDF artifact.
      It complements the shared workflow in `SKILL.md`; this file is the PDF-specific
      detail for steps 3-5 (template, render, validate).
      
      ## PDF fundamentals
      
      A PDF file is a linear byte stream, not a container:
      
      - **Header** — `%PDF-1.x` near the start (x = 2..7 in practice).
      - **Body** — numbered indirect objects (`N 0 obj ... endobj`): a catalog
        (`/Type /Catalog`), page tree (`/Type /Pages` with `/Kids` and `/Count`),
        page objects (`/Type /Page`), content streams, and font objects.
      - **Cross-reference table (xref)** — byte offsets of every object, which lets
        readers jump straight to an object; followed by the `trailer` with `/Root`.
      - **`startxref`** — byte offset of the xref table; **`%%EOF`** terminates the
        file.
      
      The validation script checks exactly the properties that break in real life:
      the `%PDF-` header, the `%%EOF` trailer, and the presence of page objects. A
      file that opens in one viewer but not another is almost always a broken xref
      or a stream whose `/Length` does not match its content — see
      `references/output-quality.md` for the cross-format checklist.
      
      ## Generation paths
      
      PDF is a **fixed-layout** format: the author, not the reader, decides where
      every glyph lands. Choose the path by how much layout control you need.
      
      ### Print-ready HTML/CSS (recommended for reports and memos)
      
      Author the document as HTML with print CSS (`@page` rules, page breaks,
      `@media print`), then render to PDF with a print-capable engine:
      
      - **WeasyPrint** (Python, pip installable) — excellent CSS paged-media support;
        embed fonts via `@font-face`.
      - **Headless Chromium** (`--headless --print-to-pdf`) — full CSS support, best
        for complex layouts; pass `--no-pdf-header-footer` for clean output.
      
      Keep the content in the content model (step 2 of the shared workflow), fill
      [templates/pdf-template.md](../templates/pdf-template.md), and render. The
      template is the layout contract; the HTML/CSS is where fonts, margins, and
      page breaks live.
      
      ### LaTeX (best for technical and long-form documents)
      
      Write LaTeX source from the content model and compile with a TeX toolchain
      (`pdflatex`, `xelatex`). Gives precise typography, references, and TOC
      control. Costs: a toolchain dependency and a longer render cycle.
      
      ### Direct PDF construction (small, dependency-free artifacts)
      
      For tiny fixed artifacts (a one-page certificate, a label), a minimal PDF can
      be written by hand with stdlib only: build the objects, compute the xref
      offsets, and write the trailer. Keep streams short and compute `/Length`
      exactly. This is what the bundled fixture `fixtures/sample.pdf` does.
      
      ### What not to do
      
      - Do not fake a PDF by renaming a text file — every PDF must start with the
        `%PDF-` header and end with `%%EOF`; readers will reject anything else.
      - Do not generate a PDF that relies on fonts that will not be embedded;
        unembedded fonts render as garbage or get substituted (see output quality).
      - Do not rasterize text to images unless the document is genuinely a scan;
        text should stay selectable.
      
      ## Text extraction (reading a PDF)
      
      PDF is a rendering format, so "reading" it means extracting text:
      
      - **pypdf** (`pip install pypdf`) — extract text per page: `PageObject.extract_text()`.
      - **pdfminer.six** — more accurate layout-aware extraction for complex layouts.
      - **pdftotext** (poppler-utils) — fast CLI extraction for simple documents.
      
      Extraction quality varies with how the PDF was produced. Scanned PDFs contain
      no text layer at all — they are images; extraction requires OCR, which is
      outside this skill's scope.
      
      ## Validation specifics
      
      ```bash
      python3 scripts/validate-documents.py --json report.pdf
      python3 scripts/validate-documents.py --render-check --json report.pdf
      ```
      
      Structural checks the script runs for PDF:
      
      - **PDF header** — `%PDF-` signature near the start.
      - **EOF marker** — `%%EOF` trailer near the end.
      - **Page objects** — at least one `/Type /Page` object.
      
      The render check renders page 1 to a raster via `pdftoppm` (or `mutool`/`gs`)
      and reports `unavailable` when no renderer is installed.
      
      ## Output-quality checklist for PDF
      
      Before delivery, verify:
      
      - **Pages render** — the render check succeeds; no blank or corrupt pages.
      - **Text is selectable** — a content-stream text marker (BT/ET) is present
        unless the document is intentionally a scan.
      - **Fonts are embedded** — no `missing glyph` boxes; embed via the generator's
        font options.
      - **Page count matches the scope** — no accidental blank trailing pages.
      - **Links and bookmarks** — internal links and the outline are functional.
      - **Metadata** — title/author set where the reader will show them.
      
      See [references/output-quality.md](output-quality.md) for the cross-format
      version of this checklist.
      
    • powerpoint.md 3.6 KB
      # PowerPoint (.pptx) — Generation & Validation Reference
      
      > **Last Updated:** 2026-08-03
      
      Load this reference when the target format is **PowerPoint** — generating a
      .pptx deck, modifying one, or validating a presentation artifact. It
      complements the shared workflow in `SKILL.md`; this file is the
      PowerPoint-specific detail for steps 3-5 (template, render, validate).
      
      ## PPTX fundamentals
      
      A .pptx file is an **OPC ZIP archive**:
      
      - **`[Content_Types].xml`** — content types for presentation and slide parts.
      - **`_rels/.rels`** — package relationships; points at `ppt/presentation.xml`.
      - **`ppt/presentation.xml`** — the deck: `<p:sldIdLst>` lists slide IDs; the
        relationships file `ppt/_rels/presentation.xml.rels` maps each `r:id` to a
        slide part. Also carries slide size (`<p:sldSz cx cy/>`).
      - **`ppt/slides/slideN.xml`** — each slide: `<p:cSld>` with `<p:spTree>`
        (the shape tree). Text boxes are `<p:sp>` shapes whose `<p:txBody>` holds
        `<a:p>` paragraphs with `<a:r>` runs and `<a:t>` text.
      - **`ppt/notesSlides/notesSlideN.xml`** — speaker notes.
      - **`ppt/media/`** — embedded images.
      
      The validation script checks the ZIP container, `[Content_Types].xml`,
      `ppt/presentation.xml`, at least one `ppt/slides/slideN.xml`, and their XML
      well-formedness.
      
      ## Generation paths
      
      ### python-pptx (recommended)
      
      `pip install python-pptx`, then build from the slide outline:
      
      ```python
      from pptx import Presentation
      prs = Presentation()
      slide = prs.slides.add_slide(prs.slide_layouts[1])   # title + content
      slide.shapes.title.text = "Q3 Results"
      slide.placeholders[1].text = "Revenue up 12%\nMargins stable"
      prs.save("deck.pptx")
      ```
      
      Drive the content from the slide outline (title + bullets + notes per slide);
      the layout choice is separate from the content.
      
      ### Raw OPC construction (small, dependency-free artifacts)
      
      For tiny decks, write the OOXML package directly with stdlib `zipfile` + XML:
      `[Content_Types].xml`, `_rels/.rels`, `ppt/presentation.xml`,
      `ppt/_rels/presentation.xml.rels`, and `ppt/slides/slide1.xml`. This is what
      the bundled fixture `fixtures/sample.pptx` does. Keep the
      `xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"`
      namespace and remember that every slide ID in `sldIdLst` needs a matching
      relationship.
      
      ## Validation specifics
      
      ```bash
      python3 scripts/validate-documents.py --json deck.pptx
      python3 scripts/validate-documents.py --render-check --json deck.pptx
      ```
      
      Structural checks the script runs for PPTX:
      
      - **ZIP container** — `PK..` magic; the file is a real archive.
      - **Content types** — `[Content_Types].xml` present.
      - **Presentation part** — `ppt/presentation.xml` present and well-formed XML.
      - **Slides** — at least one `ppt/slides/slideN.xml` present.
      - **Text content** — informational check for `<a:t>` text markers.
      
      The render check converts the deck to PDF via LibreOffice and reports
      `unavailable` when LibreOffice is not installed.
      
      ## Output-quality checklist for PowerPoint
      
      Before delivery, verify:
      
      - **Slide IDs resolve** — every `<p:sldId>` in `sldIdLst` maps through
        `presentation.xml.rels` to a real slide part.
      - **Text fits** — no text boxes overflowing their shapes; keep bullets short.
      - **Images are embedded** — media parts exist in `ppt/media/` and are
        referenced by relationship.
      - **Notes present where required** — speaker notes are part of the deck for
        presentation use.
      - **Slide size is intentional** — `sldSz` matches the intended aspect ratio
        (16:9 vs 4:3).
      - **Reading order** — shapes in `spTree` appear in a sensible order for screen
        readers and tabbing.
      
      See [references/output-quality.md](output-quality.md) for the cross-format
      version of this checklist.
      
    • word.md 3.7 KB
      # Word (.docx) — Generation & Validation Reference
      
      > **Last Updated:** 2026-08-03
      
      Load this reference when the target format is **Word** — generating an
      editable .docx, modifying one, or validating a Word artifact. It complements
      the shared workflow in `SKILL.md`; this file is the Word-specific detail for
      steps 3-5 (template, render, validate).
      
      ## DOCX fundamentals
      
      A .docx file is an **OPC (Open Packaging Conventions) ZIP archive**:
      
      - **`[Content_Types].xml`** — declares the content type of every part.
      - **`_rels/.rels`** — package relationships; points at the main document part.
      - **`word/document.xml`** — the document body: `w:p` (paragraphs) containing
        `w:r` (runs) containing `w:t` (text); `w:tbl` for tables; `w:sectPr` for
        section properties.
      - **`word/styles.xml`** — named styles (`w:style` with `w:styleId`); the
        document references them with `w:pStyle`/`w:rStyle`.
      - **`word/media/`** — embedded images referenced via relationships in
        `word/_rels/document.xml.rels`.
      - **`word/header*.xml` / `word/footer*.xml`** — headers and footers.
      - **`docProps/core.xml`** — metadata (title, author, dates).
      
      Content lives in `word/document.xml`; everything else supports it. The
      validation script checks the ZIP container, `[Content_Types].xml`, the
      `word/document.xml` part, and its XML well-formedness.
      
      ## Generation paths
      
      ### python-docx (recommended for prose documents)
      
      `pip install python-docx`, then build from the content model:
      
      ```python
      from docx import Document
      doc = Document()
      doc.add_heading(title, level=0)
      for heading, body in sections:
          doc.add_heading(heading, level=1)
          doc.add_paragraph(body)
      doc.save("report.docx")
      ```
      
      Use styles (`add_heading` applies built-in heading styles) rather than
      manual formatting so the document stays navigable and re-styleable.
      
      ### Pandoc (markdown → docx)
      
      `pandoc report.md -o report.docx` produces clean, style-based output and is
      ideal when the content model is markdown. Use a reference doc (`--reference-doc`)
      to control styles.
      
      ### Raw OPC construction (small, dependency-free artifacts)
      
      For tiny or highly controlled documents, write the OOXML package directly with
      stdlib `zipfile` + XML: `[Content_Types].xml`, `_rels/.rels`,
      `word/document.xml`, and optional `word/styles.xml`. This is what the bundled
      fixture `fixtures/sample.docx` does. Keep the XML namespaced correctly
      (`xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"`).
      
      ## Validation specifics
      
      ```bash
      python3 scripts/validate-documents.py --json brief.docx
      python3 scripts/validate-documents.py --render-check --json brief.docx
      ```
      
      Structural checks the script runs for DOCX:
      
      - **ZIP container** — `PK..` magic; the file is a real archive.
      - **Content types** — `[Content_Types].xml` present.
      - **Main part** — `word/document.xml` present and well-formed XML.
      - **Text content** — informational check for `w:t` text markers.
      
      The render check converts the file to PDF via LibreOffice (`--headless
      --convert-to pdf`) and reports `unavailable` when LibreOffice is not installed.
      
      ## Output-quality checklist for Word
      
      Before delivery, verify:
      
      - **Styles, not ad-hoc formatting** — headings use heading styles so the
        navigation pane and TOC work.
      - **Tables are real tables** — `w:tbl` structure, not tab-separated text.
      - **Images are embedded** — media parts exist and are referenced, not hot-linked.
      - **No unresolved fields** — update or remove stale TOC/field placeholders.
      - **Spellable, openable text** — text is in `w:t` runs, not encoded oddly.
      - **Header/footer present when required** — page numbers, document title.
      
      See [references/output-quality.md](output-quality.md) for the cross-format
      version of this checklist.
      
  • scripts
    • validate-documents.py 19.1 KB
      #!/usr/bin/env python3
      """Structural sanity and render validation for PDF, Word, Excel, and PowerPoint files.
      
      Checks the four modern document formats the ``documents`` skill generates:
      
        * PDF   -- %PDF- header, %%EOF trailer, at least one page object
        * .docx -- OOXML Word: valid ZIP container, [Content_Types].xml, word/document.xml
        * .xlsx -- OOXML Excel: valid ZIP container, [Content_Types].xml, xl/workbook.xml,
                   at least one xl/worksheets/sheet*.xml
        * .pptx -- OOXML PowerPoint: valid ZIP container, [Content_Types].xml,
                   ppt/presentation.xml, at least one ppt/slides/slide*.xml
      
      Legacy binary Office formats (.doc/.xls/.ppt) are recognized via their OLE2 magic
      bytes and reported as legacy containers with a reduced structural check.
      
      Every check is static and uses only the Python standard library, so validation
      never depends on third-party packages.
      
      Render check (``--render-check``): when an external renderer is installed, the
      script attempts to actually render the file -- pdftoppm/mutool/ghostscript for PDF,
      LibreOffice for Office formats -- and reports whether rendering produced output.
      When no renderer is available for a format the render check reports ``unavailable``
      and exits 0: the missing renderer is not treated as a document defect (graceful
      degradation).
      
      Exit codes:
        0  all files pass structural validation (and any attempted render succeeded,
           or no renderer was available); unsupported extensions are skipped, not failed
        1  at least one file fails structural validation or an attempted render failed
        2  usage or I/O error (missing path, unreadable file)
      """
      
      import argparse
      import json
      import re
      import shutil
      import subprocess
      import sys
      import tempfile
      import zipfile
      import xml.etree.ElementTree as ET
      from pathlib import Path
      
      VERSION = "1.0.0"
      
      FORMAT_BY_EXT = {
          ".pdf": "pdf",
          ".docx": "docx",
          ".xlsx": "xlsx",
          ".pptx": "pptx",
          ".doc": "legacy-word",
          ".xls": "legacy-excel",
          ".ppt": "legacy-powerpoint",
      }
      
      # Required OOXML parts per format; worksheet/slide discovery is regex-based so a
      # workbook can hold any number of sheets.
      REQUIRED_PARTS = {
          "docx": ["word/document.xml"],
          "xlsx": ["xl/workbook.xml"],
          "pptx": ["ppt/presentation.xml"],
      }
      
      SHEET_PATTERN = re.compile(r"^xl/worksheets/sheet\d+\.xml$")
      SLIDE_PATTERN = re.compile(r"^ppt/slides/slide\d+\.xml$")
      PDF_HEADER = re.compile(rb"%PDF-\d\.\d")
      PAGE_OBJECT = re.compile(rb"/Type\s*/Page[^s]")
      PAGES_TREE_COUNT = re.compile(rb"/Count\s+([1-9]\d*)")
      OLE2_MAGIC = b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1"
      
      
      def check(name, ok, detail, fatal=True):
          """Build one check result entry. ``fatal`` marks checks that decide whether
          the file passes; non-fatal checks are informational only."""
          return {"name": name, "ok": bool(ok), "detail": detail, "fatal": fatal}
      
      
      def detect_format(path):
          """Return the logical format for a path, or None when unsupported."""
          return FORMAT_BY_EXT.get(path.suffix.lower())
      
      
      def check_pdf(data):
          """Structural sanity checks for a PDF byte stream."""
          results = []
          results.append(
              check("PDF header", bool(PDF_HEADER.search(data[:1024])), "expected a %PDF-x.y signature near the start")
          )
          results.append(
              check("EOF marker", b"%%EOF" in data[-2048:], "expected a %%EOF trailer marker near the end")
          )
          # Page objects may be stored in compressed object streams (PDF 1.5+ ObjStm),
          # which hides their "/Type /Page" bytes from a raw scan. The Pages tree's
          # /Count entry stays legible in the common case, so accept either signal.
          raw_pages = len(PAGE_OBJECT.findall(data))
          count_match = PAGES_TREE_COUNT.search(data)
          tree_pages = int(count_match.group(1)) if count_match else 0
          total_pages = max(raw_pages, tree_pages)
          results.append(
              check("page objects", total_pages >= 1,
                    "found %d raw page object(s) and a /Count of %d in the page tree"
                    % (raw_pages, tree_pages))
          )
          if total_pages == 0:
              results.append(
                  check("content stream", b"BT" in data and b"ET" in data,
                        "no page objects; checked for a text stream (BT/ET) as a fallback",
                        fatal=False)
              )
          return results
      
      
      def parse_xml_bytes(raw):
          """Parse XML bytes; return (ok, message)."""
          try:
              ET.fromstring(raw)
              return True, "well-formed XML"
          except ET.ParseError as exc:
              return False, "malformed XML: %s" % exc
      
      
      def check_ooxml(data, fmt):
          """Structural sanity checks for an OOXML (docx/xlsx/pptx) byte stream."""
          results = []
          results.append(
              check("ZIP container", data[:4] == b"PK\x03\x04", "expected a ZIP (PK..) container signature")
          )
          try:
              # ZipFile needs a file-like object; BytesIO keeps the check in-memory.
              import io
      
              with zipfile.ZipFile(io.BytesIO(data)) as zf:
                  names = zf.namelist()
                  results.append(
                      check("content types", "[Content_Types].xml" in names,
                            "[Content_Types].xml present" if "[Content_Types].xml" in names
                            else "[Content_Types].xml missing from archive")
                  )
                  for part in REQUIRED_PARTS[fmt]:
                      present = part in names
                      results.append(check(part, present, "present" if present else "missing required part %s" % part))
                  if fmt == "xlsx":
                      sheets = [n for n in names if SHEET_PATTERN.match(n)]
                      results.append(check("worksheets", len(sheets) >= 1,
                                           "%d worksheet(s) found" % len(sheets) if sheets else "no xl/worksheets/sheetN.xml found"))
                  if fmt == "pptx":
                      slides = [n for n in names if SLIDE_PATTERN.match(n)]
                      results.append(check("slides", len(slides) >= 1,
                                           "%d slide(s) found" % len(slides) if slides else "no ppt/slides/slideN.xml found"))
                  # XML well-formedness of the key parts.
                  for part in REQUIRED_PARTS[fmt]:
                      if part in names:
                          ok, message = parse_xml_bytes(zf.read(part))
                          results.append(check("%s XML" % part, ok, message))
                  # Informational: does the content carry any text at all?
                  text_markers = {
                      "docx": b"<w:t",
                      "xlsx": b"<c ",
                      "pptx": b"<a:t",
                  }
                  marker = text_markers[fmt]
                  has_text = False
                  for name in names:
                      if name.endswith(".xml"):
                          try:
                              if marker in zf.read(name):
                                  has_text = True
                                  break
                          except (KeyError, RuntimeError):
                              continue
                  results.append(check("text content", has_text, "text markers found" if has_text else "no text content detected", fatal=False))
          except zipfile.BadZipFile as exc:
              results.append(check("ZIP readable", False, str(exc)))
          except (OSError, RuntimeError) as exc:
              results.append(check("ZIP readable", False, str(exc)))
          return results
      
      
      def check_legacy_ole(data):
          """Structural sanity for legacy binary Office files (.doc/.xls/.ppt)."""
          results = [
              check("OLE2 container", data[:8] == OLE2_MAGIC, "expected a Compound File (OLE2) magic signature"),
          ]
          results.append(
              check("size", len(data) > 512, "legacy OLE container is %d byte(s); the compound file header is 512 bytes"
                    % len(data))
          )
          return results
      
      
      def check_file(path, fmt, data):
          """Run the structural checks for a detected format; returns (status, checks)."""
          if fmt == "pdf":
              checks = check_pdf(data)
          elif fmt in ("docx", "xlsx", "pptx"):
              checks = check_ooxml(data, fmt)
          elif fmt in ("legacy-word", "legacy-excel", "legacy-powerpoint"):
              checks = check_legacy_ole(data)
          else:
              return "skipped", []
          failed = any(item["ok"] is False and item.get("fatal", True) for item in checks)
          return ("fail" if failed else "pass"), checks
      
      
      def find_pdf_renderer():
          """Locate an installed PDF renderer binary, or None."""
          for name in ("pdftoppm", "mutool", "gs"):
              found = shutil.which(name)
              if found:
                  return found
          return None
      
      
      def find_office_renderer():
          """Locate an installed Office (OOXML) renderer binary, or None."""
          for name in ("libreoffice", "soffice"):
              found = shutil.which(name)
              if found:
                  return found
          return None
      
      
      def render_pdf(path, tmpdir):
          """Render the first page of a PDF to a raster; returns a render result dict.
      
          Each supported renderer has a different CLI: pdftoppm takes ``-png``,
          ``mutool`` needs ``draw -o``, and ghostscript needs ``-sDEVICE``. The
          argument list is dispatched per renderer so a machine with any one of the
          three can run the render check.
          """
          renderer = find_pdf_renderer()
          if not renderer:
              return {
                  "status": "unavailable",
                  "renderer": None,
                  "reason": "no PDF renderer installed (pdftoppm, mutool, or ghostscript)",
              }
          name = Path(renderer).name
          prefix = str(tmpdir / "page")
          if name == "pdftoppm":
              cmd = [renderer, "-png", "-r", "72", "-f", "1", "-l", "1", str(path), prefix]
          elif name == "mutool":
              cmd = [renderer, "draw", "-o", prefix + "-%d.png", "-r", "72", str(path), "1-1"]
          elif name == "gs":
              cmd = [
                  renderer,
                  "-dSAFER", "-dBATCH", "-dNOPAUSE",
                  "-sDEVICE=png16m", "-r72",
                  "-sOutputFile=" + prefix + "-%d.png",
                  str(path),
              ]
          else:
              return {
                  "status": "unavailable",
                  "renderer": name,
                  "reason": "unsupported PDF renderer %r (expected pdftoppm, mutool, or gs)" % name,
              }
          try:
              proc = subprocess.run(cmd, capture_output=True, timeout=60)
          except (OSError, subprocess.TimeoutExpired) as exc:
              return {"status": "failed", "renderer": name, "reason": "renderer error: %s" % exc}
          pages = sorted(tmpdir.glob("page*.png"))
          if proc.returncode != 0:
              reason = (proc.stderr or proc.stdout or b"").decode("utf-8", "replace").strip()[:300]
              return {"status": "failed", "renderer": name, "reason": reason or "renderer exited nonzero"}
          if not pages:
              return {"status": "failed", "renderer": name, "reason": "renderer produced no output pages"}
          return {"status": "ok", "renderer": name, "pages": len(pages)}
      
      
      def render_ooxml(path, tmpdir):
          """Convert an Office file to PDF via LibreOffice; returns a render result dict."""
          renderer = find_office_renderer()
          if not renderer:
              return {
                  "status": "unavailable",
                  "renderer": None,
                  "reason": "no Office renderer installed (libreoffice or soffice)",
              }
          try:
              proc = subprocess.run(
                  [renderer, "--headless", "--convert-to", "pdf", "--outdir", str(tmpdir), str(path)],
                  capture_output=True,
                  timeout=180,
              )
          except (OSError, subprocess.TimeoutExpired) as exc:
              return {"status": "failed", "renderer": Path(renderer).name, "reason": "renderer error: %s" % exc}
          pdfs = sorted(tmpdir.glob("*.pdf"))
          if proc.returncode != 0:
              reason = (proc.stderr or proc.stdout or b"").decode("utf-8", "replace").strip()[:300]
              return {"status": "failed", "renderer": Path(renderer).name, "reason": reason or "renderer exited nonzero"}
          if not pdfs:
              return {"status": "failed", "renderer": Path(renderer).name, "reason": "conversion produced no PDF output"}
          return {"status": "ok", "renderer": Path(renderer).name, "pages": len(pdfs)}
      
      
      def attempt_render(path, fmt, tmpdir):
          """Render one file with an installed renderer; graceful when none exists."""
          if fmt == "pdf":
              return render_pdf(path, tmpdir)
          if fmt in ("docx", "xlsx", "pptx"):
              return render_ooxml(path, tmpdir)
          return {
              "status": "unavailable",
              "renderer": None,
              "reason": "no renderer applies to format %r" % fmt,
          }
      
      
      def renderer_available_for(fmt):
          """True when an installed renderer can render the given format."""
          if fmt == "pdf":
              return find_pdf_renderer() is not None
          if fmt in ("docx", "xlsx", "pptx"):
              return find_office_renderer() is not None
          return False
      
      
      def validate_files(paths, render_check=False):
          """Validate every path; returns the report dict (see module docstring)."""
          entries = []
          io_error = False
          for raw in paths:
              path = Path(raw)
              if not path.exists():
                  entries.append({
                      "path": raw,
                      "format": None,
                      "size": None,
                      "status": "error",
                      "checks": [check("exists", False, "path does not exist")],
                      "render": {"status": "not_requested"},
                  })
                  io_error = True
                  continue
              if not path.is_file():
                  entries.append({
                      "path": raw,
                      "format": None,
                      "size": None,
                      "status": "error",
                      "checks": [check("file", False, "path is not a regular file")],
                      "render": {"status": "not_requested"},
                  })
                  io_error = True
                  continue
              try:
                  data = path.read_bytes()
              except OSError as exc:
                  entries.append({
                      "path": raw,
                      "format": None,
                      "size": None,
                      "status": "error",
                      "checks": [check("readable", False, "cannot read file: %s" % exc.strerror)],
                      "render": {"status": "not_requested"},
                  })
                  io_error = True
                  continue
      
              fmt = detect_format(path)
              if fmt is None:
                  entries.append({
                      "path": raw,
                      "format": None,
                      "size": len(data),
                      "status": "skipped",
                      "reason": "unsupported extension %r (expected .pdf, .docx, .xlsx, .pptx)" % path.suffix,
                      "checks": [],
                      "render": {"status": "not_requested"},
                  })
                  continue
      
              status, checks = check_file(path, fmt, data)
              render_result = {"status": "not_requested"}
              if render_check:
                  with tempfile.TemporaryDirectory(prefix="documents-render-") as tmp:
                      render_result = attempt_render(path, fmt, Path(tmp))
              entry = {
                  "path": raw,
                  "format": fmt,
                  "size": len(data),
                  "status": status,
                  "checks": checks,
                  "render": render_result,
              }
              entries.append(entry)
      
          failed = [e for e in entries if e["status"] == "fail"]
          errored = [e for e in entries if e["status"] == "error"]
          skipped = [e for e in entries if e["status"] == "skipped"]
          passed = [e for e in entries if e["status"] == "pass"]
      
          status = "ok"
          exit_code = 0
          if errored and not failed:
              # Missing/unreadable paths are usage-level problems, not document failures.
              status = "error"
              exit_code = 2
          if failed:
              status = "fail"
              exit_code = 1
          elif render_check and not errored:
              # Render-check mode: when no renderer applies to any of the files, the
              # render check is unavailable rather than a failure (graceful
              # degradation). An unsupported file also yields "unavailable": there is
              # nothing to render.
              renderable = [e for e in entries if e["status"] == "pass" and e["format"]]
              if not renderable:
                  status = "unavailable"
              elif not any(renderer_available_for(e["format"]) for e in renderable):
                  status = "unavailable"
              elif any(e.get("render", {}).get("status") == "failed" for e in entries):
                  status = "fail"
                  exit_code = 1
      
          return {
              "tool": "validate-documents.py",
              "version": VERSION,
              "render_check": render_check,
              "status": status,
              "ok": status == "ok",
              "files": entries,
              "summary": {
                  "files": len(entries),
                  "passed": len(passed),
                  "failed": len(failed),
                  "skipped": len(skipped),
                  "errors": len(errored),
              },
              "exit_code": exit_code,
          }
      
      
      def print_human(report):
          """Print a human-readable report to stdout."""
          for entry in report["files"]:
              fmt = entry["format"] or "unknown"
              if entry["status"] == "pass":
                  verdict = "PASS"
              elif entry["status"] == "fail":
                  verdict = "FAIL"
              elif entry["status"] == "error":
                  verdict = "ERROR"
              else:
                  verdict = "SKIPPED"
              print("%s: %s - %s" % (entry["path"], fmt, verdict))
              if entry.get("reason"):
                  print("  reason: %s" % entry["reason"])
              for item in entry["checks"]:
                  mark = "ok" if item["ok"] else "FAIL"
                  print("  [%s] %s: %s" % (mark, item["name"], item["detail"]))
              render = entry["render"]
              if render.get("status") != "not_requested":
                  print("  render: %s (%s)" % (render.get("status"), render.get("reason") or render.get("renderer") or "n/a"))
          summary = report["summary"]
          print(
              "%d file(s): %d passed, %d failed, %d skipped, %d error(s) - overall %s"
              % (summary["files"], summary["passed"], summary["failed"], summary["skipped"], summary["errors"], report["status"])
          )
      
      
      def build_parser():
          parser = argparse.ArgumentParser(
              prog="validate-documents.py",
              description=(
                  "Validate PDF, Word (.docx), Excel (.xlsx), and PowerPoint (.pptx) files: "
                  "structural sanity (container signatures, required parts, XML well-formedness) "
                  "plus an optional render check against an installed renderer. Emits a "
                  "machine-readable JSON report with --json. Exit 0 when all files pass, 1 when a "
                  "file fails validation or rendering, 2 on usage or I/O errors."
              ),
              epilog=(
                  "Examples:\n"
                  "  validate-documents.py report.pdf brief.docx\n"
                  "  validate-documents.py --json report.pdf\n"
                  "  validate-documents.py --render-check --json report.pdf data.xlsx"
              ),
          )
          parser.add_argument("files", nargs="+", metavar="FILE", help="document file(s) to validate")
          parser.add_argument("--json", action="store_true", help="emit a machine-readable JSON report")
          parser.add_argument(
              "--render-check",
              action="store_true",
              help="attempt to render each file with an installed renderer "
                   "(pdftoppm/mutool/ghostscript for PDF, LibreOffice for Office formats); "
                   "reports 'unavailable' when no renderer is present",
          )
          parser.add_argument("--version", action="version", version="%(prog)s " + VERSION)
          return parser
      
      
      def main(argv=None):
          parser = build_parser()
          args = parser.parse_args(argv)
          report = validate_files(args.files, render_check=args.render_check)
          if args.json:
              print(json.dumps(report, indent=2))
          else:
              print_human(report)
          return report["exit_code"]
      
      
      if __name__ == "__main__":
          sys.exit(main())
      
  • templates
    • excel-template.md 1.6 KB
      # Excel (.xlsx) Generation Template
      
      Fill every `[fill: ...]` marker with content from the data model, then
      generate the .xlsx (openpyxl, or raw OPC for tiny workbooks). Delete this
      instruction block after filling.
      
      ## Workbook metadata
      
      - **Workbook title:** _[fill: workbook name]_
      - **Owner / source:** _[fill: team and data source]_
      - **Date generated:** _[fill: YYYY-MM-DD]_
      - **Version:** _[fill: 1.0]_
      
      ## Scope contract
      
      - **Purpose:** _[fill: what decisions this workbook supports]_
      - **Sheet list:** _[fill: one line per sheet: name and content]_
      - **Source of truth:** _[fill: path to the source data (CSV/JSON/etc.)]_
      
      ## Sheet layout — _[fill: sheet name]_
      
      ### Columns
      
      | Column | Header text | Type (number/date/text/currency) | Notes |
      |--------|-------------|----------------------------------|-------|
      | A | _[fill: header]_ | _[fill: type]_ | _[fill: notes]_ |
      | B | _[fill: header]_ | _[fill: type]_ | _[fill: notes]_ |
      
      ### Rows
      
      - **Row 1 header row:** _[fill: yes]_
      - **Data rows:** _[fill: source path or inline rows]_
      - **Formulas:** _[fill: which cells hold formulas and what they compute; every
        formula cell must carry a cached value]_
      
      ### Styling
      
      - **Number formats:** _[fill: currency/date formats per column]_
      - **Column widths:** _[fill: widths so data is not clipped]_
      - **Freeze panes:** _[fill: header row frozen?]_
      
      ## Validation gate
      
      ```bash
      python3 scripts/validate-documents.py --render-check --json output.xlsx
      ```
      
      - **Structure passed:** _[fill: script exit code and status]_
      - **Render check:** _[fill: ok or unavailable, with renderer used]_
      - **Spot check:** _[fill: 2-3 cells compared against source data, confirmed]_
      
    • pdf-template.md 1.7 KB
      # PDF Generation Template
      
      Fill every `[fill: ...]` marker with content from the content model, then
      render to PDF (print-ready HTML/CSS or LaTeX). Delete this instruction block
      after filling.
      
      ## Document metadata
      
      - **Title:** _[fill: document title]_
      - **Author / owner:** _[fill: author or team]_
      - **Audience:** _[fill: who reads this and what decision it supports]_
      - **Date:** _[fill: YYYY-MM-DD]_
      - **Version:** _[fill: 1.0]_
      
      ## Scope contract
      
      - **Purpose:** _[fill: one sentence on what this PDF is for]_
      - **Page budget:** _[fill: expected page count / upper bound]_
      - **Layout engine:** _[fill: print HTML/CSS (WeasyPrint or headless Chromium), LaTeX, or direct PDF]_
      - **Source of truth:** _[fill: path to the content model this is generated from]_
      
      ## Content outline
      
      ### Section 1 — _[fill: section title]_
      
      - **Body:** _[fill: paragraph or bullet content]_
      - **Layout notes:** _[fill: fonts, spacing, page-break constraints]_
      
      ### Section 2 — _[fill: section title]_
      
      - **Body:** _[fill: paragraph or bullet content]_
      - **Layout notes:** _[fill: fonts, spacing, page-break constraints]_
      
      ### Section N — _[fill: section title]_
      
      - **Body:** _[fill: paragraph or bullet content]_
      - **Layout notes:** _[fill: fonts, spacing, page-break constraints]_
      
      ## Assets
      
      - **Images/figures:** _[fill: image paths and placement]_
      - **Fonts:** _[fill: font names; confirm they will be embedded]_
      
      ## Validation gate
      
      ```bash
      python3 scripts/validate-documents.py --render-check --json output.pdf
      ```
      
      - **Structure passed:** _[fill: script exit code and status]_
      - **Render check:** _[fill: ok or unavailable, with renderer used]_
      - **Page count observed:** _[fill: matches scope?]_
      
    • powerpoint-template.md 1.7 KB
      # PowerPoint (.pptx) Generation Template
      
      Fill every `[fill: ...]` marker with content from the slide outline, then
      generate the .pptx (python-pptx, or raw OPC for tiny decks). Delete this
      instruction block after filling.
      
      ## Deck metadata
      
      - **Deck title:** _[fill: presentation title]_
      - **Presenter / owner:** _[fill: presenter or team]_
      - **Audience:** _[fill: who sees this and the setting]_
      - **Date:** _[fill: YYYY-MM-DD]_
      - **Version:** _[fill: 1.0]_
      
      ## Scope contract
      
      - **Purpose:** _[fill: what the talk decides or informs]_
      - **Slide budget:** _[fill: expected slide count]_
      - **Aspect ratio:** _[fill: 16:9 or 4:3]_
      - **Layout family:** _[fill: which layout to reuse across slides]_
      - **Source of truth:** _[fill: path to the outline content model]_
      
      ## Slide outline
      
      ### Slide 1 — Title
      
      - **Title:** _[fill: deck title]_
      - **Subtitle:** _[fill: presenter, date]_
      
      ### Slide 2 — _[fill: section title]_
      
      - **Title:** _[fill: slide title]_
      - **Bullets:** _[fill: one bullet per line; keep short enough to fit]_
      - **Notes:** _[fill: speaker notes]_
      
      ### Slide N — _[fill: section title]_
      
      - **Title:** _[fill: slide title]_
      - **Bullets:** _[fill: one bullet per line; keep short enough to fit]_
      - **Notes:** _[fill: speaker notes]_
      
      ## Assets
      
      - **Images:** _[fill: image paths; confirm they will be embedded in ppt/media]_
      - **Charts/tables:** _[fill: any data visuals and their source data]_
      
      ## Validation gate
      
      ```bash
      python3 scripts/validate-documents.py --render-check --json output.pptx
      ```
      
      - **Structure passed:** _[fill: script exit code and status]_
      - **Render check:** _[fill: ok or unavailable, with renderer used]_
      - **Slide count observed:** _[fill: matches scope?]_
      - **Overflow check:** _[fill: longest bullet verified to fit, or flagged]_
      
    • word-template.md 1.8 KB
      # Word (.docx) Generation Template
      
      Fill every `[fill: ...]` marker with content from the content model, then
      generate the .docx (python-docx, pandoc, or raw OPC). Delete this instruction
      block after filling.
      
      ## Document metadata
      
      - **Title:** _[fill: document title]_
      - **Author / owner:** _[fill: author or team]_
      - **Audience:** _[fill: who reads this]_
      - **Date:** _[fill: YYYY-MM-DD]_
      - **Version:** _[fill: 1.0]_
      
      ## Scope contract
      
      - **Purpose:** _[fill: one sentence on what this document is for]_
      - **Style base:** _[fill: template or style set to build on]_
      - **Source of truth:** _[fill: path to the content model]_
      
      ## Content outline (styles, not ad-hoc formatting)
      
      ### Heading 1 — _[fill: section title]_
      
      - **Paragraph(s):** _[fill: body text, one paragraph per bullet]_
      - **Style to apply:** _[fill: Heading 2 / Normal / List Bullet]_
      - **Table needed:** _[fill: yes/no; if yes, headers and row content]_
      
      ### Heading 2 — _[fill: section title]_
      
      - **Paragraph(s):** _[fill: body text, one paragraph per bullet]_
      - **Style to apply:** _[fill: Heading 2 / Normal / List Bullet]_
      - **Table needed:** _[fill: yes/no; if yes, headers and row content]_
      
      ## Tables
      
      | Table name | Header row | Rows |
      |------------|------------|------|
      | _[fill: name]_ | _[fill: column headers]_ | _[fill: row content or source path]_ |
      
      ## Assets
      
      - **Images:** _[fill: image paths; confirm they will be embedded in word/media]_
      - **Header/footer:** _[fill: page numbers, running title]_
      - **Metadata fields:** _[fill: title/author for docProps]_
      
      ## Validation gate
      
      ```bash
      python3 scripts/validate-documents.py --render-check --json output.docx
      ```
      
      - **Structure passed:** _[fill: script exit code and status]_
      - **Render check:** _[fill: ok or unavailable, with renderer used]_
      - **Headings check:** _[fill: all headings use heading styles, confirmed]_
      
  • tests
    • test_validate_documents.py 9 KB
      #!/usr/bin/env python3
      """Tests for documents/scripts/validate-documents.py.
      
      Runs standalone (``python3 documents/tests/test_validate_documents.py``) and is
      discovered by scripts/check-artifacts.py's ``unittest`` discovery pass. Every
      test is environment-independent: renderer availability is simulated by
      monkeypatching the module's renderer-discovery functions, never by assuming a
      renderer is or is not installed.
      """
      
      import importlib.util
      import json
      import os
      import subprocess
      import sys
      import tempfile
      import unittest
      from pathlib import Path
      
      REPO_ROOT = Path(__file__).resolve().parents[2]
      SCRIPTS_DIR = REPO_ROOT / "documents" / "scripts"
      FIXTURES_DIR = REPO_ROOT / "documents" / "fixtures"
      
      SPEC = importlib.util.spec_from_file_location(
          "validate_documents", SCRIPTS_DIR / "validate-documents.py"
      )
      vd = importlib.util.module_from_spec(SPEC)
      SPEC.loader.exec_module(vd)
      
      FIXTURES = ["sample.pdf", "sample.docx", "sample.xlsx", "sample.pptx"]
      
      
      class StructuralValidationTests(unittest.TestCase):
          def test_all_fixtures_pass(self):
              for name in FIXTURES:
                  with self.subTest(fixture=name):
                      report = vd.validate_files([str(FIXTURES_DIR / name)])
                      self.assertEqual(report["status"], "ok")
                      self.assertEqual(report["exit_code"], 0)
                      self.assertEqual(report["files"][0]["status"], "pass")
      
          def test_each_format_detected(self):
              expected = {
                  "sample.pdf": "pdf",
                  "sample.docx": "docx",
                  "sample.xlsx": "xlsx",
                  "sample.pptx": "pptx",
              }
              for name, fmt in expected.items():
                  with self.subTest(fixture=name):
                      report = vd.validate_files([str(FIXTURES_DIR / name)])
                      self.assertEqual(report["files"][0]["format"], fmt)
      
          def test_json_output_has_status_and_ok(self):
              report = vd.validate_files([str(FIXTURES_DIR / "sample.pdf")])
              self.assertIn("status", report)
              self.assertIn("ok", report)
              self.assertTrue(report["ok"])
              self.assertTrue(json.dumps(report))  # serializable
      
          def test_broken_pdf_fails(self):
              with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp:
                  tmp.write(b"this is not a pdf at all")
                  path = tmp.name
              try:
                  report = vd.validate_files([path])
                  self.assertEqual(report["status"], "fail")
                  self.assertEqual(report["exit_code"], 1)
                  self.assertEqual(report["files"][0]["status"], "fail")
              finally:
                  os.unlink(path)
      
          def test_corrupt_zip_fails(self):
              with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as tmp:
                  tmp.write(b"PK\x03\x04 not really a zip")
                  path = tmp.name
              try:
                  report = vd.validate_files([path])
                  self.assertEqual(report["status"], "fail")
                  self.assertEqual(report["exit_code"], 1)
              finally:
                  os.unlink(path)
      
          def test_missing_file_is_error(self):
              report = vd.validate_files([str(REPO_ROOT / "documents" / "fixtures" / "nope.pdf")])
              self.assertEqual(report["status"], "error")
              self.assertEqual(report["exit_code"], 2)
      
          def test_unsupported_extension_skipped(self):
              report = vd.validate_files([str(REPO_ROOT / "documents" / "SKILL.md")])
              self.assertEqual(report["files"][0]["status"], "skipped")
              self.assertEqual(report["status"], "ok")
              self.assertEqual(report["exit_code"], 0)
      
      
      class RenderCheckTests(unittest.TestCase):
          def setUp(self):
              # Force "no renderer installed" for deterministic degradation tests.
              self.orig_pdf = vd.find_pdf_renderer
              self.orig_office = vd.find_office_renderer
              vd.find_pdf_renderer = lambda: None
              vd.find_office_renderer = lambda: None
      
          def tearDown(self):
              vd.find_pdf_renderer = self.orig_pdf
              vd.find_office_renderer = self.orig_office
      
          def test_render_check_unavailable_without_renderer(self):
              for name in FIXTURES:
                  with self.subTest(fixture=name):
                      report = vd.validate_files([str(FIXTURES_DIR / name)], render_check=True)
                      self.assertEqual(report["status"], "unavailable")
                      self.assertEqual(report["exit_code"], 0)
                      self.assertEqual(report["files"][0]["render"]["status"], "unavailable")
      
          def test_render_check_unavailable_for_unsupported_file(self):
              report = vd.validate_files([str(REPO_ROOT / "documents" / "SKILL.md")], render_check=True)
              self.assertEqual(report["status"], "unavailable")
              self.assertEqual(report["exit_code"], 0)
      
          def test_render_ok_with_pdf_renderer(self):
              vd.find_pdf_renderer = lambda: "/usr/bin/env-test-pdftoppm"
              vd.find_office_renderer = lambda: None
              # Renderer discovery is stubbed; render_pdf is stubbed to success so no
              # real binary is required.
              original = vd.render_pdf
              vd.render_pdf = lambda path, tmpdir: {"status": "ok", "renderer": "pdftoppm", "pages": 1}
              try:
                  report = vd.validate_files([str(FIXTURES_DIR / "sample.pdf")], render_check=True)
                  self.assertEqual(report["status"], "ok")
                  self.assertEqual(report["exit_code"], 0)
                  self.assertEqual(report["files"][0]["render"]["status"], "ok")
              finally:
                  vd.render_pdf = original
      
          def test_render_failure_fails_report(self):
              vd.find_pdf_renderer = lambda: "/usr/bin/env-test-pdftoppm"
              original = vd.render_pdf
              vd.render_pdf = lambda path, tmpdir: {"status": "failed", "renderer": "pdftoppm", "reason": "boom"}
              try:
                  report = vd.validate_files([str(FIXTURES_DIR / "sample.pdf")], render_check=True)
                  self.assertEqual(report["status"], "fail")
                  self.assertEqual(report["exit_code"], 1)
              finally:
                  vd.render_pdf = original
      
          def test_render_pdf_dispatches_renderer_specific_args(self):
              # Each supported PDF renderer has its own CLI; a machine with only
              # mutool or ghostscript (no pdftoppm) must still render correctly.
              captured = {}
      
              def fake_run(cmd, capture_output, timeout):
                  captured["cmd"] = cmd
                  return subprocess.CompletedProcess(cmd, 0, b"", b"")
      
              original_run = vd.subprocess.run
              original_find = vd.find_pdf_renderer
              vd.subprocess.run = fake_run
              try:
                  with tempfile.TemporaryDirectory() as tmp:
                      vd.find_pdf_renderer = lambda: "/usr/bin/pdftoppm"
                      vd.render_pdf(FIXTURES_DIR / "sample.pdf", Path(tmp))
                      self.assertIn("-png", captured["cmd"])
                      self.assertNotIn("draw", captured["cmd"])
      
                      vd.find_pdf_renderer = lambda: "/usr/bin/mutool"
                      vd.render_pdf(FIXTURES_DIR / "sample.pdf", Path(tmp))
                      self.assertIn("draw", captured["cmd"])
                      self.assertIn("-o", captured["cmd"])
      
                      vd.find_pdf_renderer = lambda: "/usr/bin/gs"
                      vd.render_pdf(FIXTURES_DIR / "sample.pdf", Path(tmp))
                      self.assertIn("-sDEVICE=png16m", captured["cmd"])
                      self.assertTrue(any(arg.startswith("-sOutputFile=") for arg in captured["cmd"]))
              finally:
                  vd.subprocess.run = original_run
                  vd.find_pdf_renderer = original_find
      
      
      class CliTests(unittest.TestCase):
          def run_cli(self, *args):
              return subprocess.run(
                  [sys.executable, str(SCRIPTS_DIR / "validate-documents.py")] + list(args),
                  capture_output=True,
                  text=True,
              )
      
          def test_help_exits_zero_and_advertises_json(self):
              proc = self.run_cli("--help")
              self.assertEqual(proc.returncode, 0)
              self.assertIn("--json", proc.stdout)
              self.assertIn("--render-check", proc.stdout)
      
          def test_cli_json_on_fixture(self):
              proc = self.run_cli("--json", str(FIXTURES_DIR / "sample.xlsx"))
              self.assertEqual(proc.returncode, 0)
              report = json.loads(proc.stdout)
              self.assertEqual(report["status"], "ok")
      
          def test_cli_exit_1_on_broken(self):
              with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp:
                  tmp.write(b"junk")
                  path = tmp.name
              try:
                  proc = self.run_cli("--json", path)
                  self.assertEqual(proc.returncode, 1)
              finally:
                  os.unlink(path)
      
          def test_cli_render_check_unavailable_when_no_renderer(self):
              # The CLI uses the real renderer discovery; run it only when we can
              # force the no-renderer path via the module-level stub in-process,
              # which is covered by RenderCheckTests. Here we only assert the CLI
              # accepts the flag and returns 0 or 1 without crashing.
              proc = self.run_cli("--render-check", "--json", str(FIXTURES_DIR / "sample.pdf"))
              self.assertIn(proc.returncode, (0, 1))
              report = json.loads(proc.stdout)
              self.assertIn(report["status"], ("ok", "unavailable", "fail"))
      
      
      if __name__ == "__main__":
          unittest.main()
      
  • README.md 4.3 KB
    # Documents — PDF, Word, Excel & PowerPoint Skill
    
    One skill that lets your agent generate, inspect, validate, and fix PDF, Word (.docx), Excel (.xlsx), and PowerPoint (.pptx) files — with a shared workflow, per-format references, and a validation script that verifies output quality before anything ships.
    
    ## Why Install This Skill
    
    Document output is one of the most common things people ask agents to produce, yet it is easy to get subtly wrong: files that open in one viewer but corrupt in another, spreadsheets with broken cell references, decks with missing slide relationships. This skill packages the full generation-to-delivery loop so your agent produces files that are structurally sound and actually render.
    
    After installing, your agent can turn a markdown brief into a formatted PDF, build a Word report with proper headings and tables, generate a spreadsheet from CSV data, assemble a slide deck from an outline — and then run the included validation script on every artifact to prove it is well-formed before you ever open it. Because the four formats share one workflow, one skill covers them all; you do not need four overlapping skills with four sets of instructions to maintain.
    
    ## What You Get
    
    | Directory | Purpose |
    |-----------|---------|
    | `SKILL.md` | Shared six-step workflow (scope → content model → template → render → validate → deliver) with per-format load-on-demand |
    | `references/pdf.md` | PDF generation, tooling, and validation specifics |
    | `references/word.md` | Word (.docx) package layout, generation, and validation specifics |
    | `references/excel.md` | Excel (.xlsx) workbook structure, generation, and validation specifics |
    | `references/powerpoint.md` | PowerPoint (.pptx) deck structure, generation, and validation specifics |
    | `references/output-quality.md` | Cross-format output-quality checklist for all four formats |
    | `scripts/validate-documents.py` | Stdlib-only validation script: structural sanity + optional render check, with `--json` output and graceful degradation when no renderer is installed |
    | `templates/pdf-template.md` | Fillable generation template for PDF (print-ready HTML/CSS or LaTeX) |
    | `templates/word-template.md` | Fillable generation template for Word documents |
    | `templates/excel-template.md` | Fillable generation template for Excel workbooks |
    | `templates/powerpoint-template.md` | Fillable generation template for PowerPoint decks |
    | `fixtures/` | One small valid sample per format, used to smoke-test the validation script |
    
    ## Quick Start
    
    ```bash
    # Validate a finished artifact (human report)
    python3 scripts/validate-documents.py report.pdf
    
    # Validate with a render check and machine-readable output
    python3 scripts/validate-documents.py --render-check --json report.pdf data.xlsx deck.pptx
    
    # Smoke-test the script against the bundled per-format fixtures
    python3 scripts/validate-documents.py --json fixtures/sample.pdf fixtures/sample.docx fixtures/sample.xlsx fixtures/sample.pptx
    ```
    
    The render check uses `pdftoppm` (poppler-utils) for PDF and LibreOffice for Office formats when they are installed. When neither is present, validation still performs full structural checks and reports the render check as `unavailable` instead of failing — no renderer required to use the skill.
    
    ## Triggers
    
    Load this skill when the user mentions any of:
    
    - **Generating documents**: "create a PDF report", "make a Word document", "turn this CSV into a spreadsheet", "build a slide deck"
    - **Editing documents**: "update the docx", "change the Excel file", "fix this presentation"
    - **Extracting from documents**: "read the text from this PDF", "pull the table out of this xlsx"
    - **Converting**: "docx to PDF", "export this data as an Excel file"
    - **Validating**: "check that this document is valid", "why won't this file open", "verify the output before sending"
    
    Do not load for ebooks (use the `epub` skill), image/video/media production, or data pipeline work (use `data-engineering`).
    
    ## Requirements
    
    - Python 3.8+ — the validation script uses only the standard library.
    - Optional renderers (only for the render-check step): `pdftoppm`/`mutool`/`ghostscript` for PDF, `libreoffice`/`soffice` for Office formats. Generation libraries such as python-docx, openpyxl, or python-pptx are optional per format and documented in the references.
    
  • SKILL.md 8.9 KB
    ---
    name: documents
    description: >-
      Generate, inspect, validate, and fix PDF, Word (.docx), Excel (.xlsx), and
      PowerPoint (.pptx) documents: turn structured content into render-ready
      artifacts, verify structural and output quality before delivery, and repair
      broken files. Use when a task involves creating, editing, converting, or
      validating office documents and PDFs. Do not use for ebook packaging (use
      epub), for images, video, or other media production, for API or code
      documentation, or for data pipelines (use data-engineering).
    license: MIT
    compatibility: >-
      Python 3.8+ for scripts; validation uses only the standard library. Optional
      renderers for the render check: poppler-utils (pdftoppm) for PDF and
      LibreOffice for Office formats; both degrade gracefully when absent.
      Portable across all AgentSkills-compatible harnesses.
    metadata:
      skills: documents, pdf, docx, xlsx, pptx, word, excel, powerpoint, office, report, generation
      tags: documents, pdf, docx, xlsx, pptx, word, excel, powerpoint, office, generation, validation
    ---
    
    # Documents — PDF, Word, Excel & PowerPoint Skill
    
    One skill for the four most common document formats. All four share a single
    agent workflow — structured content in, render-ready, validated artifact out —
    so they live in ONE family skill with per-format references, following the
    `epub` precedent. Load the shared workflow below, then pull the per-format
    reference for the format you are actually touching.
    
    | Format | Extension | Reference (load on demand) |
    |--------|-----------|----------------------------|
    | PDF | `.pdf` | [references/pdf.md](references/pdf.md) |
    | Word | `.docx` | [references/word.md](references/word.md) |
    | Excel | `.xlsx` | [references/excel.md](references/excel.md) |
    | PowerPoint | `.pptx` | [references/powerpoint.md](references/powerpoint.md) |
    | All formats | — | [references/output-quality.md](references/output-quality.md) |
    
    Generation templates for each format live in [templates/](templates/), and the
    validation script with per-format fixtures lives in [scripts/](scripts/).
    
    ## When to use
    
    Load this skill when the task involves any of the four formats:
    
    - **Generate**: build a report, memo, spreadsheet, or deck from structured
      content (markdown, JSON, data tables, outlines).
    - **Edit**: modify an existing document's content, layout, or metadata in place.
    - **Extract**: pull text, tables, or structure out of an existing file.
    - **Convert**: move content between formats or from a data source into a document.
    - **Validate**: check that a produced artifact is structurally sound and will
      render correctly before it is delivered.
    
    ## When not to use
    
    - **Ebooks and EPUB** — use the `epub` skill; it owns the EPUB container,
      reading order, and package validation.
    - **Images, video, and other media** — this skill covers document formats only;
      route media production to the appropriate media skills.
    - **Code and API documentation sites** — use the technical-documentation and
      documentation-site conventions, not office documents.
    - **Data pipelines** — moving or transforming raw data belongs to
      `data-engineering`; Excel here is a *deliverable format*, not a data store.
    - **Office documents to Markdown** — converting an existing office document
      (docx, xlsx, pptx, pdf, odt, rtf, epub, csv) to GitHub-Flavored Markdown
      belongs to the `anydoc` skill; this skill owns generation, editing, and
      validation, not document-to-markdown extraction.
    
    ## The Shared Workflow
    
    Every document task follows the same six steps, regardless of format. Deep
    format-specific detail is deferred to the per-format reference — read it at the
    step where it matters.
    
    ### 1. Scope
    
    Pin down what the document is for before touching a file:
    
    - **Audience and purpose** — who reads it and what decision it supports.
    - **Format** — PDF (fixed layout, print, archival), Word (editable prose,
      review), Excel (data, calculations), PowerPoint (presentation).
    - **Boundaries** — page/slide count, size limits, brand or style constraints.
    - **Source of truth** — the structured content the document is generated from
      (markdown, JSON, CSV, outline), so the artifact is reproducible.
    
    ### 2. Content model
    
    Represent the document's content as structured data before rendering:
    
    - A **title, sections/headings, body text, and metadata** for prose documents.
    - A **table model** (headers, rows, column types) for spreadsheets.
    - A **slide outline** (title + bullets per slide, speaker notes) for decks.
    - Keep content and layout separate: content in the model, layout in the
      template. This is what makes regeneration cheap.
    
    ### 3. Template
    
    Choose the generation template for the target format from [templates/](templates/):
    
    - [templates/pdf-template.md](templates/pdf-template.md) — fixed-layout
      document skeleton (print-ready HTML/CSS or LaTeX source).
    - [templates/word-template.md](templates/word-template.md) — Word processing
      document structure (styles, headings, tables).
    - [templates/excel-template.md](templates/excel-template.md) — workbook
      structure (sheets, cells, shared strings, formulas).
    - [templates/powerpoint-template.md](templates/powerpoint-template.md) — slide
      deck structure (slides, layouts, notes).
    
    Fill the `[fill: ...]` markers in the template with content from the content
    model. Templates are the contract between content and layout — changing the
    template is how you change appearance without touching content.
    
    ### 4. Render
    
    Produce the artifact file:
    
    - **PDF** — render the template to PDF (print CSS in a browser or engine, or a
      LaTeX toolchain). See [references/pdf.md](references/pdf.md) for tooling.
    - **Word / Excel / PowerPoint** — write the OOXML package directly (stdlib
      `zipfile` + XML for small artifacts) or with the conventional library for the
      format (python-docx, openpyxl, python-pptx). See the per-format reference for
      the exact package layout to produce.
    
    ### 5. Validate
    
    Never deliver unvalidated output. Run the validation script:
    
    ```bash
    python3 scripts/validate-documents.py --render-check --json report.pdf brief.docx data.xlsx deck.pptx
    ```
    
    The script performs **structural sanity** (container signatures, required
    parts, XML well-formedness) and, when a renderer is installed, a **render
    check** (actually renders the file). When no renderer is present it reports
    `unavailable` instead of failing — validation never hard-requires a renderer.
    See [references/output-quality.md](references/output-quality.md) for the full
    output-quality checklist, and the fixture files in
    [fixtures/](fixtures/) (one per format) to smoke-test the script itself:
    
    ```bash
    python3 scripts/validate-documents.py --json fixtures/sample.pdf fixtures/sample.docx fixtures/sample.xlsx fixtures/sample.pptx
    ```
    
    ### 6. Deliver
    
    Hand off the artifact with its provenance:
    
    - The **source content model** (so it can be regenerated).
    - The **template version** used.
    - The **validation result** (structure passed; render checked or unavailable).
    - Any **known deviations** (fonts substituted, images downscaled, layout drift).
    
    ## Exit conditions
    
    The task is complete when the artifact exists, passes structural validation
    (and the render check when a renderer is available), and the content matches
    the agreed scope. Stop after delivering the validated artifact with its
    provenance; do not keep iterating on layout without a new scope instruction.
    
    ## Scripts
    
    All scripts live in [scripts/](scripts/) relative to this skill's directory and
    follow cli-builder conventions: `--json` for machine output, non-interactive,
    errors to stderr. Run with `--help` for full flag details.
    
    ### validate-documents.py — Structural Sanity + Render Check
    
    ```bash
    python3 scripts/validate-documents.py report.pdf            # human report
    python3 scripts/validate-documents.py --json report.pdf    # machine report
    python3 scripts/validate-documents.py --render-check --json report.pdf data.xlsx deck.pptx
    ```
    
    Behavior:
    
    - **Structural sanity** per format: PDF header/EOF/page objects; OOXML ZIP
      container, `[Content_Types].xml`, required parts, XML well-formedness.
      Legacy `.doc/.xls/.ppt` files are recognized via OLE2 magic bytes.
    - **Render check** (`--render-check`): renders PDF via `pdftoppm`/`mutool`/`gs`
      and Office formats via LibreOffice. Reports `unavailable` — exit 0 — when no
      renderer is installed (graceful degradation, never a crash).
    - **Exit codes**: 0 all pass (or render check unavailable); 1 a file fails
      structure or rendering; 2 usage/I/O error.
    - **JSON output**: top-level `status` (`ok` / `fail` / `unavailable` / `error`)
      with per-file checks and render results.
    
    ## Related skills
    
    - [epub](../epub/SKILL.md) — ebook container skill; the sibling family-skill
      precedent for this format family.
    - [data-engineering](../data-engineering/SKILL.md) — data pipelines and
      transformation; Excel is a deliverable format here, not a data store.
    - [cli-builder](../cli-builder/SKILL.md) — the CLI conventions the validation
      script follows (`--json`, non-interactive, exit codes).
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related