diff --git a/external/mattpocock/skills/chief-of-staff/SKILL.md b/external/mattpocock/skills/chief-of-staff/SKILL.md new file mode 100644 index 0000000..97da622 --- /dev/null +++ b/external/mattpocock/skills/chief-of-staff/SKILL.md @@ -0,0 +1,45 @@ +--- +name: chief-of-staff +description: Pursue a long-running goal in a single session by co-ordinating subagents. +disable-model-invocation: true +--- + +You are a chief of staff, co-ordinating subagents and schedules to pursue a long-running goal. This session will run for a long time, accruing tribal knowledge and helping you make long-term strategic decisions. + +You are the Directly Responsible Individual for this goal. You are empowered to think much longer-term than you're used to. You must think on two tracks simultaneously: + +- Tactical: how do I complete the immediate task? +- Strategic: how do I modify the environment to improve the outcomes of the _next_ task? + +## Schedules + +Harness-permitting, you will suggest recurring schedules which can help in achieving the goal. + +## Subagents + +All work should be done in subagents. Protect your context window. + +Use background agents so you can stay in active dialogue with the user. + +Communication to and from subagents should be sparse. Communicate primarily through **context pointers**: research notes, previous commits, and others. Don't duplicate information already available via pointers. + +## Strategic View + +As part of any and all work, FIRST consider how the environment the agents operate in might be improved. Agents thrive in the **pit of success**: + +- API's and functions which are extremely constrained and limited +- Lint rules which force correctness +- CODING_STANDARDS.md files which let code reviewers enforce best practices + +They also need relevant **data sources** to succeed: + +- Logs from critical running processes, like dev servers (or production logs) +- Access to test environment databases +- Access to the browser (when necessary) for clicking around and taking screenshots + +Finally, create environments (and codebases) that obey the **"no workarounds"** rule: + +- No one-off workarounds, or hacks that bypass established processes +- Any deviations from conventions must be fixed proactively, before feature work is done + +Be relentless in improving the environment. Use every user message as an excuse to search for these improvements. diff --git a/external/mattpocock/skills/chief-of-staff/agents/openai.yaml b/external/mattpocock/skills/chief-of-staff/agents/openai.yaml new file mode 100644 index 0000000..080598b --- /dev/null +++ b/external/mattpocock/skills/chief-of-staff/agents/openai.yaml @@ -0,0 +1,5 @@ +interface: + display_name: "Chief Of Staff" + short_description: "Pursue a long-running goal in a single session by co-ordinating subagents" +policy: + allow_implicit_invocation: false diff --git a/external/officecli/skills/officecli-academic-paper/SKILL.md b/external/officecli/skills/officecli-academic-paper/SKILL.md new file mode 100644 index 0000000..ac82aba --- /dev/null +++ b/external/officecli/skills/officecli-academic-paper/SKILL.md @@ -0,0 +1,507 @@ +--- +name: officecli-academic-paper +description: "Use this skill to build academic-style .docx output: journal / conference / thesis chapters carrying formal citation style (APA, Chicago, IEEE, MLA), numbered equations, figure & table cross-references, footnotes/endnotes, bibliography, or multi-column journal layout. Trigger on: 'research paper', 'journal paper', 'conference paper', 'manuscript', 'thesis', 'APA', 'MLA', 'Chicago', 'IEEE two-column', 'bibliography', 'hanging indent', 'citation style', 'abstract + keywords', 'equation numbering', 'cross-reference', paper with footnotes/endnotes. Output is a single .docx." +--- + +# OfficeCLI Academic Paper Skill + +**This skill is a scene layer on top of `officecli-docx`.** Every docx hard rule — style architecture, heading hierarchy, shell quoting, page-break rules, live PAGE field, Delivery Gate, renderer quirks — is inherited, not re-taught. This file adds only what academic papers need on top: citation styles, equations, SEQ / PAGEREF cross-refs, multi-column journal layout, bibliography hanging indent, abstract/keywords/affiliation block. + +When the docx base rules cover it, the text here says `→ see docx v2 §X`. Read docx v2 first if you have not. + +## Setup + +If `officecli` is missing: + +- **macOS / Linux**: `curl -fsSL https://d.officecli.ai/install.sh | bash` +- **Windows (PowerShell)**: `irm https://d.officecli.ai/install.ps1 | iex` + +Verify with `officecli --version` (open a new terminal if PATH hasn't picked up). If install fails, download a binary from https://github.com/iOfficeAI/OfficeCLI/releases. + +## ⚠️ Help-First Rule + +**This skill teaches what an academic paper requires, not every command flag.** When a prop name, enum value, or field instruction is uncertain, consult help BEFORE guessing. + +```bash +officecli help docx # All docx elements +officecli help docx # Full schema (e.g. section, equation, field, footnote) +officecli help docx --json # Machine-readable +``` + +Help is pinned to the installed CLI version. **When this skill and help disagree, help wins.** Every `--prop X=` in this file has been grep-verified against `officecli help docx ` — if help adds / renames a prop in a later version, trust help. + +## Mental Model & Inheritance + +**Inherits docx v2.** You should have read `skills/officecli-docx/SKILL.md` first. This skill assumes you know how to add paragraphs, set styles, build tables, insert images, manage TOC/footer/headers, force page breaks, and run the Delivery Gate. If any of those are unfamiliar, open a second session on docx v2 before continuing. + +## Shell & Execution Discipline + +**Shell quoting, incremental execution, `$FILE` convention** → see docx v2 §Shell & Execution Discipline. The same rules apply here verbatim — quote `[N]` paths, single-quote any value containing `$` (including `$2.8B` in a body paragraph or `@` DOIs), never hand-write `\$ \t \n` in executable examples, one command at a time. Academic-paper examples below use `$FILE` as a shell variable (`FILE="thesis.docx"`). + +## What "academic" means here (identity) + +An academic paper is a docx with a **scholarly layer** on top: verifiable citations, precise equations, cross-refs that stay in sync, a formatted reference list. The base docx rules still apply; academic adds six deltas: + +1. **Citation style is a contract.** APA / Chicago / IEEE / MLA each dictate author format, date placement, reference-list order, in-text marker shape. Pick one at the start; every later decision (hanging indent, footnote vs parenthetical, `[1]` vs `(Smith, 2024)`) follows. +2. **Equations are first-class content** — inline `oMath` inside prose, display `oMathPara` as standalone blocks, optionally numbered. +3. **Figures and tables auto-number.** `SEQ Figure` / `SEQ Table` fields count them; `PAGEREF` links "see Fig. 2" to its live page number. +4. **Bibliography uses hanging indent** (first line flush left, continuation lines indented). Not first-line indent. Not left indent alone. Hanging. +5. **Abstract / keywords / affiliation block** is a first-page three-piece, not a cover in the marketing sense. Block-style abstract, no first-line indent, no decoration. +6. **Multi-column layout** appears in IEEE / ACM / Nature / many journals: single-column abstract + two-column body. + +### Reverse handoff — when to go BACK to docx + +Stay in **docx v2** for white papers, policy briefs, technical reports, HR templates — anything without a venue / citation style. Use **this skill** only when the document will carry at least TWO of: citation-style biblio, equations, SEQ/PAGEREF cross-refs, multi-column, abstract + keywords block. + +## Workflow — 5 verbs + +1. **Read the venue spec.** APA 7 / Chicago 17 / IEEE / MLA 9 / journal-specific. Line spacing, font, citation shape, biblio sort order — everything downstream follows from this one decision. +2. **Plan the sections.** Abstract → keywords → introduction → methods → results → discussion → conclusion → references. Estimate heading count for TOC decision (3+ headings = add a TOC, see docx v2 §Table of Contents). +3. **Set styles up front.** Heading1 / Heading2 / Heading3 / Caption / AbstractTitle / Bibliography. Define all styles BEFORE any content (→ see docx v2 §Paragraphs and styles — same rule here, same failure mode if skipped). +4. **Build body in order.** Cover / title block → abstract → keywords → TOC (if needed) → body sections in reading order → figures / tables with SEQ captions → bibliography → footnotes are added last by paragraph path. +5. **QA — Delivery Gate.** Inherit docx v2 Gates 1-3, then add academic Gates 4-5 below. + +## Requirements (academic floor on top of docx v2) + +Everything in docx v2 §Requirements for Outputs applies. On top of that, academic papers MUST meet these additional rules: + +### Typography and spacing (venue-aware) + +- **Font.** Times New Roman 11-12pt body (default) or venue-specified (IEEE uses Times 10pt 2-col; APA allows Calibri 11pt). Same body font throughout; no decorative heading fonts. +- **Heading hierarchy.** H1 = 20pt bold, H2 = 14pt bold, H3 = 12pt bold italic, body = 11-12pt. (Same numbers as docx v2 — restated because academic papers never rely on Word defaults.) +- **Line spacing.** APA 7 = 2x (double). Chicago / IEEE / most journals = 1.5x. Never below 1.15x. Set on body paragraphs and on References. +- **Margins.** 1 inch (1440 twips) all sides unless the venue says otherwise (some journals require 1.25in left for binding — check the spec). + +### Abstract, bibliography, caption placement + +- **Abstract is block-style.** NO `firstLineIndent`. Use `spaceAfter=12pt` for paragraph separation. If `view issues` reports "body paragraph missing first-line indent" on an Abstract paragraph, it's a false positive — ignore. +- **Bibliography uses hanging indent.** Each entry is one paragraph with `indent=720 hangingIndent=720` (left indent 0.5", first-line reversed by same amount). First line flush left; wraps indent under author name. +- **Figure captions go BELOW the figure.** Table captions go ABOVE the table. This is the single rule most non-academics get wrong — APA, Chicago, IEEE, MLA all agree on it. +- **Citation round-trip.** Every in-text citation key must resolve to an entry in the reference list. Delivery Gate 4 verifies. +- **SEQ presence.** Any paper with numbered figures or tables must carry live `SEQ Figure` / `SEQ Table` fields (not hardcoded "Figure 1" text that drifts when you insert a new figure mid-document). Delivery Gate 5 verifies. + +### Cover / first-page block + +Academic covers differ from professional covers. Minimum elements: title (centered, 20-22pt bold), author(s), affiliation, submission target or journal, date, abstract, keywords. The "60% fill" rule from docx v2 §Visual delivery floor still applies — a three-line cover with half a page of whitespace is a fail. See §Abstract / keywords / affiliation block below for the first-page recipe. + +### Section numbering convention (STYLE-DEPENDENT — do not apply blindly) + +Academic section numbers are **part of the heading text**, not computed via list numbering. `officecli`'s `numId`/`listStyle` mechanism is fragile across Heading1 re-use, so hand-write the prefix. BUT the prefix shape varies by style — DO NOT use the same form for all four: + +| Style | H1 format | H2 format | Example | +|---|---|---|---| +| **APA 7** | **UNNUMBERED centered bold** | Unnumbered left-aligned bold | `Introduction` / `Methods` (centered) | +| **Chicago** | `"N. Title"` left-aligned | `"N.M Title"` | `1. Introduction`, `2.1 Policy Formation` | +| **IEEE** | `"N. TITLE"` ALL CAPS + Roman numerals | `A. Subtitle` title case | `I. INTRODUCTION`, `II. RELATED WORK`, `A. Datasets` | +| **MLA 9** | Unnumbered left-aligned bold | Same | `Literature Review` (no prefix) | + +APA 7 L1 headings are **centered, bold, unnumbered**; L2 are flush-left bold; L3 flush-left bold italic; L4/L5 run-in. Do NOT prefix APA headings with `1. / 2.` — that is Chicago/IEEE convention. IEEE wants ALL CAPS with Roman numerals (`I. INTRODUCTION`); inside each section, use `A./B./C.` sub-headings (title case). Arabic-numbered body sections are Chicago-style only. + +**Exception for all four**: References / Bibliography / Works Cited / Acknowledgments are unnumbered regardless of style — omit the `N.` prefix. + +## Quick Start — minimal APA paper + +```bash +FILE="paper.docx" +officecli create "$FILE" +officecli open "$FILE" +officecli set "$FILE" / --prop defaultFont="Times New Roman" +officecli add "$FILE" /body --type paragraph --prop text="Remote Work and Team Cohesion" --prop align=center --prop size=20pt --prop bold=true --prop spaceAfter=24pt +officecli add "$FILE" /body --type paragraph --prop text="Alice Chen" --prop align=center --prop size=12pt +officecli add "$FILE" /body --type paragraph --prop text="Department of Psychology, Stanford University" --prop align=center --prop size=11pt --prop spaceAfter=24pt +officecli add "$FILE" /body --type paragraph --prop text="Abstract" --prop align=center --prop size=14pt --prop bold=true --prop spaceBefore=12pt --prop spaceAfter=6pt +officecli add "$FILE" /body --type paragraph --prop text="This study examines remote-work adoption on team cohesion across 18 months..." --prop size=12pt --prop lineSpacing=2x --prop spaceAfter=12pt +officecli add "$FILE" /body --type paragraph --prop text="Keywords: remote work, team cohesion, psychological safety" --prop italic=true --prop size=11pt --prop spaceAfter=18pt +officecli add "$FILE" /body --type paragraph --prop text="1. Introduction" --prop style=Heading1 --prop size=20pt --prop bold=true --prop spaceBefore=18pt --prop spaceAfter=12pt +officecli add "$FILE" /body --type paragraph --prop text="Remote-work research (Smith, 2024) has expanded since 2020..." --prop size=12pt --prop lineSpacing=2x --prop firstLineIndent=720 +officecli add "$FILE" /body --type paragraph --prop text="References" --prop style=Heading1 --prop size=20pt --prop bold=true --prop spaceBefore=18pt --prop spaceAfter=12pt +officecli add "$FILE" /body --type paragraph --prop text="Smith, J. (2024). Remote work and cohesion. Journal of Applied Psychology, 109(3), 412-430." --prop size=12pt --prop lineSpacing=2x --prop indent=720 --prop hangingIndent=720 +officecli add "$FILE" / --type footer --prop type=default --prop align=center --prop size=10pt --prop field=page +officecli close "$FILE" +officecli validate "$FILE" +``` + +Ten-line skeleton. Real papers grow by adding more body paragraphs, more bibliography entries (each with the same `indent=720 hangingIndent=720` pair), figures / tables with captions, and a TOC if there are 3+ Heading1s. The Quick Start validates clean; the sections below elaborate each dimension. + +## Citation style recipes + +Four mainstream families. Pick one at project start; every downstream decision follows. **Per-style decision table:** + +| Style | In-text shape | Reference list order | Body line spacing | Footnotes? | +|---|---|---|---|---| +| APA 7 | `(Smith, 2024)` or `Smith (2024)` | Alphabetical by author | 2x (double) | Rare (content notes only) | +| Chicago 17 (Notes-Bib) | Superscript footnote number | Alphabetical by author | 1.5x-2x | **Primary** (full citation in footnote) | +| IEEE | `[1]`, `[2]`, ..., `[N]` | Order of first citation | 1.15x-1.5x, 2-col | Rare | +| MLA 9 | `(Smith 412)` page-number | Alphabetical by author, "Works Cited" | 2x | Rare | + +Shared defaults across all four: reference-list paragraphs use `indent=720 hangingIndent=720` (hanging indent 0.5"); add a live TOC if 3+ Heading1s (→ see docx v2 §Table of Contents); set `updateFields=true` and report TOC page numbers as uncomputed until a Word-compatible field engine updates them. + +### APA 7 (social sciences — psychology, education, management) + +- In-text: `(Author, Year)` or `Author (Year)` for narrative. Page number required on direct quotes: `(Smith, 2024, p. 15)`. Three+ authors: `(Smith et al., 2024)` after first citation. +- Reference list order: **alphabetical by first author's surname**. Title caps: sentence case for article titles, title case for journal names (italic). +- Reference shape: `Author, A. A., & Co-Author, B. B. (Year). Title of article. Journal Name, Volume(Issue), pages.` DOI preferred over URL; present as https URL, not `doi:` prefix. +- Double-space everything (`lineSpacing=2x`) including abstract and references. Body first-line indent = 0.5" (`firstLineIndent=720`). + +```bash +# Body paragraph with parenthetical citation +officecli add "$FILE" /body --type paragraph --prop text="Remote work adoption accelerated during the pandemic (Kramer & Kramer, 2020)." --prop size=12pt --prop lineSpacing=2x --prop firstLineIndent=720 +# Reference entry with hanging indent +officecli add "$FILE" /body --type paragraph --prop text="Kramer, A., & Kramer, K. Z. (2020). The potential impact of the Covid-19 pandemic on occupational status. Journal of Vocational Behavior, 119, 103442." --prop size=12pt --prop lineSpacing=2x --prop indent=720 --prop hangingIndent=720 +# DOI hyperlink appended to the reference paragraph +officecli add "$FILE" "/body/p[last()]" --type hyperlink --prop url="https://doi.org/10.1016/j.jvb.2020.103442" --prop text="https://doi.org/10.1016/j.jvb.2020.103442" +``` + +QA: `officecli query "$FILE" 'paragraph[hangingIndent]'` returns every reference entry; zero references with first-line indent instead of hanging. + +### Chicago 17 — Notes-Bibliography (humanities — history, philosophy, religion) + +- In-text: superscript footnote number; full citation in the first footnote (`Timothy Brook, The Troubled Empire (Cambridge, MA: Harvard UP, 2010), 142.`); **shortened form** thereafter (`Brook, Troubled Empire, 150.`). +- **Repeat-citation rule (Chicago 17, op. cit. deprecated):** + - **Immediately-consecutive** citation of **the same source, same page** → `Ibid.` + - **Immediately-consecutive, different page** of same source → `Ibid., 22.` + - Non-consecutive repeat → **shortened form** (`Brook, Troubled Empire, 150.`), NOT `op. cit.`. Chicago 17 drops `op. cit.` — use shortened form every time except for immediate repeats. +- Bibliography at end, **alphabetical by first author's surname** ("Brook, Timothy."), hanging indent. Footnote body renders at the viewer's footnote default (typically 10pt); bibliography entries 12pt. (The `footnote` element exposes only `text` — size is not settable per-footnote; trust renderer defaults.) +- Typical split for primary-source-heavy papers: `Primary Sources` and `Secondary Sources` as two Heading2s under a single `Bibliography` Heading1. Book titles italic in both footnotes and bibliography. +- Chicago also has an Author-Date variant used in the sciences — if the venue specifies Chicago Author-Date, fall back to the APA recipe and change only the punctuation (no comma between author and year: `(Smith 2024)`). + +```bash +# Body paragraph that will anchor a footnote, then the footnote itself +officecli add "$FILE" /body --type paragraph --prop text="The Ming dynasty's 海禁 policy shaped coastal trade for two centuries." --prop size=12pt --prop lineSpacing=1.5x --prop firstLineIndent=720 +officecli add "$FILE" "/body/p[last()]" --type footnote --prop text="Timothy Brook, The Troubled Empire: China in the Yuan and Ming Dynasties (Cambridge, MA: Harvard University Press, 2010), 142." +# Next footnote — shortened form +officecli add "$FILE" "/body/p[last()]" --type footnote --prop text="Brook, Troubled Empire, 150." +# Bibliography section split — primary sources first +officecli add "$FILE" /body --type paragraph --prop text="Bibliography" --prop style=Heading1 --prop size=20pt --prop bold=true --prop spaceBefore=18pt +officecli add "$FILE" /body --type paragraph --prop text="Primary Sources" --prop style=Heading2 --prop size=14pt --prop bold=true --prop spaceBefore=12pt +officecli add "$FILE" /body --type paragraph --prop text="Ming Shilu 明實錄. Taipei: Academia Sinica, 1966." --prop size=12pt --prop indent=720 --prop hangingIndent=720 +officecli add "$FILE" /body --type paragraph --prop text="Secondary Sources" --prop style=Heading2 --prop size=14pt --prop bold=true --prop spaceBefore=12pt +officecli add "$FILE" /body --type paragraph --prop text="Brook, Timothy. The Troubled Empire: China in the Yuan and Ming Dynasties. Cambridge, MA: Harvard University Press, 2010." --prop size=12pt --prop indent=720 --prop hangingIndent=720 +``` + +QA: `officecli query "$FILE" 'footnote'` count ≥ body-paragraph citation count. + +### IEEE (engineering — transactions, conference proceedings) + +- In-text: `[1]`, `[2]`. Numbered in **order of first appearance**, not alphabetical. Reuse the same number for repeat citations. `[1, p. 15]` for page refs, `[1]-[3]` for a range. +- Reference entry starts with the bracketed number: `[1] A. Smith and B. Jones, "Title," IEEE Trans. X, vol. 5, no. 3, pp. 1-10, 2024, doi: ...`. Authors are initial-first; journal names abbreviated per IEEE list (`IEEE Trans. Neural Netw.`, not full name). +- Body is **two-column** (see §Multi-column below). Abstract is single-column above the fold, 10pt, 1.15x line spacing, typically 200-250 words. +- First-line indent on body paragraphs = 0.2" (`firstLineIndent=288` twips ≈ 14pt). Smaller than APA's 0.5" because the 2-col width is narrower. +- **Section headings: ALL CAPS with Roman numerals** — `I. INTRODUCTION`, `II. RELATED WORK`, `III. METHOD`. Sub-sections `A. Datasets`, `B. Baselines` in title case. Do NOT use `1. Introduction` (Arabic) for IEEE — that is Chicago style. +- **Tables are numbered Roman**: `Table I`, `Table II`, `Table III`. Figures remain Arabic (`Fig. 1`, `Fig. 2`). `recalcFields=seq` writes Arabic cached values for both — for IEEE Roman tables, either patch the cached `` to Roman manually after recalc, or accept Arabic and note it in the cover letter. + +```bash +# Body citing reference 1 +officecli add "$FILE" /body --type paragraph --prop text="Attention-based anomaly detection has been applied to industrial sensor data [1], [2]." --prop size=10pt --prop lineSpacing=1.15x +# Reference list entry — number in the text +officecli add "$FILE" /body --type paragraph --prop text="[1] A. Smith and B. Jones, \"Attention for anomaly detection,\" IEEE Trans. Neural Netw., vol. 35, no. 2, pp. 412-430, 2024." --prop size=10pt --prop indent=720 --prop hangingIndent=720 +officecli add "$FILE" /body --type paragraph --prop text="[2] C. Lee, \"Time-series anomaly survey,\" in Proc. ICML, 2023, pp. 1200-1215." --prop size=10pt --prop indent=720 --prop hangingIndent=720 +``` + +QA: the highest `[N]` in body must equal the number of reference-list entries. Grep: `officecli view "$FILE" text | grep -oE '\[[0-9]+\]' | sort -u | tail -5`. + +### MLA 9 (literature, languages, cultural studies) + +Diff vs APA: in-text is `(Author Page)` **no comma** (e.g. `(Smith 412)`); direct quotes always carry the page number. Reference section titled **Works Cited** (not References / Bibliography). Entries alphabetical by surname, hanging indent, 2x spacing, nine "core elements" separated by periods: `Author. Title. Container, Other Contributors, Version, Number, Publisher, Date, Location.` — skip any that don't apply. Book titles italic; article titles in quotes. Otherwise identical to APA paragraph setup. + +## Equations (OMML — inline vs display) + +`--type equation` parses a LaTeX-ish formula into OMML. Two modes, selected by `--prop mode=`: + +| Mode | XML | Visual | Use | +|---|---|---|---| +| `display` (default) | `` at `/body` | Standalone centered block | Numbered equations, theorem statements | +| `inline` | `` appended to a run inside a paragraph | Runs with the text | `if $x > 0$` style in prose | + +```bash +# Display equation (own paragraph, centered) — explicitly set mode=display for clarity +officecli add "$FILE" /body --type equation --prop mode=display --prop formula="x^2 + y^2 = z^2" +# Display equation with Greek / subscript / integral — verify rendering below +officecli add "$FILE" /body --type equation --prop mode=display --prop formula="\\lambda_1 + \\alpha" +officecli add "$FILE" /body --type equation --prop mode=display --prop formula="\\frac{1}{2\\pi} \\int_0^{\\infty} e^{-x^2} dx" +# Inline equation INSIDE prose — required whenever variables like x_{t+1}, \lambda, etc. appear in a body paragraph: +officecli add "$FILE" /body --type paragraph --prop text="Given the weight " --prop size=11pt +officecli add "$FILE" "/body/p[last()]" --type equation --prop mode=inline --prop formula="W_t" +officecli add "$FILE" "/body/p[last()]" --type run --prop text=" we define the loss..." +``` + +**Verify equations render as OMML math**, not plain-text LaTeX tokens. After `close`, run: +```bash +officecli view "$FILE" text | head -20 # λ₁ + α, ∫₀∞, x² must appear as unicode math (verified renders) +officecli raw "$FILE" /document | grep -c '` may leave a stray empty paragraph; prefer `--index` or `--after` for a clean reorder. + +**Equation numbering** — no native `\eqno`. The journal-standard layout is **one line**: equation centered, number flush-right at the column edge (`Y = A(X) ⊗ X (1)`). Build it with two paragraph **tab stops** — a `center` tab at the column mid-point and a `right` tab at the column right edge — then lay out `[tab] equation(inline) [tab] (1)` in a single paragraph. Do NOT use a centered display equation followed by a separate right-aligned `(1)` line — that splits the number onto its own line and is the most common reason agents fail to reproduce the expected look. + +```bash +# Tab positions depend on the COLUMN width (twips). Default blank doc = A4, 3.18cm margins +# → text width = 8300 twips. +# single column: center tab = 4150, right tab = 8300 +# two columns (default 720-twip gutter): col = (8300-720)/2 = 3790 → center 1895, right 3790 +# (other page size / margins: text width = pageWidth - marginLeft - marginRight; recompute.) +officecli add "$FILE" /body --type paragraph # the equation paragraph (say it lands at p[N]) +officecli add "$FILE" "/body/p[N]" --type tab --prop pos=4150 --prop val=center +officecli add "$FILE" "/body/p[N]" --type tab --prop pos=8300 --prop val=right +officecli add "$FILE" "/body/p[N]" --type run --prop text=$'\t' # tab → jump to center +officecli add "$FILE" "/body/p[N]" --type equation --prop mode=inline --prop formula='Y = A(X) \otimes X' +officecli add "$FILE" "/body/p[N]" --type run --prop text=$'\t(1)' # tab → jump to right edge, then the number +``` + +For a two-column section just use the two-column tab positions (1895 / 3790); the same paragraph then centers the equation within its column with `(1)` at the column's right edge. Schema: `officecli help docx tab`. + +**Do NOT place `--type equation` directly on a table cell `tc[N]` path** — it is rejected with a guard error (`table cells only accept paragraphs, tables, or SDTs`), so no bad XML is written. Target `tc[N]/p[1]` with `mode=inline` if you need equations in cells. + +Full equation schema: `officecli help docx equation`. + +## Figures, tables, and cross-references (SEQ + PAGEREF) + +Two primitives, both **native fieldTypes** (`officecli help docx field` for the enum): `seq` for auto-numbered caption counters, `pageref` for "see Fig. 2 on page 7" back-references. Native fields insert with an unevaluated cached result; a single `set "$FILE" / --prop recalcFields=seq` (see §SEQ numbering below) fills the real numbers in document order — no per-field patching. + +### SEQ auto-numbering — figures and tables + +A SEQ field is a counter with a name (`identifier`). Every `SEQ Figure` increments the Figure counter on **recalc**; every `SEQ Table` increments the Table counter. + +**SEQ cached values — one command, after all captions are added.** A freshly-added SEQ field has no evaluated cached number; `view text` shows the `#OCLI_NOTEVAL!{SEQ Figure}` sentinel (and `Format["evaluated"]=false`) until you recalc. **Do NOT patch each field by hand.** Once every figure/table caption is in place, run once: +```bash +officecli set "$FILE" / --prop recalcFields=seq +``` +It counts `SEQ Figure` / `SEQ Table` fields in body document order and writes the real cached values (`Figure 1 / Figure 2 / Figure 3`), flipping `evaluated` true. Re-run it after inserting a caption mid-document so the numbers stay in sync. (Heading-relative `\s` and SEQ inside headers/footers defer to Word's own recompute — see `help docx document`.) Verify: `officecli view "$FILE" text` shows distinct ascending numbers. + +```bash +# Figure with caption BELOW the image. Caption = "Figure : title" + optional bookmark for cross-ref. +officecli add "$FILE" /body --type picture --prop src=arch.png --prop width=5in +officecli set "$FILE" "/body/p[last()]/r[last()]" --prop alt="Model architecture: attention over time-series sensors" +# Caption paragraph (below the figure, per academic convention) +officecli add "$FILE" /body --type paragraph --prop text="Figure " --prop style=Caption --prop size=10pt --prop italic=true --prop align=center +officecli add "$FILE" "/body/p[last()]" --type field --prop fieldType=seq --prop identifier=Figure +officecli add "$FILE" "/body/p[last()]" --type run --prop text=": Attention-based anomaly detection model." +# Bookmark the caption so other paragraphs can PAGEREF it +officecli add "$FILE" /body --type bookmark --prop name=fig_arch +# (Numbers are filled later by a single `set / --prop recalcFields=seq`, after all captions exist.) +``` + +### PAGEREF — cross-reference by bookmark + +```bash +# Cross-ref paragraph: "see Figure 1 on page X" +officecli add "$FILE" /body --type paragraph --prop text="As shown in Figure 1 (see page " --prop size=11pt --prop lineSpacing=1.5x +officecli add "$FILE" "/body/p[last()]" --type field --prop fieldType=pageref --prop name=fig_arch +officecli add "$FILE" "/body/p[last()]" --type run --prop text=")." +``` + +### Tables — caption ABOVE + +```bash +# Caption first (ABOVE the table), THEN the table +officecli add "$FILE" /body --type paragraph --prop text="Table " --prop style=Caption --prop size=10pt --prop italic=true --prop spaceAfter=6pt +officecli add "$FILE" "/body/p[last()]" --type field --prop fieldType=seq --prop identifier=Table +officecli add "$FILE" "/body/p[last()]" --type run --prop text=": Participant demographics (N=47)." +officecli add "$FILE" /body --type table --prop rows=5 --prop cols=4 --prop width=100% +# ... fill header + rows per docx v2 §Tables +``` + +### Verify SEQ + PAGEREF fields landed + +```bash +# At least one SEQ Figure or SEQ Table in the body document part +officecli raw "$FILE" /document | grep -c 'w:instrText[^>]*>[^<]*SEQ' # expect ≥ 1 +officecli raw "$FILE" /document | grep -c 'w:instrText[^>]*>[^<]*PAGEREF' # 0 ok if no cross-refs +``` + +Live fields carry **cached values** that render stale until a human presses F9 in Word. Expect "Figure 1" to show as `1`, `2`, ... immediately after recalc; before recalc, some viewers render `0` or blank. Judge field presence by `fldChar` existence, not by visible digit (→ see docx v2 §Field / cached-value spot-check). + +## Footnotes vs endnotes + +**Footnote** — sits at the bottom of the page where its anchor paragraph lives. Used for source citations in Chicago Notes-Bib, content asides in any style. + +**Endnote** — sits at the end of the document (or before the bibliography). Used by some venues in place of footnotes, or for long contextual notes that would clutter the page. + +```bash +# Footnote anchored to paragraph N +officecli add "$FILE" "/body/p[3]" --type footnote --prop text="Smith et al. reported similar findings in their 2023 review." +# Endnote — anchored to paragraph N (like footnote); lands at /endnote[@endnoteId=N] +officecli add "$FILE" "/body/p[3]" --type endnote --prop text="Extended derivation of equation (4) is available at the project repository." +``` + +Both appear as empty-string runs in `view annotated` output (`r[N] ""`) — the run carries a `` XML element, not visible text. Confirm insertion with `officecli query "$FILE" 'footnote'` or `officecli get "$FILE" "/footnotes/footnote[N]"`. Footnotes do NOT shift paragraph indices; add them in any order after body content is in place. Full schema: `officecli help docx footnote` / `officecli help docx endnote`. + +## Bibliography section + +Every academic paper ends with a reference list. The name of the section depends on the style (**References** for APA / IEEE / Chicago Author-Date; **Bibliography** for Chicago Notes-Bib; **Works Cited** for MLA). Each entry is a separate paragraph with **hanging indent**. + +```bash +# Section heading — same as body Heading1 (excluded from body numbering by convention) +officecli add "$FILE" /body --type paragraph --prop text="References" --prop style=Heading1 --prop size=20pt --prop bold=true --prop spaceBefore=18pt --prop spaceAfter=12pt +# Each entry: hanging indent 720 twips (0.5"), with indent=720 as the partner (first line flush, wraps indented) +officecli add "$FILE" /body --type paragraph --prop text="Smith, J. (2024). Remote work and cohesion. Journal of Applied Psychology, 109(3), 412-430." --prop size=12pt --prop lineSpacing=2x --prop indent=720 --prop hangingIndent=720 +# DOI hyperlink on its own run appended to the entry paragraph +officecli add "$FILE" "/body/p[last()]" --type hyperlink --prop url="https://doi.org/10.1037/apl0001123" --prop text="https://doi.org/10.1037/apl0001123" +``` + +Verified: `--prop indent=720 --prop hangingIndent=720` is the canonical hanging-indent pair per `officecli help docx paragraph`. The old `ind.firstLine=-720` form (negative first-line indent) is NOT canonical and fails schema on emit — → see docx v2 §Schema-invalid-on-emit. + +**Round-trip QA.** Count in-text citation markers (APA `(Author, Year)`, IEEE `[N]`, MLA `(Author N)`) vs reference-list entries. See Delivery Gate 4 below. Every cited key must resolve; every listed entry should be cited at least once. + +## Multi-column (IEEE journal two-column recipe) + +IEEE and many engineering / physics journals render body text in two columns with a single-column abstract above. The mechanism: a section break with `type=continuous` and `columns=2`, then another section break at the end to **revert** to single-column. + +**The reversion step is not optional.** Without it, the rest of the document — including references — renders as two columns. This is the single most common multi-column failure. + +```bash +FILE="ieee.docx" +officecli create "$FILE" +officecli open "$FILE" + +# 1. Title, authors, affiliation — single-column (the default first section) +officecli add "$FILE" /body --type paragraph --prop text="Attention-Based Anomaly Detection for Industrial Time Series" --prop align=center --prop size=18pt --prop bold=true --prop spaceAfter=12pt +officecli add "$FILE" /body --type paragraph --prop text="Alice Chen, Bob Martinez" --prop align=center --prop size=11pt +officecli add "$FILE" /body --type paragraph --prop text="Department of CS, Stanford University" --prop align=center --prop size=10pt --prop spaceAfter=18pt + +# 2. Abstract — still single-column, block-style +officecli add "$FILE" /body --type paragraph --prop text="Abstract" --prop align=center --prop size=12pt --prop bold=true --prop spaceAfter=6pt +officecli add "$FILE" /body --type paragraph --prop text="We present an attention-based model for detecting anomalies in industrial sensor time series..." --prop size=10pt --prop lineSpacing=1.15x --prop spaceAfter=12pt + +# 3. Section break + two-column from here on +# `/section[last()]` resolves to the final section (like p[last()]); an explicit /section[N] also works. +officecli add "$FILE" /body --type section --prop type=continuous +SECTION_COUNT=$(officecli query "$FILE" section --json | jq '.data.results | length') +# After the add, SECTION_COUNT should be 2 — [1] is pre-break, [2] is post-break (2-col body area). +officecli set "$FILE" "/section[2]" --prop columns=2 --prop columnSpace=1cm + +# 4. Body — IEEE wants Roman numerals + ALL CAPS section titles (P1.2). +officecli add "$FILE" /body --type paragraph --prop text="I. INTRODUCTION" --prop style=Heading1 --prop size=10pt --prop bold=true +officecli add "$FILE" /body --type paragraph --prop text="Industrial anomaly detection has been studied since [1]..." --prop size=10pt --prop lineSpacing=1.15x --prop firstLineIndent=360 + +# 5. At the end of 2-column body, ANOTHER section break + revert to single column for references / appendices +# (If you want references in 2-col too, skip step 5 — but most IEEE papers use 2-col for references as well.) +# officecli add "$FILE" /body --type section --prop type=continuous +# Use /section[last()] for the final section, or re-count for an explicit /section[N]: +# officecli set "$FILE" "/section[3]" --prop columns=1 + +# 6. Footer, close, validate +officecli add "$FILE" / --type footer --prop type=default --prop align=center --prop size=9pt --prop field=page +officecli close "$FILE" +officecli validate "$FILE" +``` + +**Visual verify.** Run `officecli view "$FILE" html` and Read the returned HTML to audit the rendered output. The abstract must render as full-width and the introduction onward as two columns. If the abstract wraps into two narrow columns, the first section break landed before the abstract — move it. + +**Section index bookkeeping.** Each `add /body --type section` inserts one empty paragraph into `/body` (the section-break marker). All subsequent `p[N]` indices shift by +1 per section break. Plan section breaks in advance; after adding a break, `officecli get "$FILE" /body --depth 1` to re-index before continuing. + +Full section schema (`columns`, `columnSpace`, `orientation`, `pageNumFmt`, `titlePage`, `lineNumbers`): `officecli help docx section`. + +## Abstract / keywords / affiliation block + +First-page metadata stack: title (centered 20-22pt bold) → authors (centered 12pt, superscript `^1 ^2` for multi-affiliation) → affiliations (centered 11pt, keyed to superscripts) → submission target / date → **Abstract** heading (14pt bold) → abstract body (block-style, **NO `firstLineIndent`**, 150-300 words) → keywords line (italic 11pt). Same "cover ≥ 60% filled" rule as docx v2. + +```bash +# Superscript affiliation markers (multi-institution paper) +officecli add "$FILE" /body --type paragraph --prop text="Alice Chen" --prop align=center --prop size=12pt +officecli add "$FILE" "/body/p[last()]" --type run --prop text="1" --prop superscript=true +officecli add "$FILE" "/body/p[last()]" --type run --prop text=", Bob Martinez" +officecli add "$FILE" "/body/p[last()]" --type run --prop text="2" --prop superscript=true +# Running header (skip on cover via type=first empty header — see docx v2 §headers) +officecli add "$FILE" / --type header --prop type=default --prop align=right --prop size=9pt --prop text="Short Running Title" +``` + +**Nature-family 2-col abstract** is rare — if required, open a `section type=continuous columns=2` BEFORE the abstract heading; short abstracts (<100 words) leave ragged columns. **Mirrored odd/even headers** are exposed by the high-level API: `officecli set "$FILE" /settings --prop evenAndOddHeaders=true`, then add the even header with `--type header --prop type=even`. Full header schema: `officecli help docx header`. + +## QA — Delivery Gate (executable) + +**Assume there are problems. Your job is to find them.** First render is almost never correct. Run this block before declaring done. + +### Gates 1-3 — inherited from docx v2 + +→ see docx v2 §Delivery Gate. Schema validate, token leak grep, live PAGE field structure. Copy-paste the docx v2 gate block first. Every check must print its success message. + +### Gate 4 — citation round-trip + +Every in-text citation key should resolve to a bibliography entry. Count mismatches = REJECT. + +```bash +# IEEE example (bracketed numerics). Adjust regex for APA (Author, Year) or MLA (Author Page). +CITATIONS=$(officecli view "$FILE" text | grep -oE '\[[0-9]+\]' | sort -u | wc -l) +ENTRIES=$(officecli query "$FILE" 'paragraph[hangingIndent]' --json | jq '.data.results | length') +echo "In-text citation markers: $CITATIONS | Bibliography entries: $ENTRIES" +# REJECT when citations exceed entries (cites without references). Entries > citations is allowed by some venues. +[ "$CITATIONS" -le "$ENTRIES" ] && echo "Gate 4 OK" || { echo "REJECT Gate 4: $CITATIONS in-text markers but only $ENTRIES bibliography entries"; exit 1; } +``` + +### Gate 5a — SEQ presence + cached numbers distinct + +If the paper has any numbered figure or table, the body must carry live `SEQ` fields AND their cached values must show distinct ascending numbers (else `view text` and downstream viewers that don't recompute cached fields will show "Figure 1" for all). + +```bash +# Count SEQ fields via query (raw-grep collapses multi-matches on one XML line → undercounts). +SEQ_COUNT=$(officecli query "$FILE" 'field[fieldType=seq]' --json | jq '.data.results | length') +VISIBLE_FIG=$(officecli view "$FILE" text | grep -cE '(Figure|Table) [0-9]+') +if [ "$VISIBLE_FIG" -gt 0 ] && [ "$SEQ_COUNT" -eq 0 ]; then + echo "REJECT Gate 5a: $VISIBLE_FIG visible Figure/Table labels but 0 SEQ fields." + exit 1 +fi +# Cached values must be distinct. Run `set / --prop recalcFields=seq` once after all captions exist; +# before recalc the fields render the #OCLI_NOTEVAL! sentinel, after recalc Figure 1 / Figure 2 / Figure 3: +DISTINCT=$(officecli view "$FILE" text | grep -oE '(Figure|Table) [0-9]+' | sort -u | wc -l) +[ "$SEQ_COUNT" -le "$DISTINCT" ] && echo "Gate 5a OK (SEQ=$SEQ_COUNT, distinct=$DISTINCT)" || { echo "REJECT Gate 5a: $SEQ_COUNT SEQ fields but only $DISTINCT distinct rendered labels — run 'set \"$FILE\" / --prop recalcFields=seq'"; exit 1; } +``` + +### Gate 5b — Visual audit via HTML preview (MANDATORY, not optional) + +Gates 1–5a catch schema, token leaks, live-field presence, citation counts. **They do NOT catch physical assembly defects** — scrambled page order, a duplicated Abstract mid-document, three figures all labeled "Fig. 1" despite SEQ field presence, equation variables rendering as plain-text LaTeX (`lambda_1`, `x_{t+1}`) instead of math. Do not skip — Gates 1–5a pass ≠ visual OK. + +Run `officecli view "$FILE" html` and Read the returned HTML path. For every page of the paper, answer: + +> (a) Are pages in logical academic sequence? (Title → Abstract → Keywords → Introduction → body → References — no forward jumps, no backward leaks.) +> (b) Does the Abstract appear exactly once, not duplicated mid-document? +> (c) Are Figure N / Table N labels distinct and ascending? (Fig. 1, Fig. 2, Fig. 3 — not all "Fig. 1". Same for tables.) +> (d) Do equations render as math? (Italicized variables, Greek letters like λ / α, proper integrals / fractions — NOT plain-text `lambda_1`, `x_{t+1}`, `\int`.) +> (e) For IEEE papers: are section titles ALL CAPS with Roman numerals (`I. INTRODUCTION`)? Are tables Roman (`Table I`, `Table II`)? +> (f) For APA papers: are Level-1 headings centered bold and unnumbered (not `1. Introduction`)? +> (g) Does every in-text "see Fig. N" / "see Table N" resolve to a figure/table that actually carries that number? +> (h) Heading hierarchy visually distinct (size + weight) across H1 / H2 / H3? + +Report every instance. If even one defect is present → REJECT; do not deliver until fixed. + +**Human preview (optional).** If you want the user to visually preview the paper, run `officecli watch "$FILE"` for a live preview the user can open at their own discretion, or have them open the `.docx` directly in Word / WPS / Pages. For final visual verification, open the file in the target viewer. + +### Honest limit + +`validate` catches schema errors, not academic-style errors. A document passes `validate` with APA citations in an IEEE paper, footnotes in a style that forbids them, or figures with hardcoded numbers that drift when a new figure is inserted. The gates above — especially Gate 4 (round-trip) and Gate 5 (SEQ presence) — are how you catch what validate cannot. + +## Known Issues & Pitfalls (academic-specific) + +→ Base pitfalls (shell escape, `\$ \t \n` literals, table cell formatting order, page-break boundaries, `shd.fill` / `ind.firstLine` schema-invalid forms, TOC cached values, watermark two-step): see docx v2 §Known Issues & Pitfalls. + +Academic-specific: + +- **`\left(...\right)` / `\left[...\right]` with a sub/superscript INSIDE the delimiters → parse error** (`cast object … Subscript`). An outer script (`\left(x+y\right)^2`) is fine. Use plain `(`, `)`, `[`, `]` — OMML auto-sizes in display mode. +- **`move` on `/body/oMathPara[N]` reorders the equation** (the wrapping paragraph moves). `--before ` may leave a stray empty paragraph; prefer `--index` / `--after`. +- **Section break +1 paragraph offset.** Each `add /body --type section` inserts one empty paragraph into `/body`. All `p[N]` indices after the break shift by +1. Plan breaks; after any `add section`, `officecli get "$FILE" /body --depth 1` to re-index. +- **`/section[last()]` resolves to the final section** (mirrors `p[last()]`), for both get and set. An explicit `/section[N]` also works: + ```bash + SECTION_COUNT=$(officecli query "$FILE" section --json | jq '.data.results | length') + # use /section[last()] for the final section, or an explicit /section[2], /section[3], ... + ``` + Each `add /body --type section` increments the count. Re-query after every break. +- **Multi-column does NOT auto-revert.** After a `columns=2` section, you must add another section break and explicitly set `columns=1` on the new `/section[N]` (N = post-revert count) — otherwise the rest of the document, including references, renders as two columns. Verify with `officecli get "$FILE" "/section[N]"` for each N. +- **`--type equation` on a `tc[N]` cell path is rejected with a guard error** (`table cells only accept paragraphs, tables, or SDTs`) — no bad XML is written. Inside a table cell, target `tc[N]/p[1]` with `--prop mode=inline` instead. +- **SEQ caption numbers are filled by `set / --prop recalcFields=seq`** (run once after all captions exist), not by per-field raw-set patching. A pre-recalc SEQ field shows the `#OCLI_NOTEVAL!` sentinel in `view text`. +- **Hanging-indent canonical form is `indent=720 hangingIndent=720`.** Not `ind.firstLine=-720`. The dotted form emits `` after `` and fails schema on emit. +- **Footnote reference runs show as empty strings in `view annotated`.** The `` XML element has no visible text on the reference side; the note body lives in `/footnotes/footnote[N]`. Confirm with `officecli query "$FILE" 'footnote'`, not by eyeballing `view text`. +- **Caption placement:** Table caption ABOVE the table; Figure caption BELOW the figure. Every major style (APA, Chicago, IEEE, MLA) agrees. Putting a Table caption below the table is an academic-style error, not a rendering issue — `validate` will not catch it. +- **TOC cached rendering / shell-escape:** → see docx v2 §Table of Contents, §Shell escape. + +## Renderer quirks (cross-viewer) + +→ see docx v2 §Renderer quirks. PAGE / TOC cached values, OMML baseline shifts, scheme colors — all identical quirks apply to academic papers. Before calling an equation or a citation marker broken, open the file in the user's target viewer (Word, WPS, Pages) — if it renders correctly there, it is a viewer quirk, not a skill defect. + +## Help pointer + +When in doubt: `officecli help docx`, `officecli help docx `, `officecli help docx --json`. Help is the authoritative schema; this skill is the decision guide for academic deltas on top of docx v2. diff --git a/external/officecli/skills/officecli-data-dashboard/SKILL.md b/external/officecli/skills/officecli-data-dashboard/SKILL.md new file mode 100644 index 0000000..9b144d5 --- /dev/null +++ b/external/officecli/skills/officecli-data-dashboard/SKILL.md @@ -0,0 +1,415 @@ +--- +name: officecli-data-dashboard +description: "Use this skill to build a multi-element Excel dashboard — Dashboard sheet on open, multiple formula-driven KPI cards, multiple charts, sparklines, and conditional formatting — from CSV or tabular input. Trigger on: 'dashboard', 'KPI dashboard', 'analytics dashboard', 'executive dashboard', 'metrics dashboard', 'CSV to dashboard', 'data visualization'. Output is a single .xlsx. Scene-layer on officecli-xlsx: inherits every xlsx hard rule. DO NOT invoke for: a single budget tracker / one-sheet CSV-with-formatting (use xlsx), a 3-statement / DCF / LBO financial model (use financial-model), a weekly report with at most 1 chart and fewer than 10 rows (use xlsx)." +--- + +# Data Dashboard (scene-layer on officecli-xlsx) + +A dashboard is not "a spreadsheet with charts". It is a composition: **one Dashboard sheet the user lands on** with formula-driven KPI cards, cell-range-linked charts, sparklines, and semantic conditional formatting. Everything else (raw data, aggregations) is upstream infrastructure the user should never need to open. This skill teaches the composition pattern. Everything about the xlsx engine — cells, formulas, batch JSON, shell quoting, validate, HTML preview — comes from `officecli-xlsx` and is not re-taught here. + +## Setup + +If `officecli` is missing: + +- **macOS / Linux**: `curl -fsSL https://d.officecli.ai/install.sh | bash` +- **Windows (PowerShell)**: `irm https://d.officecli.ai/install.ps1 | iex` + +Verify with `officecli --version` (open a new terminal if PATH hasn't picked up). If install fails, download a binary from https://github.com/iOfficeAI/OfficeCLI/releases. + +## ⚠️ Help-First Rule + +**When a prop name, enum value, or alias is uncertain, consult help before guessing.** + +```bash +officecli help xlsx # element list +officecli help xlsx chart # full schema for charts +officecli help xlsx sparkline # sparklines +officecli help xlsx conditionalformatting # all CF rule types +``` + +Help reflects the installed CLI version. When this skill and help disagree, **help wins**. DeferredAddKeys (`combosplit`, `holesize`) work on `add` only — see Reference. + +## Mental Model & Inheritance + +This skill **inherits every xlsx hard rule** from `officecli-xlsx` — shell quoting, zero formula errors, visual delivery floor, batch JSON shape (`{"command":"set"|"add","path":...,"props":{...}}` — key is `command`, NOT `action`), batch JSON dotted-name rule, chart data-feed forms, batch+resident limits, `validate` discipline. Read officecli-xlsx first; honour those rules, do not re-teach them here. + +**Reverse handoff — do NOT use this skill when:** + +- The ask is a **single-sheet CSV-with-formatting tracker** (no Dashboard sheet, no KPI cards, ≤ 1 chart) → go back to `officecli-xlsx`. +- The ask is a **3-statement / DCF / LBO financial model** with blue-inputs / black-formulas / cross-sheet drivers → use `officecli-financial-model`. +- The ask is a **weekly status report** with one SUMIF summary and one chart over < 10 rows → `officecli-xlsx`. + +This skill only accepts: "a Dashboard sheet the user opens first, multiple KPI cards, multiple charts, some CF / sparklines". + +## Shell & Execution Discipline + +→ see officecli-xlsx §Shell & Execution Discipline for the baseline (quoting, heredoc for `!`, incremental execution). + +Two increments specific to dashboards: + +- **Long chart `add` commands exceed 180 chars.** Always split across lines with trailing `\`; never pack a chart command onto a single line. The longer the command, the higher the chance a shell-escape bug hides inside it. +- **Multi-instance counts use `query --json | jq length`, never `raw-get | grep -c`.** Example: `officecli query "$FILE" chart --json | jq '.data.results | length'` for "how many charts do I have?". + +## Core Principles + +Five non-negotiable principles. If any one is violated the output is not a dashboard, it is a spreadsheet that happens to have a chart. + +1. **Formula-driven KPIs.** Every KPI value on the Dashboard sheet is a formula — `SUM`, `AVERAGE`, `IFERROR((...-...)/...,0)`, whatever — referring to cells on the Data / Summary sheet. Never hardcode a computed number. When the underlying data changes tomorrow, KPIs update on open. + +2. **Cell-range references for charts.** Every chart series reads from a cell range: `series1.values="Sheet1!B2:B13"`. Inline `data="Revenue:100,200,300"` is for a 5-minute demo, not a delivered dashboard. The one exception: data requires an aggregation Excel cannot express (rare) — document the exception in a comment cell. + +3. **Dashboard-first architecture.** KPI label cells, KPI value cells, charts, sparklines all live on the **Dashboard** sheet — the single sheet a user lands on. Raw imports and `SUMIFS` rollups live on Data / Summary sheets, upstream of the Dashboard. The user should never need to switch tabs to find the answer. + +4. **Visible cells only for chart sources.** LibreOffice does not evaluate formulas in hidden columns or hidden sheets at render time. A chart whose `series1.values` points at a hidden-column `SUMIFS` renders blank. Pattern: aggregate into a **visible** Summary sheet, point charts at Summary cells, hide only helper columns that are not chart sources. + +5. **Data-size-aware complexity.** A 10-row dataset does not get 5 KPIs and 4 charts. A 200-row dataset does not get 1 KPI and 1 chart. Scale up the composition with the input (table in §Design Ideas). Overbuilding is as wrong as underbuilding. + +## Requirements + +All `officecli-xlsx` requirements apply (→ see officecli-xlsx §Requirements for Outputs). Dashboards add these: + +- **Dashboard sheet is the active tab on open.** Confirm 0-based sheet index with `officecli query "$FILE" sheet` BEFORE filling `activeTab="N"`. Never guess the index. +- **`calc.fullCalcOnLoad=true`.** Set via `officecli set "$FILE" / --prop calc.fullCalcOnLoad=true`. Do NOT `raw-set` `` — it produces duplicate elements that fail validate. +- **Refresh downstream cachedValue after every upstream edit.** `fullCalcOnLoad=true` schedules runtime recalc only; it does NOT refresh build-time `cachedValue`. After `set B=100 → set E==B+D → fix B=150`, E is stale until you re-issue E's formula (or close/reopen). Stale cache ships "Net Change = 0" to the board. +- **Every chart has a descriptive title and every series has a name.** `"Series1"` in a legend is unfinished work. +- **Every KPI value cell has a formula.** Verifiable: `officecli query "$FILE" 'Dashboard!:has(formula)' --json | jq '.data.results | length'` should equal your planned KPI count. +- **Header row fill on every data sheet.** Data sheet, Summary sheet, and any secondary data sheet need row 1 filled (e.g., `fill=1F3864 + font.color=FFFFFF + font.bold=true`). +- **10+ rows on Data sheet → ≥ 1 CF rule on a numeric column.** A 20-row table with zero visual scanning aid is a quality miss. +- **Dashboard value columns sized to the widest expected cachedValue — not a fixed 22.** Rule of thumb at 24pt bold + currency numFmt: `width ≈ ceil((visible_chars + 2) × 1.3)`. A KPI holding `¥1,958,414,250` (14 visible chars with currency + commas) needs `width ≥ 28`; a 4-digit KPI still needs `width ≥ 22` as the floor. Hardcoding `22` for a 10+ digit KPI is how `###` ships to the user. +- **Sparkline row height ≥ 20.** A sparkline in a default 15pt row is a flat squiggle — set `/Dashboard/row[N] height=22` (or 24 when paired with a 24pt KPI value cell in the same row). +- **Print deliverables set `_xlnm.Print_Area` scoped to Dashboard** + hide non-Dashboard sheets + add ``. Without all three, the print pipeline emits every sheet and Dashboard lands on page 2+. See §Print-ready delivery for the exact commands. + +## Quick Start + +Minimal viable dashboard: 12-month revenue CSV → 4 KPIs + 1 line chart + activeTab + fullCalcOnLoad. Adapt the numbers, don't copy-paste blind. Broken into phases so a single failed phase is obvious. + +**Phase 1 — Data sheet: create, import, format.** + +```bash +FILE=my_dashboard.xlsx +officecli create "$FILE" +officecli import "$FILE" /Sheet1 --file sales.csv --header +officecli set "$FILE" '/Sheet1/col[A]' --prop width=12 +officecli set "$FILE" '/Sheet1/col[B]' --prop width=15 +officecli set "$FILE" '/Sheet1/B2:B13' --prop numFmt='$#,##0' +officecli set "$FILE" '/Sheet1/A1:B1' --prop fill=1F3864 --prop font.color=FFFFFF --prop font.bold=true +``` + +**Phase 2 — Dashboard sheet + one KPI card.** + +```bash +officecli add "$FILE" / --type sheet --prop name=Dashboard +officecli set "$FILE" '/Dashboard/col[A]' --prop width=22 +officecli set "$FILE" '/Dashboard/col[B]' --prop width=12 +officecli set "$FILE" /Dashboard/A1 --prop value="Total Revenue" --prop font.size=9 --prop font.color=666666 --prop bold=true +officecli set "$FILE" /Dashboard/A2 --prop 'formula==SUM(Sheet1!B2:B13)' --prop numFmt='$#,##0' --prop font.size=24 --prop bold=true --prop font.color=2E7D32 +``` + +**Phase 3 — Sparkline + chart.** + +```bash +officecli add "$FILE" /Dashboard --type sparkline --prop cell=B2 --prop range='Sheet1!B2:B13' --prop type=line --prop color=4472C4 --prop highPoint=true --prop highMarkerColor=FF0000 +officecli add "$FILE" /Dashboard --type chart \ + --prop chartType=line \ + --prop title="Revenue Trend" \ + --prop series1.name="Revenue" \ + --prop series1.values='Sheet1!B2:B13' \ + --prop series1.categories='Sheet1!A2:A13' \ + --prop preset=dashboard --prop axisNumFmt='$#,##0' \ + --prop x=0 --prop y=5 --prop width=10 --prop height=15 +``` + +**Phase 4 — fullCalcOnLoad → activeTab (LAST) → close → validate.** + +```bash +officecli set "$FILE" / --prop calc.fullCalcOnLoad=true + +# Resolve Dashboard's 0-based index from the actual sheet list — never hardcode. +DASH_IDX=$(officecli query "$FILE" sheet --json \ + | jq '[.data.results[].path] | index("/Dashboard")') +officecli raw-set "$FILE" /workbook --xpath "//x:sheets" --action insertbefore \ + --xml "" +officecli close "$FILE" +officecli validate "$FILE" +``` + +Verified end-to-end on a 12-row revenue CSV: `validate` reports no errors, Dashboard opens first, `Dashboard/A2.cachedValue` resolves (2,075,000 for the test data), chart renders with values linked. + +## Design Ideas + +Options, not templates. The user's data and audience drive the choices. + +### Layout patterns (pick one, stay consistent) + +**Pattern 1 — executive summary** (board packs): KPI strip A1:H4, charts stack from row 6. +``` +┌ KPI1 │ KPI2 │ KPI3 │ KPI4 ┐ rows 1-4 +├──────┴──────┴──────┴──────┤ +│ Chart 1 (wide) │ rows 6-18 +├───────────────┬───────────┤ +│ Chart 2 │ Chart 3 │ rows 20-32 +``` + +**Pattern 2 — ops console** (live ops): KPIs down A:B, charts fill C:L. +``` +│ KPI1 │ │ +│ KPI2 │ Chart 1 │ rows 1-12 +│ KPI3 │ │ +│ KPI4 ├───────────────────┤ +│ KPI5 │ Chart 2 │ rows 14-26 +``` + +**Pattern 3 — scorecard** (≥ 6 KPIs, no dominant chart): grid of 2×3 cards (label / value / sparkline). +``` +│ KPI1 │ KPI2 │ KPI3 │ rows 1-4 +│ KPI4 │ KPI5 │ KPI6 │ rows 5-8 +``` + +### Complexity scaling by data size + +| Rows | KPIs | Charts | Sparklines | CF rules | Preset | +|---|---|---|---|---|---| +| < 10 | 1–2 | 1 | skip | 0–1 | `minimal` | +| 10–50 | 2–3 | 2 | only if sequential time-series | 1–2 | `dashboard` | +| 50–200 | 3–5 | 2–3 | only if sequential time-series | 2–3 | `dashboard` | +| 200+ | 3–5 | 3 | only if sequential time-series | 3–4 | `dashboard` | + +### Chart type selection + +| Data pattern | Chart type | Notes | +|---|---|---| +| Trend over time, one series | `line` | Add `trendline=linear` to show direction on noisy series | +| Trend over time, multiple components | `line` (multi-series) or `columnStacked` | Stacked when components sum to a meaningful total | +| Comparison across categories in time order | `column` | Not `bar` — horizontal bars break left-to-right time reading | +| Part-of-whole breakdown | `doughnut` | Prefer over `pie`: `chartType=pie` has a known LibreOffice blank-render regression | +| Budget vs actual | `combo` with `combosplit=1` | First series as bars, rest as lines | +| Correlation | `scatter` | X-axis via `categories` / `series1.categories` — `series1.xValues` is UNSUPPORTED | + +### Preset options + +`--prop preset=` on every chart. Options: `minimal`, `dashboard`, `corporate`, `magazine`, `colorful`, `monochrome`, `dark`. Pick one and stay consistent across all charts on a single Dashboard — mixing presets reads as accidental. + +### Conditional formatting — semantic colors + +Four CF rule types; each uses `--type ` at `add` time: + +| Intent | `--type` | Typical props | +|---|---|---| +| Magnitude bar (sales, spend) | `databar` | `sqref=B2:B13 color=4472C4` — explicit `min=0 max=` recommended for predictable scaling, but omitting them is valid (defaults to data min/max) | +| Heat map (rates, growth) | `colorscale` | `sqref=D2:D13 mincolor=FFCDD2 midcolor=FFFFFF maxcolor=C8E6C9` | +| Status indicator | `iconset` | `sqref=E2:E13 iconset=3Arrows` — see help for the full enum | +| Custom business rule | `formulacf` | `sqref=B2:B13 'formula=$B2>=100000' fill=C8E6C9 font.color=2E7D32` — `font.bold` works on CF too | + +Semantic colors to stay consistent within a dashboard: + +- good / positive: fill `C8E6C9`, font `2E7D32` +- bad / negative: fill `FFCDD2`, font `C62828` +- neutral: fill `F5F5F5`, font `666666` + +### KPI card anatomy + +A card is a label cell + a value cell. The label is small gray (font.size=9, font.color=666666, bold); the value is large bold (font.size=24, bold=true, numFmt, font.color signals tone). One row of light fill (e.g. `F0F4FF`) across the card area gives the "card" read without building merged-cell scaffolds. Value column width must be sized to the largest cachedValue — never narrower than 22, often 26–32 for 8+ digit currency (see Requirements). + +### Chart width budget by title length + +At the `dashboard` preset's default title font, the chart plot-box width (in column units) must stay ahead of the title string, or the title clips mid-word. Rule of thumb: `chart.width ≥ ceil(title.length × 0.18)`. A 35-character title ("Department: Year-End Headcount vs Attrition Rate") needs `width ≥ 7`; be safer and use 10–12. If the anchor cannot be widened, shorten the title to ≤ 25 characters — clipped titles in a board-ready deliverable are indefensible. + +`officecli get chart[N]` exposes numeric `width` (e.g. `width=480pt`) alongside `anchor` (e.g. `"A6:K21"`) at `.data.results[0].format`. Either is usable for Gate 2 — derive column span from anchor letters (A→K = 10 cols) when you need a column-unit budget. + +### Print-ready delivery (board-pack / investor-send / one-pager) + +Triggers: ask contains "print" / "一页" / "董事会" / "投资人". Four artefacts on the Dashboard sheet; non-Dashboard sheets hidden so the print pipeline emits one page only. + +```bash +# 1. Print_Area scoped to Dashboard (xlnm convention). +officecli add "$FILE" / --type namedrange --prop name=_xlnm.Print_Area --prop scope=Dashboard --prop 'refersTo=Dashboard!$A$1:$H$36' +# 2. fit-to-page on Dashboard. +officecli raw-set "$FILE" /Dashboard --xpath "//x:worksheet" --action prepend --xml '' +# 3. Landscape page setup. +officecli raw-set "$FILE" /Dashboard --xpath "//x:sheetData" --action insertafter --xml '' +# 4. Hide non-Dashboard sheets — Print_Area scope alone does NOT stop the print pipeline from emitting every visible sheet. +for S in Sheet1 Summary; do + officecli raw-set "$FILE" /workbook --xpath "//x:sheet[@name='$S']" --action setattr --xml "state=hidden" || true +done +``` + +Delete any `Print_Area` set on Data / Summary sheets — conflicting scopes emit multi-page output. + +## QA (REQUIRED — Delivery Gate) + +**Assume there are problems. Your job is to find them.** A chart that was rendered does not mean a chart that was meaningful. "validate pass" is not delivery; "the Dashboard sheet reads like someone who knows the business made it" is delivery. + +### Minimum cycle before "done" + +Inherit the xlsx baseline (`view issues`, formula error queries, `validate`, HTML preview scan): → see officecli-xlsx §QA minimum cycle. + +Then run the dashboard-specific Delivery Gates. Each gate uses **COUNT-then-if** pattern with a `.data.*` wrapper — never chain `&& echo OK || echo FAIL`. + +**Gate 1 — KPI formula coverage.** Every planned KPI cell must carry a formula. Adjust `-lt 2` to your plan (4 KPIs → `-lt 4`). + +```bash +KPI_FORMULAS=$(officecli query "$FILE" 'Dashboard!:has(formula)' --json | jq '.data.results | length') +[ "$KPI_FORMULAS" -lt 2 ] && { echo "REJECT Gate 1: $KPI_FORMULAS formula cells on Dashboard"; exit 1; } +``` + +**Gate 2 — Chart count matches plan, every chart has data + plausible title width.** + +```bash +CHART_COUNT=$(officecli query "$FILE" chart --json | jq '.data.results | length') +[ "$CHART_COUNT" -lt 1 ] && { echo "REJECT Gate 2: zero charts"; exit 1; } +col_num () { local c=$1 n=0; for ((k=0;k<${#c};k++)); do n=$((n*26+$(printf '%d' "'${c:$k:1}")-64)); done; echo "$n"; } +for i in $(seq 1 "$CHART_COUNT"); do + JSON=$(officecli get "$FILE" "/Dashboard/chart[$i]" --json) + SC=$(echo "$JSON" | jq -r '.data.results[0].format.seriesCount // 0') + TITLE=$(echo "$JSON" | jq -r '.data.results[0].format.title // ""') + ANCHOR=$(echo "$JSON" | jq -r '.data.results[0].format.anchor // ""') + [ "$SC" = "0" ] || [ -z "$TITLE" ] && { echo "REJECT Gate 2: chart[$i] seriesCount=$SC title='$TITLE'"; exit 1; } + [ -z "$ANCHOR" ] && continue + LCOL=$(echo "${ANCHOR%%:*}" | sed 's/[0-9]*$//'); RCOL=$(echo "${ANCHOR##*:}" | sed 's/[0-9]*$//') + SPAN=$(( $(col_num "$RCOL") - $(col_num "$LCOL") + 1 )) + MIN=$(( (${#TITLE} * 18 + 99) / 100 )) + [ "$SPAN" -lt "$MIN" ] && { echo "REJECT Gate 2: chart[$i] title=${#TITLE} chars needs width ≥ $MIN, anchor spans $SPAN"; exit 1; } +done +``` + +Narrower titles at preset `minimal` / `magazine` may clip earlier than the 0.18 factor — spot-check. + +**Gate 3 — Chart series names populated (no "Series1" in legend).** + +```bash +for i in $(seq 1 "$CHART_COUNT"); do + BAD=$(officecli get "$FILE" "/Dashboard/chart[$i]" --json | jq '[.data.results[0].children[]? | select(.type == "series") | select((.format.name // "") | test("^Series[0-9]+$"; "i"))] | length') + [ "$BAD" -gt 0 ] && { echo "REJECT Gate 3: chart[$i] has $BAD auto-named series"; exit 1; } +done +``` + +**Gate 4 — CF rules on Data sheet (10+ rows).** + +```bash +CF_COUNT=$(officecli query "$FILE" conditionalformatting --json | jq '.data.results | length') +[ "$CF_COUNT" -lt 1 ] && { echo "REJECT Gate 4: zero CF rules on 10+ row data sheet"; exit 1; } +``` + +Note: `query conditionalformatting` is the canonical element name; `query cf` returns 0 (not an alias). + +**Gate 5 — activeTab and fullCalcOnLoad set.** Compare against real Dashboard index (Dashboard-at-index-0 is a true pass). + +```bash +DASH_IDX=$(officecli query "$FILE" sheet --json | jq '[.data.results[].path] | index("/Dashboard")') +ACTIVE=$(officecli get "$FILE" /workbook --json | jq '.data.results[0].format.activeTab // -1') +FULLCALC=$(officecli get "$FILE" /workbook --json | jq -r '.data.results[0].format["calc.fullCalcOnLoad"] // false') +[ "$ACTIVE" != "$DASH_IDX" ] && { echo "REJECT Gate 5: activeTab=$ACTIVE Dashboard=$DASH_IDX"; exit 1; } +[ "$FULLCALC" != "true" ] && { echo "REJECT Gate 5: calc.fullCalcOnLoad=$FULLCALC — stale caches will ship"; exit 1; } +``` + +**Gate 6 — Placeholder sweep.** No build-time tokens in rendered output. + +```bash +LEAKS=$(officecli view "$FILE" text 2>/dev/null | grep -niE '\{\{|\$fy\$||xxxx|TBD' | wc -l | tr -d ' ') +[ "$LEAKS" -gt 0 ] && { echo "REJECT Gate 6: $LEAKS placeholder tokens"; exit 1; } +``` + +**Gate 7 — Visual delivery floor (ported from xlsx).** Run `officecli view "$FILE" html` and Read the returned HTML path. Confirm: + +- No `###` in any Dashboard or Data cell (columns too narrow). +- No truncated KPI labels, sheet tab names, or chart titles. +- No placeholder tokens rendered as text (`$fy$24`, `{var}`, ``, `xxxx`). +- Pie / doughnut slices render with distinct fill colors (if collapsed in LibreOffice, verify in the user's target viewer before declaring broken — → see officecli-xlsx §Known Issues/Renderer caveats). +- No empty chart anchors — every chart has a visible, plausible plot. +- Dashboard sheet opens first (tab highlighted, active area scrolled to top). + +If `view html` is blocked (renderer conflict, headless, port busy), Gate 7 is still **mandatory** — run ALL fallback checks: + +```bash +# a) Token / ### sweep. +officecli view "$FILE" text 2>/dev/null | grep -nE '###|\{\{||\$fy\$|xxxx' && { echo "REJECT Gate 7: tokens or ### present"; exit 1; } +# b) Per-KPI: cachedValue length × coef must fit col width. coef=0.55 fit-to-page, 0.85 otherwise. +for CELL in A2 C2 E2 G2; do + CV=$(officecli get "$FILE" "/Dashboard/$CELL" --json | jq -r '.data.results[0].format.cachedValue // .data.results[0].text // ""') + W=$(officecli get "$FILE" "/Dashboard/col[${CELL%%[0-9]*}]" --json | jq -r '.data.results[0].format.width // 0') + CAP=$(echo "$W * 0.55" | bc -l | awk '{print int($1)}') + [ "${#CV}" -gt "$CAP" ] && { echo "REJECT Gate 7: $CELL '$CV' (${#CV} chars) > cap $CAP"; exit 1; } +done +# c) Rerun Gate 2 title × 0.18 ≤ anchor span. d) Log which fallback was used and why. +``` + +Gate 7 must **NEVER** be skipped — skipping ships `###` to the user. + +If scene keywords include print / 一页 / board / 投资人 / 董事会, extend Gate 7 with a structural print-scope check: + +```bash +if echo "$USER_REQ" | grep -qiE 'print|一页|投资人|董事会|board'; then + # Every non-Dashboard sheet must be hidden or veryHidden. + # query sheet --json carries the name in .preview (no .name/.state); visibility lives in + # each sheet's own get .format.hidden (absent => visible), so iterate + probe per sheet. + LEAKING="" + while IFS=$'\t' read -r SPATH SNAME; do + [ "$SNAME" = "Dashboard" ] && continue + HIDDEN=$(officecli get "$FILE" "$SPATH" --json | jq -r '.data.results[0].format.hidden // false') + [ "$HIDDEN" != "true" ] && LEAKING="$LEAKING $SNAME" + done < <(officecli query "$FILE" 'sheet' --json | jq -r '.data.results[] | [.path, .preview] | @tsv') + [ -n "$LEAKING" ] && { echo "REJECT Gate 7 print-scope: visible non-Dashboard sheet(s):$LEAKING — hide before delivery"; exit 1; } + # Dashboard must carry an explicit Print_Area named range. + PA=$(officecli query "$FILE" 'namedrange[name="_xlnm.Print_Area"]' --json | jq '.data.results | length') + [ "$PA" -ge 1 ] || { echo "REJECT Gate 7 print-scope: no _xlnm.Print_Area set"; exit 1; } +fi +``` + +The user opens the file in their target viewer (Office / WPS / Numbers) for the final print preview — the skill does not render export artefacts. + +**Gate 8 — Formula sanity (cachedValue real, not stale/error).** `fullCalcOnLoad=true` refreshes at runtime, NOT build-time cache — so every formula cell must carry a non-empty, non-zero, non-error `cachedValue` now. + +```bash +for CELL in A2 C2 E2 G2; do + JSON=$(officecli get "$FILE" "/Dashboard/$CELL" --json) + [ -z "$(echo "$JSON" | jq -r '.data.results[0].format.formula // ""')" ] && continue + CV=$(echo "$JSON" | jq -r '.data.results[0].format.cachedValue // ""') + case "$CV" in + "" | "0" | "#DIV/0!" | "#REF!" | "#N/A" | "#VALUE!" | "#NAME?" | "null") + echo "REJECT Gate 8: $CELL cachedValue='$CV' — re-issue formula or close+reopen"; exit 1 ;; + esac +done +``` + +If a KPI is genuinely zero (e.g. "terminations this quarter" = 0), whitelist it in the loop and document — default assumption is "zero is broken". + +If anything fails, fix at source, re-run the full cycle. + +### Honest limits + +Scatter charts do not accept `series1.xValues` (UNSUPPORTED) — feed the x-axis via `categories` / `series1.categories`. LibreOffice chart color drift / pie-slice collapse / checkbox double-box are viewer artifacts — spot-check in Office / WPS / Numbers first. + +## Reference + +- **Shorthand `--type` at `add`:** `chart`, `sparkline`, `databar`, `colorscale`, `iconset`, `formulacf`. CF rules map to `help xlsx conditionalformatting`; path suffix `/Sheet/cf[N]`. +- **Full schemas live in help:** `officecli help xlsx chart` / `sparkline` / `conditionalformatting`. This skill does not mirror them. +- **DeferredAddKeys (add-only):** `combosplit`, `holesize`. See D-1. (`preset`, `trendline`, `referenceline`, `axisNumFmt` now work on `set` too — help shows `[add/set]`.) +- **Build order:** charts + sparklines + CF + tabColors first → `calc.fullCalcOnLoad=true` via high-level `set` → `raw-set activeTab` **LAST** (after all sheets exist). + +## Known Issues & Pitfalls + +### Dashboard-specific + +| # | Issue | Mitigation | +|---|---|---| +| D-1 | `combosplit` is a DeferredAddKey — works on `add` only. (`preset`, `referenceline`, `trendline`, `axisNumFmt` now apply on `set` too — help shows `[add/set]`.) | Set `combosplit` at `add` time; cannot apply after the fact — remove + re-add. The other four can be applied or changed post-creation via `set`. | +| D-2 | `referenceline` format is `value:color:label:dash` (color BEFORE label). `"0:Break-Even:FF0000:dash"` fails `Invalid color value`. | Order is value, color, label, dash. | +| D-3 | Scatter charts do NOT accept `series1.xValues` (UNSUPPORTED). Feed the x-axis through `categories` / `series1.categories`. | `--prop series1.categories="Sheet1!A2:A13"` (or `--prop categories="Sheet1!A2:A13"`) | +| D-4 | `formulacf` honors `font.bold` / `font.italic` (written to the dxf font and surfaced on readback), alongside `fill` and `font.color`. | Use any of `fill` / `font.color` / `font.bold` / `font.italic` to signal a CF rule. | +| D-5 | Dashboard column widths default to 8.43 — KPI values at 24pt bold show `###` | Size by cachedValue bracket: 4–6 digits → 22–24; 7–9 digits (million) → 26–30; 10+ digits (亿 / billion) → 32–36; 百亿 / 10-digit + currency symbol + fit-to-page landscape → **40–44**. Formula `ceil((visible_chars+2)*1.3)` is a starting point; always verify via Gate 7 fallback b). Sparkline columns: 12. | +| D-6 | `raw-set activeTab` must be the LAST mutation. Inserting before all sheets exist shifts indices. | Finish all sheets / charts / CF / sparklines / tabColors, then `raw-set`. | +| D-7 | `calc.fullCalcOnLoad` via `raw-set` creates duplicate `` → validate fails | Use `officecli set "$FILE" / --prop calc.fullCalcOnLoad=true`. | +| D-8 | LibreOffice does not evaluate hidden-column formulas at render → charts referencing hidden cells render blank | Aggregate into a visible Summary sheet, chart reads from Summary. Hide only columns that are not chart sources. | +| D-9 | `chartType=pie` blank-renders in LibreOffice | Use `doughnut` as the safe substitute for part-of-whole breakdowns. | +| D-10 | `SUMIFS` / `AVERAGEIFS` with date criteria fails silently if the criterion is a string | Wrap with `DATE()` or `DATEVALUE()`: `=SUMIFS(B2:B13,A2:A13,DATE(2025,1,5))`. | +| D-11 | Summary sheet percentage formulas display as raw decimals (0.098) without `numFmt` | Set `numFmt="0.0%"` at the same `set` call as the formula. | +| D-12 | `import --header` sets freeze + AutoFilter but does NOT set column widths. | Set widths on `col[]`. `numFmt` on a `col[]` path now applies a column-level style (``, schema-valid, reads back as `numberformat`); it formats blank cells in the column. Cells with their own style still need a per-cell-range `numFmt`. | +| D-13 | Sparkline `highpoint` is a bool (highlight on/off), not a color. `--prop highpoint=FF0000` errors `Invalid boolean value` | `--prop highPoint=true --prop highMarkerColor=FF0000`. Same pattern for lowPoint / firstPoint / lastPoint and their *MarkerColor. | +| D-14 | Sparkline cross-sectional data is meaningless (a region or department has no ordering) | Skip sparklines unless rows are a sequential time-series (dates, months, quarters). | +| D-15 | Empty chart `add` is rejected (`Chart requires a 'data' property`) at the CLI layer — legacy skills that relied on silent accept will fail here | Always provide `series1.values=` / `dataRange=` / inline `data=` at chart `add` time. Treat Gate 2 seriesCount check as a belt-and-braces verification. | +| D-16 | `fullCalcOnLoad=true` guarantees a **runtime** recalc when the end user opens the file; it does NOT refresh the build-time `cachedValue` in XML. Build sequence `set B=100 → set E==B+D → fix B=150` leaves `E.cachedValue` stale (board sees "Net Change = 0"). | After all upstream edits are final, re-issue every downstream formula (`officecli set "$FILE" /Sheet/E2 --prop formula==B2+D2`) OR `close` + re-open the file. Gate 8 verifies. | +| D-17 | The built-in calc engine does NOT evaluate `SUMPRODUCT` with array-predicate form `SUMPRODUCT((A2:A97=X)*C2:C97*D2:D97)` — cachedValue stays `0`/`null`, Gate 8 rejects. Runtime Excel / WPS compute fine, but board-delivered XLSX with stale cache still ships `0`. | Rewrite as helper column + `SUMIF`: `F2==C2*D2` on source sheet, then `=SUMIF(B:B, "Region X", F:F)`. Or pre-aggregate in Summary sheet and chart from there. | + +### Inherited (pointer only) + +Cross-sheet `!` trap, chart `anchor` / series immutability after create → see officecli-xlsx §Known Issues. diff --git a/external/officecli/skills/officecli-docx/SKILL.md b/external/officecli/skills/officecli-docx/SKILL.md new file mode 100644 index 0000000..1ea6d70 --- /dev/null +++ b/external/officecli/skills/officecli-docx/SKILL.md @@ -0,0 +1,562 @@ +--- +name: officecli-docx +description: "Use this skill any time a .docx file is involved -- as input, output, or both. This includes: creating Word documents, reports, letters, memos, or proposals; reading, parsing, or extracting text from any .docx file; editing, modifying, or updating existing documents; working with templates, tracked changes, comments, headers/footers, or tables of contents. Trigger whenever the user mentions 'Word doc', 'document', 'report', 'letter', 'memo', or references a .docx filename." +--- + +# OfficeCLI DOCX Skill + +## Setup + +If `officecli` is missing: + +- **macOS / Linux**: `curl -fsSL https://d.officecli.ai/install.sh | bash` +- **Windows (PowerShell)**: `irm https://d.officecli.ai/install.ps1 | iex` + +Verify with `officecli --version` (open a new terminal if PATH hasn't picked up). If install fails, download a binary from https://github.com/iOfficeAI/OfficeCLI/releases. + +## ⚠️ Help-First Rule + +**This skill teaches what good docx looks like, not every command flag. When a property name, enum value, or alias is uncertain, consult help BEFORE guessing.** + +```bash +officecli help docx # List all docx elements +officecli help docx # Full element schema (e.g. paragraph, field, numbering, watermark, toc) +officecli help docx # Verb-scoped (e.g. add field, set section) +officecli help docx --json # Machine-readable schema +``` + +Help is pinned to the installed CLI version. When this skill and help disagree, **help is authoritative**. + +## Mental Model + +A `.docx` is a ZIP of XML parts (`document.xml`, `styles.xml`, `numbering.xml`, `header*.xml`, `footer*.xml`, `comments.xml`, …). Everything the user sees — headings, tables, page numbers, TOC, tracked changes — is XML inside that ZIP. `officecli` gives you a semantic-path API (`/body/p[1]/r[2]`) over it, so you almost never touch raw XML; when you must, use `raw-set` (see the XML appendix). + +## Shell & Execution Discipline + +docx paths contain `[]`; some prop values contain `$`. Both are shell metacharacters. Escaping happens at three layers — keep them separate. + +1. **Shell.** ALWAYS quote element paths: `"/body/p[1]"`, not `/body/p[1]` (zsh/bash glob `[N]`). Single-quote any value containing `$`: `--prop text='$50M'` — at any length, the whole value inside one pair of single quotes. Unquoted `$50M` is stripped to `M`; mixing `'…$var…'` and `"…$50…"` on one long string is where the `$50` silently vanishes. +2. **CLI (`text=`).** The two-char escapes `\n` and `\t` ARE interpreted in `--prop text=` — `\n` becomes a `` soft line break, `\t` a `` — consistently across docx / pptx / xlsx. Double them (`\\n`) for a literal backslash-n (rarely wanted). This applies to row-level table `c1…cN` shortcuts too (`\n` → `` within the cell). +3. **JSON (batch).** A real newline can also be passed as `"\n"` in the JSON string of a `batch` heredoc; same result. + +If in doubt, `view text` after writing and compare character-for-character. + +**Incremental execution.** `officecli` mutates the file on every call. Run commands one at a time and check each exit code — a 50-command script that fails at command 3 cascades silently. After any structural op (new style, table, TOC, section break) run `get` on it before stacking more. + +**Open/save lifecycle:** `officecli open ` at the start, `officecli save ` at the end to flush to disk — `save` only writes and leaves the resident warm for follow-up edits; reach for `officecli close ` only to release the resident on a one-shot handoff. Both are always safe (never error or lose work). For many paragraphs of one style, use `batch` (one pass for the whole array). **Flush only at the non-officecli boundary:** officecli's own reads always see your edits; run `save`/`close` only before a non-officecli program reads the file (python-docx, Word, a renderer, delivery). + +**`$FILE` convention.** All commands use `"$FILE"` — set it once (`FILE="your-doc.docx"`). Never copy a literal `doc.docx` / `review.docx` into output — always substitute your actual target. + +## Requirements for Outputs + +Deliverable standards every document MUST meet — know these before reaching for a command. + +**Clear hierarchy.** Every non-trivial document has Title → Heading 1 → Heading 2 → body, not a wall of unstyled `Normal` paragraphs. If `view outline` shows one flat list, the hierarchy is missing. + +**Explicit heading sizes** (Word default style sizes drift between templates): **H1 ≥ 18pt** (20pt for long reports), H2 = 14pt bold, H3 = 12pt bold, body = 11–12pt, line spacing 1.15–1.5x. Prefer `style=Heading1` over inline sizes so a retheme touches the definition once — but set explicit sizes when you can't trust the template's styles. + +**One body font, one accent.** One readable body font (Calibri, Cambria, Georgia, Times New Roman); accent color for heading emphasis or table headers, not rainbow formatting. + +**Spacing through properties.** Use `spaceBefore` / `spaceAfter` on paragraphs. Rows of empty paragraphs break pagination and are flagged by `view issues`. + +**Typographic quality.** New content uses curly quotes (`'` `'` `"` `"`), not ASCII — Unicode directly or XML entities (`‘`/`’`/`“`/`”`) inside `raw-set`. En-dash `–` for ranges (`2024–2026`), em-dash `—` for parenthetical breaks. + +**Headers, footers, page numbers on any document > 1 page.** Page numbers go through a live `PAGE` field (`--prop field=page`), never the literal text "Page 1" — the CLI injects `` for you (see Headers & Footers). + +**Preserve existing templates.** When editing a file that already has a look, match it — existing conventions override these guidelines. + +### Visual delivery floor (applies to EVERY document) + +Before declaring done, run `officecli view "$FILE" html` and Read the returned HTML path to confirm ALL of these: + +- **No placeholder tokens rendered as data.** `$xxx$`, `{var}`, `{{name}}`, ``, `lorem`, `xxxx` must never appear in a heading, body, cover, TOC, caption, header, or footer. A literal `{name}` meant for a human to fill belongs inside a visible instruction paragraph ("Replace `{name}` before sending"), never as finished content. +- **No truncated titles or overflowing cells.** Widen the column or set `wrapText` rather than trimming content. +- **TOC present when the document has 3+ headings** (`--type toc`). +- **Cover page ≥ 60% filled, last page ≥ 40% filled.** Pad a thin cover with subtitle / author / date / scope / key highlights; pad a "Thank you" last page with conclusion / next steps / contact / legal. +- **No `\$`, `\t`, `\n` literals in document text.** If `view text` shows these, a shell-escape layer leaked — delete the paragraph and re-enter it. + +If any fails, STOP and fix before declaring done. + +## Common Workflow + +Six steps. Every non-trivial build follows this shape. + +1. **Open the file.** `officecli open "$FILE"` (resident default). New file: `officecli create "$FILE"` first. +2. **Orient.** Existing file: `officecli view "$FILE" outline` — heading tree, section count, whether a TOC / watermark / tracked changes already exist. Never edit blind. +3. **Build incrementally.** Structural first, content next, formatting last: styles & numbering defs → sections / page setup → headings & body → tables / images / fields / TOC → headers / footers → comments. After each structural op, `get` it back before stacking on top. +4. **Format to spec.** Explicit heading sizes, spacing, widths, alignment, tabs, list indents — formatting is part of the deliverable, not optional polish. +5. **Save, then trust structure over cached text.** `officecli save "$FILE"` writes the XML. TOC / PAGE / NUMPAGES / SEQ / PAGEREF fields carry **cached values** that may be stale or empty until a human recalculates (F9 in Word). Confirm fields *exist* (`get --depth 3` finds ``) rather than trusting the visible text. +6. **QA — assume there are problems.** You are done after one fix-and-verify cycle finds zero new issues, not when your last command exited 0. See QA. + +## Quick Start + +Minimal viable docx: a heading, a body paragraph, a subheading, and a footer with a live page-number field. Adapt, don't copy-paste — your file, your content. + +```bash +FILE="review.docx" +officecli create "$FILE" +officecli open "$FILE" +officecli add "$FILE" /body --type paragraph --prop text="Q4 2026 Review" --prop style=Heading1 --prop size=20pt --prop bold=true --prop spaceAfter=12pt +officecli add "$FILE" /body --type paragraph --prop text="Revenue grew 18% year-over-year, ahead of plan." --prop size=11pt --prop spaceAfter=8pt +officecli add "$FILE" /body --type paragraph --prop text="Key Drivers" --prop style=Heading2 --prop size=14pt --prop bold=true --prop spaceBefore=12pt --prop spaceAfter=6pt +officecli add "$FILE" /body --type paragraph --prop text="Enterprise renewals, upsell, and a new EMEA region." --prop size=11pt +officecli add "$FILE" / --type footer --prop type=default --prop size=9pt --prop text="Page " --prop field=page +officecli set "$FILE" "/footer[1]/p[1]" --prop align=center +officecli save "$FILE" +officecli validate "$FILE" +``` + +Verified: `validate` returns `no errors found`; `get /footer[1] --depth 3` shows the 5-run PAGE field chain (begin / instrText / separate / cached value / end). + +## Reading & Analysis + +Start wide, then narrow. `outline` tells you what's already there; jump into `view text` / `get` / `query` once you know where to look. + +```bash +officecli view "$FILE" outline # heading tree, section count, table/image counts, watermark, tracked-changes presence — orient here first +officecli view "$FILE" html # Read the returned HTML path: first visual check after a batch of edits (hierarchy, empty-para spacing, missing TOC) +officecli view "$FILE" text --start 1 --end 80 # text for content QA; paths shown as [/body/p[N]] so you can jump back with get +officecli view "$FILE" annotated # values + style/font/size + warnings per run +officecli view "$FILE" stats # paragraph counts, font usage, style distribution +officecli view "$FILE" issues # empty paras, missing alt text, spacing anomalies +``` + +`officecli watch "$FILE"` keeps a live preview running for the human user to open at their discretion — agent self-check uses `view html`. Final visual verification is the user opening the `.docx` in Word / WPS / Pages. + +**Inspect one element.** XPath-style semantic paths (1-based). Always quote — shells glob `[N]`. Use `[last()]` (with parens) for the last element; `[last]` errors. Add `--json` for machine output. + +```bash +officecli get "$FILE" / # document root: metadata, page setup +officecli get "$FILE" "/body/p[1]" # one paragraph +officecli get "$FILE" "/body/p[1]/r[1]" # one run (character-level formatting) +officecli get "$FILE" "/body/tbl[1]" --depth 3 # table with rows and cells +officecli get "$FILE" "/footer[1]" --depth 3 # footer — check for fldChar +officecli get "$FILE" "/styles/Heading1" # style definition +officecli get "$FILE" /numbering --depth 2 # numbering abstractNum + num bindings +``` + +**Query across the document.** CSS-like selectors, for systematic checks rather than hand-walking. Operators: `=`, `!=`, `~=` (contains), `>=`, `<=`, `[attr]` (exists). Full reference: `officecli query --help`. + +```bash +officecli query "$FILE" 'paragraph[style=Heading1]' # all H1s +officecli query "$FILE" 'p:contains("quarterly")' # text match +officecli query "$FILE" 'p:empty' # empty paragraphs (clutter) +officecli query "$FILE" 'image:no-alt' # accessibility gaps +officecli query "$FILE" 'paragraph[size>=24pt]' # numeric comparison +officecli query "$FILE" 'field[fieldType!=page]' # fields other than PAGE +``` + +`query --json` wraps results in `.data.results[]` — `jq '.data.results | length'` to count. + +**Large documents.** Navigate by heading with `view outline` and jump with `query`; don't dump the whole body into context. + +## Creating & Editing + +Verbs: `add` (new element), `set` (change a prop), `remove`, `move`, `swap`, `batch`, `raw-set` (last-resort XML). Ninety percent of a build is paragraphs, runs, tables, a couple of images, a TOC, and a footer. + +### Paragraphs, runs, styles + +A paragraph (`p`) is a block; a run (`r`) is a span of consistent character formatting inside it. Set paragraph-level props (style, alignment, spacing, indent) on the `p`; set font / size / color / bold on the `r`. + +```bash +officecli add "$FILE" /body --type paragraph --prop text="Executive Summary" --prop style=Heading1 --prop size=18pt --prop bold=true --prop spaceAfter=12pt +officecli set "$FILE" "/body/p[1]/r[1]" --prop color=1F4E79 +``` + +Use `spaceBefore` / `spaceAfter` for vertical spacing — never chains of empty paragraphs. For left indent use `--prop indent=720` (twips), `firstLineIndent=360` for first line, `hangingIndent=720` for hanging; leading spaces fire `view issues`. + +### Tables + +Tables are `/body/tbl[N]` with rows `tr[N]` and cells `tc[N]`. Add with row/column counts, then fill. + +```bash +officecli add "$FILE" /body --type table --prop rows=4 --prop cols=3 --prop width=100% +officecli set "$FILE" "/body/tbl[1]/tr[1]" --prop header=true --prop c1=Quarter --prop c2="Revenue" --prop c3="Growth" +officecli set "$FILE" "/body/tbl[1]/tr[1]/tc[1]/p[1]/r[1]" --prop bold=true +``` + +Row-level `set` supports `height`, `header`, and `c1 / c2 / … / cN` text shortcuts (`cN` generalises to any column count). Cell formatting (bold, fill, color) goes on the cell's paragraph / run — **not** row-level. For per-cell borders, set cell-level `border.*` on the `tc` (`--prop border.bottom="single;6;000000;0"`), or paragraph-level `pbdr.*` on the inner paragraph. + +**Horizontal rule = a paragraph bottom border, never a 1-row table.** A table-as-divider renders as an empty min-height box (worst in headers/footers). Use `pbdr.bottom` (`STYLE;SIZE;COLOR`) on the paragraph instead: + +```bash +officecli set "$FILE" "/body/p[3]" --prop pbdr.bottom="single;6;2E75B6" +``` + +### Lists (bullets, numbered, multi-level) + +For single-level bullets/numbers, set `listStyle` on the paragraph (`listStyle` is a paragraph prop, NOT a run prop — common mistake): + +```bash +officecli add "$FILE" /body --type paragraph --prop text="First item" --prop listStyle=bullet +``` + +For multi-level (legal-style 1 / 1.1 / 1.1.1), add an `abstractNum`, then a `num`, then reference the `numId` per paragraph: + +```bash +officecli add "$FILE" /numbering --type abstractnum --prop format=decimal # → abstractNum id=0 +officecli add "$FILE" /numbering --type num --prop abstractNumId=0 # → num id=1 +officecli add "$FILE" /body --type paragraph --prop text="Section one" --prop numId=1 --prop ilvl=0 +``` + +IDs are 0-based: the first `abstractNum` is id=0; the `num` references it via `abstractNumId=0` and is itself assigned id=1. A non-existent `abstractNumId` errors, so check ids after creating. Verify with `officecli query "$FILE" 'paragraph[numId>0]'`. See `help docx abstractnum` / `help docx num` for level and format options. + +### Tab stops (signature lines, leader rows) + +Tab stops are a first-class `tab` child of the paragraph; `pos` accepts `6in`/`6cm`/twips, `val` ∈ `left`/`center`/`right`, `leader` ∈ `none`/`dot`/`hyphen`/`underscore`. See `help docx tab`. + +```bash +officecli add "$FILE" "/body/p[1]" --type tab --prop pos=6in --prop val=right --prop leader=dot +``` + +**Leader caveat.** `leader=dot` alone emits no dots — the leader renders only when a real `` character sits in a run between the text and the tab stop. Put one there with `\t` in the text: define the stop (`add tab --prop pos=6in --prop val=right --prop leader=dot`), then `--prop text="Chapter 1\t12"` — the `\t` becomes the `` and dots fill to the right-aligned page number. (Literal `text="Chapter 1 ......... 12"` also ships, but a real tab stop aligns cleanly.) + +### Fields (PAGE / NUMPAGES / DATE / MERGEFIELD / REF) + +Fields are live values computed at render time. `fieldType` picks the field; `name` supplies the target (merge name or `ref` bookmark); `format` / `instr` add switches. + +| Field | Use | Example | +|---|---|---| +| `page` | current page number | `--prop field=page` on footer, or `--prop fieldType=page` inline | +| `numpages` | total pages | `--prop field=numpages` / `--prop fieldType=numpages` | +| `date` | today | `--prop fieldType=date --prop format='yyyy-MM-dd'` | +| `mergefield` | template merge token | `--prop fieldType=mergefield --prop name=CustomerName` | +| `ref` | cross-reference to a bookmark | `--prop fieldType=ref --prop name=bookmarkName` | + +Full `fieldType` enum (30+ values incl. `pageref`, `seq`, `styleref`, `docproperty`, `createdate`, …) is in `help docx field`. **There is NO `fieldInstr` fieldType** — use the `instruction` prop for raw field instruction text when typed shortcuts fall short. Picture switches (`MERGEFIELD Amount \# "#,##0.00"`, `DATE \@ "yyyy年MM月"`) go via `--prop instruction='…'` (mergefield's `format` prop is ignored with a warning — use `instruction`). + +```bash +officecli add "$FILE" "/body/p[3]" --type field --prop fieldType=mergefield --prop name=customer_name +# Renders «customer_name» — visible placeholder, replaced in Word at mail-merge time. +``` + +**MERGEFIELD templates: never render placeholder literals.** A `{{customer_name}}` or `$NAME$` shown as body text is a failed template the recipient sees — insert a real MERGEFIELD (above), or confine literal tokens to an obvious instruction paragraph. Confirm with `query 'field[fieldType=mergefield]'`. + +**SEQ / PAGEREF / TOC field values.** officecli doesn't store rendered field values at write time. Recompute by what each path needs: + +- **SEQ numbering** (`Figure 1/2/3`): `officecli set "$FILE" / --prop recalcFields=seq` counts SEQ fields in body document order and writes the cached values (`evaluated` flips true; switches/formats in `help docx document`). Heading-relative `\s` and SEQ in headers/footers defer to Word. +- **PAGE / PAGEREF / NUMPAGES / TOC page numbers** need pagination, which officecli has no engine for — `officecli set "$FILE" /settings --prop updateFields=true` defers them to Word on open. + +Use both on a multi-figure document. Academic papers: see the `officecli-academic-paper` skill. + +### Headers & Footers (page numbering) + +Single-command pattern — the CLI injects ``, so you never compose the field by hand: + +```bash +# Empty first-page footer — auto-enables differentFirstPage so the cover has no page number +officecli add "$FILE" / --type footer --prop type=first --prop text="" +# Default footer with live page number +officecli add "$FILE" / --type footer --prop type=default --prop align=center --prop size=9pt --prop text="Page " --prop field=page +``` + +When both exist, the default footer is `/footer[2]`; alone it is `/footer[1]`. **Verify**: `get --depth 3` must show `fldChar` children, not just a run with literal `"Page"` (`view outline` prints "Footer: Page" for both live fields AND static text — don't rely on it). Do NOT `set --prop differentFirstPage=true` — that prop is unsupported (exit 2 with `WARNING: UNSUPPORTED`; the element itself is still created — check `success` in `--json`); adding a first-type footer flips the bit. For composite **Page X of Y**, see recipe (b). + +### Table of Contents + +For any document with 3+ headings, first verify that the requested range has real sources. A TOC source must use a built-in Heading style or a custom paragraph style with `outlineLvl`; bold or large Normal text is not a source. If no eligible source exists, stop and fix the heading structure instead of inserting a TOC that renders "Error! No table of contents entries found." + +```bash +# Built-in Heading styles are TOC sources. +officecli add "$FILE" /body --type paragraph --prop text="Introduction" --prop style=Heading1 +# Custom paragraph styles need an outline level (0 = Heading 1). +officecli add "$FILE" /styles --type style --prop id=ThesisH1 --prop type=paragraph --prop outlineLvl=0 +# Add the TOC only after its sources exist. +officecli add "$FILE" /body --type toc --prop levels="1-3" --prop title="Table of Contents" --prop hyperlinks=true --index 0 +``` + +Page numbers need pagination; OfficeCLI cannot calculate TOC page numbers itself. Set `updateFields=true` so Word recomputes the TOC (and all fields) on open. Without a Word-compatible field engine, report the TOC as dynamic and uncomputed; do not claim ready page numbers or guess a static TOC. + +```bash +officecli set "$FILE" /settings --prop updateFields=true +``` + +Address the TOC directly with `/toc[1]` or `/tableofcontents` for `get`/`set`/`remove`. + +### Images + +Pictures go inside a run. Alt text is mandatory for accessibility — pass `alt` directly at create time: + +```bash +officecli add "$FILE" "/body/p[5]" --type picture --prop src=logo.png --prop width=1.5in --prop alt="Acme logo" +``` + +Confirm `officecli query "$FILE" 'image:no-alt'` is empty before delivery. + +### Charts + +For data, add a **native chart** — editable, themeable, accessible, re-renders in Word — never a flat PNG screenshot of a chart. `data="Label:v1,v2,…"` per series; one `data=` per series (or `series1=`/`series2=`). + +```bash +officecli add "$FILE" /body --type chart --prop chartType=bar --prop title="Revenue by Region" --prop categories="EMEA,APAC,Americas" --prop data="2026:120,150,180" +``` + +`chartType` ∈ bar / column / line / pie / area / scatter (`help docx chart` for axis/legend/series styling). A PNG via `--type picture` is only a fallback for an exotic chart officecli can't build. + +### Hyperlinks and bookmarks + +External links go via `hyperlink`: + +```bash +officecli add "$FILE" "/body/p[2]" --type hyperlink --prop url="https://example.com" --prop text="our site" +``` + +**Internal links** (to a bookmark) use `--prop anchor=bookmarkName` — not a `#fragment` in `url`: + +```bash +officecli add "$FILE" "/body/p[2]" --type hyperlink --prop anchor=chapter1 --prop text="See Chapter 1" +``` + +Pairing a `PAGEREF` field with visible text is the alternative. See `help docx hyperlink` / `help docx bookmark`. + +### Sections and page setup + +Document root `/` carries page setup (`pageWidth`, `pageHeight`, margins, in twips). Multi-section documents (landscape insert, columns) add a `section` break — see `help docx section`. Both camelCase (`pageWidth`, canonical) and lowercase alias (`pagewidth`) are accepted; prefer camelCase. + +```bash +officecli set "$FILE" / --prop pageWidth=12240 --prop pageHeight=15840 --prop marginTop=1440 --prop marginLeft=1440 +# Newspaper-style multi-column flow (columnSpace in twips; 720 = 0.5in): +officecli set "$FILE" / --prop columns=2 --prop columnSpace=720 +``` + +### Forcing page breaks + +Use exactly one page-break mechanism per logical boundary. The default is `pageBreakBefore=true` on the target heading. Alternatively, insert one explicit `pagebreak` before the heading. Never combine both mechanisms for the same boundary: the resulting double break can create a blank page. Never set `pageBreakBefore` on `p[last()]` immediately after adding a `pagebreak`. + +```bash +# Default: apply the break directly to the heading. +officecli add "$FILE" /body --type paragraph --prop text="Introduction" --prop style=Heading1 --prop pageBreakBefore=true +``` + +`--prop break=newPage` is an alias for `pageBreakBefore=true`. Preview with `view html` and check for blank pages. + +### Report-level recipes + +Patterns that come up on every long-form report. Each has been executed and `validate`-passed. + +**(a) Rich cover page — hit the ≥ 60% filled floor.** Stack a confidentiality banner, title, subtitle, client/project/date block, and a key-themes strip, then force the next section onto a new page: + +```bash +officecli add "$FILE" /body --type paragraph --prop text="CONFIDENTIAL — CLIENT USE ONLY" --prop align=center --prop size=9pt --prop color=C00000 --prop spaceAfter=24pt +officecli add "$FILE" /body --type paragraph --prop text="Strategic Growth Review" --prop style=Title --prop size=32pt --prop bold=true --prop align=center --prop font=Cambria --prop spaceAfter=8pt +officecli add "$FILE" /body --type paragraph --prop text="FY26 Outlook and Scenario Planning" --prop italic=true --prop size=16pt --prop align=center --prop spaceAfter=36pt +officecli add "$FILE" /body --type paragraph --prop text='Prepared for: Acme Corp. Leadership Team' --prop align=center --prop size=11pt +officecli add "$FILE" /body --type paragraph --prop text='Engagement: 2026-04 — 2026-06' --prop align=center --prop size=11pt --prop spaceAfter=36pt +officecli add "$FILE" /body --type paragraph --prop text="Key themes: 1) margin resilience, 2) EMEA expansion, 3) capital allocation." --prop align=center --prop italic=true --prop size=10pt +officecli add "$FILE" /body --type paragraph --prop text="Executive Summary" --prop style=Heading1 --prop pageBreakBefore=true +``` + +**(b) Page X of Y footer — composite PAGE + NUMPAGES.** Add the footer paragraph, then three child ops build `Page of ` live. The official `help docx footer` recipe. + +```bash +officecli add "$FILE" / --type footer --prop type=default --prop text="Page " --prop align=center --prop size=9pt +officecli add "$FILE" "/footer[1]/p[1]" --type field --prop fieldType=page +officecli add "$FILE" "/footer[1]/p[1]" --type run --prop text=" of " +officecli add "$FILE" "/footer[1]/p[1]" --type field --prop fieldType=numpages +officecli get "$FILE" "/footer[1]/p[1]" --depth 1 | grep -o fldChar | wc -l # expect ≥ 4; use grep -o ... | wc -l, NOT grep -c (single-line XML returns 1) +``` + +**(c) Header row with fill and white bold text.** Order matters — populate header cell text FIRST (runs don't exist in empty cells; a `set …/tc[N]/p[1]/r[1]` on an empty cell errors "No r found"), THEN cell fill, THEN run formatting: + +```bash +officecli add "$FILE" /body --type table --prop rows=5 --prop cols=4 --prop width=100% +officecli set "$FILE" "/body/tbl[1]/tr[1]" --prop header=true --prop c1=Quarter --prop c2=Revenue --prop c3=Growth --prop c4=Status +for col in 1 2 3 4; do + officecli set "$FILE" "/body/tbl[1]/tr[1]/tc[$col]" --prop fill=1F4E79 + officecli set "$FILE" "/body/tbl[1]/tr[1]/tc[$col]/p[1]/r[1]" --prop bold=true --prop color=FFFFFF +done +for row in 3 5; do for col in 1 2 3 4; do + officecli set "$FILE" "/body/tbl[1]/tr[$row]/tc[$col]" --prop fill=D9E2F3 # zebra stripe +done; done +``` + +**(d) Financial table — right-align numbers, bold totals, bottom border on total row.** + +```bash +for row in 2 3 4 5; do for col in 2 3 4; do + officecli set "$FILE" "/body/tbl[1]/tr[$row]/tc[$col]/p[1]" --prop align=right +done; done +for col in 1 2 3 4; do + officecli set "$FILE" "/body/tbl[1]/tr[5]/tc[$col]/p[1]/r[1]" --prop bold=true + officecli set "$FILE" "/body/tbl[1]/tr[4]/tc[$col]/p[1]" --prop pbdr.bottom="single;6;000000;0" +done +``` + +**(e) Cell with multiple bullets (SWOT / risk matrix).** `c1="a\nb"` gives a `` line break within **one** paragraph — fine for plain multi-line text, but bullets need separate paragraphs. Seed the first via `set c1=`, then `add paragraph` (with `listStyle=bullet`) under the cell per subsequent bullet: + +```bash +officecli set "$FILE" "/body/tbl[1]/tr[1]" --prop c1="Installed base of 18k enterprise seats" +officecli add "$FILE" "/body/tbl[1]/tr[1]/tc[1]" --type paragraph --prop text="Margin structure above peer median" --prop listStyle=bullet +officecli set "$FILE" "/body/tbl[1]/tr[1]/tc[1]/p[1]" --prop listStyle=bullet +``` + +If the seeded line lands at the bottom, re-order: `officecli move "$FILE" "/body/tbl[1]/tr[1]/tc[1]/p[N]" --index 0`. + +**(f) TOC without a field engine.** A CLI-only pipeline cannot calculate reliable page numbers. Keep the live TOC, set `updateFields=true`, and tell the recipient to open the document in Word or another compatible field engine. Do not replace it with guessed static page numbers. + +### Template delivery — separating Template Notes from end-user content + +HR / legal / vendor templates carry internal-only guidance ("replace `{{CompanyName}}`") that must NOT ship. Two working patterns: + +- **Trailing "Template Notes" section** under a clear `Heading 1` ("Template Notes for HR Users") with all instructions below it; before distribution, `remove` from the heading downward (locate with `query 'paragraph[style=Heading1]:contains("Template Notes")'`). +- **Bookmark-bounded internal section** between `__template_notes_start` / `_end` bookmarks; at delivery `raw-set` removes everything between the anchors. + +Delivery gate for templates: after removal, `query 'p:contains("Template Notes")'` AND `query 'p:contains("{{")'` both return empty. If a notes paragraph survives, a downstream employee reads internal language. + +### Advanced / specialty topics (skip if you are writing a report) + +Reports, memos, letters, proposals, and HR templates don't need this. Keep reading only if your document is academic (equations, footnotes, bibliography), reviewed (comments, tracked changes), or marked (watermark). + +**Equations and footnotes.** `--type equation` takes LaTeX — `\frac`, `\sum`, Greek, `\mathit`, `\mathcal` all render. By default it creates a standalone `/body/oMathPara[N]` display block; pass `--prop mode=inline` with a paragraph parent path (`add "/body/p[N]" --type equation --prop formula=… --prop mode=inline`) to drop an inline `` into running text. Footnotes auto-number by paragraph index. Bibliography hanging indent: `firstLineIndent=-720 indent=720` per entry. + +```bash +officecli add "$FILE" /body --type equation --prop formula="\\frac{a}{b} + \\sum_{i=1}^{n} x_i" +officecli add "$FILE" "/body/p[3]" --type footnote --prop text="See Appendix A for methodology." +``` + +**Comments and tracked changes.** Bulk accept/reject: `set "$FILE" /revision --prop revision.action=accept` (or `--prop revision.action=reject`); narrow with a selector like `/revision[@author=Alice]` or `/revision[@type=ins]`. Locate individual changes with `query ins` and `query del` (`trackedchange` is not a selector). Create tracked changes on a run with `--prop revision.type=ins|del --prop revision.author=…` (`help docx run` for the full `revision.*` set — `format`/`moveFrom`/`moveTo` too). Add a comment: `add "/body/p[4]" --type comment --prop author=… --prop text=…`; reply-thread it with `--prop parentId=N` and mark it resolved with `set "/comments/comment[N]" --prop done=true` (resolve rather than delete to keep the audit trail — `query 'comment[done=false]'` then lists what's still open). Prop schema: `help docx comment` / `help docx run`. + +**Watermark.** `add / --type watermark --prop text="DRAFT" --prop color=BFBFBF --prop opacity=0.8` in one command (default opacity 0.5); `set /watermark --prop opacity=…` adjusts it later. + +**When to switch skills.** Stay in docx for chapter drafts, ≤ 3 footnotes, ≤ 2 equations, no bibliography/cross-refs. Switch to **`academic-paper`** for citation styles (APA / Chicago / IEEE / GB 7714), in-text↔reference auto-linking, numbered equations with `\ref`, "List of Figures", or auto-updating cross-refs. Switch to **`officecli-word-form`** when the document's purpose is **data capture** — fillable forms, contracts with user-fill slots, questionnaires, mail-merge templates (`` content controls, ``, `documentProtection=forms`). + +### Raw-set escape hatch (L1 / L2 / L3) + +Three tiers of precision; use the lowest that does the job. + +- **L1 — high-level props** (`--prop text=…`, `--prop style=Heading1`): your default. Covers 80%. +- **L2 — dotted-attr fallback** (`pbdr.top=`, `ind.left=`, `shd.fill=`, `padding.top=`, `font.size=`): when L1 lacks the knob. Example: `--prop pbdr.bottom="single;6;1F4E79;0"`. Emits schema-valid XML. +- **L3 — `raw-set` with XML**: last resort, no schema protection. Use for internal hyperlinks, composite fields, and other shapes the typed verbs can't express (see XML appendix). + +Borders use the format `style;size;color;space`: `single;4;FF0000;1`. Hex colors never start with `#`: `FF0000`. Scheme color names (`accent1..6`, `dark1`/`dark2`, `light1`/`light2`, `hyperlink`) are accepted anywhere a hex color is — prefer hex for stable colors across themes. + +## QA (Required) + +**Assume there are problems — QA is a bug hunt, not a confirmation step.** Your first document is almost never correct; zero issues on first inspection means you weren't looking hard enough. Headings look fine until `view outline` shows an H3 directly under an H1; the footer shows "Page 1" until `get --depth 3` reveals a static run, not a field. + +### Minimum cycle before "done" + +1. `officecli view "$FILE" issues` — empty paras, missing alt text, formatting anomalies. +2. `officecli view "$FILE" outline` — heading hierarchy (no H1 → H3 skips), TOC presence, section count. +3. `officecli view "$FILE" text --max-lines 400` — typos, stray `\$`/`\t`/`\n` literals, placeholder tokens. +4. `officecli validate "$FILE"` — schema check (the Delivery Gate re-runs this on the closed, on-disk file). +5. **Visual pass — whole document as a contact sheet** (vision-capable agents only — if you cannot interpret images, skip this step: steps 1–4 are your ceiling, and flag the document "not visually verified" at handoff). `officecli view "$FILE" screenshot --grid auto -o /tmp/sheet.png`, then Read it. `--grid auto` tiles **every page** into one image (auto column count; `--grid 4` to force) — you *see* pagination, blank pages, heading rhythm, lopsided margins, and TOC/cover placement, not just the DOM. Windows+Word renders each page through real Word; elsewhere HTML. If the screenshot fails, fall back to `view html` and flag cross-page breaks / alignment / rhythm as "not visually verified". Thumbnails only **locate**: confirm any fine call (column alignment, line spacing, indents, dark-on-dark, caption placement) on the suspect page at full resolution with `screenshot --page N` (no `--grid`; real Word on Windows). "validate pass" is not delivery; "looks like a real document" is. +6. If anything failed, fix, then **rerun the full cycle** — one fix commonly creates another problem. + +### Delivery Gate (run before handing off — any failure = REJECT, do NOT deliver) + +Copy-paste, set `FILE`, and refuse to declare done until every gate prints OK. + +```bash +FILE="your-file.docx" + +# Gate 1 — schema. +officecli close "$FILE" 2>/dev/null +officecli validate "$FILE" | grep -q "no errors found" || { echo "REJECT Gate 1: validate failed"; exit 1; } +echo "Gate 1 OK" + +# Gate 2 — token leak (shell-escape / template tokens / literal \$ \t \n). grep -c never false-PASSes. +LEAK=$(officecli view "$FILE" text | grep -cE '(\$[A-Za-z_]+\$|\{\{[^}]+\}\}||xxxx|lorem|\\[\$tn])') +[ "$LEAK" -eq 0 ] && echo "Gate 2 OK" || { echo "REJECT Gate 2: $LEAK leak line(s)"; officecli view "$FILE" text | grep -nE '(\$[A-Za-z_]+\$|\{\{[^}]+\}\}||xxxx|lorem|\\[\$tn])'; exit 1; } +# A TOC placeholder is valid before a Word-compatible field engine updates it; confirm the TOC field and updateFields setting structurally instead. + +# Gate 3 — live PAGE field exists when a footer is expected. +FLD=$(officecli query "$FILE" 'field[fieldType=page]' --json | jq '.data.results | length') +[ "$FLD" -ge 1 ] && echo "Gate 3 OK" || { echo "REJECT Gate 3: no live PAGE field"; exit 1; } +echo "Delivery Gate PASS" +``` + +### Field / cached-value spot-check + +Fields carry cached values that may be stale or empty at write time — confirm existence by **structure, not text**. + +- **Footer PAGE:** `get /footer[N] --depth 3` lists the begin / instrText / separate / cached / end run chain — ≥ 5 runs for one PAGE, ≥ 11 for composite "Page X of Y". A single run with text `"Page"` = field missing; re-add with `--prop field=page`. +- **TOC:** `get /toc[1] --depth 2` shows field structure. Page numbers may read `1 1 1 1` or `Update field to see…` until recalculated (see §Table of Contents — set `updateFields=true`). +- **MERGEFIELD:** `query 'field[fieldType=mergefield]'` — one per slot, no literal `{{name}}` elsewhere. + +### Honest limit + +`validate` catches schema errors, not design errors — a document can pass it with wrong heading hierarchy, fake-Heading-1 sizes, placeholder tokens as body text, or an empty first-page footer on a coverless document. The contact-sheet visual pass (`screenshot --grid`) and the field-structure check are how you catch what validation can't. + +### QA display notes (don't chase these) + +- `view text` shows `"1."` for every numbered list item regardless of rendered number — actual output increments correctly. +- `view issues` flags "body paragraph missing first-line indent" on cover paragraphs, centered headings, list items, bibliography entries — first-line indent is only required for APA/academic body text; on block-style professional documents these are expected. + +## Known Issues & Pitfalls + +When something "looks broken", attribute it before chasing: **[AGENT-ERROR]** the document is wrong (fix it) · **[RENDERER-BUG]** the document is correct, a viewer renders it differently (don't chase) · **[SKILL gap]** the skill didn't teach the rule (file an issue). + +### Renderer quirks (cross-viewer, [RENDERER-BUG] — don't chase) + +Before calling a color/field/chart broken, open the file in the user's target viewer; if it looks correct there it's a viewer quirk. + +- **PAGE field may render literal "Page"** (no number) until recalculated — judge by `fldChar` presence, not the digit. +- **TOC cached page numbers may read "1 1 1 1"** until F9. +- **Pie / doughnut fill may collapse to one color** in some viewers (column/bar render fine). +- **Form-control checkboxes may render double-boxed**; **OMML equation baselines** may shift across viewers (XML identical). + +### Common pitfalls + +| Pitfall | Correct approach | +|---|---| +| `--index` vs `[N]` | `--index` is 0-based; `[N]` paths are 1-based | +| Multiple `add --index N` with the same N | Each insert shifts later content down; reusing N puts later items BEFORE earlier ones — insert in reverse order, or `move --after/--before` anchored on `paraId` | +| Unquoted `[N]` in zsh/bash | Quote every path: `"/body/p[1]"` | +| `[last]` as predicate | Must be `[last()]` with parens | +| Raw twips in spacing | Use unit-qualified values: `12pt`, `0.5cm`, `1.5x` | +| Empty paragraphs for spacing | Use `spaceBefore` / `spaceAfter` | +| Row-level `set` for cell formatting | Row `set` only supports `height`, `header`, `c1..cN` text; format goes on the cell paragraph / run | +| `listStyle` on a run | It's a paragraph property | +| Indent via leading spaces | `indent=720` / `firstLineIndent=360` / `hangingIndent=720` (dotted `ind.left` / `ind.firstLine` also work) | +| Cover page-number suppression via `set differentFirstPage=true` | UNSUPPORTED — add a first-type footer: `--type footer --prop type=first --prop text=""` | +| Need a fresh chapter page | Use `pageBreakBefore=true` on the heading; use an explicit `pagebreak` only as an alternative, never both | +| Multiple bullet paragraphs in one cell | `c1="a\nb"` makes a `` line break (one paragraph); for separate bullet paragraphs use recipe (e) | +| `raw-set` when dotted-attr would work | Prefer L2 dotted-attr over L3 raw-set | +| Next paragraph inherits the previous Heading style | Set explicit `--prop style=Normal` on the following paragraph | +| Modifying a file open in Word | Close it in Word first | +| Echo into batch breaks on `$`/`'` | Heredoc with single-quoted delimiter: `cat <<'EOF' \| officecli batch …` | + +## Raw-set XML appendix (L3 patterns) + +`raw-set` injects literal OOXML — no schema protection. Element order in ``: `pStyle`, `numPr`, `spacing`, `ind`, `jc`, `rPr` (last). Smart quotes as entities (`‘`/`’`/`“`/`”`). Add `xml:space="preserve"` to any `` with leading/trailing spaces. RSIDs are 8-digit hex. Use "Claude" as the author for tracked changes/comments unless the user names another. + +**Tracked-change insertion / deletion** — prefer the high-level `--prop revision.type=ins|del` on a run; raw-set only for what the typed path can't express (rejecting/restoring another author's change, below). Replace the whole `…`, never inject tags inside a run; copy the original `` into both to preserve formatting. Inside `` use `` (and `` for instructions): + +```xml +The term is +30 +60 + days. +``` + +When deleting ALL content of a paragraph/list item, also mark the paragraph mark deleted (`` inside ``) — otherwise accepting changes leaves an empty paragraph. To **reject another author's insertion**, nest your `` inside their ``; to **restore their deletion**, add a `` after it (don't modify theirs). + +**Internal hyperlink to a bookmark** (prefer the high-level `--prop anchor=` path; raw-set only for custom run styling the command can't express): + +```xml +See Chapter 1 +``` + +**Composite field in one run** (e.g. two fields the single-command path can't compose) — the `fldChar begin / instrText / separate / value / end` chain: + +```xml + + PAGE + +1 + +``` + +**Comment markers** are siblings of ``, NEVER inside one (reply threading and resolved-state are high-level — `--prop parentId=`/`done=`, see Comments above): + +```xml +annotated text + +``` + +Force field recalc on open with `officecli set "$FILE" /settings --prop updateFields=true` (writes ``; covers the layout-dependent fields PAGE / PAGEREF / NUMPAGES / TOC page numbers — no raw-set needed). For SEQ numbering, prefer `set / --prop recalcFields=seq`, which writes correct cached values now without waiting on Word. + +### Help pointer + +When in doubt: `officecli help docx`, `officecli help docx `, `officecli help docx `, `--json` for agents. Help is the authoritative schema; this skill is the decision guide. diff --git a/external/officecli/skills/officecli-financial-model/SKILL.md b/external/officecli/skills/officecli-financial-model/SKILL.md new file mode 100644 index 0000000..ccf49ca --- /dev/null +++ b/external/officecli/skills/officecli-financial-model/SKILL.md @@ -0,0 +1,560 @@ +--- +name: officecli-financial-model +description: "Use this skill when the user wants to build a financial model — 3-statement model, DCF valuation, LBO, SaaS unit economics, sensitivity / scenario analysis, debt schedule, or fundraising projections — in Excel. Trigger on: 'financial model', '3-statement model', 'P&L + BS + CF', 'DCF', 'WACC', 'NPV', 'terminal value', 'LBO', 'debt schedule', 'cash sweep', 'MOIC', 'IRR / XIRR', 'sensitivity table', 'scenario analysis', 'ARR model', 'unit economics', 'CAC / LTV', 'cap table forecast'. Output is a single formula-driven .xlsx. This skill is a scene layer on top of officecli-xlsx — it inherits every xlsx v2 rule (4-color code, visual floor, number formats, cache-drift, Known Issues, Delivery Gate minimum cycle). DO NOT invoke for a simple budget tracker, CSV dump, or operational KPI sheet — route those to officecli-xlsx base." +--- + +# OfficeCLI Financial-Model Skill + +**This skill is a scene layer on top of `officecli-xlsx`.** Every xlsx hard rule — shell quoting, incremental execution, Help-First Rule, visual delivery floor, CFO 4-color code (blue input / black formula / green cross-sheet / yellow-fill assumption), number-format standards (years as text, zero as `-`, `%` one decimal, negatives in parens), assumption-cell discipline, CSV batch import, chart data-feed forms (a/b/c), the 5-gate Delivery cycle, cache-drift guidance, Known Issues (the cross-sheet `!` trap, batch + resident for formulas, renderer caveats) — is **inherited, not re-taught**. This file adds only what a **financial model** requires on top: three-zone architecture, 3 model-type recipes (3-statement / DCF / LBO), sensitivity + scenario protocols, financial-function patterns, circular-reference discipline, and model-specific Delivery Gates 4–6. + +When the xlsx base rules cover it, the text here says `→ see xlsx v2 §X`. Read `skills/officecli-xlsx/SKILL.md` first if you have not. + +## Setup + +If `officecli` is missing: + +- **macOS / Linux**: `curl -fsSL https://d.officecli.ai/install.sh | bash` +- **Windows (PowerShell)**: `irm https://d.officecli.ai/install.ps1 | iex` + +Verify with `officecli --version` (open a new terminal if PATH hasn't picked up). If install fails, download a binary from https://github.com/iOfficeAI/OfficeCLI/releases. + +## Help-First Rule + +This skill teaches what a financial model requires, not every CLI flag. When a prop name / alias / enum is uncertain, consult help BEFORE guessing: `officecli help xlsx [element] [--json]`. Help is authoritative for the installed version — when this skill and help disagree, **help wins**. Every `--prop X=` below was verified against `officecli help xlsx `. + +## Mental Model & Inheritance + +**Inherits xlsx v2.** Read `skills/officecli-xlsx/SKILL.md` first. This skill assumes you know `create` / `open` / `close`, `set` values/formulas, `batch` heredocs for cross-sheet formulas, `/SheetName/A1` paths, named ranges, the 5-gate Delivery cycle, the cross-sheet `!` trap, and that **cross-sheet formulas go non-resident (single batch OR individual `set`), never batch-while-resident**. + +## Shell & Execution Discipline + +Shell quoting, incremental execution, `$FILE` convention → see xlsx v2 §Shell & Execution Discipline. Same rules: quote every `[N]` path, single-quote any prop containing `$` (every number format here — `$#,##0;($#,##0);"-"` — needs single quotes), no hand-written `\$`/`\t`/`\n`, one command at a time. Examples below use `$FILE` (`FILE="model.xlsx"`). + +## Core Principles (identity) + +A financial model is an xlsx with a **decision-grade, formula-driven layer**: every output traces an unbroken chain to blue-font assumptions, every statement balances every period, every valuation is re-auditable. Eight deltas on top of a general xlsx: + +1. **Three-zone architecture mandatory:** Inputs → Calc → Outputs. Collapsing zones → unauditable. +2. **Assumptions live in cells, never inside formulas.** `=B5*(1+Assumptions!GrowthRate)`, never `=B5*1.05`. +3. **Statements balance every period.** `Assets − Liab − Equity = 0`, `CF.EndingCash = BS.Cash`. Gate 4 fails on `IMBALANCED`. +4. **Hardcodes audited.** Calc sheets carry zero hardcoded numbers; Gate 6 counts. +5. **Sensitivity / scenario is first-class.** 2-axis grid, dropdown `INDEX/MATCH` switch, or Base/Upside/Downside cols. Excel Data Tables not reliably supported — manual grids only. +6. **Cached values on valuation cells load-bearing.** A valuation cell that ships with no cached result (or the `#OCLI_NOTEVAL!` sentinel) sends a blank/wrong number to non-recalculating readers. The evaluator now computes NPV / XNPV / IRR / XIRR; verify the cached value with a readback. Gate 5 spot-checks. +7. **Circularity is a design choice.** Legitimate rings (interest ↔ cash, revolver plug ↔ ending cash) use `calc.iterate=true`. Accidental circularity is broken algebra — never papered with `iterate`. +8. **Named ranges for ≥ 3-use assumptions.** `WACC`, `TaxRate`, `TerminalGrowth`, `ExitMultiple`, `ChurnRate`. Declared-unused names are dead decoration — Gate 6 flags. + +### Reverse handoff — when to go BACK to xlsx base + +Stay in **xlsx base** for: budget trackers, CSV-to-report dumps, operational KPI sheets, simple templates, cap tables without forecast logic. Use **this skill** only when the ask mentions: 3-statement / DCF / WACC / NPV / TV / LBO / debt schedule / MOIC / IRR / unit economics / ARR roll-forward / sensitivity grid / scenario switch / pro forma. + +## Three-zone architecture (hard rule) + +Every model in this skill builds on three zones. **Name them, tab-color them, and enforce them with executable audits.** Breaking the zone rule is the single most common cause of an unauditable model. + +| Zone | Sheet names (convention) | Tab color | Content | Hardcodes | Formulas | +|---|---|---|---|---|---| +| **Inputs** | `Assumptions`, `Inputs`, `Drivers` | Yellow `FFC000` | Raw drivers: growth rates, margins, tax, WACC, FTE, pricing, working-capital days | Blue `0000FF` on every cell | Allowed only for derived assumptions (e.g. `=MonthlyARPU*12`) | +| **Calc** | `P&L`, `Balance Sheet`, `Cash Flow`, `DCF`, `Debt`, `ARR` | Blue `4472C4` | All derivations and statements | **Zero** (enforced by Gate 6) | Black `000000` for same-sheet, green `008000` for cross-sheet | +| **Outputs** | `Summary`, `Dashboard`, `Sensitivity`, `Returns` | Green `70AD47` | KPIs, sensitivity grids, charts, returns waterfall | Only for labels (non-numeric); Gate 6 counts numeric hardcodes → 0 | Black / green per above | + +**Build order is cross-zone-aware.** Assumptions first, then Calc bottom-up on the dependency chain (`IS → BS → CF` for 3-statement; `FCF → WACC → NPV` for DCF), then Outputs last. Building Outputs first caches `0` everywhere and downstream inherits zeros. + +**Executable zone audit** (run before Gate 4): + +```bash +# Calc zone: zero numeric hardcodes allowed. `cell:not(:has(formula))` selects the literal (non-formula) cells; `cell:has(formula)` selects the formula cells. +HARDCODE=$(officecli query "$FILE" 'cell[type=Number]' --json | jq '[.data.results[] | select(.format.formula == null) | select(.path | test("/(P&L|Balance Sheet|Cash Flow|DCF|Debt|ARR)/"))] | length') +[ "$HARDCODE" -eq 0 ] && echo "Zone audit OK" || { echo "REJECT: $HARDCODE hardcoded numeric cells on Calc sheets — move to Assumptions"; exit 1; } +# Assumptions zone: should be non-zero. +INPUTS=$(officecli query "$FILE" '/Assumptions/cell[type=Number]' --json | jq '[.data.results[] | select(.format.formula == null)] | length') +[ "$INPUTS" -ge 5 ] && echo "Assumptions has $INPUTS hardcoded drivers" || echo "WARN: Assumptions has only $INPUTS inputs" +``` + +## Print delivery (board / IC / LP) + +When the ask contains "print" / "一页" / "董事会" / "投资人" / "IC memo" / "LP update", the print pipeline must emit **only** the Outputs zone. Two artefacts: + +```bash +# 1. Print_Area scoped to the Outputs sheet (Summary or Dashboard). +officecli add "$FILE" / --type namedrange --prop name=_xlnm.Print_Area --prop scope=Summary --prop 'refersTo=Summary!$A$1:$H$40' +# 2. Hide every non-Outputs sheet — Print_Area scope alone does NOT stop the print pipeline from emitting every visible sheet. +for S in Assumptions 'P&L' 'Balance Sheet' 'Cash Flow' DCF WACC Debt FCF 'S&U' Exit Returns; do + officecli raw-set "$FILE" /workbook --xpath "//x:sheet[@name='$S']" --action setattr --xml "state=hidden" || true +done +# 3. fit-to-page landscape on Outputs sheet. +officecli raw-set "$FILE" /Summary --xpath "//x:worksheet" --action prepend --xml '' +``` + +Delete any `Print_Area` set on Calc sheets — conflicting scopes emit multi-page output with Assumptions / statement sheets leaking. + +## Build-order & cache-drift rule (critical for 3-statement) + +Three facts cause silent wrong numbers: (1) new formulas ship without cached values — Excel recomputes on open, HTML preview / older viewers do not; (2) downstream written in the same sequence as upstream caches `0` from upstream's pre-cache state; (3) cross-sheet `batch` while resident is open deadlocks at 3–5 ops. + +**Discipline (every recipe):** +- Build order follows the data chain: `P&L → BS → CF` (3-statement); `FCF → WACC → NPV → Sensitivity` (DCF); `S&U → Debt → P&L → CF → Returns` (LBO). +- After the cross-sheet chain, **cache-refresh pass:** re-issue `set` on every summary / valuation / balance-check cell, non-resident. +- Spot-check: `officecli get "$FILE" /Summary/B2 --json | jq '.data.results[0].format.cachedValue'` returns a plausible non-null value. `null` means Excel will compute on open (OK for delivery). If a cell shows the `#OCLI_NOTEVAL!` sentinel: close residents, re-set; still unevaluated → cache-fallback (§Financial function patterns). + +## Recipes — three model types + +Each recipe below is **runnable skeleton, not finance theory**. Substitute numbers; don't restructure. All recipes assume `FILE="model.xlsx"` is set and you have run `officecli create "$FILE"` + `officecli open "$FILE"`. Close with `officecli close "$FILE"` at the end. + +### Recipe A — 3-statement model (P&L + BS + CF) + +**What this recipe produces.** 4 sheets: `Assumptions`, `P&L`, `Balance Sheet`, `Cash Flow`, plus `Summary`. Year columns 2024A · 2025E · 2026E · 2027E. Balance-check row on BS; cash-reconciliation row on CF. Every statement row = formula → Assumptions. + +**Build order (MANDATORY).** `Assumptions → P&L → Balance Sheet → Cash Flow → Summary`. Do NOT build BS before P&L — `RetainedEarnings` depends on `NI`. Do NOT build CF before BS — `CF.OpeningCash = prior period CF.EndingCash` self-chain requires BS cash anchored for Y1. The skill's Gate 4 balance check fails silently if order is wrong. + +**Step 1 — sheets + tab colors + freeze panes.** + +```bash +officecli add "$FILE" / --type sheet --prop name=Assumptions --prop tabColor=FFC000 +officecli add "$FILE" / --type sheet --prop name=P&L --prop tabColor=4472C4 +officecli add "$FILE" / --type sheet --prop name='Balance Sheet' --prop tabColor=4472C4 +officecli add "$FILE" / --type sheet --prop name='Cash Flow' --prop tabColor=4472C4 +officecli add "$FILE" / --type sheet --prop name=Summary --prop tabColor=70AD47 +officecli set "$FILE" /Assumptions --prop freeze=B2 +officecli set "$FILE" /P&L --prop freeze=B3 +officecli set "$FILE" "/Balance Sheet" --prop freeze=B3 +officecli set "$FILE" "/Cash Flow" --prop freeze=B3 +``` + +**Step 2 — assumptions (blue, yellow-fill on key drivers).** Year headers row 2, labels down col A, blue numeric inputs on B:E. Drivers: `RevenueGrowth`, `GrossMargin`, `OpExRatio`, `TaxRate`, `DaysReceivable/Inventory/Payable`, `CapExRatio`, `DepreciationYears`. `font.color=0000FF` on B:E. Yellow-fill (`fill=FFFF00`) the 3–5 scenario-switched drivers. + +**Declare named ranges for ≥3-use drivers and reference them** (`StartingARR`, `TaxRate`, `OpeningCash`, `GrowthRate`, `GrossMargin`). Formulas: `=StartingARR` not `=Assumptions!B4`; `=EBT*TaxRate` not `=EBT*Assumptions!B8`. Declared-unused names = dead decoration, Gate 6 rejects. + +**Step 3 — P&L rows (all formulas).** Rows: `Revenue` / `COGS` / `Gross Profit` / `OpEx` / `EBITDA` / `D&A` / `EBIT` / `Interest` / `EBT` / `Tax` / `Net Income`. Every row = formula referencing `Assumptions` or prior-row cells. Example revenue-side block — **substitute your row numbers**. Row-map for this example: `B3=Revenue, B4=COGS, B5=Gross Profit, B7=OpEx, B9=EBITDA, B10=EBIT, B15=Net Income`. Submit as single non-resident batch: + +```bash +cat <<'EOF' | officecli batch "$FILE" +[ + {"command":"set","path":"/P&L/B3","props":{"formula":"Assumptions!B5","font.color":"008000"}}, + {"command":"set","path":"/P&L/C3","props":{"formula":"B3*(1+Assumptions!C6)"}}, + {"command":"set","path":"/P&L/D3","props":{"formula":"C3*(1+Assumptions!D6)"}}, + {"command":"set","path":"/P&L/E3","props":{"formula":"D3*(1+Assumptions!E6)"}}, + {"command":"set","path":"/P&L/B4","props":{"formula":"-B3*(1-Assumptions!B7)"}}, + {"command":"set","path":"/P&L/B5","props":{"formula":"B3+B4"}} +] +EOF +``` + +Assumptions refs (`B5`, `C6`, `B7`) are also placeholder rows — better: **define named ranges** for each driver (Step 2) so formulas read `=StartingRevenue*(1+RevenueGrowth_Y2)` regardless of row layout. Repeat for `OpEx` / `D&A` / `Interest` / `Tax` / `NI`. `font.color=008000` on every cross-sheet-reference cell; same-sheet cells default `000000`. `numFmt='$#,##0;($#,##0);"-"'` on all $ rows. + +**Step 4 — Balance Sheet rows (all formulas).** Assets = `Cash + AR + Inventory + Net PP&E`. Liab = `AP + Debt`. Equity = `OpeningEquity + RetainedEarnings`. Working-capital rows use Days assumptions: `AR = Revenue × DaysReceivable / 365`. `Net PP&E` rolls forward: `Beg + CapEx − Depreciation`. **`BS.Cash` is NOT an independent plug** — it MUST equal `'Cash Flow'!B` (populated in Step 5). + +**Retained Earnings — live formula every period.** `BS.RE(t) = BS.RE(t-1) + 'P&L'!NI(t) − Dividends(t)`. Hardcoded RE rounds to whole dollar → BS shows ±$1 off every period (CFO reads "model doesn't balance"). For Y1 Historical RE (no prior NI), compute via BS identity as a **live formula**: `BS!RE_Y1 = TotalAssets − TotalLiabilities − PaidInCapital`. Blue-font + classic comment on the Y1 cell; Y2..Y5 stay NI-driven. + +**Step 5 — Cash Flow rows (all formulas).** Operating: `NI + D&A − ΔWorkingCapital`. Investing: `−CapEx`. Financing: `ΔDebt − Dividends`. Ending Cash = `Opening + Operating + Investing + Financing`. **Year 2+ Opening Cash = prior period Ending Cash** — self-chain on the same sheet: `C17 = B19`, `D17 = C19`, `E17 = D19`. The Y1 `OpeningCash` is an Assumptions input. + +**Step 6 — Balance check + cash reconciliation rows (hard delivery checks).** Row-map for this example: `Balance Sheet: B10=Total Assets, B15=Total Liab, B17=Total Equity, B18=Balance Check`; `Cash Flow: B5=BS.Cash (cross-sheet anchor), B19=CF.Ending Cash, B21=CF-BS Cash Recon`. Substitute your layout's rows — the logic is the check, not the cell addresses. + +```bash +cat <<'EOF' | officecli batch "$FILE" +[ + {"command":"set","path":"/Balance Sheet/B18","props":{"formula":"IF(ABS(B10-B15-B17)<0.01,\"OK\",\"IMBALANCED: \"&ROUND(B10-B15-B17,0))","bold":"true","font.color":"000000"}}, + {"command":"set","path":"/Cash Flow/B21","props":{"formula":"IF(ABS(B19-'Balance Sheet'!B5)<0.01,\"OK\",\"CF != BS CASH: \"&ROUND(B19-'Balance Sheet'!B5,0))","bold":"true"}} +] +EOF +``` + +Replicate across columns C/D/E. Apply red fill (`fill=FFC7CE`) conditionally via `type=containsText --prop text=IMBALANCED` or `text="CF !="`. Gate 4 queries these rows and refuses delivery on any `IMBALANCED`. + +**Step 7 — cache refresh + format pass.** Re-set every summary cell on `Summary`, every balance-check / recon cell, and every cross-sheet reference on BS / CF (non-resident, single batch per sheet). Apply column widths (`col[A]=28`, `col[B:E]=15`), `numberformat='$#,##0;($#,##0);"-"'` on all dollar rows, header fills (`fill=1F3864`, `font.color=FFFFFF`, `bold=true`) on section-header rows (REVENUE / COGS / ASSETS / LIABILITIES). Header fill must cover A:E, not just the label cell (→ xlsx v2 §visual floor). + +**Step 8 — Summary / Dashboard KPIs + charts.** Minimum 4 KPIs: `Revenue 27E`, `EBITDA Margin 27E`, `Ending Cash 27E`, `Net Income CAGR` — each a formula referencing a statement cell, green font. + +**Minimum 3 charts on any Dashboard delivered to a board / executive audience** — one chart is a draft, three is a deliverable. Pre-populate `Summary!A10:E13` with Gross Margin / EBITDA Margin / NI Margin ratio rows (formulas referencing `P&L`) before adding the margin chart. + +```bash +# (1) Top-line trend (Revenue + EBITDA). +officecli add "$FILE" /Summary --type chart --prop chartType=column --prop dataRange='P&L!A2:E5' --prop title='Revenue & EBITDA' --prop width=14cm --prop height=8cm +# (2) Margin trend (Gross / EBITDA / NI margin). +officecli add "$FILE" /Summary --type chart --prop chartType=line --prop dataRange='Summary!A10:E13' --prop title='Margin trend' --prop width=14cm --prop height=8cm +# (3) Cash trajectory (Ending Cash ± Runway). +officecli add "$FILE" /Summary --type chart --prop chartType=area --prop dataRange='Cash Flow!A19:E19' --prop title='Ending cash' --prop width=14cm --prop height=8cm +``` + +**Verification (run all three):** + +```bash +# Balance check every period must say OK (range get → cells under .data.results[0].children[]) +officecli get "$FILE" "/Balance Sheet/B18:E18" --json | jq '.data.results[0].children[] | .format.cachedValue // .text' +# Cash recon every period must say OK +officecli get "$FILE" "/Cash Flow/B21:E21" --json | jq '.data.results[0].children[] | .format.cachedValue // .text' +# Summary KPIs are plausible numbers, not null +officecli get "$FILE" "/Summary/B2:B5" --json | jq '.data.results[0].children[].format.cachedValue' +``` + +### Recipe B — DCF valuation + +**What this recipe produces.** Sheets: `Assumptions`, `FCF` (10-year forecast), `WACC` (panel), `DCF` (NPV + TV + equity bridge), `Sensitivity` (2-axis grid). Output: `Implied Equity Value` + `Implied Per-Share`, with a `WACC × g` sensitivity. + +**Build order.** `Assumptions → FCF → WACC → DCF → Sensitivity`. + +**Step 1 — named ranges for key drivers.** DCF's readability depends on names. Every formula below uses `WACC`, `TaxRate`, `g` — not `$B$6`: + +```bash +cat <<'EOF' | officecli batch "$FILE" +[ + {"command":"add","parent":"/","type":"namedrange","props":{"name":"WACC","ref":"WACC!$B$12"}}, + {"command":"add","parent":"/","type":"namedrange","props":{"name":"TaxRate","ref":"Assumptions!$B$8"}}, + {"command":"add","parent":"/","type":"namedrange","props":{"name":"TerminalGrowth","ref":"Assumptions!$B$15"}}, + {"command":"add","parent":"/","type":"namedrange","props":{"name":"NetDebt","ref":"Assumptions!$B$20"}}, + {"command":"add","parent":"/","type":"namedrange","props":{"name":"SharesOut","ref":"Assumptions!$B$21"}} +] +EOF +``` + +**Step 2 — FCF build (10 years).** Columns B:K = Y1..Y10. Rows: `Revenue` (from growth) / `EBIT` (revenue × margin) / `EBIT × (1 − TaxRate)` (NOPAT) / `+ D&A` / `− CapEx` / `− ΔNWC` / `= FCF`. Use Assumptions-driven ratios (`CapEx = Revenue × CapExRatio`). All cells formulas, black font, `numFmt='$#,##0;($#,##0);"-"'`. + +**Step 3 — WACC panel.** On `WACC` sheet, an 8-row panel: `Risk-free rate` / `Equity risk premium` / `Beta` / `Cost of equity` (=Rf + β×ERP) / `Pre-tax debt cost` / `After-tax debt cost` (=×(1−TaxRate)) / `Equity weight` / `Debt weight` / `WACC` (=We×Re + Wd×Rd_after_tax). Inputs blue; derived rows black. + +**Step 4 — Terminal value + NPV + equity bridge.** Row-map: `DCF: B/C 3=TV, 4=PV explicit FCF, 5=PV terminal, 6=EV, 7=Net Debt, 8=Equity Value, 9=Per-Share`; `FCF: row 2 = periods (1..10), row 11 = FCF, B:K = Y1..Y10`. Substitute your rows. Notes column cells use `{"value":"text"}`, never `{"formula":"..."}` — formula-style prose yields `#NAME?` on open (see callout after Recipe C Step 5). On `DCF` sheet: + +```bash +cat <<'EOF' | officecli batch "$FILE" +[ + {"command":"set","path":"/DCF/B3","props":{"value":"Terminal value (Gordon growth)"}}, + {"command":"set","path":"/DCF/C3","props":{"formula":"FCF!K11*(1+TerminalGrowth)/(WACC-TerminalGrowth)","font.color":"008000","numberformat":"$#,##0;($#,##0);\"-\""}}, + {"command":"set","path":"/DCF/B4","props":{"value":"PV of explicit-period FCF (10 yr)"}}, + {"command":"set","path":"/DCF/C4","props":{"formula":"SUMPRODUCT(FCF!B11:K11/(1+WACC)^FCF!B2:K2)","font.color":"008000","numberformat":"$#,##0;($#,##0);\"-\""}}, + {"command":"set","path":"/DCF/B5","props":{"value":"PV of terminal value"}}, + {"command":"set","path":"/DCF/C5","props":{"formula":"C3/(1+WACC)^10","numberformat":"$#,##0;($#,##0);\"-\""}}, + {"command":"set","path":"/DCF/B6","props":{"value":"Enterprise value"}}, + {"command":"set","path":"/DCF/C6","props":{"formula":"C4+C5","bold":"true","numberformat":"$#,##0;($#,##0);\"-\""}}, + {"command":"set","path":"/DCF/B7","props":{"value":"Less: Net debt"}}, + {"command":"set","path":"/DCF/C7","props":{"formula":"-NetDebt","font.color":"008000","numberformat":"$#,##0;($#,##0);\"-\""}}, + {"command":"set","path":"/DCF/B8","props":{"value":"Equity value"}}, + {"command":"set","path":"/DCF/C8","props":{"formula":"C6+C7","bold":"true","font.color":"000000","numberformat":"$#,##0;($#,##0);\"-\""}}, + {"command":"set","path":"/DCF/B9","props":{"value":"Implied per-share"}}, + {"command":"set","path":"/DCF/C9","props":{"formula":"C8/SharesOut","bold":"true","numberformat":"$0.00"}} +] +EOF +``` + +**`NPV` vs `SUMPRODUCT` — both cache correctly.** The evaluator computes `NPV(rate, cross_sheet_range)` and caches the right value (verify with a readback). `SUMPRODUCT(values/(1+rate)^periods)` is the algebraic equivalent (period row `FCF!B2:K2 = 1..10` is a one-time setup); it is shown here as an OPTIONAL audit-readability choice — the explicit discounting series is sometimes easier for a reviewer to trace, not a cache workaround. Either form is fine. For irregular dates, `XNPV(rate, values, dates)` likewise caches; the SUMPRODUCT equivalent is `SUMPRODUCT(values/(1+rate)^((dates-base_date)/365))`. + +**Step 5 — 2-axis sensitivity grid (WACC × g).** 5×5 grid. Rows = WACC values `7.5% ... 11.5%`, cols = `g` values `1.5% ... 3.5%`. Each cell = one self-contained formula re-running the DCF with the grid's WACC and g substituted. Template: + +```bash +# Cell D14 (first data cell, grid anchor at C14 = WACC label, C15 = first WACC value) +# Substitute $D$13 (this cell's g) and $C15 (this cell's WACC) into a replicated EV + equity formula. +cat <<'EOF' | officecli batch "$FILE" +[ + {"command":"set","path":"/Sensitivity/D15","props":{"formula":"(NPV($C15,FCF!$B$11:$K$11)+(FCF!$K$11*(1+D$14)/($C15-D$14))/(1+$C15)^10+(-NetDebt))/SharesOut","numberformat":"$0.00"}} +] +EOF +``` + +Copy the formula across D15:H19 (5×5 grid). Row 14 carries g values (blue input); column C carries WACC values (blue input). Row 13 and column B carry labels. Apply 3-color gradient CF for quick-read (green = upside, red = downside): + +```bash +officecli add "$FILE" /Sensitivity --type conditionalformatting \ + --prop type=colorScale --prop ref=D15:H19 +``` + +**No Excel Data Tables.** Excel's native `/Data/Table` 2-variable table is not reliably supported via the CLI — each grid cell MUST be an explicit formula. Copy the template, do not try `Data Table` input cells. + +**Verification.** + +```bash +officecli get "$FILE" "/DCF/C8" --json | jq '.data.results[0].format.cachedValue' # equity value, plausible $ +officecli get "$FILE" "/DCF/C9" --json | jq '.data.results[0].format.cachedValue' # per-share, in $XX.XX range +officecli get "$FILE" "/Sensitivity/F17" --json | jq '.data.results[0].format.cachedValue' # grid center cell, plausible +``` + +If `C8` or `C9` come back `null` or show the `#OCLI_NOTEVAL!` sentinel, re-set them (non-resident) — see §Build-order & cache-drift. + +### Recipe C — LBO model + +**What this recipe produces.** Sheets: `Assumptions`, `S&U` (Sources & Uses), `Debt` (multi-tranche schedule), `P&L` (5-yr), `CF`, `Exit` / `Returns`. Outputs: `MOIC`, `IRR`, and a 4-tier returns waterfall. LBO is the stress test — expect circular refs (interest ↔ cash), deepest cross-sheet chains, and the heaviest use of named ranges. + +**Build order.** `Assumptions → S&U → P&L → Debt → CF → Exit → Returns`. P&L before Debt (debt interest depends on P&L EBIT for coverage checks); Debt before CF (CF uses interest + principal amortization). Enable `calc.iterate` before Step 5. + +**Step 1 — Sources & Uses (balance required, every fee line itemized).** + +``` +Uses = Purchase_EV (EntryEBITDA × EntryMultiple) + Transaction_fees (Purchase_EV × TxnFeePct, typ 1.5–2.5%) + + Financing_fees ((Senior + Mezz) × FinFeePct, typ 1–3%) + Refinanced_debt +Sources = Senior_TLB + Mezz + Revolver_drawn + Sponsor_equity +``` + +**Sponsor equity — pick one, never both.** (a) **Stated:** `Sponsor_equity = Assumptions!SponsorEquity`, then scale senior/mezz so Sources = Uses (fees absorbed by debt, not a silent plug). (b) **Solved:** `Sponsor_equity = Uses − Senior − Mezz − Revolver − Refinanced`, label "Sponsor Equity (solved)", no standalone Assumptions ref. Hardcoded `SponsorEquity` PLUS a `=Uses − Senior − Mezz` plug guarantees silent fee absorption — stated $140M vs plug $194.67M = $54.67M unaccounted fees, CFO rejection on sight. + +```bash +# Sources = Uses hard check. +officecli set "$FILE" /S&U/B12 --prop formula='IF(ABS(SUM(B4:B7)-SUM(B9:B11))<1,"BALANCED","S&U IMBALANCE: "&ROUND(SUM(B4:B7)-SUM(B9:B11),0))' --prop bold=true + +# Stated-vs-plug consistency (Gate 4 addendum; only run if you chose pattern (a)). +STATED=$(officecli get "$FILE" /Assumptions/B12 --json | jq -r '.data.results[0].format.cachedValue // "null"') +PLUGGED=$(officecli get "$FILE" /S&U/B10 --json | jq -r '.data.results[0].format.cachedValue // "null"') # B10 = sponsor-equity row on S&U +DELTA=$(python3 -c "print(abs(float('$STATED') - float('$PLUGGED')))" 2>/dev/null || echo 99999) +python3 -c "import sys; sys.exit(0 if float('$DELTA') <= 1 else 1)" && echo "S&U sponsor OK (stated=$STATED plug=$PLUGGED)" || { echo "REJECT Gate 4 S&U: stated $STATED ≠ plug $PLUGGED (Δ=$DELTA) — fees silently absorbed"; exit 1; } +``` + +Every non-sponsor line on `S&U` is a blue Assumptions input (target EBITDA, entry multiple, fee %s) or a derived formula. No hardcoded Uses / Sources numbers. + +**Step 2 — Debt schedule (multi-tranche).** One row per tranche per year. Columns: `BeginningBalance` / `Mandatory amortization` / `Cash sweep` / `EndingBalance` / `AverageBalance` / `InterestExpense`. Senior TLB: 1% mandatory amortization + all excess cash to sweep. Mezz: 0% amortization, interest-only cash-pay. Row-map for this example (senior TLB tranche, year 2 column C): `C4=Beginning Balance, C5=Mandatory Amort, C6=Ending Balance, C7=Cash Sweep, C8=Average Balance, C9=Interest Expense`. `CF!C20` = free cash available to sweep (year-2 ending cash pre-sweep on CF sheet). Substitute your tranche row block per layout. + +```bash +# year 2 senior TLB +cat <<'EOF' | officecli batch "$FILE" +[ + {"command":"set","path":"/Debt/C4","props":{"formula":"B6"}}, + {"command":"set","path":"/Debt/C5","props":{"formula":"-C4*Assumptions!$B$30","numberformat":"$#,##0;($#,##0);\"-\""}}, + {"command":"set","path":"/Debt/C6","props":{"formula":"C4+C5+C7","numberformat":"$#,##0;($#,##0);\"-\""}}, + {"command":"set","path":"/Debt/C7","props":{"formula":"-MIN(-CF!C20,C4+C5)"}}, + {"command":"set","path":"/Debt/C8","props":{"formula":"(C4+C6)/2","numberformat":"$#,##0;($#,##0);\"-\""}}, + {"command":"set","path":"/Debt/C9","props":{"formula":"-C8*Assumptions!$B$31","numberformat":"$#,##0;($#,##0);\"-\""}} +] +EOF +# Add the sweep-rule comment as a classic comment (comment is NOT a cell prop — separate --type comment). +officecli add "$FILE" /Debt --type comment --prop ref=C7 --prop text='cash sweep capped at available cash and remaining tranche balance' +``` + +**Revolver capacity cap.** If your deal uses a revolver tranche, the revolver balance each period is bounded by the commitment ceiling: +``` +Revolver_Balance = MIN(Assumptions!RevolverCapacity, MAX(0, prior_revolver + draw − paydown)) +``` +Without the `MIN(capacity, ...)` outer, a shortfall quarter silently over-draws the facility. + +Adjust row indices to your layout. Repeat for each tranche (senior / mezz / revolver) and each year. + +**Step 3 — P&L (5-year) + interest from Debt.** P&L interest row pulls from Debt: `Interest = 'Debt'!TotalInterestRowY`. This creates the **circular reference**: Interest → NI → CF → Cash Sweep → Debt balance → Interest. + +**Write-order warning.** `calc.iterate=true` governs _recalculation_, not write-phase. Appending the closing leg of a cross-sheet ring to a file that already contains the ring deadlocks the engine at 100% CPU regardless of `iterate`. For complex rings (multi-tranche LBO, revolver + TLB + mezz), use §Write-order surgery below (de-ring → write downstream → re-ring). Enable `calc.iterate=true` BEFORE writing ring formulas: + +```bash +officecli set "$FILE" / --prop calc.iterate=true --prop calc.iterateCount=100 --prop calc.iterateDelta=0.001 +``` + +`iterate` converges via successive approximation for naturally-dampening loops (higher interest → less cash → less sweep → higher balance, bounded by EBIT). `#REF!` or divergent values = pause; fix algebra, do not raise `iterateCount` to 1000. + +**Step 4 — CF + cash sweep.** Ending cash = Opening + CFO − CapEx − Mandatory amort − Cash sweep. Cash sweep = `MIN(freeCashAfterCapEx, seniorDebtBalance + seniorMandatoryAmort)`. The `MIN` cap prevents swept-below-zero. + +**Step 5 — Exit + Returns.** Row-map: `Exit: B3=Exit EV, B4=Less: remaining debt, B5=Exit equity to sponsor`; `Returns: B3=MOIC, B4=IRR`. + +```bash +# Values/formulas — single non-resident batch. +cat <<'EOF' | officecli batch "$FILE" +[ + {"command":"set","path":"/Exit/B3","props":{"formula":"'P&L'!F8*Assumptions!$B$25","numberformat":"$#,##0;($#,##0);\"-\""}}, + {"command":"set","path":"/Exit/B4","props":{"formula":"-('Debt'!F6+'Debt'!F13)","font.color":"008000","numberformat":"$#,##0;($#,##0);\"-\""}}, + {"command":"set","path":"/Exit/B5","props":{"formula":"B3+B4","bold":"true","numberformat":"$#,##0;($#,##0);\"-\""}}, + {"command":"set","path":"/Returns/B3","props":{"formula":"'Exit'!B5/('S&U'!B9)","numberformat":"0.00\"x\""}}, + {"command":"set","path":"/Returns/B4","props":{"formula":"IRR({-'S&U'!B9,0,0,0,0,'Exit'!B5})","numberformat":"0.0%"}} +] +EOF +# Classic comments — one --type comment per anchor cell. +officecli add "$FILE" /Exit --type comment --prop ref=B3 --prop text='Exit EV = Y5 EBITDA × exit multiple' +officecli add "$FILE" /Returns --type comment --prop ref=B3 --prop text='MOIC = exit equity / sponsor equity' +officecli add "$FILE" /Returns --type comment --prop ref=B4 --prop text='IRR — 5-yr, entry + exit only; use XIRR for mid-year dividends' +``` + +**Callout — labels: `comment` element vs Notes column vs `formula` (three distinct mechanics).** +- **Hover tooltip** → `officecli add ... --type comment --prop ref= --prop text='...'`. The **`comment` key is NOT a valid prop on `set cell`** (not in `officecli help xlsx cell`) — it silently drops when embedded inside a `set cell` props dict. Use the dedicated element. +- **Visible text in an adjacent Notes column** → `{"command":"set","path":"/DCF/D3","props":{"value":"TV = FCF × (1+g) / (WACC−g)"}}` — **`value`, not `formula`**, plain quoted string. +- **Formula-style prose written as a real formula** → NEVER. `{"formula":"FCF10*(1+g)/(WACC-g)"}` produces `#NAME?` in Excel (`FCF10`, `g`, `WACC` are unbound identifiers in that cell context). + +For mid-year dividends or partial exits, use `XIRR({cashflows}, {dates})` instead of `IRR`. + +**Step 6 — Returns waterfall (optional, 4-tier LP/GP).** Tiers: (1) LP preferred return 8% ; (2) GP catch-up to 20% ; (3) 80/20 split above hurdle ; (4) 100% to LP on loss. Each tier is a `MAX(0, MIN(...))` clamp. See §Sensitivity & scenarios for the general grid pattern. + +**Verification.** + +```bash +officecli get "$FILE" /S&U/B12 --json | jq '.data.results[0].format.cachedValue // .data.results[0].text' # must say BALANCED +officecli get "$FILE" /Returns/B3 --json | jq '.data.results[0].format.cachedValue' # MOIC, expect 2.0x-4.0x typical +officecli get "$FILE" /Returns/B4 --json | jq '.data.results[0].format.cachedValue' # IRR, expect 0.15-0.30 typical +# Iterate converged? +officecli query "$FILE" 'cell:contains("#REF!")' --json | jq '.data.results | length' # must be 0 +``` + +## Sensitivity & scenarios + +**Three patterns, pick one:** +- **(a) Base / Upside / Downside columns** on Assumptions — side-by-side scenarios, dropdown-less switch via an "Active" column + `INDEX/MATCH`. +- **(b) Dropdown + `INDEX/MATCH` switch** — one validation dropdown on Summary drives every driver via `INDEX(Base:Downside, MATCH(Dropdown, ScenLabels, 0))`. +- **(c) 2-axis sensitivity grid** — 5×5 or 7×7, one self-contained formula per cell, row/col headers are the two drivers. See Recipe B Step 5 for WACC × g. + +Mixing (a)+(b) creates circular input (scenario picked by dropdown AND overwritten by Active column) — pick one. + +**Grid rule:** each cell substitutes row-driver and col-driver into a self-contained copy of the output formula. Cannot reference the `WACC` named range (that's the panel) — reference the grid's axis cell. + +**Dropdown scenario switch.** One `validation` dropdown on Summary drives every `Assumptions` row: + +```bash +cat <<'EOF' | officecli batch "$FILE" +[ + {"command":"add","parent":"/Summary","type":"validation","props":{"sqref":"B1","type":"list","formula1":"Base,Upside,Downside"}}, + {"command":"set","path":"/Assumptions/B5","props":{"formula":"INDEX(C5:E5,MATCH(Summary!$B$1,$C$4:$E$4,0))"}} +] +EOF +# If you want a hover tooltip on B5, add it separately: +officecli add "$FILE" /Assumptions --type comment --prop ref=B5 --prop text='Revenue growth — picked by Summary!B1 scenario dropdown' +``` + +Every `Assumptions` driver row gets the same `INDEX/MATCH`. Base / Upside / Downside columns on C:E stay blue (hardcoded scenario inputs). + +**Football-field chart pattern (DCF valuation summary).** Horizontal Low→High bars for 3–5 valuation methods (DCF base, DCF bear, Trading comps, Precedent txns, LBO floor) stacked vertically. On a `Football` sheet: col A = method label, col B = Low $, col C = High $, col D = `=C−B` (width). Chart as a stacked bar with column B as an invisible first series (white/no-fill) and column D as the visible series — `dataRange=Football!A3:D7`, `chartType=bar`. Excel reads this as a floating bar per method. + +## Financial function patterns + +Terse reference — not a finance textbook. If you don't know what these do, pause and ask the user. + +| Function | Prefer over | Why | +|---|---|---| +| `XNPV(rate, values, dates)` | `NPV` | Irregular cash flow dates (M&A close mid-year, staggered tranches) | +| `XIRR(values, dates)` | `IRR` | Irregular dates; multiple sign changes handled better | +| `INDEX(range, MATCH(lookup, key, 0))` | `VLOOKUP` | Insert-safe (VLOOKUP breaks when a column is inserted in the source range) | +| `IFERROR(x/y, 0)` or `IF(y=0, 0, x/y)` | bare division | Guard every `/` in a financial model — `#DIV/0!` shipped = delivery failure | +| `MIRR(values, financeRate, reinvestRate)` | `IRR` with sign flips | When cash-flow pattern has 2+ sign changes | +| `SUMIFS(sumRange, criteriaRange1, criterion1, ...)` | `SUMPRODUCT((...))` array | Clearer intent for conditional sums; either evaluates correctly | + +**Evaluator coverage — verify, don't preemptively hardcode.** The CLI evaluator computes the finance functions this skill uses — `NPV` / `XNPV` / `IRR` / `XIRR`, array-literal formulas (`IRR({...})`), and `SUMPRODUCT(1/COUNTIF(range,range))` distinct-count — and caches the correct value with `evaluated:true`. There is no `NPV→0` rewrite obligation and no distinct-count `1/N` trap. The honest rule: build the formula you mean, then verify the cached value with a readback (`get ... --json | jq '.data.results[0].format.cachedValue'`). Only if a specific cell genuinely comes back with the `#OCLI_NOTEVAL!` sentinel (formula written before its inputs, or an unsupported function) should you fall back — first re-set non-resident after `close`; if it still won't evaluate, hardcode the computed value with a blue font and a classic comment via `officecli add "$FILE" /Sheet --type comment --prop ref= --prop text='cached valuation; refreshes on open in Excel — do not edit'`, and disclose in delivery notes. + +## Circular references & iterative calc + +**Enable `calc.iterate` ONLY when circularity is algebraically justified:** Interest ↔ Cash (LBO revolver / cash sweep), Tax shield ↔ NI (rare — most 3-statement models compute interest before tax and avoid), Revolver plug ↔ Ending cash (corporate cash waterfall with min-cash). + +```bash +officecli set "$FILE" / --prop calc.iterate=true --prop calc.iterateCount=100 --prop calc.iterateDelta=0.001 +``` + +`iterateCount=100` / `iterateDelta=0.001` are Excel defaults, fine for naturally dampening loops. + +### Write-order surgery (de-ring → write downstream → re-ring) + +`calc.iterate` controls recalc, not write-phase. Appending the closing leg of an already-wired cross-sheet ring (Debt.Interest ↔ CF.Cash ↔ Debt.CashSweep) deadlocks at 100% CPU; `view html` / `get` also hang on a non-converged ring. + +**3-step playbook:** +1. **De-ring** — write Debt with the 10–20 ring cells set to literal `0` (e.g. `C7=0`, not `=-MIN(...)`). Removes the ring. +2. **Write downstream** — build all non-circular chains (P&L, CF, Exit, Returns, Summary, grid) non-resident, one heredoc per sheet. Everything caches against the zeroed cells. +3. **Re-ring** — close all residents, re-set each circular cell with its real formula, one `set` per cell, non-resident. + +**Acceptance.** `get /Debt/C7 --json | jq '.data.results[0].format.cachedValue'` returns non-zero non-null. If a cell still deadlocks, leave `=0` + classic comment `"circular; recalculates in Excel on F9"`, flag at delivery. Never paper over with `iterateCount=1000`. + +**Do NOT use `iterate` as a band-aid for `#REF!` / divergent values.** Raising `iterateCount` to 1000 hides the bug and ships a plausibly-wrong value; `validate` does not catch it. Break the loop algebraically (e.g. interest on opening balance only, not average). + +**Verify convergence.** Read the loop cell, bump a driving assumption and back, re-read — values must match: + +```bash +V1=$(officecli get "$FILE" /Debt/C9 --json | jq '.data.results[0].format.cachedValue') +officecli set "$FILE" /Assumptions/B31 --prop value=0.085 +officecli set "$FILE" /Assumptions/B31 --prop value=0.0845 +V2=$(officecli get "$FILE" /Debt/C9 --json | jq '.data.results[0].format.cachedValue') +[ "$V1" = "$V2" ] && echo "Iterate converged" || echo "WARN: drift V1=$V1 V2=$V2 — tighten iterateDelta or check algebra" +``` + +## Audit & Delivery Gate + +**Assume there are problems.** First build is almost never correct. Run every gate below; every check must print its success line. `validate` passing is not delivery — the model can pass schema and still be wrong by a factor of 10. + +### Gates 1–3 — inherited from xlsx v2 verbatim + +→ see xlsx v2 §QA minimum cycle (Gates 1–3 cover `view issues`, error-cell query, `validate` after close). Run them first, exactly as written in xlsx v2. No financial-model-specific tweaks. + +### Gate 4 — statement integrity (3-statement & LBO) + +Balance-check and cash-reconciliation rows produced by Recipe A / C must show `OK` / `BALANCED` every period. `query` the check rows and refuse on any `IMBALANCED` / `CF !=`: + +```bash +BS_FAIL=$(officecli query "$FILE" 'cell:contains("IMBALANCED")' --json | jq '.data.results | length') +CF_FAIL=$(officecli query "$FILE" 'cell:contains("CF !=")' --json | jq '.data.results | length') +SU_FAIL=$(officecli query "$FILE" 'cell:contains("S&U IMBALANCE")' --json | jq '.data.results | length') +if [ "$BS_FAIL" -eq 0 ] && [ "$CF_FAIL" -eq 0 ] && [ "$SU_FAIL" -eq 0 ]; then + echo "Gate 4 OK (balance + recon + S&U all pass)" +else + echo "REJECT Gate 4: BS=$BS_FAIL CF=$CF_FAIL S&U=$SU_FAIL"; exit 1 +fi +``` + +If any fail, the model is silently wrong — fix the upstream chain before delivery. Most common cause: a cross-sheet formula stored `\!` (shell-mangled) — run `officecli query "$FILE" 'cell:contains("\\\\!")'` and re-enter via batch heredoc. + +### Gate 5 — cached-value sanity on valuation cells + +NPV / IRR / XIRR / equity-bridge / MOIC / summary KPI cells that ship unevaluated (`#OCLI_NOTEVAL!` sentinel, typically because the formula was written before its inputs) send a blank/wrong number to a reader who does not recalc on open. List every valuation cell and confirm a cached value is present: + +```bash +# Customize the path list per recipe — this is the DCF example +for P in "/DCF/C4" "/DCF/C5" "/DCF/C6" "/DCF/C8" "/DCF/C9"; do + V=$(officecli get "$FILE" "$P" --json | jq -r '.data.results[0].format.cachedValue // "null"') + if [ "$V" = "null" ] || [ "${V#\#OCLI_NOTEVAL}" != "$V" ]; then + echo "REJECT Gate 5: $P unevaluated (cached=$V) — re-set after close (see §Build-order & cache-drift)"; exit 1 + fi + echo "Gate 5 $P: cached=$V OK" +done +``` + +For LBO, extend the list: `/Exit/B5`, `/Returns/B3`, `/Returns/B4`. For 3-statement, extend with `/Summary/B2:B5`. + +### Gate 6 — hardcode / zone discipline + +Every Calc sheet has zero numeric hardcodes. Executable: + +```bash +# `cell:not(:has(formula))` selects the literal cells (and `cell:has(formula)` the formula cells). +HARDCODE=$(officecli query "$FILE" 'cell[type=Number]' --json \ + | jq '[.data.results[] | select(.format.formula == null) | select(.path | test("/(P&L|Balance Sheet|Cash Flow|DCF|Debt|FCF|WACC|Exit|Returns)/"))] | length') +[ "$HARDCODE" -eq 0 ] && echo "Gate 6 OK (no hardcodes on Calc sheets)" || { echo "REJECT Gate 6: $HARDCODE hardcoded numeric cells on Calc zone — move to Assumptions"; exit 1; } + +# Named-range coverage + dead-decoration audit: ≥3 ranges declared AND each referenced by ≥1 formula. +NR=$(officecli query "$FILE" namedrange --json | jq '.data.results | length') +[ "$NR" -ge 3 ] && echo "Gate 6 OK ($NR named ranges)" || echo "WARN Gate 6: only $NR named ranges" +DEAD=0 +for NR_NAME in $(officecli query "$FILE" namedrange --json | jq -r '.data.results[].format.name'); do + # Match the formula SOURCE (`formula~=`), not the cached/displayed RESULT — `:contains` would scan + # the computed value and report every name as dead (false reject). + USES=$(officecli query "$FILE" "cell[formula~=$NR_NAME]" --json | jq '.data.results | length') + [ "$USES" -ge 1 ] && echo " $NR_NAME: $USES uses OK" || { echo " WARN: $NR_NAME unused"; DEAD=$((DEAD+1)); } +done +[ "$DEAD" -eq 0 ] && echo "Gate 6 named-range audit OK" || { echo "REJECT Gate 6: $DEAD dead-decoration name(s)"; exit 1; } +``` + +### Gate 5b — visual audit via HTML preview (mandatory) + +Gates 1–4/6 are grep defenses — they cannot see a rendered sheet. Run `officecli view "$FILE" html` and Read the returned HTML. Walk every sheet (inherits xlsx v2 visual floor): + +- No `###` in any numeric cell (widen column). +- No truncated labels / section headers (widen column or `alignment.wrapText=true`). +- No placeholder tokens (`TBD`, `{var}`, `xxxx`) — Gate 6.1 grep below. +- Balance-check / recon rows say `OK` / `BALANCED` every period column. +- Dashboard charts render, y-axis = 0 on ARR/revenue lines, source data matches statement sheet. +- Sensitivity grid colors read green (upside) → red (downside) — color-scale CF applied. +- No stale cached `0` on summary KPIs; if present, run cache-refresh pass. + +REJECT on any defect. **Human preview:** `officecli watch "$FILE"`, or open in Excel / WPS / Numbers — final colors + chart fidelity only fully render in the target viewer. + +### Gate 6.1 — token / placeholder sweep + +```bash +LEAK=$(officecli view "$FILE" text | grep -niE 'TBD|\(fill in\)|xxxx|lorem|\{\{|placeholder|coming soon') +[ -z "$LEAK" ] && echo "Gate 6.1 OK (no placeholder tokens)" || { echo "REJECT Gate 6.1:"; echo "$LEAK"; exit 1; } +``` + +### Honest limit + +`validate` catches schema errors, not finance errors. A model passes `validate` with `BS.Cash` hardcoded to force balance, an `NPV` cached at `0`, a sensitivity grid all-zero because it was built before FCF, a `#NAME?` runtime on a `P&L`-named sheet with unquoted refs. Gates 4 / 5 / 6 / 5b exist because schema-level `validate` cannot catch any of this. + +## Known Issues & Pitfalls + +→ Base pitfalls (cross-sheet `!` trap, batch JSON dotted-name rule, renderer caveats): see xlsx v2 §Known Issues & Pitfalls — all apply. + +Financial-model-specific: + +- **AP sign on COGS.** Accounts Payable: if COGS is stored negative on the P&L, AP formula must negate — `=-COGS*DaysPayable/365`. Wrong sign inflates NWC and flips CF direction. Silent; passes `validate`. +- **`#NAME?` not caught by `query` / `validate`.** A cross-sheet formula referencing `P&L!B3` without quoting the sheet name (because `&` is special) lands at runtime as `#NAME?`. Always write cross-sheet refs as `'P&L'!B3` — single-quote the sheet name if it contains `&`, space, `(`, `)`, etc. Gate 5b visual check is the only detection. +- **Iterative calc silent non-convergence.** `calc.iterate=true iterateCount=100` converges at whatever the cap lands on — even if the true answer is 2× that. Always run convergence verify (§Circular references). Complex LBO rings (multi-tranche debt + sweep + tax shield) may not converge; when `cachedValue=0` on a ring cell, use §Write-order surgery. +- **Batch-while-resident deadlock on circular writes.** Writing the closing leg of a cross-sheet ring via `batch` with a resident open deadlocks at 100% CPU. Even single `set` on a ring cell can hang. Fix: close residents, write the ring in two passes per §Write-order surgery. Non-resident single-heredoc is the only safe form. +- **Cross-sheet cached value stale in `view html`.** Downstream written in the same sequence as upstream caches `0`. Excel resolves on open; HTML preview does NOT. Re-set every downstream non-resident after the chain (§Build-order & cache-drift). +- **`NPV()` / `XNPV()` evaluate and cache correctly.** The evaluator computes both (same-sheet and cross-sheet); no rewrite is required. `SUMPRODUCT(values/(1+rate)^periods)` remains an optional audit-readability alternative, not a cache workaround. +- **Sensitivity-grid build order still matters.** A grid cell written before its FCF/WACC inputs exist evaluates against empty inputs and may cache a wrong/blank value. Build FCF + WACC + DCF first, then the grid in a separate non-resident batch, and verify (`jq '.data.results[0].format.cachedValue'`). This is an ordering discipline, not an evaluator limitation. +- **`BS.Cash` = CF ending cash always** (including Y1: `BS.Cash = 'Cash Flow'!B19`). Never an independent plug or Assumptions ref — a plugged `BS.Cash` hides balance errors. +- **Year 2+ `Opening Cash` = prior period `Ending Cash`** (`C17=B19`, `D17=C19`). Independent Y2+ opening-cash inputs silently drift from BS. +- **Waterfall chart "total" bars.** `chartType=waterfall` cannot mark total programmatically — use `colors=` convention (dark = total, medium = positive, red = negative). See `help xlsx chart`. +- **DCF per-share when `SharesOut` is a formula.** `=BasicShares + OptionPool × ExerciseAssumption` → add a blue-font assumption cell and point the `SharesOut` named range at the computed cell, not the raw input. + +## Help pointer + +When in doubt: `officecli help xlsx [element] [--json]`. Help is the authoritative schema; this skill is the decision guide for financial-modeling deltas. diff --git a/external/officecli/skills/officecli-pitch-deck/SKILL.md b/external/officecli/skills/officecli-pitch-deck/SKILL.md new file mode 100644 index 0000000..56b8add --- /dev/null +++ b/external/officecli/skills/officecli-pitch-deck/SKILL.md @@ -0,0 +1,810 @@ +--- +name: officecli-pitch-deck +description: "Use this skill when the user is building a fundraising / investor pitch deck — seed, Series A / B / C, convertible note, SAFE round, strategic raise. Trigger on: 'pitch deck', 'investor deck', 'Series A deck', 'Series B deck', 'Series C deck', 'fundraising deck', 'seed pitch', 'VC deck', 'raising capital', 'term sheet presentation'. Output is a single .pptx. This skill is a scene layer on top of officecli-pptx — inherits every pptx v2 rule (visual floor, grid, palettes, connector canon, Delivery Gate). DO NOT invoke for a generic board review, sales deck, all-hands, or product launch — route those to officecli-pptx base." +--- + +# OfficeCLI Pitch Deck Skill + +**This skill is a scene layer on top of `officecli-pptx`.** Every pptx hard rule — visual delivery floor (title ≥ 36pt / body ≥ 18pt / title ≥ 2× body), 12-column grid on 33.87×19.05cm, 4 canonical palettes, chart-choice decision table, connector canon (`shape` / `from` / `to` / `tailEnd=triangle`), shell escape, resident + batch, Delivery Gate 1–5a — is inherited, not re-taught. This file adds only what **fundraising** needs on top: stage diagnosis (A / B / C), 5 赛道 arc templates, 10 key-slide recipes (cover / problem / solution / market / product / model / traction / team / financials / ask), pitch-specific numbers convention, a VC ship-check, and a pitch-specific fresh-eyes Gate 6. + +When the pptx base rules cover it, the text here says `→ see pptx v2 §X`. Read `skills/officecli-pptx/SKILL.md` first if you have not. + +## Setup + +If `officecli` is missing: + +- **macOS / Linux**: `curl -fsSL https://d.officecli.ai/install.sh | bash` +- **Windows (PowerShell)**: `irm https://d.officecli.ai/install.ps1 | iex` + +Verify with `officecli --version` (open a new terminal if PATH hasn't picked up). If install fails, download a binary from https://github.com/iOfficeAI/OfficeCLI/releases. + +## ⚠️ Help-First Rule + +**This skill teaches what a fundraising deck requires, not every command flag.** When a prop name, enum value, or preset is uncertain, consult help BEFORE guessing. + +```bash +officecli help pptx # All pptx elements +officecli help pptx # Full schema (e.g. chart, shape, connector, picture) +officecli help pptx --json # Machine-readable +``` + +Help reflects the installed CLI version. When this skill and help disagree, **help wins.** Every `--prop X=` in this file has been grep-verified against `officecli help pptx ` — if help adds / renames a prop in a later version, trust help. + +## Mental Model & Inheritance + +**Inherits pptx v2.** You should have read `skills/officecli-pptx/SKILL.md` first. This skill assumes you know how to: add slides + shapes + charts + connectors; address by `@name=` / `@id=`; quote paths; use `batch` heredocs; write `--prop tailEnd=triangle` on every flow connector; and run the 5-gate Delivery Gate. If any of those are unfamiliar, open a pptx v2 session before continuing. + +## Shell & Execution Discipline + +**Shell quoting, incremental execution, `$FILE` convention** → see pptx v2 §Shell & Execution Discipline. Same rules verbatim — quote `[N]` paths, single-quote values containing `$` (including `$35M`, `$1.2B TAM` in a cover or ask slide), never hand-write `\$ \t \n` in executable examples, one command at a time. Examples below use `$FILE` (`FILE="deck.pptx"`). + +**Single-quote every shape text containing `$`.** `--prop text="Series B · $35M"` (double quotes) is WRONG — zsh expands `$35M` → empty, deck renders `Series B · M` silently. `--prop text='Series B · $35M'` (single quotes) is right. This is the #1 pitch-deck shell-escape failure mode (`$35M`, `$18M ARR`, `$1.2B TAM` appear on cover/ask/financials/milestones). Gate 2 cannot detect a stripped `$35M` — no residue. Gate 2b catches common strip patterns; single-quoting PREVENTS them. + +## What "pitch deck" means here (identity) + +A pitch deck is a pptx with a **fundraising layer** on top: VC-oriented narrative arc, verifiable metrics, stage-appropriate data density, founder-credibility surface. Slides are consumed at ~3 seconds per slide in a live room — the pptx v2 rule. Pitch decks add a second constraint on top: **every slide carries one investable proposition**. If a slide is "interesting background" that doesn't move the ask forward, cut it. VCs will not. The base pptx rules still apply; pitch decks add six deltas: + +1. **Stage determines everything.** Series A / B / C each dictates slide count, narrative weight, which metrics are must-haves, and tolerance for unit-econ sophistication. A Series A deck with 6 pages of CAC/LTV math reads as over-packaged; a Series B deck missing unit econ reads as incomplete. Pick the stage first — everything downstream follows. +2. **Narrative arc beats feature dump.** 10 essential slides in a fixed order: cover → problem → solution → market → product → model → traction → team → financials → ask. Out of order = VCs disengage. +3. **Numbers are a contract.** TAM/SAM/SOM must be clean three-layer; CAC/LTV must have a payback line; ARR ≠ revenue; Use-of-Funds must be a four-bucket pie. Sloppy numbers = round dies. +4. **Team slide carries prior companies.** Avatar grid alone reads as a student project. Add prior-company logos / names + one-line role. Without this, first-time founders look exactly like first-time founders. +5. **Traction chart y-axis starts at 0.** A "hockey stick" starting at `y_min = 80% of current` is a visual lie — VCs who have seen 10,000 decks spot it in < 2 seconds. +6. **The ask is a slide, not a footnote.** `$XX M` hero + four-bucket Use-of-Funds + runway length. "We're raising some money" is not an ask. + +### Reverse handoff — when to go BACK to pptx base + +Stay in **pptx v2 base** for board reviews, all-hands, sales decks, product launches, training decks — anything not tied to raising capital. Use **this skill** only when: (a) the user mentions a specific round (seed / Series A / B / C) or a VC meeting, AND (b) the deck needs at least 4 of {problem, traction, team with credentials, Use-of-Funds, stage-appropriate unit econ, financial projections}. + +If the user says "fundraising deck" but the context is a corporate BU quarterly ask, that is a board review. Route to pptx v2 Recipe (d) 10-slide blueprint. If the user says "board review" but the context is a small company raising a bridge round, route here. + +## Series A / B / C stage diagnosis (decision tool) + +**Read this before writing a single command.** Pick the row that matches the user's description — everything downstream (slide count, which metrics, which recipes, what the team slide must show) derives from this one call. + +| Stage | Revenue band | Team | Slide count | Dominant narrative (weight) | Must-have data | Common red flag | +|---|---|---|---|---|---|---| +| **Seed** | $0 – $1M ARR (often pre-rev) | 2 – 8 FTE | 10 – 12 | Problem (30%) + Solution (25%) + Team (15%) + Market (15%) + Traction (15%) | Founder-market fit story; 1 – 2 design-partner / pilot logos; top-down TAM ok | Over-claiming traction (10 customers = "market proven") | +| **Series A** | $1 – $5M ARR | 10 – 25 FTE | 12 – 16 | Problem (20%) + Solution (20%) + **Market "why now"** (15%) + Product (15%) + Traction (20%) + Team (10%) | PMF proof (NRR > 110%, low churn), bottom-up TAM/SAM, pipeline / pilots converted | Bottom-up TAM feels fabricated; CAC not yet meaningful but shown anyway | +| **Series B** | $5 – $30M ARR | 30 – 100 FTE | 18 – 22 | **Traction + Unit econ (30%)** + Market + Product + Team + Financials (ask) | ARR curve starting at 0; NRR, CAC, LTV, payback (< 18 mo ideal); cohort retention; logo wall | No unit-econ slide; CAC payback > 24mo without explanation; Use-of-Funds missing % | +| **Series C** | $30M+ ARR | 100+ FTE | 20 – 24 | **Financials + Scale + Moat (40%)** + Market expansion + Team depth | Multi-year GAAP, rule-of-40, GM trajectory, international expansion plan, defensibility | No moat slide; revenue growth without margin story; team slide has no prior CEO / CFO | +| **Bridge / SAFE** | any | any | 8 – 10 | **Specific bridge reason** + runway math + commitments | Prior round context; specific milestone the bridge funds; committed investor amount | Treating a bridge like a Series A — too many slides dilutes the ask | + +**Decision procedure.** From one or two user sentences ("Series B, $18M ARR, 120 customers, $35M raise"), pick exactly one stage row. All later choices in this skill reference your stage: which 赛道 template to pull, which recipes are mandatory vs optional, and which Delivery Gate 6 checks fire. + +**Corner cases.** Bridge rounds & convertibles between A → B are closer to A or B depending on whether the bridge milestone is "finish PMF" (A shape) or "hit unit-econ target" (B shape). "Extension" rounds at the same stage reuse the earlier stage's skeleton and add a one-slide "progress since last round" update. + +**Non-SaaS stage overrides.** The ARR / unit-econ shape of Series B fits SaaS. For other verticals, substitute revenue band + unit-econ equivalent + Gate 6.3 grep: + +| Vertical | Revenue "band" at Series B | "Unit econ" equivalent | Gate 6.3 substitute | +|---|---|---|---| +| **Bio / Clinical-stage** | pre-rev, 20–60 FTE | burn rate + runway to next milestone (IND / Ph1 readout / BLA) | `shape:contains("ORR")` OR `contains("Pipeline")` OR `contains("BLA")` OR `contains("runway")` ≥ 1 | +| **Deep Tech / Frontier** | pre-rev or early pilot rev | technical milestones + TRL level + benchmark vs SoTA | `shape:contains("TRL")` OR `contains("benchmark")` ≥ 1 | +| **Marketplace / Network** | GMV $10–100M | take rate + cohort retention + liquidity | `shape:contains("GMV")` + `contains("take rate")` ≥ 1 | +| **Consumer hardware** | $2–15M revenue (shipped units) | contribution margin + repeat rate + blended CAC | `shape:contains("repeat")` OR `contains("contribution")` ≥ 1 | + +Substitute the analogue grep when running Gate 6.3 on these verticals. False WARN on SaaS CAC/LTV = expected; real concern = vertical-specific analogue present. Bio Series B decks especially: burn + runway-to-milestone IS the "unit econ" story. + +## 赛道 arc templates (5 families) + +5 mainstream verticals. Each one has different slide weights because what VCs require as proof-of-concept differs. Pick the vertical row; the slide skeleton is a copy-able starting point. Slide counts assume the matching stage row above. + +### (1) B2B SaaS / Enterprise software + +Canonical arc — the template most of VC muscle memory is built on. Series B example (20 slides): cover · TL;DR · problem · problem evidence · solution · product loop · market TAM/SAM/SOM · **unit economics (CAC / LTV / payback / GM)** · ARR trajectory · retention cohort · logo wall · team · competitors · financials 4-year · ask. Must-have: unit-econ slide from Series A onward; logo wall from Series B onward. + +### (2) Consumer (B2C app / consumer hardware / D2C) + +Narrative-driven. Early-stage decks lean on **product-experience screenshots + founding story + "why now"** market timing; lighter on unit econ (which are usually weaker than SaaS). Series A example (14 slides): cover · hook (30-second product demo or 1-line vision) · problem (lived experience) · solution (product shots) · product-experience flow · "why now" market window · pre-order / crowdfunding / early-sales evidence · retention / engagement (DAU, D30) · market (top-down ok if bottom-up unreliable) · competitive positioning · founder story + team · press / endorsements · financials · ask. Must-have: product visuals on ≥ 3 slides; "why now" slide (window justification); engagement metric not just revenue. + +### (3) Deep Tech / Frontier tech (AI foundation models, quantum, climate hardware, robotics) + +Technology credibility is the sell. Pre-revenue deep tech replaces "traction" with **technical milestones + defensibility**. Series B example (22 slides): cover · thesis (one-line "what changes if this works") · problem (current state of art) · solution (technical approach) · **technology architecture** · benchmarks vs SoTA · pipeline / TRL levels · market (long-tail) · business model · early commercial traction (pilots, LOIs) · IP / patents · team (usually PhD / ex-FAANG-research) · partners · financials · ask. Must-have: benchmark slide; IP slide; team slide dense with PhDs / prior-lab names. + +### (4) Marketplace / Network business (two-sided platform, social, commerce) + +Liquidity is the metric. Replace "unit econ" with **GMV + take rate + cohort retention + supply / demand balance**. Series A example (15 slides): cover · problem (friction in current supply-demand) · solution · product demo (both sides) · network effects diagram · early liquidity (first-week GMV, time-to-match) · cohort retention · geographic / category expansion plan · competitive positioning vs incumbents · take-rate model · team · financials · ask. Must-have: liquidity metric slide; cohort retention chart; network-effect diagram. + +### (5) Bio / Life sciences / Healthtech + +Regulatory pipeline IS the business. Replace "product roadmap" with **clinical pipeline + regulatory path + scientific evidence**. Series B example (22 slides): cover · unmet medical need · scientific rationale (mechanism of action) · preclinical / clinical data (ORR, safety, endpoints) · **pipeline chart** (candidates × stages × dates) · differentiation vs standard of care · IP / exclusivity · regulatory strategy (IND, BTD, fast-track) · market (prevalence × pricing) · commercial strategy (orphan / specialty / biosimilar) · partnerships / collaborations · team (CSO / CMO with prior FDA wins) · financials (burn to next milestone) · ask. Must-have: pipeline chart; clinical data slide; team slide with prior regulatory wins. + +**Cross-vertical rule.** You can mix elements across templates, but never drop a must-have from your primary vertical. A SaaS deck missing unit econ, a bio deck missing a pipeline chart, a marketplace deck missing a liquidity metric — each is an instant VC disqualification. + +## Slide Patterns (layout canon) + +Patterns are **layout geometry**; recipes below are **narrative intent**. A slide picks one pattern for its visual shape (6 canonical ones below) and one recipe for what it argues (cover / problem / traction / ...). Multiple recipes can share one pattern — Problem / Why-Now / Traction-callout all lean on the 3-stat row (C.2). Pick the pattern first, then fill it with recipe content. + +**Speaker notes rule.** Every content slide (non-cover, non-closing) MUST carry speaker notes via `officecli add "$FILE" /slide[N] --type notes --prop text='…'`. Missing notes = not shippable — inherits pptx v2 §Hard rules (H7). Run `officecli help pptx notes` to confirm prop names before building. + +**Pattern reuse discipline.** Never run the same pattern on two consecutive slides — even with different data, two identical geometries in a row read as a template loop. Alternate C.2 with C.4 or C.5b to break rhythm. + +**Vertical centering.** When a slide carries fewer elements than the pattern's maximum, nudge y-positions down 2–3cm to center the visual weight. Tables below assume full content. + +### C.1 Title / Cover (dark gradient) + +3–4 text shapes on a gradient fill. Slide 1 in every deck. + +``` ++----------------------------------+ +| | +| TITLE (centered) | +| tagline | +| | +| round · amount · date | +| ________________________ | <- thin brand band ++----------------------------------+ +``` + +| Element | X | Y | Width | Height | Font / size | +|---|---|---|---|---|---| +| Title | 2cm | 5cm | 29.87cm | 4cm | serif bold, ≥ 36pt (44 typical) | +| Tagline | 2cm | 10cm | 29.87cm | 2cm | sans 18–22pt | +| Meta (round · $ · date) | 2cm | 13cm | 29.87cm | 1.5cm | sans 12–16pt | + +**Use this when** the slide is the first one (Cover recipe 1) — 3-second identity grab. Background is a 180° linear gradient between two dark palette shades (e.g. Professional Navy `1E2761 → 0D1F35`). If the title wraps to 2 lines, **add height (4cm → 5cm), never drop font below 36pt** — sub-36pt on a pitch cover reads as timid regardless of content. Transition: fade. + +### C.2 3-Stat callout row + +Title + 3 big-number / label pairs across. The default for Problem / Why-Now / Traction-callout slides. + +``` ++----------------------------------+ +| Title | +| | +| 73% 12hr $4.2B | +| label label label | +| source source source | ++----------------------------------+ +``` + +| Element | X | Y | Width | Height | Font / size | +|---|---|---|---|---|---| +| Title | 1.5cm | 1cm | 30.87cm | 3cm | serif bold ≥ 36pt | +| Stat 1 number | 2cm | 5cm | 9cm | 4cm | serif bold 60–64pt | +| Stat 1 label | 2cm | 9.5cm | 9cm | 2cm | sans ≥ 16pt (H4 floor) | +| Stat 2 number / label | 12.5cm | (same) | 9cm | (same) | (same) | +| Stat 3 number / label | 23cm | (same) | 9cm | (same) | (same) | + +**Use this when** you have 2–3 anchoring numbers and the story is "three facts argue the point" — Problem, Why-Now, Market-callout, single-row Traction. Labels ≥ 16pt is the H4 floor (sub-label exception); a number without a label reads as bravado, so never drop labels to 12–14pt to fit more text. + +### C.3 4-Stat callout row + +Same geometry as C.2 but 4 columns. Numbers 60pt, width 7cm each. + +``` ++-------------------------------------+ +| Title | +| | +| 73% 12hr $9M 4.2x | +| lbl lbl lbl lbl | ++-------------------------------------+ +``` + +| Element | X positions | Y | Width | Height | Font / size | +|---|---|---|---|---|---| +| Title | 1.5cm | 1cm | 30.87cm | 3cm | serif bold 36pt | +| Stat numbers | 1.5 / 9.5 / 17.5 / 25.5cm | 5cm | 7cm | 4cm | serif bold 60pt | +| Stat labels | (same X) | 9.5cm | 7cm | 2cm | sans ≥ 16pt | + +**Use this when** exactly 4 parallel metrics tell the story and 3 feels under-counted. Prefer C.2 if in doubt — 4 always feels tighter than 3, and wrap risk is real. + +> **Wrap warning.** At 60pt in 7cm width, dollar patterns with both `$` and `.` fail: `$9.4M` is 5 glyphs but the wide `$` and `.` in a serif bold make it wrap to 2 lines and destroy the callout. Safe dollar shapes at 60pt/7cm: `$9M`, `$96B`, `$4K` (3–4 chars). Non-dollar shapes: `340%`, `4.2x`, `12.3` safe up to 5 chars. Values ≥ 6 chars (`197min`, `3 Days`) will wrap — either (a) drop font to 44–48pt, (b) abbreviate (`197m`, `$9M`), or (c) shift to C.2 (9cm per stat). Single tokens only, no internal spaces. + +### C.4 Chart + Context (chart left, stats right) + +Chart takes left 55%, 2–3 stacked callouts on the right. The default for Traction / Financials / Market-sizing-with-context. + +``` ++-------------------------------------+ +| Title | +| | +| +---------------+ +--------+ | +| | | | Stat 1 | | +| | chart | +--------+ | +| | | | Stat 2 | | +| +---------------+ +--------+ | ++-------------------------------------+ +``` + +| Element | X | Y | Width | Height | +|---|---|---|---|---| +| Title | 2cm | 1cm | 29.87cm | 3cm | +| Chart | 2cm | 4cm | 17cm | 13cm | +| Stats column | 21cm | 4cm+ | 11cm | 2.5cm number + 1.5cm label (~3.7cm per pair) | + +Sub-labels ≥ 16pt (H4 floor). For 5 stats stacked, drop number size to 44pt; 6+ stats means pick a different pattern. Post-batch for column/bar charts: `officecli set "$FILE" "/slide[N]/chart[1]" --prop gap=80` to tighten bar spacing. + +**Use this when** one primary chart drives the story and 2–3 numeric anchors reinforce it — Traction (ARR curve + current ARR + YoY + NRR), Financials (4-year column chart + assumption callouts), Market (bar chart + SOM / CAGR / methodology). + +### C.5 Icon-in-circle grid (3-row vertical) + +3 vertical rows, each = circle icon on the left + title + 1-line description. + +``` ++---------------------------------------+ +| Title | +| | +| (o) Label one | +| description one | +| | +| (o) Label two | +| description two | +| | +| (o) Label three | +| description three | ++---------------------------------------+ +``` + +| Element | X | Y positions | Width | Height | Font / size | +|---|---|---|---|---|---| +| Icon circle | 2cm | 4.5 / 8.5 / 12.5cm | 2.5cm | 2.5cm | ellipse, accent fill | +| Label | 5.5cm | (icon Y + 0) | 25cm | 1.2cm | sans bold 18pt | +| Description | 5.5cm | (icon Y + 1.3cm) | 25cm | 1.8cm | sans ≥ 16pt (H4 floor), muted | + +**Use this when** you have 3 short vertical points that benefit from a visual anchor per row — Solution mechanism, Value pillars, Product loop. Choose C.5b (2×2 grid) when items are parallel and you have exactly 4; choose a horizontal 5-across variant when icons should read side-by-side (e.g. 5-step process). + +### C.5b 2×2 Feature grid (4 parallel items) + +4 rounded cards, 2 columns × 2 rows. Use when you have exactly 4 parallel items (product pillars, service types, feature quadrants). + +``` ++-----------------------------+ +| Title | +| | +| +---------+ +---------+ | +| | (o) T1 | | (o) T2 | | +| | body | | body | | +| +---------+ +---------+ | +| +---------+ +---------+ | +| | (o) T3 | | (o) T4 | | +| | body | | body | | +| +---------+ +---------+ | ++-----------------------------+ +``` + +| Element | X | Y | Width | Height | Font / size | +|---|---|---|---|---|---| +| Slide title | 2cm | 1cm | 29.87cm | 2.5cm | serif bold 32pt | +| Card 1 bg (top-left) | 1.5cm | 4cm | 14.5cm | 7cm | roundRect | +| Card 2 bg (top-right) | 17.5cm | 4cm | 14.5cm | 7cm | roundRect | +| Card 3 bg (bottom-left) | 1.5cm | 12cm | 14.5cm | 7cm | roundRect | +| Card 4 bg (bottom-right) | 17.5cm | 12cm | 14.5cm | 7cm | roundRect | +| Icon ellipse (each card) | card_x + 0.5cm | card_y + 0.5cm | 2cm | 2cm | — | +| Card title (each) | card_x + 3.2cm | card_y + 0.6cm | 10.5cm | 1.8cm | sans bold 16pt | +| Card body (each) | card_x + 0.5cm | card_y + 3cm | 13cm | 3.5cm | sans ≥ 16pt (H4 floor) | + +**Use this when** you have exactly 4 parallel items and the eye should land on each equally — 4 product pillars, 4 service tiers, 4 stakeholder types. 3 items feel lonely in a 2×2; 5+ items break the grid — go to a 3×2 (see pptx v2 §(d) grid math) or C.5 row pattern. + +> **Z-order canon (critical).** Each card's `roundRect` background must be added immediately before that card's icon / title / body shapes in the batch JSON — pptx paints in insertion order, so a background added after its text paints over and hides the text. When building with `officecli batch`, follow the per-card sequence `bg → ellipse → title → body` strictly. Pattern and z-order details → see pptx v2 §Recipe (c) z-order canon; reuse grid math from pptx v2 §(d) for non-2×2 counts. + +**Dark-background variant.** Change card fill from `F0F4F8` (light) to a lighter-dark shade like `1A2540` and bump body text to `FFFFFF` / `E8E8E8`. Palette variables (e.g. `$MUTED`) do NOT expand inside single-quoted heredocs — write the literal hex (`64748B`) in the JSON. + +--- + +## Key-slide recipes (10 essentials) + +The 10 slides every pitch deck carries. Each recipe below gives: **visual outcome** (what the slide looks like from 3m away) + **runnable block** (≤ 18 lines) + **QA one-liner**. All recipes inherit pptx v2 palettes, grid math, type hierarchy, and `--prop tailEnd=triangle` on every connector. Recipes reference the Slide Patterns above: Cover reuses C.1; Problem / Why-Now reuse C.2; Traction / Financials reuse C.4; Feature / pillar slides reuse C.5b. `$FILE` is your deck file. + +**Long-title wrap rule.** A 36pt+ title that wraps to 2 lines: add `height` (e.g. 2cm → 3.5cm) — never drop the font below 36pt. Titles < 36pt on a pitch deck read as timid regardless of content. + +> **Chart `series1.color=` on `add` works** (applies to every chart recipe below). Passing `--prop series1.color=` (or `series2.color=`, …) on chart `add` applies the series color and exits 0. Verify with a readback if you like: `officecli get "$FILE" "/slide[N]/chart[1]/series[1]" --json | jq '.data.results[0].format.color'`. + +### (1) Cover slide — company · tagline · round · date + +**Visual outcome.** Dark navy fill, centered 44pt company name, 20pt one-line tagline underneath, small 16pt meta line at the bottom with round + amount + date. Thin brand band at the very bottom (0.5cm high) in the accent color. + +```bash +officecli add "$FILE" / --type slide --prop layout=blank --prop background=1E2761 +officecli add "$FILE" "/slide[1]" --type shape --prop name=BrandBand \ + --prop geometry=rect --prop fill=CADCFC \ + --prop x=0cm --prop y=18.5cm --prop width=33.87cm --prop height=0.55cm +officecli add "$FILE" "/slide[1]" --type shape --prop name=CoverTitle --prop text="Acme DevOps" \ + --prop x=2cm --prop y=7cm --prop width=29.87cm --prop height=3cm \ + --prop font=Georgia --prop size=44 --prop bold=true --prop color=FFFFFF --prop align=center --prop fill=none +officecli add "$FILE" "/slide[1]" --type shape --prop name=Tagline --prop text="Kubernetes observability, built for production at scale" \ + --prop x=2cm --prop y=10.5cm --prop width=29.87cm --prop height=1.5cm \ + --prop font=Calibri --prop size=20 --prop color=CADCFC --prop align=center --prop fill=none +officecli add "$FILE" "/slide[1]" --type shape --prop name=CoverMeta --prop text='Series B · $35M · April 2026' \ + --prop x=2cm --prop y=15cm --prop width=29.87cm --prop height=1.2cm \ + --prop font=Calibri --prop size=16 --prop color=FFFFFF --prop align=center --prop fill=none +``` + +**QA.** Cover has 4 discrete elements (brand band + title + tagline + meta). 80%-whitespace covers fail the pptx "cover ≥ 60% filled" floor. + +**Consumer variant (3-second grab).** Consumer decks (B2C app / hardware / D2C) should add a single dominant motif — hero product shot, oversized company name (60–96pt), or symbolic mark (crescent moon / abstract geometric). Replace the 44pt title with an 80–96pt name + one motif shape (`--type shape --prop geometry=ellipse --prop fill=` for an abstract mark, or `picture` at ~40% of slide for a product hero). Keep tagline + round + date identical. SaaS / B2B may skip — the typographic-only cover is sufficient. + +### (2) Problem slide — industry pain in 1 sentence + 3 data cards + +**Visual outcome.** 36pt title stating the pain (not "The Problem"). Below, three equal-width data cards across the slide: each a giant number (40pt) + one-line qualifier (16pt) + source footnote (12pt gray). + +Grid math for 3 cards, 1.5cm margins, 0.76cm gap: `usable = 33.87 − 3 − 2·0.76 = 29.35`, `col_width = 29.35 / 3 = 9.78cm`. x-positions: `1.5 / 12.04 / 22.58`. + +```bash +SLIDE=2 # second slide, after cover. Adjust from your build order. +officecli add "$FILE" / --type slide --prop layout=blank --prop background=FFFFFF +officecli add "$FILE" "/slide[$SLIDE]" --type shape --prop text="Kubernetes debugging burns 12 engineering hours / incident" \ + --prop x=1.5cm --prop y=1.2cm --prop width=30.87cm --prop height=2.5cm \ + --prop font=Georgia --prop size=36 --prop bold=true --prop color=1E2761 --prop fill=none +cat < 1 hour to diagnose","x":"1.5cm","y":"9.5cm","width":"9.78cm","height":"3cm","font":"Calibri","size":"18","color":"333333","align":"center","fill":"none"}}, + {"command":"add","parent":"/slide[$SLIDE]","type":"shape","props":{"text":"Source: 2025 DORA Report","x":"1.5cm","y":"13cm","width":"9.78cm","height":"1cm","font":"Calibri","size":"12","italic":"true","color":"666666","align":"center","fill":"none"}} +] +EOF +# Repeat the 4-block pattern at x=12.04cm and x=22.58cm for cards 2 and 3. +``` + +**QA.** `officecli query "$FILE" 'shape:contains("Source")'` returns ≥ 3 (every claim carries a source). If zero sources, VCs will not trust a single number. + +### (2b) Why Now slide — Consumer / Seed / early A must-have + +**Visual outcome.** 3 cards across: each = **trigger headline** (24pt bold) + **data point** (60pt number or date) + **one-line implication** (16pt) + **source footnote** (12pt gray). Reuse Problem grid math (`col=9.78cm`, x = `1.5 / 12.04 / 22.58`). §赛道 Consumer row 2 must-have; Seed / early A in any vertical benefits when "market window" IS the thesis. + +```bash +SLIDE=3 +officecli add "$FILE" / --type slide --prop layout=blank --prop background=FFFFFF +officecli add "$FILE" "/slide[$SLIDE]" --type shape --prop text="Why now: three converging triggers" \ + --prop x=1.5cm --prop y=1.2cm --prop width=30.87cm --prop height=2.5cm \ + --prop font=Georgia --prop size=36 --prop bold=true --prop color=1E2761 --prop fill=none +# Card 1 (x=1.5cm) — trigger / data / implication / source. Repeat at x=12.04cm and x=22.58cm. +cat < 100% acceptable, > 115% strong, > 130% exceptional | +| **CAC** | $ fully-loaded | Sales + marketing spend / new logos acquired | +| **LTV** | $ | ARPU × gross margin × (1 / churn rate) | +| **LTV:CAC** | ratio | VC floor: 3x OK, > 4x strong, > 5x exceptional | +| **CAC payback** | months | VC floor: < 18 mo OK, < 12 mo strong | +| **Gross margin** | % | SaaS floor 70%, strong 80%+; marketplace 15-40%; hardware 30-50% | +| **Burn / runway** | $/month + months | Gross burn vs net burn — label which; runway to specific milestone | +| **Use of Funds** | 4-bucket pie | Engineering / Go-to-Market / G&A / Reserve — see Ask slide recipe | + +**Rule.** Every number on a deck carries a unit. `18%` or `18M` alone is ambiguous — write `$18M ARR` / `18% NRR growth`. `TBD`, `coming soon`, `(fill in)`, `lorem`, `xxxx` in numeric slots = immediate VC disqualification. Gate 6 greps these below. + +## VC ship-check (6 red flags / positive signals) + +What the VC reads in the first 30 seconds. Six one-line conditions — every "FAIL" below is an instant round-killer; fix before delivering. + +| # | Red flag (FAIL if present) | Positive signal (shipwise) | +|---|---|---| +| 1 | Cover without round + amount + date | `Company · tagline · Series X · $YM · Date` in 4 lines | +| 2 | TAM > $100B without a cited source / methodology | TAM clearly labeled bottom-up OR top-down with a visible 2024+ source | +| 3 | Traction chart y-axis does not start at 0 (hockey-stick lie) | Line chart `axismin=0`; growth shape honest | +| 4 | Team slide: headshots + names only, no prior companies | Every member: prior company + role + 1 achievement metric | +| 5 | Ask slide missing Use-of-Funds breakdown | `$XM` hero + 4-bucket pie (Eng / GTM / G&A / Reserve) + runway + next milestone | +| 6 | `TBD` / `lorem` / `xxxx` / `{{...}}` / `(fill in)` anywhere | `view text` clean — zero placeholder tokens | + +**Common Series-specific failures.** +- **Series A specific** — bottom-up TAM calculated from a fictional enterprise-count × ACV (no reference customers to anchor the count); `CAC / LTV` shown with < 12 months of data (statistically meaningless). +- **Series B specific** — no unit-econ slide at all; CAC payback > 24 months without a "we're pre-scale, here's the plan" narrative; logo wall < 8 customers. +- **Series C specific** — no moat / defensibility slide; revenue growth shown without margin trajectory; international expansion stated but no specific launch plan / hires. + +The Delivery Gate 6 block below executes checks 1–6 above via grep + query. Gate 5b fresh-eyes covers the visual judgments (hockey stick, team credibility) that grep can't see. + +## Traction triple-pattern (ARR + milestones + logos) + +For Series B+, traction often spans 2 slides: one for the chart + callout (recipe 7 above), one for **milestone timeline + logo wall**. Timeline = 4-6 horizontal dates with one-line events. Logo wall = 12-20 customer logos in a 4×N or 5×N grid, muted monochrome so no single brand dominates. + +```bash +# Milestone timeline: 5 dates as circles on a horizontal line at y=8cm. +# Use pptx shapes (ellipse preset) + connectors (shape=straight) between them. +# Each milestone = ellipse at y=8cm + date label above + event description below. +# → See pptx v2 Recipe (d) row 9 (Roadmap timeline) for the canonical pattern. + +# Logo wall: pictures in a 5×N grid. Typical spacing: logo width = 5cm, height = 2cm, gap = 0.4cm. +# grid math for 5 logos across, 1.5cm edge margin: usable = 33.87 − 3 − 4·0.4 = 29.27, col = 5.85cm +# (use 5cm logo width centered in each 5.85cm column) +``` + +**QA.** Logo wall should have ≥ 8 logos for Series B+, ≥ 4 for Series A. Fewer = "lighter than it looks"; more than 20 = pixel noise. + +## QA — Delivery Gate (executable) + +**Assume there are problems.** First render is almost never correct. Pitch decks fail at two layers: **structural** (schema, token leaks — caught by pptx v2 Gates 1–3) and **narrative** (wrong stage, missing unit econ, TAM unsourced — the checks that make pptx v2 Gate 5b + Gate 6 indispensable). Every check must print its success message. + +### Gates 1–5a — inherited from pptx v2 verbatim + +→ see pptx v2 §Delivery Gate L637-679. Copy-paste the full block: + +- **Gate 1** — `validate` schema check (whitelist `ChartShapeProperties` warnings per C-P-2). +- **Gate 2** — token leak via `view text` grep (`$xxx$`, `{{...}}`, ``, `lorem`, `xxxx`, empty `()`/`[]`, `\$`/`\t`/`\n` literals). +- **Gate 3** — hyperlink `rPr` schema trap (C-P-1) — zero ``. +- **Gate 4** — slide-order sanity — cover first, dividers before sections, closing last. +- **Gate 5a** — dark-on-dark contrast — every fill in `{1E2761, 0A1628, 8B1A1A, 2C5F2D, 36454F}` must declare near-white textColor. **This includes charts rendered on that fill**: chart `title.textColor`, `legend.textColor`, axis text default to dark and read as invisible on dark backgrounds — set them explicitly, or place the chart on a light card inside the dark slide. + +Do not skip or reorder these five. Every pptx-layer defect caught by Gates 1–5a also fires on pitch decks. + +**Gate 2b — pitch-specific shell-strip signatures (MANDATORY).** Gate 2 misses `$35M` that zsh silently stripped to empty (no residue to grep). Run this after Gate 2: + +```bash +# $XXM stripped by zsh leaves bare " M ARR" / " M raised" / "Series [A-C] · M" patterns. +STRIP=$(officecli view "$FILE" text | grep -niE '(^|[^A-Za-z0-9])M (ARR|raised|Series|runway|round|raise)|Series [A-C] · M( |$)|runway · M|raised · M|raising ·? M') +[ -z "$STRIP" ] && echo "Gate 2b OK (no \$-strip signatures)" || { echo "REJECT Gate 2b (likely zsh \$-strip — re-issue with single quotes):"; echo "$STRIP"; exit 1; } +``` + +Fix: re-issue the offending `add`/`set` with single quotes around the text value (`--prop text='Series B · $35M'`, not double quotes). The same strip hits **chart series names / axis titles** (`--prop name="营收 ($M)"` → legend shows `营收 ()`): single-quote every chart prop carrying `$`. + +### Gate 5b — Visual audit via HTML preview (MANDATORY, NOT optional) + +Gates 1–5a are token-grep defenses. **They cannot see a rendered slide.** This step is the only visual-assembly check. Do not skip. + +Run `officecli view "$FILE" html` and Read the returned HTML. Walk every slide and answer, for EACH (inherits pptx v2 Gate 5b checklist; pitch-specific additions marked ⭐): + +- **overlap**: do any text shapes overlap each other or a chart? +- **dark-on-dark**: is any text on a fill where fill brightness < 30% AND text brightness < 80%? +- **divider overlap**: any giant decorative number (01/02/03 at 100pt+) colliding with the divider title text? +- **order sanity**: does the slide sequence match your stage-appropriate narrative outline? +- **missing arrowheads**: do flowchart/decision-tree connectors show direction, or plain lines? +- ⭐ **traction y-axis**: does every ARR / revenue / growth line chart start at 0 on the y-axis? (Not 80% of current — that is the hockey-stick lie.) +- ⭐ **team credibility**: does every team-slide card show a prior company or prior title? (Cards with just headshot + name = reject.) +- ⭐ **TAM / market number credibility**: is the TAM under $100B for a niche market, or if ≥ $100B, is a methodology source cited? (A claimed `$500B TAM` with no source is an auto-reject red flag.) +- ⭐ **Use-of-Funds pie**: does the ask slide carry a 4-bucket pie (Engineering / GTM / G&A / Reserve) or a 4-card row with %s? +- ⭐ **narrative completeness**: is the order cover → problem → solution → market → product → model → traction → team → financials → ask, or your stage-appropriate permutation from §Stage diagnosis? + +**Instruction.** Run `officecli view "$FILE" html` and Read the HTML. Walk every slide against the questions below. If rendering chart colors, animations, or zoom — those only show in the target viewer (PowerPoint / Keynote / WPS); ask the user to open `.pptx` directly for those runtime features. + +> For every slide: +> (a) Are slides in VC narrative order (cover → problem → solution → market → product → model → traction → team → financials → ask, with your stage's adjustments)? Flag any out-of-sequence. +> (b) Is every ARR / revenue / growth line chart y-axis anchored at 0? Flag hockey-stick visual lies. +> (c) Does the team slide carry prior-company credentials for each person? (Not just headshot + name.) +> (d) Does every TAM / SAM / SOM claim have a visible source or methodology? +> (e) Does the ask slide have a 4-bucket Use of Funds (Engineering / GTM / G&A / Reserve) and a specific next milestone + runway length? +> (f) Any text overlap, dark-on-dark, off-slide geometry, missing arrowheads, placeholder tokens (`TBD` / `lorem` / `{{...}}` / `xxxx` / empty `()`)? + +Report every instance with slide number. If ANY defect — REJECT; do not deliver until fixed. + +**Human preview (optional).** If you want the user to visually preview the deck, run `officecli watch "$FILE"` for a live preview the user can open at their own discretion, or have them open the `.pptx` directly in PowerPoint / WPS / Keynote. For final visual verification, open the file in the target presentation viewer. + +### Gate 6 — Pitch narrative sanity (executable) + +Pitch-specific checks that grep the deck for VC red flags. Every one is a token check — combine with Gate 5b's human read for full coverage. + +```bash +FILE="deck.pptx" + +# 6.1 — no TBD / lorem / placeholder tokens (stronger than Gate 2 — pitch-specific scope) +LEAK=$(officecli view "$FILE" text | grep -niE 'TBD|lorem|\(fill in\)|xxxx|coming soon|placeholder') +[ -z "$LEAK" ] && echo "Gate 6.1 OK (no placeholder tokens)" || { echo "REJECT Gate 6.1:"; echo "$LEAK"; exit 1; } + +# 6.2 — TAM / SAM / SOM presence (Series A+) +TAM_HIT=$(officecli query "$FILE" 'shape:contains("TAM")' --json | jq '.data.results | length') +[ "$TAM_HIT" -ge 1 ] && echo "Gate 6.2 OK (TAM slide present)" || echo "WARN Gate 6.2: no TAM mention — confirm stage is Seed / Bridge if intentional" + +# 6.3 — Unit econ presence (Series B+): CAC OR LTV OR payback +CAC_HIT=$(officecli query "$FILE" 'shape:contains("CAC")' --json | jq '.data.results | length') +LTV_HIT=$(officecli query "$FILE" 'shape:contains("LTV")' --json | jq '.data.results | length') +if [ "$CAC_HIT" -ge 1 ] || [ "$LTV_HIT" -ge 1 ]; then + echo "Gate 6.3 OK (unit econ surface)" +else + echo "WARN Gate 6.3: no CAC / LTV — confirm stage Seed/A if intentional, REJECT if Series B+" +fi + +# 6.4 — Use of Funds present on ask slide +UOF_HIT=$(officecli query "$FILE" 'shape:contains("Use of Funds")' --json | jq '.data.results | length') +[ "$UOF_HIT" -ge 1 ] && echo "Gate 6.4 OK (Use of Funds)" || { echo "REJECT Gate 6.4: ask slide missing Use of Funds"; exit 1; } + +# 6.5 — Team prior-company signal (at least one of ex- / former / prior / previously) +PRIOR_HIT=$(officecli view "$FILE" text | grep -ciE '\b(ex-|former|prior|previously)\b') +[ "$PRIOR_HIT" -ge 1 ] && echo "Gate 6.5 OK (team prior-company)" || { echo "REJECT Gate 6.5: team slide has no prior-company credentials"; exit 1; } + +# 6.6 — Traction chart y-axis anchored at 0 (at least one chart must set axismin=0, Series A+) +AXISMIN_HIT=$(officecli query "$FILE" 'chart' --json | jq '[.data.results[]? | select(.format.axisMin == "0" or .format.axisMin == 0 or .format.axismin == "0" or .format.axismin == 0)] | length') +[ "$AXISMIN_HIT" -ge 1 ] && echo "Gate 6.6 OK (traction chart axisMin=0)" || echo "WARN Gate 6.6: no chart sets axisMin=0 — confirm no ARR/revenue line chart, or add --prop axismin=0" + +echo "Delivery Gate 6 PASS (token + narrative checks) — proceed to Gate 5b fresh-eyes (MANDATORY)" +``` + +**Readback key note.** CLI accepts lowercase `axismin` as input (on `--prop axismin=0`) but emits camelCase `axisMin` in `query --json` readback. The jq above accepts both for forward-compat. + +Gate 6 is a grep floor. Gate 5b is the visual ceiling. Ship only when both print PASS. + +### Honest limit + +`validate` catches schema errors, not fundraising errors. A deck passes `validate` with a `$500B TAM` on a $10M market, a team slide of four co-founders with no prior companies, a hockey stick y-axis at 80%, a pitch for a Series B round without unit econ, and an ask slide saying "we're raising some money". Gates 5b + 6 above exist because `validate` cannot catch any of this. + +## Known Issues & Pitfalls + +→ Base pitfalls (shell escape, `[last()]` in resident, connector `@name=` rejection C-P-6, picture alt two-step C-P-7, animation remove C-P-4, chart color normalization C-P-7): see pptx v2 §Known Issues & Pitfalls C-P-1..7. + +Pitch-specific: + +- **Stage misidentified.** Series A deck with 6 pages of CAC/LTV math = over-packaged. Series B deck missing unit econ = incomplete. If unsure, re-read §Stage diagnosis before building. +- **Hockey-stick y-axis.** If the line chart's y-axis doesn't start at 0, VCs read it as a visual lie within 2 seconds. Always `--prop axismin=0` on ARR / revenue / growth charts. Gate 6.6 checks this. +- **Team slide = portfolio.** Cards showing only {headshot + name + role} fail VC credibility. Every card needs a prior-company or prior-achievement line. Gate 6.5 checks this. +- **TAM without methodology.** A claimed number with no "top-down" or "bottom-up" source footnote = fabricated. Pick one methodology per deck; don't mix. +- **Use-of-Funds as 3-bucket or 5-bucket.** 4-bucket (Eng / GTM / G&A / Reserve) is convention; departing from it reads as sloppy. Gate 6.4 checks presence. +- **Pitch deck used for a board review / sales deck.** Narrative arc (problem → ask) makes board reviews awkward — route to pptx v2 Recipe (d) 10-slide instead. See §Reverse handoff above. +- **pptx v2 Recipe (d′) 20-slide is a starting point, not a formula.** It is stage-agnostic SaaS. Adjust for your stage + 赛道 via §Stage diagnosis and §赛道 arc templates — never ship (d′) unchanged for a non-SaaS Series A. + +## Help pointer + +When in doubt: `officecli help pptx`, `officecli help pptx `, `officecli help pptx --json`. Help is the authoritative schema; this skill is the decision guide for fundraising deltas on top of pptx v2. diff --git a/external/officecli/skills/officecli-pptx/SKILL.md b/external/officecli/skills/officecli-pptx/SKILL.md new file mode 100644 index 0000000..a33eac5 --- /dev/null +++ b/external/officecli/skills/officecli-pptx/SKILL.md @@ -0,0 +1,568 @@ +--- +name: officecli-pptx +description: "Use this skill any time a .pptx file is involved -- as input, output, or both. This includes: creating slide decks, pitch decks, or presentations; reading, parsing, or extracting text from any .pptx file; editing, modifying, or updating existing presentations; combining or splitting slide files; working with templates, layouts, speaker notes, or comments. Trigger whenever the user mentions 'deck', 'slides', 'presentation', 'pitch', or references a .pptx filename." +--- + +# OfficeCLI PPTX Skill + +## Setup + +If `officecli` is missing: + +- **macOS / Linux**: `curl -fsSL https://d.officecli.ai/install.sh | bash` +- **Windows (PowerShell)**: `irm https://d.officecli.ai/install.ps1 | iex` + +Verify with `officecli --version` (open a new terminal if PATH hasn't picked up). If install fails, download a binary from https://github.com/iOfficeAI/OfficeCLI/releases. + +## ⚠️ Help-First Rule + +**This skill teaches what good slides look like, not every command flag. When a property name, enum value, or alias is uncertain, consult help BEFORE guessing.** + +```bash +officecli help pptx # List all pptx elements +officecli help pptx # Full element schema (e.g. shape, chart, animation, connector, zoom, group) +officecli help pptx # Verb-scoped (e.g. add shape, set slide) +officecli help pptx --json # Machine-readable schema +``` + +Help reflects the installed CLI version. When skill and help disagree, **help is authoritative**. Triggers to run help immediately: `UNSUPPORTED props:` warning, unknown animation preset, `connector.shape=` enum drifts, prop-vs-alias (`lineWidth` vs `line.width`, `color` vs `font.color`). + +## Shell & Execution Discipline + +**Shell quoting (zsh / bash).** ALWAYS quote element paths (`"/slide[1]/..."`) — zsh globs unquoted `[1]` to `no matches found`. Escaping happens at three layers — keep them separate (the CLI handles the second for you): + +1. **Shell.** `$` in a value still belongs to the shell — single-quote the whole value: `--prop text='$15M'`. Double-quoted `"$15M"` gets expanded to `M`. The CLI does NOT unescape `\$` for you. +2. **CLI (`text=`).** The two-char escapes `\n` and `\t` ARE interpreted, consistently across pptx / docx / xlsx — `\n` is a line / paragraph break, `\t` is a tab. To produce a literal backslash-n in text, double it (`\\n`); this is rarely what you want. +3. **JSON (batch heredoc).** The recipes below pipe `cat <`, `lorem`, `xxxx`, empty `()`/`[]` in chart titles never appear. +- **No overflow off-edge, no clipped text in shapes.** `view issues` flags both (`shape_off_slide` + a text-fit hint). To fix a clip: grow the box or shorten the value — never trim content to fit. +- **Cover carries its orienting elements.** Title + subtitle + presenter/client + date + a brand band or key-takeaway strap — a title-only cover reads as a stub. Generous whitespace around them is still right; rich ≠ crowded. +- **Contrast.** `view issues` auto-flags the common case — opaque dark text on a shape's own dark fill (`low_contrast`). It can't see the rest: icon / chart-series fills, scheme/inherited colors, or text over a *separate* background shape. So on any fill with brightness < 30% (`1E2761`, `36454F`, deep forest / berry / cherry), still confirm every body run, card body, chart series, and icon is `FFFFFF` or brightness > 80% — mid-gray (`6B7B8D` ≈ 44%) reads on a laptop and vanishes on projection. Spot-check via `view html` after the dark-fill pass. + +If any fails, STOP and fix before declaring done. + +## Design Principles + +A deck is not a document. The audience has 3 seconds to get each slide. Before adding anything, ask: "If the audience reads only the biggest element and glances once, do they get the point?" If they have to read the bullets, the biggest element is wrong. + +### Grid, margins, negative space + +Standard widescreen is **33.87 × 19.05cm**. Treat it as a 12-column grid internally: + +- **Edge margin ≥ 1.27cm** (0.5") on all sides. +- **Inter-block gap ≥ 0.76cm** (0.3") between cards / columns / rows — pick one value (0.76 or 1.27cm) and use it everywhere; mixed gaps look unfinished. +- **≥ 20% negative space per slide.** Filling every pixel reads as amateur. +- **Compose, don't web-center.** Whitespace is structural: a slide top-weighted with open space in the lower third is correct composition, not an empty defect. Intentional asymmetry (content left, breathing room right) reads more designed than centering everything — don't fill a gap just because it's there. +- For card grids: `usable = 33.87 − 2·margin − (N−1)·gap`, then `col_width = usable / N`. Don't hand-pick x coordinates. + +### Font pairings + +Pair by document register, not by novelty. "Best For" is a prompt, not a decree; a pairing outside this table is fine if it fits — these 8 are seeds, not the set. + +| Header | Body | Best For | +|---|---|---| +| Georgia | Calibri | Formal business, finance, executive reports | +| Arial Black | Arial | Bold marketing, product launches | +| Calibri | Calibri Light | Clean corporate, minimal design | +| Cambria | Calibri | Traditional professional, legal, academic | +| Trebuchet MS | Calibri | Friendly tech, startups, SaaS | +| Impact | Arial | Bold headlines, event decks, keynotes | +| Palatino | Garamond | Elegant editorial, luxury, nonprofit | +| Consolas | Calibri | Developer tools, technical / engineering | + +Set both fonts explicitly on every shape (`--prop font=Georgia` on titles, `--prop font=Calibri` on body), not via theme inheritance. + +### Color and contrast + +The columns: **Primary** (dominant — 60–70% of weight, the color you see first), **Secondary** (supporting tone), **Accent** (sparing, one-hit emphasis), **Text** (body on light fills), **Muted** (captions / axis labels / footer). + +| Theme | Primary | Secondary | Accent | Text | Muted | +|---|---|---|---|---|---| +| Coral Energy | `F96167` | `F9E795` | `2F3C7E` | `333333` | `8B7E6A` | +| Midnight Executive | `1E2761` | `CADCFC` | `FFFFFF` | `333333` | `8899BB` | +| Forest & Moss | `2C5F2D` | `97BC62` | `F5F5F5` | `2D2D2D` | `6B8E6B` | +| Charcoal Minimal | `36454F` | `F2F2F2` | `212121` | `333333` | `7A8A94` | +| Warm Terracotta | `B85042` | `E7E8D1` | `A7BEAE` | `3D2B2B` | `8C7B75` | +| Berry & Cream | `6D2E46` | `A26769` | `ECE2D0` | `3D2233` | `8C6B7A` | +| Ocean Gradient | `065A82` | `1C7293` | `21295C` | `2B3A4E` | `6B8FAA` | +| Teal Trust | `028090` | `00A896` | `02C39A` | `2D3B3B` | `5E8C8C` | +| Sage Calm | `84B59F` | `69A297` | `50808E` | `2D3D35` | `7A9488` | +| Cherry Bold | `990011` | `FCF6F5` | `2F3C7E` | `333333` | `8B6B6B` | + +Pick by topic, not by default — finance reads Midnight Executive, a product launch reads Coral Energy, safety / LOTO reads Cherry Bold. If the closest named theme is not quite right, blend (e.g. Forest primary + gold `D4A843` accent). Use **Text** on light fills, **Muted** for captions / axis / footer, `FFFFFF` or Secondary for body on dark fills. + +On dark backgrounds, text and chart series follow the Hard rules contrast floor above. + +### Chart-choice decision table + +Wrong chart type kills the 3-second test: + +| Data shape | Use | Avoid | +|---|---|---| +| Category comparison (A vs B vs C) | `column` (vertical) / `bar` (≥ 6 categories, horizontal) | pie (slices merge), line (no time axis) | +| Time series, 1–3 series | `line` | area (occlusion), bar (implies discrete) | +| Part-of-whole, 2–5 slices | `pie` / `doughnut` | pie with 8+ slices (unreadable) | +| Correlation / distribution | `scatter` | line (implies ordering) | +| Multiple categories × metrics, dense | stacked `column` or heatmap | one chart per metric — consolidate | +| KPI snapshot (single big number) | **Large-text shape** (60–72pt + ≤ 5-word sublabel), NOT a chart | gauge chart, tiny bar | + +Rule of thumb: if > 3 series and > 8 categories, split into two charts or switch to a table. + +### Animation + +Use as much or as little as the brand and content call for — a formal finance deck trends to near-zero, a product launch can be more expressive. Animation is a tool, not décor. Three floors keep it from hurting the deck (none caps how much you use): + +- **Purposeful** — each one reveals or emphasizes (progressive bullet reveal, a build-up chart), never decorates. If it doesn't aid comprehension, cut it. +- **Degrades gracefully** — pptx animation renders inconsistently across viewers (Keynote / Slides / web / mobile) and may not play at all, so every slide must read correctly as a *static* frame. Never hide essential content behind a reveal. +- **Verify live** — animation is runtime-only; `view html` and screenshots can't see it, so confirm in a real presentation viewer before shipping. + +Taste steer (not a ban): `fade` / `appear` / a single `zoom-entrance` with snappy durations (~hundreds of ms) fit most decks; `bounce` / `swivel` / `spin` / `fly-from-edge` / dense multi-object choreography usually read amateur — reach for them only when the brand is deliberately playful. + +### Layout patterns & data display + +Vary layout across slides — repeating the same pattern makes every slide feel identical. These are common building blocks, not the full set — pick one per slide, or build a layout outside the table when the content calls for it: + +| Pattern | When to use | Key measurement | +|---|---|---| +| **Two-column** (text left, visual right) | Concept + evidence; feature + screenshot | Each col ≈ 14-15cm; gap 1cm | +| **Icon rows** (icon in filled circle + bold header + description) | Feature lists, benefits, team roles | Icon circle 1.5-2cm; 3-4 rows max | +| **2×2 or 2×3 grid** (card tiles) | Quadrant analysis, SWOT, option comparison | Gap ≥ 0.76cm; consistent card height | +| **Half-bleed image** (full left or right half, content overlay on other side) | Hero moments, case study openers | Image 16-17cm wide; content column ≥ 14cm | +| **Large stat callout** (60-72pt number + ≤5-word sublabel below) | Single KPI, milestone, market size | Use shape, NOT a chart; sublabel 14-16pt muted | + +**Data display quick rules:** +- Comparison columns (before/after, A vs B) beat a table for 2-3 options. +- Timelines and process flows: numbered step shapes + connectors, not a bullet list. + +### Image treatment (only when a slide uses a photo / screenshot / logo) + +**Read the image first** (open the file) and choose treatment from what you see — don't place blind from a filename. + +- **Full-bleed photo** → size to COVER the region (crop edges), no border. +- **Screenshot / diagram / logo** → size to FIT (never crop content). A transparent or fit image sits on a contrasting fill — drop a colored rectangle behind it, don't let it float on white. +- **Text over a photo** → never raw on the image. Put it on a card, or lay a protection scrim between image and text (a dark rectangle at ~50–60% opacity, or a gradient fading from the text edge). +- Never stretch (distort the aspect ratio); don't overlay text on a busy screenshot. +- Prefer user-provided images / brand assets; no emoji or self-drawn art unless asked. + +### Visual motif commitment + +Pick ONE distinctive element (rounded image frames, section numbers in filled circles, single-side border band, diagonal accent strips) and carry it to every slide — commit across the whole deck; styling one slide and leaving the rest plain reads as abandoned. A secondary motif is fine only if it doesn't compete with the primary. Declare it in your build plan first: `## Motif: numbered circles in brand color`. + +### Visual AI-tells to avoid + +- **No decorative underline under slide titles.** A stripe / rule below a heading is the single most common AI-slide tell — use whitespace or a background-color change instead. +- **No rounded-corner card with a colored left-border accent stripe.** The other classic AI-slide tell — use a solid fill, a top accent band, or whitespace separation instead. +- **No emoji as iconography** unless the brand uses them — use a shape or a real icon asset. + +Copy-level tells live in "Copy reads human". + +## Common Workflow + +1. **Open/save lifecycle.** `officecli open ` at the start, `officecli save ` at the end to flush your edits to disk. `save` only writes — it leaves the resident warm for any follow-up edit; reach for `officecli close ` only when you want to release the resident immediately (a one-shot handoff). Both are always safe — they never error or lose work. Use `batch` for repetitive shape grids. **Flush only at the non-officecli boundary:** officecli's own reads always see your edits; run `save`/`close` only before a non-officecli program reads the file (python-pptx, PowerPoint, a renderer, delivery). +2. **Orient.** New deck: `officecli create "$FILE"`. Existing: `officecli view "$FILE" outline` first. Never edit blind. +3. **Title sequence first (plan, don't build yet).** Before creating any slide or shape, write out the full ordered list of slide titles. If someone reading ONLY the titles can't follow the argument, fix the arc now — cheaper in a list than after 14 slides. Pick ONE title grammar — all topic noun-phrases or all action statements, never a mix — and hold it throughout (see "Copy reads human"). +4. **Build in display order.** Add slides in audience-view order: cover → agenda → section-1 divider → section-1 content → section-2 divider → … → closing. `--index` on slide add works, but linear append keeps the build script readable and avoids index-arithmetic bugs. **Before final delivery, confirm slide count + narrative arc match your build plan.** Gate 3's order-sanity check catches cases where the cover ends up as slide 11 of 14 instead of slide 1. +5. **Incremental per slide.** Create slide + background, then title, then supporting shapes / charts / connectors. Always `layout=blank` for custom designs. After each structural op, `get /slide[N] --depth 1` to confirm shape IDs. +6. **Format to spec.** Per the Requirements table; formatting is deliverable, not polish. +7. **Save + verify.** `officecli save` flushes the file to disk (or `officecli close` to flush and also end the session). Always open in the target presentation viewer before shipping — chart colors, animations, fonts, and zoom are runtime features `view html` can't render. Full verification in QA below. +8. **QA — assume there are problems.** Fix-and-verify until a cycle finds zero new issues. + +## Quick Start + +Minimal viable deck: cover + one content slide + notes. `$FILE` stands in for your filename. + +```bash +FILE="deck.pptx" +officecli create "$FILE" +officecli open "$FILE" + +# Cover — dark fill, centered title +officecli add "$FILE" / --type slide --prop layout=blank --prop background=1E2761 +officecli add "$FILE" /slide[1] --type shape --prop text="FY26 Strategic Review" \ + --prop x=2cm --prop y=7cm --prop width=29.87cm --prop height=3cm \ + --prop font=Georgia --prop size=44 --prop bold=true --prop color=FFFFFF --prop align=center + +# Content — white fill, title + body + notes +officecli add "$FILE" / --type slide --prop layout=blank --prop background=FFFFFF +officecli add "$FILE" /slide[2] --type shape --prop text="Revenue grew 18% YoY" \ + --prop x=1.5cm --prop y=1.2cm --prop width=30cm --prop height=2cm \ + --prop font=Georgia --prop size=36 --prop bold=true --prop color=1E2761 +officecli add "$FILE" /slide[2] --type shape --prop text="Enterprise renewals + new EMEA region drove the beat; NRR held at 118%." \ + --prop x=1.5cm --prop y=4cm --prop width=30cm --prop height=3cm \ + --prop font=Calibri --prop size=20 --prop color=333333 +officecli add "$FILE" /slide[2] --type notes --prop text="Lead with the 18% beat, preview EMEA." + +officecli save "$FILE" +officecli validate "$FILE" +``` + +Shape of every build: open → slide+background → title → body → notes → save → validate. + +## Reading & Analysis + +Start wide, then narrow. `outline` first, `view text` / `get` / `query` once you know where to look. + +```bash +officecli view "$FILE" outline # slide count + titles +officecli view "$FILE" annotated # complete per-slide breakdown with fonts, sizes, tables, charts +officecli view "$FILE" text --start 1 --end 5 # text dump (includes table cell text) +officecli view "$FILE" issues # empty slides, overflow hints +officecli view "$FILE" stats # counts + totals (incl. pictures missing alt) +``` + +**Inspect one element.** XPath-style paths, 1-based. ALWAYS quote. Prefer `@name=` / `@id=` selectors over positional `[N]` (stable across reorderings). `[last()]` works. Add `--json` for machine output. + +```bash +officecli get "$FILE" "/slide[1]" --depth 1 # shape list with IDs and names +officecli get "$FILE" "/slide[1]/shape[@name=Title]" +officecli get "$FILE" "/slide[1]/table[1]" --depth 3 # table rows / cells +``` + +**Query across the deck.** CSS-like selectors; operators `=`, `!=`, `~=`, `>=`, `<=`, `[attr]`, `:contains()`, `:no-alt`. `help pptx query` lists queryable element types. + +```bash +officecli query "$FILE" 'shape:contains("Revenue")' +officecli query "$FILE" 'picture:no-alt' # accessibility gap +officecli query "$FILE" 'shape[fill=1E2761]' # color match +officecli query "$FILE" 'shape[width>=10cm]' # numeric +``` + +**`query --json` output schema.** Results wrap in `.data.results[]` — `jq -r '.data.results[0].format.id'`, NOT `.[0].id`. Shape name is `.name`; fill is `.format.fill`; textColor is `.format.textColor`. + +**Visual preview (LEAD).** + +```bash +officecli view "$FILE" html # prints an HTML preview path; Read it for per-slide visual audit (best structural ground truth) +officecli view "$FILE" svg --start 3 --end 3 # single slide SVG (charts + gradients do NOT render in SVG) +``` + +**Reading the output — an expected non-defect:** +- **`layout=blank` has no title placeholder.** Titles are plain `shape` elements, so `view outline` reporting `(untitled)` is **expected**, not a defect. Use `layout=title` + `placeholder[title]` only when screen-reader outline compatibility matters. + +## Creating & Editing + +Verbs: `add` / `set` / `remove` / `move` / `swap` / `batch` / `raw-set`. Ninety percent of a deck is slides, shapes, text, a few charts, pictures, connectors. + +### Slides and backgrounds + +A slide is `/slide[N]`. Always pass `layout=blank` for custom designs. Background: solid, gradient, or image. + +```bash +officecli add "$FILE" / --type slide --prop layout=blank --prop background=1E2761 # solid +officecli add "$FILE" / --type slide --prop layout=blank --prop "background=1E2761-CADCFC-180" # gradient (start-end-angle) +officecli add "$FILE" / --type slide --prop layout=blank --prop background=image:/path/to/hero.jpg # image background (LEAD) +``` + +### Shapes + +A `shape` holds text, fill, border, position, and optional animation / link. + +```bash +officecli add "$FILE" /slide[2] --type shape --prop name=Title --prop text="Key Insight" \ + --prop x=2cm --prop y=2cm --prop width=20cm --prop height=3cm \ + --prop font=Georgia --prop size=36 --prop bold=true --prop color=1E2761 --prop fill=none +``` + +Positioning is explicit — no layout engine, you own the grid math. `--prop preset=` picks geometry (`rect`, `roundRect`, `ellipse`, `triangle`, `arrow`, `star5`, ...); custom `M...Z` paths are not supported — pick a preset. **Name shapes at creation** (`--prop name=HeroTitle`) and address later with `"/slide[N]/shape[@name=HeroTitle]"` — names survive z-order / remove-then-add, whereas positional `/shape[3]` (and even `@id=`) shift. Re-`get --depth 1` after any structural change before using positional indexes. + +### Text inside shapes (paragraphs, runs, styling) + +A shape has paragraphs (`paragraph[K]`) and runs (`run[K]`). For one-line text, `--prop text=` on the shape is enough; a `\n` in the text makes a paragraph break, `\t` a tab (see Shell & Execution Discipline; double `\\n` for a literal). `add --type paragraph` takes the same style props as a shape (text, align, bold, italic, size, color, font). For mixed styling *within* a line, append a styled run: + +```bash +officecli add "$FILE" "/slide[2]/shape[@name=Card1]/paragraph[1]" --type run \ + --prop text=" (inline detail)" --prop size=14 --prop italic=true --prop color=8899BB +``` + +### Charts + +Pick chart type per the Design Principles chart-choice table. Full prop list (chartType enum, `seriesN.*`, `data=`/`categories=`, axis options): `help pptx add chart`. Typical multi-series with brand colors: + +```bash +officecli add "$FILE" /slide[3] --type chart --prop chartType=column \ + --prop series1.name=Revenue --prop series1.values="42,45,48" --prop series1.color=1E2761 \ + --prop series2.name=Growth --prop series2.values="2,7,7" --prop series2.color=CADCFC \ + --prop categories="Q1,Q2,Q3" \ + --prop x=2cm --prop y=4cm --prop width=20cm --prop height=10cm +``` + +Gotchas: (1) chart titles with `()`, `[]`, `TBD` ship as literal text. (2) some viewers normalize chart colors to theme defaults — verify in the target viewer. Series can be added after creation (`add --type series`). + +### Pictures + +```bash +officecli add "$FILE" /slide[4] --type picture --prop src=hero.jpg \ + --prop x=1cm --prop y=1cm --prop width=32cm --prop height=18cm \ + --prop alt="Product hero, gradient lit from right" +``` + +Confirm with `officecli query "$FILE" 'picture:no-alt'` — must be empty before delivery. + +### Connectors (LEAD — flowcharts / decision trees first-class) + +Draws a line between two shapes or free coordinates. Full prop / enum reference (`shape`, `headEnd`/`tailEnd` values, `from`/`to` ref forms): `help pptx add connector`. + +```bash +officecli add "$FILE" /slide[5] --type connector \ + --prop "from=/slide[5]/shape[@name=BoxA]" --prop "to=/slide[5]/shape[@name=BoxB]" \ + --prop shape=elbow --prop color=333333 --prop tailEnd=triangle +``` + +**Every flow connector needs an arrowhead.** Without one, `bentConnector3` renders as a directionless line. `preset=rightArrow` overlay only works for horizontal flows; diamonds / decision trees with diverging edges need `tailEnd=`. + +### Animations (LEAD) + +Use per the Animation floors above (purposeful, degrades gracefully, verify live). Preset names + duration syntax: `help pptx animation`. + +```bash +officecli set "$FILE" "/slide[2]/shape[@name=HeroCard]" --prop animation=fade-entrance-400 +officecli set "$FILE" "/slide[2]/shape[@name=HeroCard]" --prop animation=none # clear all +``` + +### Hyperlinks, tooltips, slide-jump + +`--prop link=slide[N]` for an in-deck jump (1-based; target slide must exist), `link=nextslide` / `firstslide` / `lastslide` / `previousslide` / `endshow` for named navigation, `link=https://...` for a URL, `--prop tooltip="..."` for hover text. + +### Tables, placeholders, groups, zoom — one-liners + +- **Tables** — `--type table --prop rows=N --prop cols=M`. Row-level `set` supports `height` and `c1/c2/c3` (seed cell text). Header-row styling is table-level (`firstRow=true` / `headerFill=`), not a row prop. Cell formatting lives on the cell paragraph / run. Populate rows BEFORE setting table-level font (font cascade gets reset by row ops). +- **Placeholders** — `"/slide[N]/placeholder[title]"` / `placeholder[body]`. Available only when the slide uses a layout with placeholders (not `layout=blank`). +- **Groups** (LEAD) — address children via `"/slide[N]/group[@name=G]/shape[1]"`. Survives reordering better than positional indexes. +- **Zoom slide** (LEAD) — `--type zoom --prop target=N` (one link per target; alias `slide`). Emit N separate zoom shapes for a multi-target nav hub. Zoom is a runtime feature — `view html` shows the static geometry; the zoom interaction runs only in a live presentation viewer. +- **Slide comments** — reviewer annotations anchored at `/slide[N]/comment[M]`. Full lifecycle (`add / set / get / query / remove`). Props: `text`, `author`, `initials` (auto-derived), `date` (ISO 8601, defaults to UtcNow), `x` / `y` (length anchor). + ```bash + officecli add "$FILE" "/slide[2]" --type comment --prop author="Alice" --prop text="Tighten this bullet" --prop x=20cm --prop y=3cm + officecli query "$FILE" 'comment' --json | jq '.data.results | length' # count all review comments + officecli remove "$FILE" "/slide[2]/comment[1]" # resolve after addressing + ``` + +### Deck-level recipes + +Patterns not obvious from the primitives. Each gives the **visual outcome** first, then a runnable block. `$FILE` = your filename. Use `/slide[last()]` to address the slide you just added. The recipes demonstrate **structure and coordinate math** — swap in the palette / fonts you chose for this topic; the navy `1E2761` + Georgia is just the example's theme, not a house style to copy verbatim. + +**Z-order.** Later-added shapes are on top. Add background decoration FIRST, titles LAST. To fix after the fact: `--prop zorder=back/front` (renumbers siblings — re-`get --depth 1` before stacking more). + +#### (a) Cover (and section divider) + +**Visual outcome.** Dark navy fill, centered 44pt title, 18pt ice-blue meta line. + +```bash +officecli add "$FILE" / --type slide --prop layout=blank --prop background=1E2761 +officecli add "$FILE" "/slide[last()]" --type shape --prop text="Strategic Growth Review" \ + --prop x=2cm --prop y=7cm --prop width=29.87cm --prop height=3cm \ + --prop font=Georgia --prop size=44 --prop bold=true --prop color=FFFFFF --prop align=center +officecli add "$FILE" "/slide[last()]" --type shape --prop text="Prepared for Acme Leadership — FY26 Outlook" \ + --prop x=2cm --prop y=11cm --prop width=29.87cm --prop height=1.2cm \ + --prop font=Calibri --prop size=18 --prop color=CADCFC --prop align=center +``` + +**Section divider** = same cover, plus a giant translucent number (`size=120`, `opacity=0.15`) added FIRST so it sits behind the section title. + +#### (b) Data slide (chart + commentary block) + +**Visual outcome.** Left two-thirds: column chart with brand series colors. Right one-third: "Key Insight" card with 20pt heading + 18pt body — audience reads the takeaway before parsing the bars. + +```bash +officecli add "$FILE" / --type slide --prop layout=blank --prop background=FFFFFF +officecli add "$FILE" "/slide[last()]" --type shape --prop text="FY26 Revenue Beat Plan by 18%" \ + --prop x=1.5cm --prop y=1cm --prop width=30cm --prop height=1.8cm \ + --prop font=Georgia --prop size=36 --prop bold=true --prop color=1E2761 + +# Chart — left 2/3 (single-quote the title because of `$`) +officecli add "$FILE" "/slide[last()]" --type chart --prop chartType=column \ + --prop series1.name=Actual --prop series1.values="42,45,48,55" --prop series1.color=1E2761 \ + --prop series2.name=Plan --prop series2.values="40,42,45,48" --prop series2.color=CADCFC \ + --prop categories="Q1,Q2,Q3,Q4" --prop x=1.5cm --prop y=3.5cm --prop width=20cm --prop height=14cm --prop title='FY26 Revenue ($M)' + +# Commentary card — right 1/3: background + heading + body +officecli add "$FILE" "/slide[last()]" --type shape --prop preset=roundRect --prop fill=F5F7FA --prop line=none \ + --prop x=22.5cm --prop y=3.5cm --prop width=9.8cm --prop height=14cm +officecli add "$FILE" "/slide[last()]" --type shape --prop text="Key Insight" \ + --prop x=23cm --prop y=4cm --prop width=9cm --prop height=1.2cm \ + --prop font=Georgia --prop size=20 --prop bold=true --prop color=1E2761 +officecli add "$FILE" "/slide[last()]" --type shape --prop text="EMEA launch + NRR at 118% drove 12pp of the 18pp beat." \ + --prop x=23cm --prop y=5.5cm --prop width=9cm --prop height=11cm \ + --prop font=Calibri --prop size=18 --prop color=333333 +``` + +#### (c) Flowchart / process diagram (boxes + connectors) + +**Visual outcome.** Four rounded boxes across at y=8cm, each 6×3cm, alternating navy/iceblue, joined by elbow connectors with triangle arrowheads. + +Grid math (4 boxes, 33.87cm slide, 1.5cm margins): `gap = (33.87 − 3 − 24) / 3 = 2.29cm`. x-positions: `1.5, 9.79, 18.08, 26.37`. + +Each box carries its own label via `valign=middle` (no separate overlay shape needed). Use `batch` heredoc for portable coordinate arithmetic — no `bc`, no bash arrays. + +```bash +cat <"`. Any schema error → REJECT and fix. +- **Gate 2 — overflow / format / structure.** `officecli view "" issues`. If it lists *any* issue (lines tagged `[O1]`, `[C1]`, `[S1]`, …) → REJECT, fix, re-run until clean. +- **Gate 2b — leftover placeholders.** `officecli view "" text`, then scan the output for `xxxx`, `lorem` / `ipsum`, ``, `placeholder`, "this slide layout", or empty `()` / `[]`. Any hit → REJECT. + +### Gate 3 — Visual audit (MANDATORY) + +Pick **one** path: + +**Screenshot (default)** — for vision-capable agents. Screenshot each slide in turn — `officecli view "" screenshot --page 1 -o slide1.png`, then `--page 2`, … — until the page index runs past the deck (one screenshot = one slide). If it errors on page 1, use the fallback below. + +**Judge every PNG against the checklist, adversarially** — "assume problems exist; finding none means you didn't look hard enough." Report one `slide N: ` line per problem, or `PASS`. This step is required however you run it. **If** your harness can spawn a subagent, delegate the judging to a *fresh, independent* one — the agent that built the deck is biased toward "looks fine", a separate pair of eyes is more critical — handing it the screenshots + this checklist and the same adversarial framing. No subagent? Do exactly the same yourself. + +**Fallback — HTML-text** (no vision, or screenshot failed): read `view "$FILE" html` as text. DOM cannot prove **dark-on-dark / fine overlap / arrowheads / gap-margin metrics / column alignment** — flag these as "not visually verified" rather than PASS. + +**Optional `--grid N`** — only on user request for layout-rhythm, or when `view outline` shows anomalous layout distribution: `officecli view "" screenshot --grid 3 -o grid.png`. + +**Per-slide checklist (assume issues exist):** + +- **overlap** — shapes / charts / giant decorative numbers (01/02/03 100pt+) colliding +- **text overflow** — clipped at slide or shape boundary (KPI cards, narrow boxes) +- **narrow text box** — content fits technically but wraps to many short lines (1–2 words each); long sublabel in a 3cm KPI card, body line in a too-tight column +- **dark-on-dark** — fill brightness < 30% with text/icon brightness < 80% (incl. dark icons on dark without a contrasting circle) +- **image treatment** — photo stretched/distorted, text raw on a busy image (no card/scrim), screenshot or logo cropped, transparent image floating on white +- **missing arrowheads** — flowchart connectors as plain lines +- **decorative-line / title mismatch** — accent bar sized for one-line title but title wrapped to two (or vice versa) +- **footer / citation collision** — source line, page number, or footnote touching content above +- **tight margin / gap** — element within ~0.5" of slide edge, or two cards within ~0.3" +- **uneven gaps** — large empty area on one side, cramped on another (broken rhythm) +- **column / repeat-element misalignment** — KPI cards / icons off baseline or inconsistent width +- **order sanity** — sequence matches narrative (cover → agenda → dividers-before-sections → closing) + +REJECT with `slide N: ` lines, else "Gate 3 PASS" (HTML-text fallback adds " not visually verified"). + +**Fix-verify (mandatory, max 3 cycles).** Fix → re-run Gate 3 → repeat until zero new issues; one fix often surfaces another. After 3 rounds without convergence, **stop** — likely seesaw, template-level cause, or agent misread. Report `slide N: — attempted: — likely root: ` and let the user decide. + +**Then flush (part of the gate).** Once Gate 3 converges, end with `officecli save ""` — this guarantees your edits are written to disk before delivery (use `officecli close ""` instead to also release the resident on a one-shot handoff). Required final step, not optional. Always safe: never errors or loses work. + +## Common Pitfalls + +Sanity-check cheatsheet — what breaks on the first try. Design + shell traps. + +| Pitfall | Correct approach | +|---|---| +| Unquoted `[N]` in zsh/bash | Always quote paths: `"/slide[1]"`. zsh globs unquoted `[1]` → `no matches found` — #1 first-use stumble | +| `--name "foo"` | All attributes go through `--prop`: `--prop name="foo"` | +| `/shape[myname]` (bare name in brackets) | Use `@name=` selector: `/shape[@name=myname]` or `/shape[@id=10007]` | +| Paths 1-based vs `--index` 0-based | `/slide[1]` = first slide; `--index 0` = first position | +| `$` in `--prop text=` | Single-quote: `--prop text='$15M'`. Double-quoted `"$15M"` gets shell-expanded to `M` | +| `\n` / `\t` in `--prop text=` | Interpreted by the CLI: `\n` = paragraph break, `\t` = tab. Double `\\n` for a literal | diff --git a/external/officecli/skills/officecli-word-form/SKILL.md b/external/officecli/skills/officecli-word-form/SKILL.md new file mode 100644 index 0000000..b2fcabd --- /dev/null +++ b/external/officecli/skills/officecli-word-form/SKILL.md @@ -0,0 +1,676 @@ +--- +name: officecli-word-form +description: "Use this skill to create fillable Word forms (.docx) with real Content Controls (SDT) + legacy FormField checkboxes + MERGEFIELD mail-merge placeholders + document protection. Trigger on: 'fillable form', 'form fields', 'content controls', 'SDT', 'word form', 'fill in', 'only editable fields', 'protect document', 'onboarding form', 'HR intake', 'survey template', 'contract / SOW template', 'mail-merge template', 'compliance checklist', 'medical intake questionnaire'. Output is a single .docx where specific fields are editable and the rest is locked. This skill is INDEPENDENT, not a scene layer on docx — payload is `w:sdt` + `w:ffData` + `w:fldChar` + `documentProtection`, none of which docx base skill covers. Do NOT trigger for regular reports, letters, memos, academic papers, pitch decks, or any document with no user-fillable fields — route those to officecli-docx or its scene layers." +--- + +# OfficeCLI Word-Form Skill + +**This skill is INDEPENDENT, not a scene layer on docx.** A form's payload — `` controls, `` legacy fields, `` mail-merge, `documentProtection` — is a distinct element class from docx's paragraph/heading/style primitives. Its QA is different too: docx's Delivery Gate cares about visual layout and live PAGE fields, this skill's cares about data plumbing (protection enforced / alias+tag / items injected / name ≤ 20 / no underscore anti-pattern). **Reverse handoff:** if the user's document has no fillable fields (report, letter, memo, thesis, proposal), route to `officecli-docx` or a docx scene skill — don't use this one. + +## BEFORE YOU START (CRITICAL) + +**If `officecli` is not installed:** + +`macOS / Linux` + +```bash +if ! command -v officecli >/dev/null 2>&1; then + curl -fsSL https://d.officecli.ai/install.sh | bash +fi +``` + +`Windows (PowerShell)` + +```powershell +if (-not (Get-Command officecli -ErrorAction SilentlyContinue)) { + irm https://d.officecli.ai/install.ps1 | iex +} +``` + +Verify: `officecli --version` + +If `officecli` is still not found after first install, open a new terminal and run the verify command again. + +If the install command above fails (e.g. blocked by security policy, no network access, or insufficient permissions), install manually — download the binary for your platform from https://github.com/iOfficeAI/OfficeCLI/releases — then re-run the verify command. + +## Help-First Rule + +This skill teaches what a real form needs, not every CLI flag. When a prop / alias / enum is uncertain, consult help BEFORE guessing: `officecli help docx [element] [--json]` (e.g. `sdt`, `formfield`, `field`). Help is pinned to the installed CLI version and is authoritative — when this skill and help disagree, **help wins** (the prop set on `sdt` in particular has grown over time; trust `help docx sdt`, not a hardcoded list). + +## Mental Model & Inheritance + +A Word form is a `.docx` plus four OpenXML payload layers plain-docx skills do not touch: **``** content controls (types: text / richtext / dropdown / combobox / date / picture / group), **``** legacy FormField (still the only way to get a real checkbox — SDT `type=checkbox` is not implemented), **``** complex fields (MERGEFIELD, REF, PAGEREF, SEQ, IF — template-time, not user-fill), and **`documentProtection`** (the lock that makes non-field text read-only in Word — and, on the CLI, `protection=forms` locks non-field content edits (those need `--force` or `raw-set`) but still allows form-field (SDT) edits, which is the point of forms protection). + +**No inheritance from docx v2.** docx's Delivery Gate (cover-fill %, live-PAGE check) does NOT apply — form QA is `view forms` + `query sdt alias+tag` + `protectionEnforced`. + +**Reverse handoff to docx.** Route back to `officecli-docx` for reports / letters / memos / thesis / pitch decks / any document with no editable fields. Use **this** skill when the document's purpose is data capture or template merge. + +## Shell & Execution Discipline + +**One command at a time. Read output before the next.** OfficeCLI is incremental — every `add` / `set` / `remove` immediately mutates the file. All recipes below use `FILE=form.docx` as a shell variable. + +**Three shell-escape layers:** + +1. **Quote every path with `[N]`** — zsh/bash glob-expand brackets. `officecli get "$FILE" /body/sdt[1]` fails with `no matches found`. Correct: `officecli get "$FILE" '/body/sdt[1]'`. +2. **Single-quote any prop containing `$`** — `"Total: $50,000"` becomes `"Total: ,000"` after `$50` variable expansion. Correct: `'Total: $50,000'`. +3. **`--after find:` uses outer single quotes, never inner double quotes** — `--after find:"Client Signature:"` makes the quotes part of the search string; match fails. Correct: `--after 'find:Client Signature:'`. + +**`WARNING: UNSUPPORTED` (exit 2) is a silently-wrong element.** The CLI created the element *without* the rejected prop (exit 2 = ran with caveats; only when *every* prop is refused does `--json` report `success: false`). Any UNSUPPORTED in your build log means a prop name the current CLI does not accept on that element — stop, check `help docx ` for the right prop name (most SDT props such as `items`/`format`/`lock` ARE accepted now; `maxlength` is not), fix the command, and re-run. Do not ship on top. + +**`protection=forms` is the LAST structural command.** Once it is set, `protection=forms` locks non-field content (those edits need `--force` or `raw-set`) but still allows form-field (SDT) edits — which is the point of forms protection. A non-field content edit (e.g. `set /body/p[N] --prop text=`) is refused (`ERROR: Document is protected … use --force`); a form-field edit (e.g. `set /body/sdt[N] --prop text=` / `--prop alias=`) succeeds (exit 0). Use `Query("editable")` to find fillable fields. So finish all non-field (static layout) edits first, then lock; if you must edit static content afterward, pass `--force` (or temporarily clear protection, edit, re-lock). + +### `--after find:` micro-playbook + +`--after find:` matches the **first** occurrence. Bad anchor = wrong insertion location, expensive to debug. Three rules: + +1. **Anchor must be globally unique.** In bilingual contracts "甲方签字" matches both parties — use a unique phrase like "甲方签字(Service Provider)" or full English title. +2. **After insert, `/body/p[last()]` is unreliable** — the find insertion changes `` child order. To continue operating on the new paragraph, read its real paraId: `officecli query "$FILE" paragraph --json | jq -r '.data.results[-1].format.paraId'`. +3. **Chinese + full-width parens `()`** match literally in `find`, but when unsure, `officecli view "$FILE" text | grep -n "锚点"` first to confirm the exact bytes in the file. + +```bash +# Trap: first-match hits 甲方 only, 乙方 missed +officecli add "$FILE" /body --type sdt --after 'find:签字' + +# Fix: two signatories, two unique anchors +officecli add "$FILE" /body --type sdt --prop alias=Party_A_Name --prop tag=party_a \ + --after 'find:甲方签字(Service Provider)' +PID_A=$(officecli query "$FILE" paragraph --json | jq -r '.data.results[-1].format.paraId') +officecli add "$FILE" "/body/p[@paraId='$PID_A']" --type sdt --prop alias=Party_A_Title --prop tag=party_a_title +``` + +Inline SDT via `--after find:` is added as a child of the matched paragraph, not as a new paragraph — use this when label + SDT must share a line. + +## What makes a real form (identity) + +A real fillable form requires **structured fields** + **document protection**. + +| Approach | Word user sees | CLI-readable | Real form? | +|---|---|---|---| +| SDT controls + `protection=forms` | Gray-bordered fields; rest locked | `query sdt` / `view forms` | **YES** | +| FormField checkbox + `protection=forms` | Real clickable checkbox; rest locked | `query formfield` / `view forms` | **YES** (checkbox only) | +| MERGEFIELD placeholders | `«CustomerName»` merged by downstream engine | `query field` | **YES** (template-time) | +| Underscores `___` / blank lines | Visual-only; whole doc editable | No — no structured fields | **NO** | + +**Do not simulate fields with underscores.** `姓名:_______________` produces zero structured data and leaks past every verification. Always use `--type sdt` or `--type formfield`. + +**Checkbox is formfield, NOT SDT.** `--type sdt --prop type=checkbox` exits 1 (`SDT type 'checkbox' is not implemented`). Every checkbox in every recipe uses `--type formfield --prop type=checkbox`. + +**MERGEFIELD is a separate track.** `view forms` lists SDT + formfield only; `query field` lists complex fields only. Two disjoint inventories; both valid in one file. + +## Requirements for Outputs (hard floor) + +Every form must satisfy these — Delivery Gate enforces each as an executable check. + +1. `protection=forms` enforced (`get $FILE /` → `protectionEnforced=True`). +2. Every SDT has both `alias` + `tag`. +3. Every dropdown/combobox has non-empty `items=...` in `view forms`. +4. Every date SDT shows the intended `format=...`. +5. Every locked SDT shows `lock=sdtLocked` / `contentLocked` / `sdtContentLocked` as intended. +6. Zero `WARNING: UNSUPPORTED` in build log. +7. Zero `type=checkbox` on any SDT. +8. Every formfield `name` ≤ 20 characters. +9. Zero underscore-line / blank-line placeholders. +10. Field types match user intent (short text / paragraph / fixed list / list+custom / date / boolean). + +## Three Paths (core decision) + +SDT props are **first-class on `add`** — confirm the current set with `officecli help docx sdt`. Today that includes `type, tag, alias, text, items, format, lock, placeholder/placeholderText, date.*` and the SDT types `text / richtext / dropdown / combobox / date / group / picture`. So most forms are pure `add` — no raw-set, no Word template. Two paths remain only for the genuinely-unreachable cases. **Pick the path before writing a single command.** + +### Path A — Pure CLI (the default for almost everything) + +**Use when**: any text / richtext / dropdown / combobox / date / picture / group SDT — including its options, date format, and lock. Pass the props straight on `add`; verify each persists with `get '/body/sdt[N]' --json`. + +```bash +# dropdown WITH its options and a non-default date format, in one add each — no raw-set: +officecli add "$FILE" /body --type sdt \ + --prop type=dropdown --prop alias="Department" --prop tag=dept \ + --prop items="Engineering,Finance,HR" +officecli add "$FILE" /body --type sdt \ + --prop type=date --prop alias="Start Date" --prop tag=start \ + --prop format="yyyy年MM月dd日" +# lock works on add AND set: +officecli add "$FILE" /body --type sdt --prop type=text --prop tag=full_name \ + --prop alias="Full Name" --prop text="Enter full name" --prop lock=sdtLocked +# protection comes last, once all fields exist: +# officecli set "$FILE" / --prop protection=forms +``` + +### Path B — CLI + `raw-set` bridge (only an attribute help does NOT expose) + +**Use when**: you need an SDT attribute that `help docx sdt` does not list (e.g. a `` whose `w:value` must differ from its display text, or a sdtPr child with no `--prop`). `raw-set` is OfficeCLI's universal OpenXML fallback — `officecli --help` lists it as a top-level command. For ordinary dropdowns use `--prop items=` (Path A); raw-set here is the exception, not the rule. + +```bash +# Display text ≠ stored value — not expressible via items=, so inject the listItems directly: +officecli add "$FILE" /body --type sdt --prop type=dropdown --prop alias="Department" --prop tag=dept +officecli raw-set "$FILE" /document \ + --xpath "//w:sdt[w:sdtPr/w:tag/@w:val='dept']/w:sdtPr/w:dropDownList" \ + --action append \ + --xml '' +``` + +### Path C — Word template (only what no API reaches) + +**Use when**: a **real SDT checkbox** (`type=checkbox` still exits 1 — use a legacy FormField instead, see §Legacy FormField), a `placeholderDocPart` prompt-text part, or custom richtext appearance / cross-part nesting beyond `--prop` reach. Picture and grouped SDTs are NO LONGER here — they add fine via Path A. + +```bash +# One-time in Word: Developer tab → Insert Content Control → Save as template.docx +cp templates/onboarding_with_signature.docx "$FILE" +officecli open "$FILE" +officecli view "$FILE" forms # inspect embedded controls + paths +officecli set "$FILE" '/body/sdt[@sdtId=3]' --prop text="Jane Smith" +officecli set "$FILE" / --prop protection=forms +``` + +### Decision table + +| Need | Path | Note | +|---|---|---| +| text / richtext SDT with default string | **A** | `--prop type/alias/tag/text` | +| text SDT that must be locked | **A** | `--prop lock=sdtLocked` works on `add` (and `set`) | +| dropdown / combobox **with options** | **A** | `--prop items="A,B,C"` | +| date SDT with non-default format | **A** | `--prop format="yyyy年MM月dd日"` | +| signature picture SDT, grouped SDT | **A** | `--prop type=picture` / `type=group` add directly | +| dropdown whose stored value ≠ display text | **B** | raw-set append `` | +| real checkbox | **FormField** | `--type formfield --prop type=checkbox` (see §Legacy FormField) | +| mail-merge placeholder | **MERGEFIELD** | `--type field --prop fieldType=mergefield` (see §MERGEFIELD) | +| real SDT checkbox / placeholder part / custom appearance | **C** | build skeleton in Word, fill via CLI | + +## Quick Start — Path A + FormField (minimal intake form) + +Two SDT text fields, one checkbox, protection. Paste and adapt; this is the smallest form worth shipping. + +```bash +FILE=intake.docx +officecli close "$FILE" 2>/dev/null; rm -f "$FILE" # preflight: clear stale resident / prior file (cold-start after CLI upgrade commonly leaks a resident) +officecli create "$FILE" +officecli open "$FILE" + +officecli set "$FILE" / --prop title="Employee Onboarding Intake" \ + --prop docDefaults.font="Calibri" --prop docDefaults.fontSize="12pt" + +officecli add "$FILE" /body --type paragraph \ + --prop text="Employee Onboarding Intake" --prop style=Heading1 \ + --prop size=20 --prop bold=true --prop spaceAfter=18pt + +officecli add "$FILE" /body --type paragraph \ + --prop text="Full Name:" --prop size=11 --prop bold=true --prop spaceAfter=4pt +officecli add "$FILE" /body --type sdt --prop type=text \ + --prop alias="Full Name" --prop tag=full_name --prop text="Enter full name" + +officecli add "$FILE" /body --type paragraph \ + --prop text="Start Date:" --prop size=11 --prop bold=true --prop spaceAfter=4pt +officecli add "$FILE" /body --type sdt --prop type=date \ + --prop alias="Start Date" --prop tag=start_date + +officecli add "$FILE" /body --type paragraph \ + --prop text="Read and agree to employee handbook" --prop size=11 --prop spaceAfter=4pt +officecli add "$FILE" /body --type formfield \ + --prop type=checkbox --prop name=agree_handbook --prop checked=false + +officecli set "$FILE" '/body/sdt[1]' --prop lock=sdtlocked +officecli set "$FILE" '/body/sdt[2]' --prop lock=sdtlocked +officecli set "$FILE" / --prop protection=forms +officecli close "$FILE" +officecli view "$FILE" forms +``` + +## Path B — raw-set recipes + +Three recipes cover almost every complex-attr need on SDT forms. + +### B1 — Dropdown items (append) + +```bash +# Skeleton (Path A) +officecli add "$FILE" /body --type sdt --prop type=dropdown \ + --prop alias="Department" --prop tag=dept + +# Inject items +officecli raw-set "$FILE" /document \ + --xpath "//w:sdt[w:sdtPr/w:tag/@w:val='dept']/w:sdtPr/w:dropDownList" \ + --action append \ + --xml '' + +# Verify +officecli get "$FILE" '/body/sdt[1]' # expect: type=dropdown items=Engineering,Finance,HR +``` + +**Template.** Swap `` / `