# D48. The plan's document: one model for the app, Word and PDF; Word written by hand, the PDF printed by Chromium (15 Oct 2026)

*Revised on 16 Oct 2026 by D52: the document shows a version of the plan or its working copy; its head says the version and the history of the versions; the changes since the validated version replace the retouches as what reads in place and as Word's tracked changes; the notes for the client are Word comments; the version property no longer counts change requests or approved retouches.*
The plan gets the app's Document view, whose Export button opens a choice between .docx and PDF, since many agencies use Word's comments to communicate. That view is « la vue que le client signe »: the same plan in sections that fold, its properties at its head, a retouch read in place, before and after; its PDF is the « Bon pour production » attached to a purchase order.
- **One model, data without layout** (`@elabora/core` `plan-document/model.ts` `planDocument(project, { date })`, format `elabora/plan-document/1`), read by every renderer: the exports, the app's Document view (another session), and later the review link that shows the plan alone. From `plan.json`, its approval and its change requests, `course.json` for the module's properties, and the screens for the retouches. At its head: the module's title, the status (« Bon pour production » when approved, « Modifié depuis sa validation », « Brouillon », « Brouillon, à valider de nouveau » once validated before), the version (the approval's reference, the change requests accepted and the retouches approved, with their last dates), « Validé par » (the approval's name and day), the module (duration · SCORM 1.2 · the LMSs of `delivery.lms`). Then Cadrage (business goal, audience, prior knowledge, spaced reminders, waivers), Objectifs, Séquence (each item, its kind, objectives, description, voice-over, sources), Textes validés (by item, « Mot pour mot » for a verbatim text), Évaluations (options with the right ones and their feedback; a numeric answer with its tolerance, feedback and expected mistakes), Sources, and a colophon with the plan's id, its fingerprint and the day. Numbers are the document's own (item 2, text 2.1, question 5.1); every element keeps its plan id as its anchor. Texts are the plan's Markdown and the same as rich-text data (`plan-document/rich.ts`), so no renderer parses Markdown.
- **Its words are in the module's language**, French or English (`plan-document/words.ts`; another language gets the English words until it has its own), dates and numbers by `Intl` in that language, whatever the computer's.
- **Never who wrote it.** The document never says that an AI wrote the plan, which would bias a reviewer who should validate the substance. It names nobody as a writer at all: not the author of a draft, of a change request or of a retouch (Word's tracked changes are signed « Retouche dans le module »); only those who approved (the approval, approved retouches, waivers). A test looks for « agent », « Claude », « AI », « IA » in the model, the page and the .docx.
- **A retouch** (rule A5; *replaced by D52: the changes since the validated version read in place the same way, and retouches are plain edits of the plan*) not reviewed yet reads in place: the validated text and the module's wording as one text, words deleted and inserted (`diffRich`: word by word when the two texts have the same paragraphs, lists and cells, otherwise the whole text replaced; a short word kept between two changes goes with them), with « Retouché le … · à revoir avec le client ». The plan has no date for the client's next review, so none is said. A retouch approved since says « Retouche approuvée par … le … » under the text the plan took.

Word:
- **Written by hand, in the core** (`plan-document/docx.ts`, about 370 lines; `omml.ts` for maths): the parts of a WordprocessingML package, which `@elabora/node` zips with `createZip` (D8: the same project and day, the same bytes). Weighed against `docx` (9.11.0, MIT, compatible with Apache-2.0): 20 MB unpacked with jszip (MIT or GPL-3.0), nanoid, xml, xml-js and hash.js, random ids and the current time in its output unless each is overridden, and a runtime dependency of a package that must stay without I/O; what the document needs of Word is small (paragraph and character styles, two tables, numbering, bookmarks, revisions, a footer), and tracked changes and bookmarks are exactly where a writer's abstractions get in the way.
- **Made to be commented.** Word's own Title and Heading 1 to 3 (the navigation pane shows the sections, items and questions), named styles of its own (`Elabora Body`, `Elabora Meta`, `Elabora Option`…), no direct formatting but the plan's own emphasis; tables only for the properties and the plan's own Markdown tables; maths as Word equations (OMML converted from the MathML temml writes for the module); A4, a footer with the title and the page; the module's language on the text (`fr-FR`, `en-US`) for Word's spelling. A pending retouch is a tracked change: Word shows the validated wording deleted and the module's inserted.
- **Bookmarks for comments to come back.** Every objective, item, validated text, question and source is inside a bookmark named after its plan id (`wordBookmarkName`: `el_` then the id, « - » as « _ » and « _ » as « _u », since Word's names allow only letters, digits and « _ »; plan ids have no lower case, so `planIdOfBookmark` reads them back without ambiguity). Not done now, the idea: an import of the client's .docx reads `word/comments.xml` and where each comment's range starts in `word/document.xml`, finds the bookmark that encloses it, and gives the comment its plan element, as the review's comments are placed in the sources for the author's AI (D43); a comment outside every bookmark goes to its section. Word keeps bookmarks while the client edits and comments; one is lost only when the client deletes the whole element, and the comment then goes to the section. Whether the client accepts the tracked changes in their copy does not matter: nothing is read back from the text.
- **Checked** by the unit tests as a package (every part well-formed with its prefixes declared, a content type for each part, relationships that lead somewhere, every style and numbering used defined, unique annotation ids, every bookmark closed, one bookmark per element, deletions holding `delText` only), and, where they are installed, opened by python-docx and read back by pandoc (its maths as the same LaTeX, its tracked changes with their author); by hand with LibreOffice 24 (layout, equations, tracked changes). Not against ECMA-376's XML schemas: they are not shipped with any package we use, and a validator would be a new development dependency; Word itself was not available here.

PDF:
- **The page printed by Chromium** rather than a PDF writer in JavaScript. One HTML page (`plan-document/html.ts`) with its fonts inline (Inter and JetBrains Mono from `@elabora/fonts`, OFL) and an `@page` rule for A4, its margins and its footer (CSS page-margin boxes: the title, « Page n / N »), so a print needs no header template and no option but `preferCSSPageSize`, the background, a tagged PDF and an outline from the headings (`PLAN_PDF_PLAYWRIGHT`, `PLAN_PDF_ELECTRON`). The same page is the `--format html` export and can be the review link's. A writer such as pdfkit (0.20.2, MIT, 10.5 MB with fontkit and linebreak) would need a second layout of everything the page lays out (rich text, lists, tables, a retouch's marks, page breaks) and of maths, which it cannot draw from MathML.
- **The same PDF from the app and the command line.** The app prints the page the engine gives (`planDocumentPage`) with `webContents.printToPDF(PLAN_PDF_ELECTRON)`; the command line prints it the same way with Elabora's Electron when the app is installed (D26): a browser worker's main process loads the page in an offscreen window of `PLAN_PDF_WINDOW` and calls `printToPDF(PLAN_PDF_ELECTRON)` (`printWithElectron`), since Electron's DevTools protocol has no `Page.printToPDF` and Playwright's `page.pdf` fails there (tried on 16 Oct with Electron 44: « 'Page.printToPDF' wasn't found »). Without the app, Google Chrome, Microsoft Edge or Playwright's Chromium print it (`printPlanDocument`, Playwright's `page.pdf`, `PLAN_PDF_PLAYWRIGHT`). Both call `finishPlanPdf`, which sets the PDF's dates to the document's day (Chromium writes the time it printed). The same page and settings in the same Chromium give the same bytes: the app's « Exporter ▾ › PDF… » and `elabora plan export --format pdf` run with its Electron give the same file (the app's try:plan-document checks it), and an end-to-end test prints twice; another Chromium version may move a line, never the content. Without any Chromium the command says so and offers `--format docx` or `html`. The page holds a Content-Security-Policy that lets it fetch nothing (its style, fonts and images are inline), and a print through Electron that has not ended after 60 s stops.
- Skia embeds the variable fonts of `@elabora/fonts` as Type 3 fonts: the text stays selectable and searchable (each glyph maps to Unicode), but some viewers draw Type 3 less finely than TrueType.

Where: `elabora plan export [dir] [--format pdf|docx|html] [--out <file>] [--date YYYY-MM-DD] [--json]` (default `dist/<course id>-plan.<format>`); MCP `elabora_export_plan_document { format }` (into `dist/`, for an author who asks their agent to send the plan); `@elabora/node/plan-document` `exportPlanDocument`, `writePlanDocument`, `readPlanDocument`, `planDocumentPage`, `planDocumentWord`, `printPlanDocument`; the engine's `planDocument`, `planDocumentPage` and `exportPlanDocument`.

What the data does not hold yet, and the app's mockups show: a second approver for one side of the plan (`approval.by` is one name), the number of learners (« 2 300 apprenants »: nothing in `course.json`), and the date of the client's next review. Each would be a field of the format (`approval.cosigners[] { by, scope }`, `course.json` `audienceSize`, `approval.nextReview`), still to be decided; the document says what it can meanwhile.

- **The document's day is the author's** (16 Oct 2026): the default date of `planDocument` (and so of `elabora plan export`) is the day in the computer's time zone (`localDay`), not UTC's, which said yesterday between midnight and 2 a.m. in France; the approval of the plan, a retouch, a decision and a translation's approval are stamped with the same day, so that the app's Sommaire and the document's head say the same date. `--date` and `PlanDocumentOptions.date` still win.

*Revisit if:* clients send back their commented .docx (the import above, then possibly `w:commentsExtended` for replies), a client needs PDF/A (static font instances instead of the variable files, and Chromium's PDF/A is not offered: a post-processing step), a language beyond French and English asks for its own words, or Word shows a fault the tests here could not see.
