Menu

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.