Development history
On this page
- Unreleased — task-scoped agent guidance and skill discovery (2026-10-17)
- Unreleased — Windows browser worker captures (2026-10-17)
- Unreleased — automatic word alignment progress and failures (2026-10-17)
- Unreleased — compatible local model releases and Intel platform policy (2026-10-17)
- Unreleased — documentation for people and agents, ready to be public (2026-10-17)
- Unreleased — translation CLI writes use the shared command executor (2026-10-17)
- Unreleased — agent bootstrap and host-selected tools (2026-10-17)
- Unreleased — complete translation workflows on the source server (2026-10-17)
- Unreleased — shared agent operations over MCP and CLI (2026-10-17)
- Unreleased — the plan’s versions: the plan written at any time, versions made by hand, one validated (2026-10-16)
- Unreleased — the plan’s days are the author’s, not UTC’s (2026-10-16)
- Unreleased — the plan’s PDF printed by Elabora’s Electron as the app prints it (2026-10-16)
- Unreleased — the app imports Storyline and Rise modules: the brief kept, the report settled, the choices made again (2026-10-15)
- Unreleased — a plan that reads as an outline: parts, short names, a duration, screens that follow (2026-10-15)
- Unreleased — the plan’s document, in Word and in PDF (2026-10-15)
- Unreleased — Storyline and Rise modules imported as Elabora modules, beta (2026-10-15)
- Unreleased — the author’s sources, and a readable version of each that any agent reads (2026-10-15)
- Unreleased — a new course keeps what the author said of it, and the agent’s draft says what it decided (2026-10-15)
- Unreleased — lines made from recordings, and a line’s text from its recording (2026-10-15)
- Unreleased — the trash can be listed with its dates, and emptied one entry at a time (2026-10-15)
- Unreleased — a read-only key for the author’s AI, the review page’s last findings (2026-10-14)
- Unreleased — what the app’s account settings and Publier call (2026-10-14)
- Unreleased — the legal pages’ findings, the client’s side (2026-10-14)
- Unreleased — one licence for the core, CC0 for the samples’ own content (2026-10-14)
- Unreleased — the hosting’s client: accounts, publishing, review comments for the AI (2026-10-14)
- Unreleased — the review bridge (2026-10-14)
- Unreleased — the samples’ notices and credits, for the showcase (2026-10-14)
- Unreleased — a web target beside SCORM 1.2 (2026-10-14)
- Unreleased — the ElevenLabs key in one place (2026-10-14)
- Unreleased — the name Elabora (2026-10-13)
- Unreleased — every optional download in one list (2026-10-13)
- Unreleased — local voices removed and reinstalled (2026-10-13)
- Unreleased — a ledger of what paid AI services charge (2026-10-13)
- Unreleased — a plan never validated is the agent’s draft (2026-10-13)
- Unreleased — a course’s rules for agents stay in step with Elabora (2026-10-13)
- Unreleased — more warnings the author may ignore (2026-10-13)
- Unreleased — compact quiz feedback (2026-10-13)
- Unreleased — figures drawn in the page (2026-10-13)
- Unreleased — the theme’s rules give way to components; overrides are said (2026-10-13)
- Unreleased — seven faults met building the « Nietzsche » course (2026-10-13)
- Unreleased — stage directions are not read, a swallowed word may be ignored (2026-10-13)
- Unreleased — a dialogue is cut where the aligner hears its turns (2026-10-13)
- Unreleased — costs as a share of the quota, estimates the service charges (2026-10-13)
- Unreleased — ElevenLabs voices: stage directions, dialogue, credits (2026-10-12)
- Unreleased — long recordings no longer stop the engine (2026-10-12)
- Unreleased — the stack’s contents share one place (2026-10-12)
- Unreleased — the fine timeline, part 6: what the editor asked for (2026-10-12)
- Unreleased — the fine timeline, part 4: what the editor and the review need (2026-10-12)
- Unreleased — the fine timeline, part 3: word times by forced alignment (2026-10-12)
- Unreleased — the fine timeline, part 2: word anchors through translation (2026-10-12)
- Unreleased — the fine timeline, part 1: timed blocks and anchors (2026-10-11)
- Unreleased — narration for the app’s editor (2026-10-11)
- Unreleased — author narration through MCP (2026-10-08)
- Unreleased — the voices’ credits in front of learners (2026-10-08)
- Unreleased — free local voices for narration (2026-10-08)
- Unreleased — the narrated player as the app’s validated mockups have it (2026-10-08)
- Unreleased — takes, pronunciations and credits of generated voices (2026-10-08)
- Unreleased — narration and timed steps, phase 1 of the timeline (2026-10-08)
- Unreleased — captions for agents (2026-10-08)
- Unreleased — a decorative sound (2026-10-08)
- Unreleased — closing the open layer (2026-10-08)
- Unreleased — Microsoft Edge when Chrome is not there (2026-10-08)
- Unreleased — a library project knows the example it was copied from (2026-10-08)
- Unreleased — a package’s limits, with the LMS that sets them (2026-10-08)
- Unreleased — the canvas scrolls under a host’s overlay (2026-10-08)
- Unreleased — an event written from its trigger (2026-10-08)
- Unreleased — a translation seen in context, built as its own package (2026-10-08)
- Unreleased — a free object’s choices in words (2026-10-08)
- Unreleased — events, layers and variables edited in the app (2026-10-08)
- Unreleased — an open project loses no write when other watchers come and go (2026-10-07)
- Unreleased — a project is watched once open (2026-10-07)
- Unreleased — automatic versions in any process (2026-10-07)
- Unreleased — faster free-object gates (2026-10-07)
- Unreleased — free-object gates on a loaded machine (2026-10-07)
- Unreleased — fonts for Central European languages (2026-10-07)
- Unreleased — looking fast while designing (2026-10-07)
- Unreleased — lists and material colours in free objects (2026-10-07)
- Unreleased — blocks added by hand (2026-10-07)
- Unreleased — vetted fonts (2026-10-07)
- Unreleased — stable tests (2026-10-07)
- Unreleased — texts the author points at on the canvas (2026-10-07)
- Unreleased — retouches of validated texts (2026-10-07)
- Unreleased — Rise-like steps, replay, contact sheets (2026-10-07)
- Unreleased — what an agent did, and Codex by name (2026-10-07)
- Unreleased — scroll pages, Rise-like courses (2026-10-07)
- Unreleased — found by the desktop app (2026-10-07)
- Unreleased — Linux and Windows, found by CI (2026-10-07)
- Unreleased — rendition cache pruned (2026-10-07)
- Unreleased — resume on a renamed screen (2026-10-07)
- Unreleased — TypeScript packages (2026-10-06)
- Unreleased — cascade layers in the documented order (2026-10-06)
- Unreleased — broken projects, catalogue (2026-10-07)
- Unreleased — the editing engine (2026-10-06)
- Unreleased — media: import, masters, renditions, streaming packages (2026-10-06)
- Unreleased — prototype format 0.2, format migrations, rename (2026-10-06)
- Unreleased — the editor canvas (2026-10-06)
- Unreleased — project library and archives (2026-10-06)
- Unreleased — command line and MCP server: undo, history, batches, strict flags (2026-10-06)
- Unreleased — edits as operations, journal and undo (2026-10-06)
- Unreleased — the core without I/O (2026-10-06)
- Unreleased — explore in any order (2026-10-06)
- Unreleased — click to reveal (2026-10-06)
- Unreleased — what the space sample taught (2026-10-05)
- Unreleased — size classes, slides that fit, steps (2026-10-05)
- Unreleased — components (2026-10-05)
- Unreleased — design layer prototype (2026-10-05)
- 0.1.0 — 2026-10-05
Unreleased — task-scoped agent guidance and skill discovery (2026-10-17)
- Course rules and the authoring/design skills are short task routers, with detailed recipes loaded on demand. Narration, translation and Storyline/Rise importing have focused skills. Unedited course rules update through D34’s existing revision mechanism; author notes and edited rules are preserved.
elabora tools skills [<name> [<reference>]] [--json]lists bundled skills or reads one skill/reference without a course. MCP exposes the same references atelabora://skills/<name>/references/<reference>; the agent guide describes both interfaces.- Validation tests account for Windows crash exits, file permissions and folder enumeration.
Unreleased — Windows browser worker captures (2026-10-17)
- Headless Electron workers on Windows use offscreen windows so repeated screenshots, contrast and fit measurements keep receiving frames. The app applies the same setting to worker popups.
Unreleased — automatic word alignment progress and failures (2026-10-17)
- Voice generation emits a distinct
voiceProgressphasealignwhile a line’s words are timed, including when an earlier take is selected. A failed automatic alignment keeps the audio and reportsalignmentErroron the line’s result so editors can explain that its words remain estimated.
Unreleased — compatible local model releases and Intel platform policy (2026-10-17)
- Optional local models select exact compatible releases, stage upgrades beside active versions and retain directory leases until inference finishes (D55). Workers switch directories between calls, and provenance records the release actually used. Update-only intent never reinstalls an absent element.
- Intel Macs retain editing, recordings, existing narration and media conversion; the core refuses installation and inference for local voice generation, word alignment and transcription on darwin/x64 (D54). Downloaded models remain removable. The app supplies update network policy and the shared status panel.
Unreleased — documentation for people and agents, ready to be public (2026-10-17)
- One map:
docs/index.mdlists every page with one line of what it helps with, by section; GitHub shows it, the site renders the same files (elabora.studio/en/docs/developers) and itsllms.txt, andelabora docsreads it. New pages:docs/getting-started.md,CONTRIBUTING.md, and two generated references,docs/cli.md(the help) anddocs/mcp-tools.md(every agent operation and its arguments). The README is a short introduction with a capture of a sample course; its details moved to these pages. - Smaller pages: the decision log is one file per decision,
docs/decisions/D01.md…D53.md, anddocs/decisions.mdtheir generated index; the architecture is a summary of the contract (docs/architecture.md) and one page per subsystem indocs/architecture/(operations and identifiers, media, delivery and hosting, the plan, storage, import, rendering, engine). Code comments cite the new pages. elabora docs [page] [--section <heading>] [--full]and the read toolelabora_docs(alsotools call) give agents the documentation without a checkout: the map, a page by its id (design,spec,architecture/media) or a decision by its number (D29), a long page as its opening and outline, or one section. The MCP server serves the pages as resources too (elabora://docs/<id>). Tool descriptions, diagnostic fixes, the visual-design skill, the server’s instructions, the agent guide and the courseAGENTS.md(revision 6) point to it.- Rules defined:
@elabora/core/diagnostics/rulesstates each guardrail and LMS checklist rule a diagnostic cites (A1…I1, LMS-n);docs/diagnostics.mdlists them with their codes. - Public wording: the docs, the changelog and comments no longer point to private notes, reports, tracker numbers or people; the private provenance of the decisions and the open questions were moved out of the repository.
- Checks:
test/unit/docs.test.ts(every page on the map, links and anchors resolve, thedocs/….mdpaths andDnthe code names exist, no raw HTML outside code, every cited rule is defined,elabora docs), and.github/workflows/docs.yml, which runs it and fails when a generated page is out of date, on every pull request including Markdown-only ones.
Unreleased — translation CLI writes use the shared command executor (2026-10-17)
translate exportandtranslate approvenow use the same locked, validated, journaled and undoable commands as the engine. Export JSON includes its change/undo reference; the Node helpers retain their existing count/approval fields and accept explicit edit options for embedding.approve_translationrefuses an agent author in the core command (E_PLAN_PERSON_ONLY), matching plan-version approval. Human scripts identify themselves explicitly as for other plan approvals. There is still no agent approval tool.- Tests cover CLI export undone over MCP, agent approval refusal without writes, human approval and cross-interface undo, and structured translation build errors.
Unreleased — agent bootstrap and host-selected tools (2026-10-17)
elabora_agent_guide,elabora tools guide [--json]and exportedAGENT_GUIDEexpose the same provider-independent workflow index: interface choices, tool names, authoring/translation/design/narration/review/delivery, and human decisions.mcp --tools <names>andToolOptions.toolsselect tools for plugins/embedded hosts. The server fixes its selection for the connection; discovery and dispatch agree, read-only intersects it, and unknown names fail early.tools list --toolspreviews the selection.- Updated the course
AGENTS.md, examples, authoring skill, architecture and integration guide: connected MCP when available, CLI shared operations otherwise, direct edits for code/design, no automatic agent-configuration changes. D53 records the interface contract.
Unreleased — complete translation workflows on the source server (2026-10-17)
elabora_translation_statusreports text readiness, schema/invariant problems, stale/missing units, word anchors and plan approval.elabora_validate,elabora_check_fit,elabora_preview_screenshot,elabora_buildandelabora_test_packageacceptlanguageand explicitallowMissingdrafts;elabora_lookaccepts the same BCP 47 tags. Agents can finish a translated package through one source-scoped MCP connection, or throughtools call.- Checks/builds use isolated, cleaned-up translated copies, clone files where supported and omit Git metadata and author sources. Package/proof outputs stay in the source course’s
dist/. Translated-plan approval remains human, and release validation enforces it. - Tool results carrying
ok: falseset the MCPisErrorflag too, so shell callers get a failing exit code.translate status --jsonnow returns the same readiness exit code as text;translate buildandapproveaccept--json, and translation service errors in JSON mode are structured. - Authoring guidance and interface fixtures updated; tests cover transport agreement, translation lifecycle, concurrent derived copies and the complete browser/package workflow over MCP.
Unreleased — shared agent operations over MCP and CLI (2026-10-17)
elabora tools list [tool] --jsondiscovers the same schemas and annotations as MCP;tools call <tool> [dir] --from <file>|-invokes the same validated handlers, journaled edits and errors without an MCP connection. Stdout is always a JSON tool result; exit codes distinguish success, refusal and malformed input. Read-only discovery and invocation are supported.@elabora/cliexportslistAgentToolsandcallAgentToolfor embedded callers, with an explicit author and course root. MCP retains its existing names, schemas and results.docs/agents.mddocuments transport choice, workflow coverage, direct file edits and intentional human-only actions. The README no longer hardcodes an obsolete tool count.
Unreleased — the plan’s versions: the plan written at any time, versions made by hand, one validated (2026-10-16)
Clean break before publication, with no migration: a plan written before it (with status and approval) breaks the plan’s schema.
plan.jsonis the working copy (D52), written at any time by the author and their agent:write_plan(CLIelabora plan write, MCPelabora_write_plan, in place ofdraft_plan,elabora plan draftandelabora_draft_plan),set_plan_text(elabora plan text: a validated text reworded, every block that shows it follows),set_plan_voiceover(always written),revert_plan_change(elabora plan revert, « Revenir à la V2 »).statusandapprovalleaveplan.json. An agent’s write that changes a text kept « à l’identique » is refused (E_PLAN_VERBATIM_LOCKED). Commands know who runs them (CommandContext.author).- Versions in
plan-versions/V<n>.json(schemaplan-version:version,date,note,contentHash,plan,notes,sends,validation):create_plan_version(elabora plan version [--note]) andapprove_plan(elabora plan approve [--version Vn] --by … [--reference] [--date], the latest version by default, never the working copy) are a person’s: refused to an agent (E_PLAN_PERSON_ONLY), and no MCP tool does them.record_plan_sendadds a send;set_plan_note(elabora plan note) writes a note for the client on a change in no version yet (plan-versions/notes.json, schemaplan-notes), frozen with the next version. - What changed, computed:
@elabora/core/project/plan-diffplanDiff(base, current, course?)(elements matched by ids, added, removed, moved from their position, an item’s part changed, each field, texts word by word; the part an item belongs to, not the field it is written in: a plan without parts that writes each item’s part down, as the app does at the first drag, changed nobody’s part, whichplanCourse(project)lets the diff know by the course’s sections that gave them) andrevertPlanChange;@elabora/core/project/plan-versions(planState,planVersionsViewwith the changes since the validated version and those in no version,planDiffBetween,notesFor,describePlanVersions,planContentHash). CLIelabora plan diff [--from] [--to] [--json]andelabora plan status [--json]; MCPelabora_plan_diff(read-only);elabora_get_plangivesversionsin place ofvalidatedOnce. - Rules:
E_PLAN_NOT_APPROVEDmeans a release build without a validated version;W_PLAN_MODIFIED_SINCE_VALIDATIONreplacesE_PLAN_MODIFIED_AFTER_APPROVAL(a warning: the build report’splan.validatedandplan.changesSince, and the proof dossier, list the changes delivered);W_PLAN_VERBATIM_CHANGED;E_PLAN_VERSION_INVALID(a version edited by hand, not counted). Gone:E_RETOUCH_VERBATIM,E_RETOUCH_WITHOUT_TEXTREF,I_TEXT_RETOUCHED,W_CHANGE_REQUEST_INVALID. - Gone with them: change requests (
changes/CR-*.json, thechange-requestschema,propose_plan_change,apply_change_request,reject_change_request,elabora change apply|reject, MCPelabora_propose_plan_change) and retouches (the blocks’retouch,retouch_text,restore_validated_text,approve_retouches,elabora plan retouch|restore|approve-retouches, MCPelabora_retouch_textandelabora_restore_validated_text).elabora renamerewrites the plan’s waivers that name the id. - The plan’s document (format
elabora/plan-document/2) shows a version, the latest by default, or the working copy (PlanDocumentOptions.version,planDocumentTarget): its head says the version shown and the history up to it; the changes since the latest validated version read in place (EntryMark,titleChange, words deleted and inserted, elements added and removed), as Word tracked changes signed « Depuis la V2 validée », and the notes for the client as Word comments and margin notes.exportPlanDocument,elabora plan export --version Vn|current,elabora_export_plan_document { version }and the engine’sexportPlanDocumentrecord the send on the version shown (ExportedPlanDocument.version,sent) and never make one. - Engine: requests
planVersionsandplanDiff;planDocument,planDocumentPage(with theversionshown) andexportPlanDocumenttakeversion;summary.plangiveslatest,validated,sinceValidated,unversioned; commandswrite_plan,set_plan_text,revert_plan_change,set_plan_note,create_plan_version,approve_plan,record_plan_send(gone:draft_plan,propose_plan_change,apply_change_request,reject_change_request,retouch_text,restore_validated_text,approve_retouches). - A translated course holds its approved translation as its own validated
V1, and none of the source’s versions. - Agents: rule 1 and the workflow of the course
AGENTS.md(revision 4), the authoring, pedagogy and free-objects skills and the MCP server’s instructions: the agent writes the plan and says what it changed, never makes nor validates a version, never changes a verbatim text. - The examples hold their validated version;
diagnostic-reseauhas V1 sent, V2 validated, two changes since and a note for the client.
Unreleased — the plan’s days are the author’s, not UTC’s (2026-10-16)
elabora plan exportdates the document the author’s day (planDocument’s default date,PlanDocumentOptions.nowto fix the moment): between midnight and 2 a.m. in France it said yesterday, the UTC day. The approval of the plan (approve_plan), the dates of a retouch, a decision and a client’s approval of a retouch (plan-changes.ts) and a translation’s approval are the same day (@elabora/core/util/local-daylocalDay, the computer’s time zone;TZmoves it), so that the Sommaire, the Document and the files say the day the author lived.--datestill wins. A unit test fixes the moment and the zone (Paris, New York).
Unreleased — the plan’s PDF printed by Elabora’s Electron as the app prints it (2026-10-16)
elabora plan export --format pdf,elabora_export_plan_documentand the engine’sexportPlanDocumentprint with Elabora’s Electron when the app is installed (D26, D48) through its ownwebContents.printToPDFin a browser worker (@elabora/node/emulator/electronprintWithElectron): Electron’s DevTools protocol has noPage.printToPDF, so Playwright’spage.pdffailed there and the command went on to Chrome, or failed without it. The file is the same, byte for byte, as the app’s « Exporter ▾ › PDF… ».@elabora/core/plan-document/pdfPLAN_PDF_WINDOW: the offscreen window both print in. A print through Electron that has not ended after 60 s stops its worker and fails.- The document’s page fetches nothing: a Content-Security-Policy (
default-src 'none', its inline style, its fonts and images as data) inplanDocumentHtml, for every Chromium that prints it and every browser that opens the HTML export.
Unreleased — the app imports Storyline and Rise modules: the brief kept, the report settled, the choices made again (2026-10-15)
importCourse({ brief, defaultLanguage, replace })(D47, D51): the brief of « Nouveau module » kept ascreateProjectkeeps it (audience, business goal and duration in the draft plan, LMS incourse.json, notes inAGENTS.md, its presentation the presentation choice’s default);defaultLanguagewhen the source says none (Storyline);replace: <project id>imports again in that project’s place, as an archive’s replacement does (same id and folder, history and sources kept, a version just before, its files in the trash), keeping its title, course id, language,AGENTS.mdand brief.ImportCourseResult.replaced. A module imported into the library gets a course id no other module there has (the title’s, numbered).- The report holds
choices(what the import was made with) and, per decision,accepted;settle_import_report { accept?: number[], close?: boolean }(engine command,@elabora/node/editsettleImportReport) marks decisions accepted or deletesimport-report.json, one journaled change each. @elabora/node/library/archivereplaceWithStaged(targetDir, stage): a folder staged in the library takes a project’s place (the archive’s replacement and the importer share it);checkBriefis exported from@elabora/node/scaffold.
Unreleased — a plan that reads as an outline: parts, short names, a duration, screens that follow (2026-10-15)
items[].partandobjectives[].short(D50), optional inplan.json: the module’s parts by title (consecutive items with the same part; an item without one continues the part before it) and an objective’s name in three to five words. Written bydraft_plan, translated with the plan (unitsitem.<id>.part,objective.<id>.short); the authoring skill asks for them.- A duration estimated from the screens:
@elabora/core/project/duration(screenEstimates,roundedMinutes,countWords): the words read at 150 a minute or the voice when it says more, a moment per question, decision or free object, at least 10 s. The engine’ssummarygivessecondsfor every screen of its outline. - Screens follow the plan’s items (
@elabora/core/project/plan-order,followPlanOrder,itemParts):draft_planandapply_change_requestmove, in the same change, the screens of items that moved and the sections of parts that moved (renamed with their part when they were called as it was), and say so (moved: screens, sections, renamed);elabora plan draftandelabora change applyprint it. Screens covering several items stay; a plan without parts moves no section. - The engine runs
draft_plan(run), so that the app’s author writes a plan never validated as their agent does (its moves, its fields in Propriétés). - D35 amended: the app no longer says who wrote a draft plan.
Unreleased — the plan’s document, in Word and in PDF (2026-10-15)
- The plan’s document as data (D48):
@elabora/core/plan-document/modelplanDocument(project, { date })gives what the client reads, comments and signs, without layout: the module’s title, the status (« Bon pour production », « Modifié depuis sa validation », « Brouillon »…), the version, who approved and when, the module’s duration and delivery, then Cadrage, Objectifs, Séquence, Textes validés, Évaluations and Sources, in the module’s language (French, else English). Its texts are the plan’s Markdown and the same as rich-text data (plan-document/rich:parseRich,diffRich,plainOf). A retouch not reviewed yet reads in place, its words deleted and inserted; an approved one says so. It never says who wrote the plan, a change request or a retouch. - Word (
plan-document/docx): a .docx written by hand, deterministic, with Word’s Title and Heading styles, named styles and no direct formatting but the plan’s own emphasis, maths as Word equations, retouches as tracked changes, and a bookmark around every objective, item, validated text, question and source named after its plan id (wordBookmarkName,planIdOfBookmark), for a later import of the client’s Word comments. - PDF and HTML (
plan-document/html,plan-document/pdf): one HTML page with its fonts inline and an A4 print stylesheet, printed by Chromium (PLAN_PDF_PLAYWRIGHT,PLAN_PDF_ELECTRONfor the app’sprintToPDF), tagged and with an outline;finishPlanPdfdates it to the document’s day. elabora plan export [dir] [--format pdf|docx|html] [--out <file>] [--date YYYY-MM-DD] [--json](defaultdist/<course id>-plan.<format>), MCPelabora_export_plan_document { format },@elabora/node/plan-document(exportPlanDocument,writePlanDocument,readPlanDocument,planDocumentPage,planDocumentWord,printPlanDocument), and the engine’s requestsplanDocument,planDocumentPageandexportPlanDocument.@elabora/core/compile/markdownexportsparseMarkdown(the subset’s tokens).
Unreleased — Storyline and Rise modules imported as Elabora modules, beta (2026-10-15)
elabora import <file> [dir](D47) takes a Storyline 360 project (.story), a course Storyline published (web or LMS, a folder, its zip or itsstory.html) or a Rise 360 export (web, SCORM 1.2 and 2004, xAPI; zipped or not) and makes a new module of it, indiror else the app’s library;--title,--id,--lang, and--choose <decision>=<choice>,…to run it again with another choice of its report (presentation=scroll,backgrounds=keep,timing=steps,fonts=theme,media-accessibility=errors). An Elabora archive or folder goes on into the library as before;--replaceand--copyare theirs.@elabora/node/import/index:detectImport(file)(kind:storyline-source,storyline-published,rise,elabora-archive,elabora-folderor null, the course’s folder inside the source, its packaging,beta) andimportCourse({ file, dir?, id?, title?, language?, choices?, author? })(the module’s folder, the library project when nodir, the report, the files, the fonts added, the validation counts);ImportError(E_IMPORT_UNKNOWN,E_IMPORT_ELABORA,E_IMPORT_TARGET,E_IMPORT_UNREADABLE); also exported from@elabora/node.@elabora/core/import/: the readers (storyline-source.ts,storyline-published.ts,rise.ts), one model (model.ts), the slide’s reading order (layout.ts: a recursive XY cut, or the author’s focus order when a reader gives one), the conversion (convert.ts) and the report (report.ts), with an HTML to Markdown converter and a small XML reader; pure, so the cloud and a browser can import too.- The report (
import-report.jsonin the module, formatelabora-import-report/1): one sentence, the decisions grouped (fonts without a web licence replaced by vetted ones, Content Library media not taken, backgrounds, objects along the timeline, hotspots made buttons, images without alt, media without captions, questions without feedback), then every loss with its status (fallback,not-yet,dropped); in French or English after the course’s language. Blocks standing for a lost feature carry anote. ZipReader.hashEntry(entry, 'md5' | 'sha256'): a digest of an entry read in chunks (a.storynames its media by their MD5).scripts/make-import-fixtures.tswrites the synthetic Storyline and Rise modules oftest/unit/import.test.ts(no Articulate content or code).
Unreleased — the author’s sources, and a readable version of each that any agent reads (2026-10-15)
sources/(D49): the files a module is made from (a client’s brief, a deck, recorded interviews), at the top of the course folder, outside the course: no snapshot holds them, so they are never validated, journaled, watched, backed up, restored, built or published.@elabora/coreexportsSOURCES_DIRandisSourcePath.E_SOURCE_REFERENCED: an asset, a theme’s stylesheet, font orurl(), or a free object’s file that points intosources/, and a web build’s sharing image taken there; the build also checks every entry and media file it writes. SCORM, web and review packages are byte for byte the same with sources or without them (tested).@elabora/node/sources/index:addSources(dir, files, { onProgress, signal })(copied or cloned, a folder’s files with it; a content already there not copied again,already-there; a name taken numbered, « brief (2).docx »; names made portable),makeSourcesReadable(dir, { names, force, install, language, onProgress, signal }),listSources,readSource(dir, file, { refresh, install }),removeSource(for good),resolveSource,sourceFiles,sourcesSize,copySources;SourceError(E_NOT_A_COURSE,E_SOURCE_NOT_FOUND,E_SOURCE_NOT_A_FILE).- Readable versions,
sources/<name>.mdandsources/<name>.files/, made on this computer, written again when the source changes: Word documents (headings, lists, tables, images, footnotes, links, comments in place and listed with their replies, tracked changes as{++…++}/{--…--}), PowerPoint decks (slides in order, texts in reading order, tables, SmartArt, notes, comments, hidden slides, the pictures each slide places and the file’s thumbnail; slides are not drawn), video and audio (the transcript by D46’s local transcriber in timed paragraphs, a video’s frames;partialuntil the transcriber is here). No program of the author’s is used (revised on 19 Oct 2026 by D49): older office formats (.doc, .rtf, .odt, .ppt, .pps, .odp, .key) areunsupported, their notes saying to save the file as .docx or .pptx (or as a PDF), and PDFs are read as they are. No new dependency. elabora sources list|add|read|refresh|remove; MCPelabora_sourcesandelabora_source_read(read-only; the Markdown in parts, its pictures withimages: true) andelabora_source_add. The course’sAGENTS.mdgains rule 11 (template revision 2, brought to unedited courses, D34), the authoring skill and the MCP server’s instructions the same line: the sources are insources/, read them through their readable versions.- The library carries sources:
duplicateProjectandimportFoldercopy them;exportProject(dir, out, { sources: true })(elabora export --sources) puts them in the archive, which leaves them out by default;ExportResult.sources,ArchiveSummary.sources, andfiles/bytesnow count the course’s files only; a replacement by an archive without sources keeps the module’s;LibraryProject.sourcesSize. A component’s preview and a translation’s folder no longer copy them. npm run fixtures:sourceswrites the synthetic sources of the tests (test/sources-fixtures.ts) totest/fixtures/sources/.
Unreleased — a new course keeps what the author said of it, and the agent’s draft says what it decided (2026-10-15)
briefforinitProjectandcreateProject(@elabora/node/scaffoldCourseBrief, D51):audience,businessGoalanddurationwritten into the plan,presentationandlmsintocourse.json,notesunderAGENTS.md’s « Notes for this course »; refused before anything is written when it would not fit.elabora init --audience --business-goal --duration 10-20|5|30+ --presentation slides|scroll --lms a,b --notes <text>.plan.duration{ min, max }, the learner time asked for, in minutes.plan.decided[](topic,label,choice,why,items) andplan.question(text,topic,items): what the agent decided by itself while the plan is a draft, and the one question it asks; their items are checked (E_PLAN_REFERENCE);approve_planremoves both, andplanContentHashleaves them out. A duration whose min is above its max isE_SCHEMA. TypesPlanDecision,PlanTopic.initProjectescapes a title’s backslashes and control characters as well as its quotes (course.jsonandplan.jsonstay JSON).- The course
AGENTS.md(a new revision: the notes,sources/,decidedandquestion) and the authoring skill teach it.
Unreleased — lines made from recordings, and a line’s text from its recording (2026-10-15)
- Transcription on this computer (D46): NVIDIA’s Parakeet TDT 0.6B v3 (CC-BY-4.0, int8, 671 MB, 25 European languages, punctuation and capitals), run by onnxruntime-node 1.23.2 in a process of its own (
@elabora/node/transcribe/index:transcribeAudio,transcribers,estimateTranscription,disposeTranscriber). It replaces D27’s greedy wav2vec2 draft (28 % of French words wrong on the product’s voices; 10 % now, mostly numbers said in words). An optional download of its own,transcriber(ELABORA_TRANSCRIBER_MODELS_DIR,_URL,_MANIFEST;test/isolate.tsgives tests a folder).align/process.tsnow runs onmodels/process.ts, shared with the transcriber;align/indexno longer exportstranscribeAudionorTranscript(@elabora/node/transcribe/typeshas it). - ElevenLabs’ Scribe v2 when the author asks (
service: 'elevenlabs'): its share of the quota told first (from 1 %,approvedPercent,E_TRANSCRIBE_SPEND, the CLI’s exit code 3), its transcript deleted from the service at once. The key needs the « Speech to Text » permission. The ledger has its list price (elevenlabs/scribe_v2/starter, 5.5 credits a second), not measured yet. - A line made from a recording: command
add_narration_line_with_audio(@elabora/coreaddNarrationLineWithAudio,@elabora/node/narration/from-audioaddNarrationLineFromAudio): its text transcribed or given, its audio the recording trimmed and encoded, its words timed, after the last line (orafter) in its step and with its speaker, in one undoable change.elabora narration add-from-audio [dir] <screen> <file> [--text] [--after|--first] [--step] [--speaker], MCPelabora_narration_line_from_audio. - A line’s text from its recording: command
set_narration_text_from_audio(setNarrationTextFromAudio): the recording kept, the text replaced (anchors follow their words, translations become stale), its words timed again, in one undoable change; refused for a generated voice, a translation, or an audio that changed meanwhile.transcribeNarrationLine(enginetranscribeLine) shows what the recording says first.elabora narration retranscribe [dir] <screen> <line> [--dry-run] [--text], MCPelabora_narration_text_from_audio(preview). elabora narration transcribeand MCPelabora_transcribe: what a recording says, on this computer or with--service elevenlabs. Engine requeststranscribers,transcribeRecording(service,approvedPercent),transcriptionEstimate,transcribeLine; eventtranscribeProgress.
Unreleased — the trash can be listed with its dates, and emptied one entry at a time (2026-10-15)
deleteFromTrash(entry)(@elabora/node/library/index) deletes one trash entry for good, whatever its age: its folder, its history, its record and the data kept outside the project (ELABORA_DATA_DIR) unless a module of the library or another entry has the same project id; it gives the entry back, with its size, and refuses a name that is not an entry (E_TRASH_ENTRY).emptyTrashgoes through the same code.elabora projects trash delete <entry>does it from the command line, andtrash listsays from when each entry may be deleted.TrashEntry.expiresAt(trashed time +TRASH_RETENTION_DAYS, 30) says when an entry will be deleted byemptyTrash({ olderThanDays: TRASH_RETENTION_DAYS }), so that no screen has to know the number.- Restoring a module whose id is taken again (replaced by an import, or made again) keeps its history when the data lives outside the project: it is copied under the new id; the module that holds the old id keeps its own.
Unreleased — a read-only key for the author’s AI, the review page’s last findings (2026-10-14)
- A read-only AI key (D45; the hosting’s protocol 6):
@elabora/node/cloud/indexgainscreateAiKey(name, { token?, forgetSessionFile? }),listAiKeys,revokeAiKey,readAiKey,aiKeyFile; the key (ear_…) is kept in<local data>/credentials/elabora-ai-key(0600) and never printed. It lists reviews and reads their comments, nothing else.elabora account ai-key create [--name] [--replace-session] | list | revoke <id>;account statusandaccount logoutknow it. - The MCP read tools,
review listandreview commentstake the AI key first; every command that acts with only that key says it reads reviews only and that publishing, sharing, answering, resolving and the account need the author’s session (E_CLOUD_SESSION_REQUIRED). A course’s reviews are found by their versions’ course (versions[].course), so the key needs no publication list. elabora review create --note <text>: a word to reviewers under «<your name>vous invite à relire ce module » on the review page;setReviewNotechanges or clears it. A review givesnoteandinvitedBy.- The review bridge gives the narration line’s text and speaker (
location.said,location.speaker, and in the anchor; D42) and akeymessage when the reviewer presses C inside the module, so that « Commenter » works from there. Onlyreview.jschanges: a build without--review-origingains nothing. - With
--progress local, the player says that the progress stays in this browser and offers « Effacer ma progression » (the end of the outline and the page anexitleaves; D41), in the seven languages.player.jsandplayer.csschange for every build.
Unreleased — what the app’s account settings and Publier call (2026-10-14)
setSessionStore(store | null)(D43 amended): the author’s session goes through a store, the 0600 file by default, so that the app keeps it in its safeStorage when the author’s AI may not use it;readSession,SessionRecordandSessionStoreare exported (the types Node-free, in@elabora/node/cloud/types).@elabora/node/cloud/indexgainsaccountSessions,revokeSession,updateAccount,requestDeletionCode,deleteAccount,unpublish,renameReview;chooseHandle(handle, { displayName, api })(its second argument was the API). TypesAccountSession,AccountErased,PublishCheck.elabora publish --privatesends to reviewers even with red checks: a review build is a preview; the checks it let through come back aschecksand are listed. Public publications are unchanged.test/cloud-fake.tsspeaks the devices, the display name, the account’s erasure, unpublishing and a review’s title, and compiles under the app’s settings.
Unreleased — the legal pages’ findings, the client’s side (2026-10-14)
elabora account loginno longer sends the computer’s name. The session’s device is « Elabora CLI · macOS » (or Windows, Linux) instead ofElabora CLI (<computer name>), a name that is often a first name;--device <name>still names it as the author likes (D43).elabora publication versions <publication>lists the versions the hosting keeps of a publication, the current one marked;elabora publication withdraw <publication> <version> --yestakes one older version down for good while the publication goes on (its pinned address answers 404, the files only it used are deleted, a review that showed it lists it as withdrawn with its comments). The current version is refused.@elabora/node/cloud/indexgainspublicationVersions,withdrawVersionandisVersionId; a review’s version haswithdrawnand acontentthat isnullonce withdrawn. Needs the hosting’s protocol 5.
Unreleased — one licence for the core, CC0 for the samples’ own content (2026-10-14)
- Apache-2.0 covers the specification too (D44):
SPEC.mdand the README no longer say CC BY 4.0, and the licence is no longer an open decision. - The original content of the four samples is CC0 1.0: each
examples/<id>/LICENSE.mdsays so and lists what stays under its own licence (SIL OFL fonts, three.js, the simplex noise of orbital-rendezvous, the Pocket TTS narration of diagnostic-reseau with its credit);NOTICEpoints to them.
Unreleased — the hosting’s client: accounts, publishing, review comments for the AI (2026-10-14)
@elabora/node/cloud/index(D43): the client of Elabora’s hosting (protocol version 3), without a new dependency: sign-in by e-mail code, publishing by hashes (only the files the space lacks; above the API’s limit in parts; a second publication of the same content sends nothing), private publications for the review, reviews, comments withsince, the author’s reply, resolve and reopen, andresolveComments, which places each comment in the course’s sources. The API ishttps://api.elabora.studio(ELABORA_CLOUD_URLelsewhere; HTTPS only, but on this computer).elabora account login | handle | status | logout: the code is read from the input, never an argument; the session token is kept in<local data>/credentials/elabora-session(0600) and never printed;--invitation <file>|- --space <space>keeps a tester’s key instead (credentials/elabora-invitation);ELABORA_CLOUD_KEY_FILEandELABORA_CLOUD_SPACEfor scripts. A credential is only sent to the API it was made for.elabora publish [dir] [--private] [--name] [--image] [--index] [--progress] [--navigation] [--review-origin]: builds the course for the web and publishes it in the author’s space (public,noindexunless--index; or--private, a review build named<id>-review, sent to its open reviews); a folderelabora build --target webwrote is published as it is.elabora review create | list | link | close | open | delete | comments | reply | resolve | reopen: the link is printed once (or written to--link-out, 0600);review comments [--since <rev>] [--json]gives, for each comment, the screen (key, title, source file), the block (id, type, JSON pointer), the step, layer, line and word, the version commented withnewerInReviewandscreenChanged, the author, the text, the state and the replies.- MCP:
elabora_review_list,elabora_review_comments(read-only,openWorldHint: true, a newopenWorldfield of tools): the course’s reviews and their placed comments for the author’s AI. No tool publishes, shares, answers or resolves (D43). The authoring skill and the server’s instructions say how to work from comments.
Unreleased — the review bridge (2026-10-14)
elabora build --target web --review-origin <origin>(D42; repeatable; MCPelabora_buildweb.review { origins }, the engine’s build requestweb.review,build({ target: 'web', web: { review } })): a module for the Revue. It carrieselabora/review.js, loaded after the player, and speakselabora-review/1with the review page that frames it, only with the origins named (httpsorhttpon this computer; new codeE_WEB_REVIEW_ORIGIN):ready,layout,location,blocks,went,measured,errorto the page;hello,go,pause,resume,layout,measure,scrollfrom it. docs/review-bridge.md.- A review build’s defaults are progress
none, navigationfree,noindex; its course data hasdelivery.review { origins, languages }, its reportweb.review, its proof dossier the origins. A build without--review-originis unchanged, byte for byte: the player’s files are the same. @elabora/player/review-protocol: the protocol’s types, the origin rule (reviewOrigin), the text sizes of player.css (SIZE_RULES,textScale,widestColumn), pointing (blockAt) and the comment anchor (anchorFor:{ version, language, screen, block?, step?, layer?, time?, line?, word?, occurrence?, point?, frame }), for review pages too.- The canvas’s block boxes and wheel passing move to
@elabora/playerlayout-report.ts, shared by the canvas and the bridge (the canvas’s messages are unchanged). - A list flag of the command line given again adds to it (
--review-origin a --review-origin b,--profile moodle --profile docebo).
Unreleased — the samples’ notices and credits, for the showcase (2026-10-14)
- A free object’s legal comments ship with it: the build keeps
/*! … */,@licenseand@preservecomments of an object’s code at the end of itsobject.js(esbuildlegalComments: "eof", it dropped them); objects without one build to the same bytes (docs/free-object-contract.md). - orbital-rendezvous carries the copyright notice and MIT licence of webgl-noise’s simplex noise (Ashima Arts, Ian McEwan, Stefan Gustavson) in
objects/phasing-lab/noise.js, and so in every package;NOTICEnames it. - The Pocket TTS voices’ credits are complete (
packages/node/voices/pocket-tts/, D25): the model’s authors, its address and licence with its link (CC-BY 4.0), KevinAHM’s ONNX conversion; for Claire, the CML-TTS corpus’s authors, its address, licence and link, and the changes (cleaned by Kyutai, made into a synthetic voice); for the CC0 voices, the voices’ repository. diagnostic-reseau’s takes carry the new credits (their sidecars rewritten throughset_narration_audio, the audio untouched). - The player links the addresses of credits (the outline and the transcript), in a new tab without referrer, long addresses wrapped.
Unreleased — a web target beside SCORM 1.2 (2026-10-14)
elabora build --target web(D41; MCPelabora_buildtarget: "web", the engine’s build requesttargetandweb,build({ target: 'web', web })): a module for a plain web server,dist/<course>-<version>-web/and its ZIP, deterministic, withoutimsmanifest.xmlnor LMS checks (validateProjecttarget: 'web'). Itsindex.htmlcarries the course’s description, Open Graph and a Twitter card;--url(canonical,og:url),--image(a PNG, JPEG or WebP shared asshare.<ext>),--noindex. New codesE_WEB_URL,E_WEB_IMAGE,W_WEB_IMAGE_WITHOUT_URL. docs/delivery-web.md.--progress local|none: the player’s web adapter (@elabora/player/web,createWebAdapter) keeps the lesson location, status, score and suspend data inlocalStorage(elabora:web:<course id>), or nothing at all (the narration’s preferences included); no LMS search, no banner, no Quit. SCORM packages and the preview are unchanged.--navigation authored|free: free opens the course data (navigationmode: free, nolockUntilComplete, the outline shown, narrations’seek: seen→free). Defaults:local+authored(a publication); a showcase isnone+free.- Leaving a module for the web (an event’s
exitaction; the Quit button is hidden there) says where the progress is: « Votre progression est enregistrée dans ce navigateur » withlocal, « Rien n’est enregistré » withnone, instead of the LMS’s « Votre progression est enregistrée » (labelswebEndedKept,webEndedNotKept, in the seven languages; a course in another language gives them incourse.ui). - The build report has
target: "web"andweb; itsbudget.suspendDataLimitisnullthere,profilesempty; the proof dossier says « Diffusion web » and asks for a trial at the published address.
Unreleased — the ElevenLabs key in one place (2026-10-14)
- The author’s ElevenLabs key lives in one place (D40, amends D29): the 0600 file the command line and agents read when the author lets their AI use it (the default;
elabora voice key setwrites it), else the app’s keychain only; the app uses the keychain only after the author’s gesture, never at startup or when a module opens. Comments ofkey.ts, the engine’ssetVoiceKeyand the architecture say so.
Unreleased — the name Elabora (2026-10-13)
- The product’s name is Elabora (D39): packages
@elabora/*, the command, the MCP server and the tools, the skills, the variablesELABORA_*, the schemas underhttps://elabora.studio/schemas/. - Format
elabora/0.1, the first: its chain of migrations is empty, and the mechanism (elabora migrate, the engine’smigrateoption, the library import) is tested with a step of the tests’ own (checkFormat(files, chain),migrateProject(dir, { steps })). The SCORM manifest’s identifier iselabora-<course id>. ELABORA_LOCAL_DATA_DIRmoves the local data folder; the test suites’ global setup gives every run its own, with the model folders. A key the system’s storage cannot decrypt reads as no key.
Unreleased — every optional download in one list (2026-10-13)
@elabora/nodedownloads/(D38): one registry of what Elabora downloads once per computer, never into a course: a kind registers its title, what it serves, what happens without it, its downloads by language with their state (on this computer with its size on disk, or the size to download; a download under way in any process; in use) and how to install and remove one. The local voices (voices-fr,voices-en) and the word aligners (aligner-fr,aligner-en) are the first two kinds.elabora downloads list|install|uninstalland MCPelabora_downloads,elabora_download_install,elabora_download_uninstall, which replaceelabora_voice_modelsandelabora_voice_uninstallof the entry below (never released);elabora voice models,voice installandvoice uninstallstay as the voices’ part,voice uninstall <lang>beingdownloads uninstall voices-<lang>.- The word aligners are held while they work (an alignment, a transcription,
elabora narration align), so that removing one is refused meanwhile (E_ALIGN_IN_USE); their process is stopped before a removal.ELABORA_ALIGNER_MODELS_MANIFESTgives an aligner made-up files, for tests only. - Tests never reach the author’s models:
npm test,test:e2eandtest:matrixrun with a global setup (test/isolate.ts) that givesELABORA_VOICE_MODELS_DIRandELABORA_ALIGNER_MODELS_DIRfolders of the run unless they are set;scripts/pocket-voices.tskeeps its model in~/.cache/elabora-dev/pocket-tts.
Unreleased — local voices removed and reinstalled (2026-10-13)
elabora voice models [--language] [--json](MCPelabora_voice_models,@elabora/nodelocalVoices, D37): the local voices by language, their names, and their model on this computer (its size on disk) or not (its download’s size), a download under way in any process (bytes read from the disk) and whether lines are being said with it.elabora voice uninstall <language>(MCPelabora_voice_uninstall,uninstallVoices, provideruninstall): removes a language’s model, refused withE_VOICE_IN_USEwhile a generation says lines with it or it is being downloaded, in any process; the lines already made keep their audio, and the next generation downloads it again.elabora voice install <language> --force(MCPforce) removes it and downloads it again.- Generations hold the models they speak with (provider
hold,models/download.tsholdFiles), and the download lock moved beside the folder (<bundle>.lock, with what holds it), so that a hold and a removal never cross;removeFilesrenames the folder away before deleting it. The word aligner’s downloads share the lock. ELABORA_VOICE_MODELS_MANIFEST: made-up files for a bundle, for tests only.
Unreleased — a ledger of what paid AI services charge (2026-10-13)
ai-costs/(D36, README there):measurements.jsonl, one measured request a line (provider, exact model id, kind, settings, plan, UTC date, units sent and billed as maps, so that tokens, images or seconds fit as characters do, the price period, the source), andperiods.json, each provider, model and plan’s price periods (list price per unit, by settings and per a quantity; a discount’s rate, announced end and source). It starts with the 49 ElevenLabs requests of 12 and 13 October and 5 bought for it (40 credits: Eleven v3 and v4 Turbo).- Averages generated, never written:
npm run genwrites@elabora/coreai/cost-ledger.generated.tsby provider, model, plan, price settings and period, and refuses a measurement in no period or two, a unit with no price, a request recorded twice or periods that overlap (ledgerProblems); a unit test fails when the module is stale. - The voice estimate reads them (
expectedShare,@elabora/nodeexpectedCharge,chargedShare(plan, model, rate)): only the current period’s average, its discount taken off, while its list price is the oneGET /v1/modelssays; the list price where the period has fewer than 3 requests (Multilingual v2), for a model, plan or list price not measured, and once a discount with no known rate runs; never an older period’s figures. Eleven v4 on a Starter plan is told 0.4005 of the list price (the course’s 1,163 characters 466 credits for 465 charged), v4 Turbo and v3 0.4. The author’s own charges (<cache>/charges.json, with date, plan and list price;rates.json, without dates, is dropped) take over under the same keys and periods: a rise from the first request, from three requests and a quarter more where the ledger has nothing, never under the ledger, never past the list price; for a plan with no period, only the requests since the model’s last change of price. npm run costs:record [-- --dry-run], a tool for our experiments: appends the ElevenLabs history’s new requests once (history_item_id) with the plan andGET /v1/models’ multipliers at that moment, says a list price, a discount or a share the current period does not have, and generates the averages again.
Unreleased — a plan never validated is the agent’s draft (2026-10-13)
elabora plan draft [dir] (--from <plan.json> | --patch <patch.json>)(MCPelabora_draft_plan, editdraftPlan, D35): while the plan has never been validated (not approved, no approval, no change request ever applied:@elabora/coreplanEverApproved,planFrozen), the author’s agent writes it, whole or as a JSON Patch, its schema checked before anything is written; it stays a draft. Once the author or the client validated it, the command refuses and names the change request to propose.elabora plan statussays « never validated »;elabora_get_plangivesvalidatedOnce;elabora plan voiceoverandelabora_set_plan_voiceoverwrite in a plan never validated and propose once it was. Rule 1 of the courseAGENTS.md(revision 28), the authoring skill,W_PLAN_DRAFT,E_PLAN_ITEM_UNCOVEREDandE_PLAN_MODIFIED_AFTER_APPROVALsay so.- Fixes from the independent review of the four changes below: an inline SVG’s
<style>is foreign content in the page, so its<and character references are now written as CSS escapes (a<![CDATA[holding<img onerror>became an element); declarations that could load anything are dropped however written (CSS escapes, entities,image-set(),src(), inside@keyframes), as are selectors reaching the drawing’s siblings andposition: fixed; every id the drawing names is prefixed. The draw effect reads arc flags written together (a5 5 0 105 5).elabora theme upgradekeeps declarations as written (a string holding/*) and, beside overrides the author already wrote, puts what it moves in a sublayer under them.elabora undo,redoandhistoryno longer update AGENTS.md first; a read-only MCP server only says what it finds; a command finds its course among all its arguments.W_NEUROMYTHon a screen is reported per block, keyed by its text;W_NARRATION_LINE_LONGis keyed by the line’s text, not its take’s length.
Unreleased — a course’s rules for agents stay in step with Elabora (2026-10-13)
- AGENTS.md (D34): the rules sit between
elabora:rulesmarkers carrying their revision and fingerprint, then « Notes for this course », never touched. Rules nobody edited are brought up to date as a system change (update_agent_rules, undoable) when the app opens the course, an MCP server starts, aelaboracommand runs in it (a line on stderr), orelabora migrateruns; edited ones are left and said; rules from a newer Elabora make an olderelaborasay that commands may be missing.elabora rules [dir]prints the running CLI’s rules;elabora rules update [--force].templates/agents-revisions.json(every revision of the template;npm run genappends one and updates the examples’ and test courses’ AGENTS.md).
Unreleased — more warnings the author may ignore (2026-10-13)
- Keys (D31) on the checks that read the author’s own content and can misjudge a case:
W_QUIZ_NEGATION(a negation inside a quoted maxim),W_QUIZ_ALL_NONE,W_QUIZ_OPTION_COUNT,W_QUIZ_LENGTH_CUE,W_IMAGE_ALT_SUSPECT,W_NEUROMYTHon screens,W_TEXT_DENSITY,W_NARRATION_DUPLICATED,W_NARRATION_LINE_LONG; each comes back when what it read changes (findingKey). Their fixes say when ignoring them is right. elabora ignore [dir] <code> <key> [--restore]and MCPelabora_ignore_finding, for when the author asks;elabora validateprints the key of a finding the author may ignore.
Unreleased — compact quiz feedback (2026-10-13)
feedbackStyle: "compact"on a quiz (and its forks): the feedback of the chosen option without its text in bold first (the option is marked where it is; its text stays in the feedback for screen readers,.t-feedback-option.t-sr-only), the result and the explanation in one paragraph. On the « Nietzsche » evaluation, two questions side by side reserve 388 px instead of 445 each.fullstays the default: existing courses keep their look, and with several options’ feedback at once (multiple choice) each keeps its text. A template may setdata-t-feedback-style="compact"on its group.elabora fitnames, for a slide that scrolls, the quizzes whose compact feedback would reserve less room and by how much (ScreenFit.feedback,__elabora.feedbackRooms()).
Unreleased — figures drawn in the page (2026-10-13)
inline: trueon an image block (D33): an SVG asset, generated or local, is written into the page in place of its<img>, sanitized from an allowlist (no script, handler, link or outside reference; ids prefixed with the block’s; its<style>scoped), named by its alt (role="img"), so that thedraweffect traces its strokes and the theme’s CSS reaches its shapes (@elabora/corerender/svg-inline.ts;svg.t-figure-svg[data-t-svg]).W_SVG_INLINE_SANITIZED,W_IMAGE_INLINE_NOT_SVG,W_IMAGE_INLINE_HEAVY, andW_TIMING_DRAW_IMAGEfordrawon an image the page cannot trace.drawdraws each subpath: a shape’s dash is its longest subpath, so the hatching of an engraving (one path of many lines) grows line by line instead of showing at once (subpathsin@elabora/playertimed-blocks.ts).
Unreleased — the theme’s rules give way to components; overrides are said (2026-10-13)
- Cascade (D32):
elabora.base<elabora.theme<elabora.components<elabora.overrides. The course stylesheet’s rules are the course’s defaults and component stylesheets win over them; the stylesheet’s top-level@layer overrides { … }blocks are what it overrides on purpose (a package keeps them inelabora/overrides.css).course.jsontheme.cascade(format 0.2, optional):components-over-theme, whichelabora initwrites; a course without it keeps the order of 6 October (theme-over-components, the theme after the components) and its look, and is told (I_THEME_CASCADE_OLD).elabora/player.cssnow nameselabora.basealone;elabora/theme.cssand the canvas start with the course’s order (DesignStylesheets.cascade,.order;LAYER_ORDERgives way toBASE_LAYERandlayerOrder). elabora theme upgrade [--dry-run](MCPelabora_upgrade_theme, editupgradeThemeCascade): a course moves to the new order in one undoable change, its look kept: the theme’s declarations of properties the components also set go into@layer overrides(upgradeTheme,propertyFamily,splitOverridesin@elabora/corerender/cascade.ts; a CSS reader of ranges inproject/css-tree.ts).- Who wins, measured in the page (
@elabora/nodestyle-cascade.ts):elabora lookandelabora_looksay, andelabora fitreports,W_STYLE_COMPONENT_OVERRIDDEN(a component’s rule a theme rule overrides without naming the component) andW_STYLE_THEME_UNDER_COMPONENT(a theme rule for a component that gives way to it).LookResult.styles,ScreenFit.styles.W_STYLE_OVERRIDES_NESTED: an@layer overridesblock inside another rule. - The examples set
theme.cascade; medieval-charts’ gloss notes now set their emphasis in roman, as the gloss component asked.
Unreleased — seven faults met building the « Nietzsche » course (2026-10-13)
- A look server of older code is replaced (
@elabora/nodecode-version.ts): the server aelabora lookstarted kept answering newer command lines, because the version it told, and the client compared, was the package’s (0.1.0 for weeks), so captures were rendered by the old code (subtitles showed « [chuchote] »). The version is now<package version>+<hash of the sources>(of the bundles beside the entry in a packaged app); a command whose own differs stops the server (by its pid when it does not stop) and starts its own. elabora narration transcribeprinted nothing and exited 0. The aligner’s process is not held while idle;dispose()stopped it and waited for its exit, nothing else held the command’s process, Node ended it before the answer was printed. A process being stopped is now held until it has gone; and a command whose promise nobody settles ends withE_COMMAND_UNSETTLEDon stderr and exit code 70, never as a silent success (packages/cli/src/unsettled.ts).elabora voice estimateno longer undershoots (D30): the 8 lines of two narrated screens were told ~200 credits (0.5 % of a Starter quota, so no approval was asked) and charged 465 (1.2 %). Cause found with ElevenLabs’ history and 110 credits of experiments: a Starter plan is charged 0.4 of the list price for every model (30 characters: 12 credits on Eleven v4 and Multilingual v2, 6 on v4 Turbo; a dialogue, context text, timestamps, format and voice change nothing), and the 0.11 of 12 and 13 October was a promotion that ended on the morning of the 13th. The estimate is the plan’s undiscounted rate (0.4, never counting on a discount), the list price for a plan not measured until its own requests say, and follows a rise at once. A line of 70 characters is told about 0.07 % of the quota.elabora screenshotwith several shots refuses--out(exit code 2, asking for--out-dir), aselabora lookdoes; it wrote the files intoscreenshots/of the folder it ran in, unasked, and nothing at the path given.--outwith--out-diris refused too.- Animated SVG figures are captured drawn (
look,screenshot,assets --sheet): the page’s reduced-motion setting does not reach an SVG shown through<img>, whose animations started from nothing. The capture browser is launched with--force-prefers-reduced-motion(launchBrowserreducedMotion,launchMotionBrowsers: another browser, started only when asked, for pages with animations on). - A change request is checked before the client applies it:
elabora validate(andelabora_validate) applies each pending request to the plan in memory and reports the one that cannot be applied asW_CHANGE_REQUEST_INVALIDwith its id (aspacingover 50 characters, a patch that no longer applies);elabora_propose_plan_changerefuses a patch that would makeplan.jsoninvalid, with why, and writes nothing. - AGENTS.md says where the Elabora app carries the command line when
elaborais not in the PATH (macOS and Windows launchers of the packaged app).
Unreleased — stage directions are not read, a swallowed word may be ignored (2026-10-13)
- A line’s speed counts what learners read.
W_TEXT_TOO_FASTon a narration line counted its stage directions (D29) as characters to read: « [agacé, au bord de l’explosion] Ça fait trois fois ! » was said too fast for its captions at 27 characters a second; it is now measured without them. Words placed by estimate (lineWordTimeswithout word times) no longer give the directions time (a line opening with one had its first word late), and on-screen text that repeats a line (W_NARRATION_DUPLICATED) is found when directions stand between the line’s words. - Ignored by the author (D31).
W_NARRATION_WORD_SWALLOWEDis one finding per word, with akey(the line, the word and its take’s audio:swallowedKey), its score asmeasuredand 0.25 aslimit. The new commandignore_findings { findings: [{ code, key }], ignored }(engine,@elabora/nodeedit) writescourse.jsonignored(format 0.2, optional); the validator, the engine and the build mark that findingignoredand no longer count it (summary.ignored); another take brings it back.elabora validatedoes not list it,elabora_validateleaves it out (includeIgnored), as do the MCP’s edit, undo and build results, the alignment’sdoubtfuldoes not report it again, the proof dossier says it is ignored. Errors cannot be ignored. - The narration view gives each line its speaker’s voice in the language (
speakerVoice): the editor knows which lines a paid voice would say without reading course.json again.
Unreleased — a dialogue is cut where the aligner hears its turns (2026-10-13)
- An interrupted « et — » no longer loses its word. In the « Essais ElevenLabs » test module, ElevenLabs’ times for the turns after the first of an exchange ran 0.38 s ahead of its audio, so the cut after the client’s « et — » fell before it: the line’s alignment flagged « et » as swallowed (it was not said again alone: same take, the word is there at score 1).
generateNarrationnow aligns the whole exchange with the language’s aligner when it is on the computer (DialogueRequest.hear) andsplitDialoguecuts in the first silence after a turn’s last word (a breath or a sigh before the next turn’s first word stays with that turn); the service’s word times move onto the audio. Without an aligner the cut is where the service’s times say, as before. The other flag of that module, « de » in « trois de trop » (score 0.01 in the exchange, in a take of the line said alone and on the spectrogram), is a real swallow.
Unreleased — costs as a share of the quota, estimates the service charges (2026-10-13)
- One language for paid AI services (D30,
@elabora/coreai/quota.ts): what they cost is a share of the author’s monthly quota, never credits (quotaPercent: « 0.02 », « 2 »), and one threshold,APPROVAL_SHARE(1 %): under it an action runs and the author’s AI needs no approval; from it, the app confirms and agents ask. - ElevenLabs estimates follow what the service charges (D29): the list price (a credit a character times the model’s and the voice’s rates) is now the
ceiling;creditsis what Eleven v4 is charged through the API, about 0.11 of it (8 credits for a line of 74 characters on 13 October 2026, after its launch discount), learnt from each request’scharacter-costheader (voice/elevenlabs/costs.ts). An estimate carries the month’squota, itspercentandneedsApproval; a generation’sspentthe credits charged and theirpercent. - Approval in percent:
elabora voice generate --spend <percent>(was credits), MCPapprovedPercent(wasapprovedCredits), engineapprovedPercent(wasmaxCredits). Under 1 % of the quota nothing needs approval; a generation charged half as much again as allowed stops (overBudget).voice accountandelabora_voice_accountsayusedPercentandlinePercent, and whether the plan may use the voice library (library: not the free plan);account({ key })checks a key before it is kept. - The cache of ElevenLabs answers keeps another address’s answers apart (an imitation in tests never answers a real request).
- An approval covers whatever was told with its figure (« 1 % » up to 1.49 %) and a quarter more; a library voice’s higher price is kept on the computer (
<cache>/voice-rates.json) and priced after a restart; a paid sample obeys the 1 % threshold; an exchange’s name in its takes is unique to its generation (<screen>-<stamp>-<n>), so that two exchanges of a screen are never taken for one. - The cache of ElevenLabs answers moves to
<local data>/elevenlabs/cache(it was<local data>/cache/elevenlabs: on macOS, inside the app’s ChromiumCachefolder, the file system ignoring case); what it kept is moved there once.
Unreleased — ElevenLabs voices: stage directions, dialogue, credits (2026-10-12)
- Stage directions (D29): words in square brackets in a narration line (
[chuchote],[rit],[un temps]) tell an expressive voice how to say it. They stay in the text (hash, translation) but are never words, captions, transcript nor estimated time, and pronunciations skip them (@elabora/corelineDirections,maskDirections,withoutDirections,playsDirections). XLIFF wraps a narration line’s in<mrk type="elabora:direction">, with a note for translators.I_NARRATION_DIRECTIONS_IGNOREDwhen the speaker’s voice cannot play them; Pocket TTS is given the line without them. - ElevenLabs (
provider: "elevenlabs", modeleleven_v4by default, settingsstability,similarity): the author’s key (elabora voice key set|remove|status,ELEVENLABS_API_KEY, the app’s in memory), never written in a module or shown unmasked; voices searched in the account and the library (elabora voice search, MCPelabora_voice_search, enginevoiceSearch); the plan and its credits (elabora voice account [--check], MCPelabora_voice_account, enginevoiceAccount);speakers[].voices[<lang>].name(format 0.2, optional). - Credits approved before they are spent:
elabora voice estimate(MCPelabora_voice_estimate, enginevoiceEstimate);elabora voice generate --spend <credits>(exit code 3 and the estimate without it; MCPapprovedCredits,needsApproval); an out-of-date line gets back an earlier take of the same text, voice and delivery; answers cached on the computer. - Dialogue:
elabora voice generate --dialogue(MCPdialogue) says a step’s lines by several speakers as one exchange, cut into lines, their pauses from it. voice installand the engine’sinstallVoicesleave paid providers out;voiceSamplerefuses a paid voice (E_VOICE_PAID: its preview is free).
Unreleased — long recordings no longer stop the engine (2026-10-12)
- Aligner (D27): importing a recording of 90 s stopped the engine (ONNX Runtime aborted in an allocation inside the aligner’s worker thread, and took the process with it). The audio is now read in windows of 10 s whose outer 2 s are given up, the memory arena is off, and the aligner runs in a process of its own (
align/process.ts): peak memory on that recording 2.44 GB before, 1.12 GB after (the model itself 0.8 GB); word starts at the seams stay within 28 ms of those of each line alone (median 8 ms, as without windows). If ONNX Runtime still aborts, the line fails withE_ALIGN_CRASHEDand the engine goes on. A recording tried against several lines is read once. W_NARRATION_LINE_LONG: a warning for a line of more than 45 s of audio, suggesting it be cut into lines of a few sentences; nothing is blocked.
Unreleased — the stack’s contents share one place (2026-10-12)
- Stack (
stack, « Pile »): its contents now always share one grid cell, the size of the largest, in the player, the editor’s canvas, captures and at a phone’s width. The grid rule was outweighed by the slot’s flex column (.t-slot) and only held on screens with a seek bar, so in the editor’s canvas the second content showed under the first and the layout kept holes. An end-to-end test measures the stack in the canvas at three moments and at the end, at 1280 and 375 px.
Unreleased — the fine timeline, part 6: what the editor asked for (2026-10-12)
- Canvas:
elabora/viewmessages posted by the app takeat(the playhead, while the author drags it) andghost(every step, those after the first faded at 0.42 in their place: the phone width; alsocanvasView,renderScreen);BlockLayout.timedgives each timed block’s state (before,entering,shown,leaving,after) and its effect’s progress; a timed block not there ishidden. - Effects:
effectsOfand engine requesteffects(the presets then the theme’s, default durations, directions, what they do). - Findings with details:
E_TIMELINE_ANCHOR_MISSING(screen, block or event, kind, anchor, what it lost),W_TEXT_TOO_FAST(shown, needed, from, until; or a line’s characters a second),E_ACTIVITY_LEAVES_UNANSWERED(leaves, next stop),E_TIMING_ORDER,W_TIMING_STEP,W_MOTION_LONG,I_TIMELINE_WORDS_ESTIMATED,W_SEGMENT_TOO_LONG(step, its start, end and lines),W_NARRATION_WORD_SWALLOWED(line, words and scores). - Transcription (D27):
transcribeRecording(engine),elabora narration transcribe: what a recording says, its words timed, by the word aligner’s model (a draft: 28 % of French words and 20 % of English ones wrong on the product’s voices). keep_narration_audio(elabora narration keep, « La voix dit bien ce texte »): a line whose text was corrected keeps its audio; its words follow.- The plan’s voice-over is written by
set_plan_voiceover(elabora plan voiceover --from file.txt, MCPelabora_set_plan_voiceover, which proposes): in a draft plan directly, in an approved one as a change request.
Unreleased — the fine timeline, part 4: what the editor and the review need (2026-10-12)
- Marks:
@elabora/coretimelineMarks(a timed screen’s start, each block that comes or goes, each cue, each step’s end, with the time to show each and a caption);elabora look --sheet(MCPelabora_looksheet) captures one per mark on a contact sheet; engine requesttimelineMarksfor the Revue’s storyboard.elabora_lookattakes any moment (l2:imprimante+0.3s,1.5s@2,step2:end,12.5) and alanguage. - Steps: command
split_step(elabora narration split, « Couper à une réplique », the answer toW_SEGMENT_TOO_LONG): a line and those after it in its step start the next step; later steps, their blocks,steps.<n>, step anchors andreveal-stepactions move one on; blocks that come on a moved line move with it. - The plan’s voice-over: plan items’
voiceover(format 0.2, compatible: the voice-over the client validated, line by line, translated with the plan); commandnarration_from_plan(elabora narration from-plan, « Reprendre la voix off du plan ») and engine requestplanVoiceover. - Recordings:
importNarrationFiles(elabora narration import-files, engineimportNarrationFiles): each file to the line its name gives (l2.wav,accueil-l2.m4a,02.wav), the others to the line whose text the word aligner finds in it; one change a line. - Waveform: engine request
narrationPeaks(the loudest sample of each slice of a line’s audio, 0 to 255, cached per audio file). duplicate_screencopies the lines’ audio with sidecars naming the copy.- The example
diagnostic-reseau: its narration aligned (words in the sidecars); on « accueil » the ticket comes on « impression » and the module’s aims on « trajet »; on « chemin » the network path is wiped in on « schéma », a stack shows the floor’s network on « ordinateur » then the servers’ on « routeur », and the rule rises on « contrôlez ». - Fix: the engine protocol refused
timelineandtimelineAt(missing from its list of requests); the list is now checked complete by the compiler.
Unreleased — the fine timeline, part 3: word times by forced alignment (2026-10-12)
D27. Word anchors now take the times the voice says their words.
- Aligner (
@elabora/nodealign/): a wav2vec2 CTC character model per language (Frenchwav2vec2-large-xlsr-53-frenchint8, 355 MB; Englishwav2vec2-base-960hquantized, 95 MB; Apache-2.0) run by onnxruntime-node 1.23.2 in a worker thread, and a Viterbi search in TypeScript. Numbers, times and symbols are spelt out (French and English), elisions run on, other words fit the model’s vocabulary or a wildcard. Measured on the product’s voices: word starts within 22 to 24 ms (median, 20 ms correction applied), 9 in 10 within 61 and 49 ms. Models download once per language into the computer’s local data (ELABORA_ALIGNER_MODELS_DIR,ELABORA_ALIGNER_MODELS_URL), pinned and checked; the download code is shared with the voices (models/download.ts). - Sidecars: lines a voice makes or a recording imported are timed at once when their language’s aligner is installed (
ELABORA_ALIGN=0turns it off); every new sidecar keeps thetextits audio says; words carry ascore;alignersays what timed them. - Commands and tools:
set_narration_words(words into a line’s sidecar, refused when its audio changed meanwhile);elabora narration align [dir] [screen] [--line] [--language] [--force], MCPelabora_align_narration, engine requestsalignersandalignNarration(alignProgressevents, stopped bycancelVoice);NarrationLineInfo.aligner. - Validation:
W_NARRATION_WORD_SWALLOWED, a word of two letters or more the alignment scores under 0.25 (the voice may have swallowed it: another take).
Unreleased — the fine timeline, part 2: word anchors through translation (2026-10-12)
- XLIFF markers: a narration line’s unit carries the words its anchors name as
<mrk id="w-<words>" type="elabora:anchor">in its source (with a note for translators); the translation places each around its own words.readXliffreads markers apart from the text (sourceMarks,targetMarks; other inline tags are dropped, their words kept),toXliffwrites them. - The translated course anchors to the words the markers are placed on (
translatedCourserewrites block timings, cues andseek-timelinetargets) and its voice gives them their times. A marker left to place is refused byelabora translate build, anchored to its line’s start with--allow-missing(TranslationBuild.unplaced). - Status and table:
translationStatuslistsanchors.toPlace;translationTablerows of anchored lines giveanchors(each with the words it is placed on, or null),sourceMarkedandtargetMarked.translate_text(elabora_translate_text) takes markers intarget, oranchors: [{ id, word, occurrence? }]to place them by naming the words; markers already placed stay when a target is written again without them; an unknown marker id is refused. - Timelines in a language:
screenTimeline(project, screen, { language })anchors to the translation’s words; an unplaced marker is an anchor problem of kindunplaced.
Unreleased — the fine timeline, part 1: timed blocks and anchors (2026-10-11)
Phase 2 of the slide timeline (D28). Compatible additions to format 0.2.
- Anchors (
defs.schema.jsonanchor): a line, a word or a few words of it (word,occurrence), its end (edge), with anoffset; seconds from a step’s start on screens without narration (seconds,step); a step’s start or end. Event cues (timeline.reached) take them all; new actionseek-timeline(to): the playhead goes there as a seek, firing nothing it passes. - Timed blocks:
block.timing.inand.out(at,effect,duration,direction) with the effectsnone,fade,rise,slide,zoom,wipe,drawand the theme’s (course.jsontheme.effects,@keyframes t-fx-<name>). The player sets each effect (a Web Animation created paused) to the timeline’s time at every move: playing, seeking, the speed and captures are one computation. A timed block keeps its place hidden and inert before and after (data-t-timed); focus inside one a seek hides goes to the seek bar. Reduced motion: a short fade at the same time. - Screens timed in seconds, without narration, have a timeline too: a bar (no captions), the page’s clock,
steps[n].duration. stackblock (« Pile »): contents that follow one another in one place, the size of the largest (transitioncrossfade,cut,slide).- Steps’ stretches end when their last line or their last effect ends: the build lays the track’s silences from the timeline (
TimelineLayout.gaps). - Word times:
narrationWords(one tokenizer; French elisions are words), a line’s words from its sidecar’swords, followed through a changed text, estimated without. Sidecar additions:text(what the audio says),aligner, words’score. - The bar: dragging the playhead holds playback, the screen follows, a bubble says the time and the words said, playback goes on from where it is let go; the shortcuts list (Shift+? on the bar, « Raccourcis clavier » in the settings); at the end of a step the live region says it once. A screen without voice has no captions, mute or transcript button. New labels in the seven languages:
shortcuts,shortcutPlay,shortcutReplay,shortcutCaptions,shortcutSeek,shortcutStep,shortcutEnds,shortcutList,stepEnded,onScreen. - Validation:
E_TIMELINE_ANCHOR_MISSING,E_TIMING_SECONDS_NARRATED,W_TIMING_IGNORED,E_TIMING_EFFECT_UNKNOWN,W_THEME_EFFECT_UNDEFINED,E_TIMING_ORDER,W_TIMING_STEP,W_TEXT_TOO_FAST(reading time of what appears; captions over 25 characters a second),E_ACTIVITY_LEAVES_UNANSWERED,W_MOTION_LONG(WCAG 2.2.2),W_STACK_UNTIMED,I_TIMELINE_WORDS_ESTIMATED;E_TIMELINE_CUEnow accepts seconds without narration. - Commands and tools:
set_block_timing(patches ofinandout),update_narration_linetakes word anchors along with their words (anchors.moved,anchors.lost); CLIelabora timing [dir] <block> --in l2:imprimante --effect rise …, MCPelabora_set_timing;elabora timelinelists timed blocks and anchored cues;elabora look --atandelabora screenshot --attake any moment (l2:imprimante,l2:end-0.2s,1.5s@2,step2:end,12.5). - Engine: requests
timeline(lines with their words, offsets and times, timed blocks, cues:@elabora/coretimelineView) andtimelineAt(what is seen at a time:screenAt);canvasViewandrenderScreentakeat(a time, or an anchor resolved in the language shown); commandset_block_timing.
Unreleased — narration for the app’s editor (2026-10-11)
- The voice speaks in a worker thread (
@elabora/nodevoice/pocket/thread.ts,worker.ts; D25): the engine and the MCP server go on answering while a line is said (their longest wait went from 800 ms to a few), and a generation stops between two steps of the model. A stopped generation keeps the lines said so far and sayscancelled. - Engine requests:
narration(a narrated screen as an editor shows it: advance and seek as they apply and as the screen sets them, each line in a language with its audio’s state, its provider, take and credit, and its place on the timeline, estimated without audio;@elabora/corenarration/view.ts),cancelVoice(a screen’s generation, or all of them and the downloads),voiceSample(a few words said by a voice, written nowhere: WAV bytes;E_VOICE_NOT_INSTALLEDwhile its model is not on the computer). - Why a line is missing or out of date, as codes (
NarrationLineInfo.causes:untranslated,no-audio,no-sidecar,text,audio-replaced,elsewhere,voice,pronunciation,delivery) beside the reasons in English, for an editor to word them. - The canvas host serves a narration line’s audio (
assets/narration/<language>/<screen>/<line>.m4a), which no render lists, so that the editor plays it. ELABORA_VOICE_TONE_SECONDS: test tones (ELABORA_VOICE_TONES=1) take that long to say, with progress, and stop on their signal.
Unreleased — author narration through MCP (2026-10-08)
elabora_update_courseexposesspeakers,pronunciationsand the course narration defaults.elabora_update_screenexposesnarration(the script and playback settings; null removes it). Agents can now prepare a narrated screen before calling the voice tools without editing JSON files themselves.- The diagnostic network example includes a French dialogue and an explanation of the network path, with real Pocket TTS audio, captions and voice credits.
Unreleased — the voices’ credits in front of learners (2026-10-08)
- The course data carries the credits of the voices (their sidecars’
credit, the attribution CC-BY voices such as those of Pocket TTS ask for), each once:narration.creditson each narrated screen,creditsfor the module (keys left out without credits: other courses keep their bytes). - The player shows them: a « Crédits » section ends the transcript of a narrated screen (its voices) and the module’s outline (all its voices). Label
creditsin the seven languages; hooks.t-timeline-credits,.t-menu-credits.
Unreleased — free local voices for narration (2026-10-08)
Decision D25 (docs/decisions.md): Kyutai’s Pocket TTS, free, on the author’s computer, the same voices on Mac and Windows.
@elabora/nodevoice/: voice providers (VoiceProvider:voices,status,install,synthesize,dispose;listVoices,findVoice,voiceProvider);voice/pocket/runs Pocket TTS with ONNX Runtime (a TypeScript port of Kyutai’s generation: its Unigram tokenizer, text preparation and chunking, flow language model and Mimi decoder; failed chunks said again; seeded takes), its model downloaded once per language into the computer’s local data (fr 354 MB, en 126 MB; pinned, checked by SHA-256, resumed;ELABORA_VOICE_MODELS_DIR,ELABORA_VOICE_MODELS_URL);generateNarrationsays a screen’s lines with their speakers’ voices and writes each as a take throughsetNarrationAudio(take,credit,model), the takes kept in the project’s data;setSpeakerVoice,voiceTakes.ELABORA_VOICE_TONES=1adds test tones.- Voices: Estelle, Claire, Henri, Rémy, Jérôme (French), Grace, Nora, Iris, Owen, Felix (English), made from recordings given for speech synthesis (Kyutai’s Unmute donations and its own recordings, CC0; one reader of CML-TTS, CC-BY 4.0), each checked on five takes of six lines.
packages/node/voices/pocket-tts/(sources.jsonby hand;voices.jsonand the embeddings byscripts/pocket-voices.ts). - Agents:
elabora voice list|install|set|generate|takes, MCPelabora_voice_list,elabora_voice_set,elabora_voice_generate,elabora_voice_takes,elabora_voice_install; the course AGENTS.md and the skillelabora-course-authoringtell how. The help and the MCP catalogue (test fixtures) gain them. - Engine: requests
voices,installVoices,setSpeakerVoice,generateVoice,voiceTakes, andvoiceProgressevents; what a voice makes goes through the session’s queue. - New optional dependency:
onnxruntime-node1.23.2 (the last with a binary for Intel Macs).
Unreleased — the narrated player as the app’s validated mockups have it (2026-10-08)
The app’s mockups of the timeline were validated on 8 October; the player keeps their behaviours and structure, its final look coming with the app’s stylesheet.
- Three ways to advance (format 0.2, compatible):
advancelearner(default),auto(the steps chain, then the screen waits for Next) andnext-screen(they chain, then the next screen comes by itself).W_AUTO_ADVANCE(WCAG 2.2.1) is fornext-screenonly.course.jsonnarrationgives the course’s defaults ofadvanceandseek. - The bar in the footer, before Previous, in place of the counter: the footer keeps its height on every screen (
#elabora-app[data-timeline]hides the counter). Icon buttons; the part of the seek bar not heard yet is dotted, the part heard solid, a gap at each step’s end.seek: "none"has no bar: the time and the step in words. - Continue in the screen: a narrated slide keeps the place of its own Continue at its end from the start (
.t-continue--slot, hidden withdata-t-waitingwhile a step plays), shown at the end of the step heard (seekseen,none) or at any time (free); on phones and in scroll pages it comes in the page where the next step opens. At the end of a step, Play does what Continue does. Next goes to the next screen (withnavigation.lockUntilComplete, once the screen has been heard: « Écoutez l’écran jusqu’au bout pour continuer. »). - Playback settings under « 1× »: the speeds and « M’arrêter à chaque étape » (offered when the module has screens that chain). Below 1280 px, Mute and Transcript go into « … »; on phones the settings too, Captions staying in the bar.
- The learner’s Pause stops everything: the voice, the animations (paused, then played again) and the free objects. Space acts on the bar’s buttons only (no longer on the seek bar).
- Captions name the speaker when it changes. The transcript goes by step (« Étape 1 »…), with a close button; a line already heard takes the playhead there, one not heard yet is disabled.
- The large Play button says « Écouter l’écran » and why (« Le son démarre au premier clic ; ensuite, chaque écran se lance seul. »). New labels in the seven languages:
settings,more,stepOf,stepHeading,playNarrationHint,stopAtStepsHint;seekis « Position ». - The LMS emulator presses the Continue a narrated slide shows (never the one hidden in its kept place).
Unreleased — takes, pronunciations and credits of generated voices (2026-10-08)
Asked by the voice session for elabora voice generate (compatible additions to format 0.2 and to the D24 contract).
- Sidecar (
narration-audio):take(which take of a voice that says a line differently each time is in the course),credit(what the module’s credits say of the voice: the build lists the distinct credits of its tracks in the report,narration.credits, and the proof dossier),model(information: another model does not make a line out of date), and for generated voicesspokenHashandsettingsHash, written bysetNarrationAudioitself from the project, as the validator checks them. - Pronunciations:
course.jsonpronunciations(the course’s lexicon) and a line’s ownpronunciations, by language,{ text, say }: whole words, case-sensitive, the longest first, the line’s before the course’s.@elabora/corespokenText(project, screen, line, language)gives what the voice is told and its hash (applyPronunciations,spokenTextOf); captions and the transcript keep the text. A generated line whose spoken text changed is out of date (W_NARRATION_AUDIO_STALE: « the pronunciation of its words changed »). - Delivery: a line’s
delivery, settings by language over those of the speaker’s voice (deliveryOf); other settings make a generated line out of date.update_narration_linetakesdeliveryandpronunciations,update_coursetakespronunciations.
Unreleased — narration and timed steps, phase 1 of the timeline (2026-10-08)
Decision D24 (docs/decisions.md), approved on 8 October: what is seen at a time is a function of that time and of the steps reached; what happens (events) only when playback crosses a cue, never on a seek; the course state never goes back; steps are the timeline’s segments.
- Format 0.2, compatible additions (no new version nor migration: a course without narration is unchanged and keeps its bytes):
screen.narration:advance(learner, the default: playback stops at the end of each step and Continue plays the next;auto: the steps follow each other and the course goes to the next screen),seek(seen, the default: back anywhere, forward up to the furthest point reached;free;none) andlines[](id, unique in the screen;speaker;text, translated;step;pauseBefore, 0.4 s by default, 0 for the first line).course.speakers[]:id,name(shown in captions and the transcript, translated) andvoicesby language (provider,voice, freesettings), for the voice tools.- Triggers
timeline.reached(at: { line, edge }),timeline.ended,media.ended(target: a sound or a video that is not a loop); actionspause-timeline,play-timeline,play-media,pause-media(block). completeWhen: "viewed"on a narrated screen means its timeline played to its end; withnavigation.lockUntilComplete, Continue also waits for the step to be heard.- Schema
narration-audiofor the sidecar of a line’s audio.
- The audio of a line, by convention:
assets/narration/<language>/<screen>/<line>.m4a(AAC, mono) and its sidecar<line>.json(textHashof the line’s text in that language,duration,provider,voice,generated,audioHash, laterwords).@elabora/corenarrationLinessays for each line whether its audio is there and up to date (ok,missing,stale,untranslated),narrationTextHashhow the text is hashed;elabora renamemoves a screen’s audio with it; removing a line removes its audio (undoable). - The build makes one track per narrated screen (
@elabora/nodenarration/tracks.ts): the lines decoded, laid end to end with their pauses (a step’s first line at least 0.3 s after the previous step), encoded once intonarration/<screen>.m4a(AAC-LC, mono, 48 kHz, 64 kb/s: about 0.5 MB a minute), cached in the project’s data by the hash of its inputs, deterministic, with its WebVTT captions (<v Speaker>, cut at sentences, two lines at most). Cue times come from the decoded samples. Until every line of a screen has its audio, the screen has no track and its timeline runs silent on estimated times (15 characters a second), with its captions. The build report hasnarration(bytes, seconds, tracks, silent screens); package limits count the tracks, and the proof dossier lists generated voices (AI Act). - The player (
@elabora/playertimeline.ts): a timeline per narrated screen whose clock is the narration’s<audio>(currentTimeread at every frame, so the timeline waits when the audio waits and follows its speed),performance.now()without a track, a virtual clock in tests (window.ELABORA_TEST.virtualTime; hooks__elabora.timeline.state|play|pause|seek|advance|at|toEnd). Steps are segments; seeking back hides the blocks of later steps without changing the step reached, and moves focus out of a hidden block to the seek bar. Cues fire when playback crosses them (replaying a passage crosses them again). A modal layer pauses the narration and its closing resumes it; a panel does not; a sound or video the learner plays pauses it, and it pauses them. A screen left part-way reopens at the start of the step reached (the suspend data’sp: no new byte); one seen to its end waits at its end. Phones go step by step. The next screen’s track preloads. - The bar, on narrated screens only, in the footer (kept in view at the bottom with
navigation.footer: "end"): Play/Pause, Replay the step, the seek bar (WAI-ARIA APG seek slider:role="slider",aria-valuetextwith the time and the step, arrows ±5 s, Page Up/Down a step, Home, End, Space while it has the focus), the time, Captions, Speed (0.75–2×,playbackRate, pitch kept), Mute, Transcript (the script with its speakers, the line said marked), « M’arrêter à chaque étape » on auto screens, and « Voix générée par IA » when a line’s voice is. Shortcuts Ctrl+Alt+P, R and C (aria-keyshortcuts), never one character. Captions in a band over the bottom of the screen that moves nothing. When the browser refuses sound, a large Play button waits for the learner. Minimal styles, isolated under.t-timeline*(docs/design.md), to be restyled after the app’s mockups. Labels in the seven languages (narration,play,pause,replayStep,seek,captions,speed,mute,stopAtSteps,playNarration,timelineTime,timelineValue,listen,generatedVoice). - The validator:
W_AUTO_ADVANCE(WCAG 2.2.1),W_SEGMENT_TOO_LONG(more than 90 s of voice in a step),W_NARRATION_DUPLICATED(on-screen text that reads a line again, rule E4, now checked),W_NARRATION_AUDIO_MISSING,W_NARRATION_AUDIO_STALE(text, audio or voice changed),W_TIMED_INCREMENT(counting on a cue counts replays),E_NARRATION_LINE_DUPLICATE,E_NARRATION_SPEAKER,E_NARRATION_STEP_ORDER,E_TIMELINE_CUE(a cue on no line, a timeline trigger or action on a screen without narration),E_NARRATION_TRACK(the build could not make a track).media.endedandplay-media/pause-mediatargets are checked (E_EVENT_TARGET,E_ACTION_TARGET);reveal-stepreaches the steps of lines. The generated captions and transcript answer F3 for the narration. - Engine and app: commands
update_narration,add_narration_line,update_narration_line,move_narration_line,remove_narration_line,set_narration_audio(a voice tool’s M4A or a recording, measured and written with its sidecar in one change);update_coursetakesspeakers;screenPartsgives the narration’s lines (and their steps) to the event editor; the canvas shows a line’s moment (RenderView.at,RenderOptions.at). - Agents:
elabora timeline [dir] [screen] [--language] [--json](MCPelabora_timeline): the steps, lines, times, audio and cues of a narrated screen as text, or every narrated screen and its audio;elabora narration import [dir] <screen> <line> <file>(MCPelabora_import_narration): a recorded human voice, converted, its silences trimmed;elabora look --at l2(MCPelabora_lookat) andelabora screenshot --at l1,l2:end(MCPelabora_preview_screenshotat): a narrated screen as it is when a line starts or ends, its caption shown. Captures,elabora fitand the LMS emulator run narrated screens on the virtual clock: captures and fit see them still, the emulator plays them to their end (closing the dialogs their cues open). The emulator’s scenario errors name the step they stopped at. - Translation: lines (
screen.<id>.narration.<line>.text) and speaker names (course.speaker.<id>.name) are units; each language’s package carries its own voice. - LESSON.md export: a decorative sound or video, or a silent video described by its alt, no longer writes « Transcription undefined »; a narration is reported as a loss.
- Docs: D24, SPEC (§1, §4.1, §4.2, §4.4, §13), docs/design.md (the bar’s DOM hooks; narrated slides fit with their bar), docs/architecture.md (narration audio and tracks), docs/diagnostics.md, the course AGENTS.md, the skills
elabora-course-authoringandelabora-pedagogy(segmenting, contiguity, redundancy).
Unreleased — captions for agents (2026-10-08)
elabora media captions [dir] <block> <file.vtt|.srt> [--name <name>] [--json]and MCPelabora_set_captions(block, andfile, an absolute path, ortext;name): a video block’s captions, as the app gives them (engine commandset_captions,@elabora/coresetCaptions). The text is written underassets/captions/<block>.vtt(a SubRip file converted to WebVTT; the same text already there reused, another one numbered) and the block’scaptionsnames it, in one undoable change; the same captions again change nothing. Refused: a block that is not a video, a text with no cue. Until now only the app could do it, so an agent left withE_VIDEO_CAPTIONS_MISSINGhad no command to fix it.E_VIDEO_CAPTIONS_MISSING’s fix names the command and the tool; so do the courseAGENTS.md(media rule), the skillselabora-course-authoringandelabora-pedagogy, docs/architecture.md (media) and the README. The help and the MCP catalogue (test fixtures) gain them.
Unreleased — a decorative sound (2026-10-08)
decorativeon an audio block reaches the player (the compiler dropped it:AudioProps.decorativein the course data). A decorative sound (an ambiance, music) shows no transcript, even one left in its props, as a decorative video shows none; the learner still starts it, so its player stays in reach and named, unlike a decorative image or loop, which assistive technologies skip.- A sound’s player is named by its title (
titleon<audio>, as on a video’s), decorative or not: it had no accessible name, only the figure’s caption. - The agent’s screen summary marks a decorative video or sound
(decorative), as it does an image.
Unreleased — closing the open layer (2026-10-08)
hide-layermay leave outlayer(format 0.2, compatible: onlyshow-layerrequires it now; every course valid before stays valid, so no new version nor migration). Without it, the action closes the layer open on top: a dialog (modal, over everything) before a panel, the last opened first. It is the action of a Close button inside a layer, which then needs no layer named; the app’s « Fermer un calque › Le calque ouvert » writes it, and the format refused it until now. Itslayer.closedfires as for any other layer; with no layer open it does nothing.- The validator:
E_ACTION_TARGETwhen the screen has no layer to close. The compiled course keeps such an event (it was left out as malformed), the player closes the layer on top, the agent’s screen summary sayshide-layer (the open layer). - Docs: SPEC §4.4, the event schema’s descriptions of the actions and of
layer.
Unreleased — Microsoft Edge when Chrome is not there (2026-10-08)
launchBrowser('chromium')falls back to Microsoft Edge (@elabora/nodeemulator/browser.ts): the installed Google Chrome, else Edge (Playwright channelmsedge: on every Windows 10 and 11, sometimes on macOS), else the Chromium Playwright downloads; each with the same flags (software or GPU rendering, unload variants, disabled features). A tester’s computer without Chrome, running the command line an Elabora test build carries, can now look, capture, fit, test packages and objects, replay, preview components and make contact sheets, and their MCP tools. The order isCHROMIUM_CHANNELS;launchBrowser(…, { channels })takes another (launchChromiumtries them with a given Playwrightchromium).- Free-object gates run on Edge too. Edge 154 draws as Chrome 154 in software (macOS, measured): the same SwiftShader device (
ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (LLVM 10.0.0)))); of the 22 snapshots of the four example objects (diagnostic-reseau,planetary-lab,chart-table,phasing-lab), 21 are Chrome’s pixel for pixel and one differs by 16 pixels of a line of text, 2/255 apart (two runs of Chrome differ as much on other snapshots); their gates pass against the baselines Chrome recorded. Unload deprecated and allowed behave as in Chrome. Baselines stay per system, whichever of the two drew them.test/e2e/edge.test.tschecks it where both are installed (GitHub’s runners): the same renderer, the same pixels within the gates’ tolerance. - Without a browser, the message is for someone without developer tools:
Cannot open a browser: Google Chrome and Microsoft Edge are not installed on this computer. Install Google Chrome (https://www.google.com/chrome/), then run the command again (developers: npx playwright install chromium works too).A browser that is there but does not start is named with its reason (« Update or reinstall Google Chrome » when it is Chrome), without Playwright’s call log. - Docs: README (requirements), architecture (browser for measures, captures and tests).
Unreleased — a library project knows the example it was copied from (2026-10-08)
importArchive({ origin })(@elabora/nodelibrary/): what the archive is a copy of, an example the app ships ({ kind: 'example', id, version }), written in the project’s data (<data>/origin.json, with the time of the copy) before the project appears in the library, or after a replacement under its lock.LibraryProject.originreports it (listProjects,getProject,findProjectsByCourseId); null for any other project. Machine-local: an export does not carry it, a duplicate does not inherit it, a replacement keeps the replaced project’s. The app opens the copy it made from an example instead of making another, and « Repartir de l’exemple » makes a new one.
Unreleased — a package’s limits, with the LMS that sets them (2026-10-08)
course.jsondelivery.lms(format 0.2, compatible): the LMS the course is delivered to, ids of LMS profiles.elabora build,validateandtest,elabora_build,elabora_test_packageand the engine’sbuildtarget them when no--profileis given (targetProfileIds); an id no profile has isE_LMS_UNKNOWNat/delivery/lms/<n>(a preview still runs, without it).- No package limit is assumed for an unknown LMS. The
unknownprofile (no LMS chosen) had the smallest documented size, 100 MB (Teach Up’s), and stopped builds on it. Its size, file and compression limits are now null: the build warns instead with the documented LMSs that would refuse the package, so the author chooses the LMS rather than shrinking the course for a limit it may not have (W_PACKAGE_TOO_LARGE_FOR_SOME_LMS,W_PACKAGE_TOO_MANY_FILES_FOR_SOME_LMS,W_COMPRESSION_RATIO_FOR_SOME_LMS). A chosen LMS’s limit still stops the build (E_PACKAGE_TOO_LARGE,E_PACKAGE_TOO_MANY_FILES,E_COMPRESSION_RATIO), and the extracted size is now checked too (maxUnzippedMB: 360Learning, Workday). - Package findings carry their facts (
Diagnostic.details,PackageLimitDetailsin@elabora/core/lms/package-limits): the measure, its value, the lowest limit passed, the LMSs that set it (id, name as people say it:lmsName) and whether they were chosen, the media quality, the package at each quality (measured or estimated), the best quality under the limit (fits; null when none is enough), and the heaviest files (path, asset, size, kind, duration). The English message says all of it, and what to do when no quality is enough: shorten the video, host it on a video platform and link to it, or split the course. The engine’sbuildresult haswarningstoo, and so hasbuildTranslation’s (a language’s package, the app’s « Versions »: its build targets the course’s LMS as the source’s does). - Profiles checked against their sources (October 2026): SuccessFactors publishes no package size (a tenant setting, CONTENT IMPORT
maximumFileSizeKB; KBAs 2294748, 2622763, 2428881; files over 1,800 MB are not served by iContent, KBA 2984975): 2048 MB → null. Workday: 2 GB by default for new tenants, 4 GB once raised, 20 GB extracted, 100,000 files: 4096 → 2048 MB,maxUnzippedMB20480. Docebo only advises 15,000 files:maxFiles→ null. Confirmed: Teach Up 100 MB (lower on some subscriptions), Rise Up 1 GB, Docebo 1024 MB, Cornerstone 2 GB per file, 360Learning 4 GB extracted, 50,000 files, ratio 200. SCORM Cloud: no limit on paid accounts (trial: 100 MB extracted, noted). A unit test checks that every package limit of a profile comes with a source. - Docs: architecture (Media, engine
build), decisions, theelabora-course-authoringskill and the course AGENTS.md template (name the LMS before delivering), docs/diagnostics.md and docs/lms-profiles.md (extracted size and ratio in the table).
Unreleased — the canvas scrolls under a host’s overlay (2026-10-08)
elabora/scroll(host → canvas message,@elabora/playercanvas-protocol.ts): a host that lays something over the canvas page to take the pointer (the app’s selection) takes the wheel with it and passes it on,byin CSS px of the canvas viewport andatthe pointer there. The canvas scrolls the nearest box under the point that can still go that way, else the page, at once (smooth scrolling would lag a trackpad’s many small distances). From the parent window it needs the page’s token, as the other messages. The app let its overlay through for 400 ms once the wheel turned instead; Chrome sends a whole gesture (a trackpad swipe, a mouse wheel turned on) to what was under the pointer when it began, so most gestures stayed on the overlay and the page did not move.
Unreleased — an event written from its trigger (2026-10-08)
- An event may have no actions (format 0.2, compatible:
dolosesminItems: 1; every course valid before stays valid, so no new version nor migration). The app writes an event as soon as its author has chosen what triggers it, in its place among the others, and its first action follows; until then it does nothing.upsert_eventand MCPelabora_upsert_eventacceptdo: [], and the player runs nothing for it. W_EVENT_NO_ACTION(warning, « Événement sans action » in the app): an event with nothing indonor inelse. A warning, not an error: it breaks nothing for the learner and does not stop a build, but it is unfinished work an author or an agent should see. An event with conditions may leavedoempty and act inelse: no warning then.- Docs: SPEC §4.4, the event schema’s description of
do,elabora-course-authoring(an event the author started: finish it as they meant it, or ask), docs/diagnostics.md.
Unreleased — a translation seen in context, built as its own package (2026-10-08)
screenTranslation(engine request,@elabora/corescreenTranslation): one screen’s texts in a language, astranslationTablegives them, each with the block and the field that show it on the canvas (data-t-field:options.1.text); the plan’s validated texts and questions its blocks show are among them, at their block. The canvas already shows a screen in a language (canvasView { language },renderScreen { language }): the app’s « En contexte ».- A language renders while some of its translations break the text:
translatedCourse({ skipBroken: true })leaves those out (their source text stays, listed inbroken); renders and looks use it, so one lost placeholder no longer makes the whole language unrenderable. Builds still refuse them. translate_texttakestexts(several units in one change, one undo) and starts a language the course does not have yet (translations/<lang>.xlfwritten with them); one refused text refuses the batch.approve_translation(engine command,@elabora/coreapproveTranslation): the translated plan approved by a person, aselabora translate approvewrites it, as a change to undo.buildTranslation(engine request): a language as its own package, the translated course generated in the project’s data folder (.elabora/translations/<lang>, never copied) and built into<project>/dist(<course>-<lang>-<version>-scorm12.zip); texts left to translate stop it unlessallowMissing(a draft).elabora translate buildaccepts an output folder in.elaboraordistof the source.- MCP
elabora_translation_table(a language as a bilingual table,screenfor one screen’s texts with where they show,todofor what is left) andelabora_translate_text(many texts per call, one undoable change).
Unreleased — a free object’s choices in words (2026-10-08)
x-enum-labels(object manifest, on a prop, on the items of a list of simple values or on a field of a list’s items): how each value of an enum reads in the editor, in the manifest’s language ({ "parefeu": "Pare-feu" }); the object still receives the values. Each key must be a value of the enum (E_OBJECT_ENUM_LABELS, numbers written as text). The network simulation ofdiagnostic-reseauwords its four devices.props.enumLabels(free-object block): the same words for one block ({ "failingDevice": { "parefeu": "Firewall" } },"<prop>.<field>"for a field of a list), checked against the manifest’s enums (E_OBJECT_ENUM_LABELS). Translations write there: unitsblock.<id>.enumLabels.<prop>.<value>, markededitor(authors read them, learners do not), sofindTextandreplace_textleave them alone.screenPartsgives each setting of a free object itsenumLabels(the block’s over the manifest’s), for event editors that offer its values (set-object-props).
Unreleased — events, layers and variables edited in the app (2026-10-08)
screenParts(engine request,@elabora/coreproject/parts.ts): what the events of a screen can name, so that an event editor offers only what the validator accepts: its blocks in order (layers’ included), each with its behaviour, the names of its controls (what a click names; none: it cannot be clicked), a decision’s options, a free object’s declared events (with the payload fields a trigger can filter on) and its authors’ simple settings (forset-object-props); its layers and steps; the course’s screens and variables.upsert_layerandupsert_eventwithout an id add a new layer or event with a free id (<screen>-layer,<screen>-event, then-2…), returned aslayerorevent; the blocks of a layer written without ids get free ones too (<layer>-<type>).- Steps by command:
update_blocktakesstep(a block of the screen itself: the step that reveals it;nullor 1: from the start; refused for a block in a layer or a component instance),update_screentakessteps(how each step opens, replaced whole;nullremoves them). MCPelabora_update_blockandelabora_update_screentoo. remove_variable(engine command): a variable no event uses any more; one still read or written is refused, with the events that use it.replace_text(engine command,@elabora/corereplaceText) andfindText(engine request,findText): the course’s words found and replaced in one change, one undo: every string a learner reads incourse.jsonand the screens, as translation extracts them (titles, blocks’ words, layers, step buttons, free objects’ wording); never the plan’s validated texts (a retouch, block by block), ids, files or values of a choice; in Markdown, never a link’s address, maths or code. A replacement that would change an object string’s placeholders, exceed its length or empty it is skipped and said;unitslimits it to chosen matches.- MCP tools for the app’s new commands:
elabora_find_text,elabora_replace_text,elabora_duplicate_screen,elabora_remove_layer,elabora_rename_variable,elabora_remove_variable;elabora_upsert_layertakes a layer without id;elabora_update_coursetakespresentation; edit results give the ids made (layer,event) and what a replacement did (replaced,units,skipped). - Translations as changes (the app’s « Traduction »): engine commands
export_translation(a language started or brought up to date,translations/<lang>.xlfaselabora translate exportwrites it) andtranslate_text(one unit translated, reviewed or cleared; its source becomes the current one; a translation that loses the source’s maths, links, code or placeholders, or exceeds its length, is refused); engine requeststranslationStatusandtranslationTable(@elabora/coretranslationTable: every unit with its translation and what it asks for: to translate, to review, stale with the source it was translated from, translated; and what breaks the text). create_screentakeslayoutandlayoutProps: a new screen in one of the course’s screen components, in one change (the app’s « Nouvel écran » offers them).duplicate_screen(engine command,@elabora/coreduplicateScreen): a copy of a screen after it in its section; its blocks, layers and events get new ids (those named after the screen follow its new id), and the copy’s events point at its own blocks and layers.update_coursetakespresentation(scroll or slides, reading widths, the sizes slides fit), so that the app’s settings can change it.rename_variable(engine command,@elabora/corerenameVariable): variables have ids of their own, whichrenamedoes not take; the variable gets its new name incourse.json, in the events that read or write it (triggers, conditions, actions), and in the state lock, so the value saved for learners stays theirs.
Unreleased — an open project loses no write when other watchers come and go (2026-10-07)
- On macOS
FsStorewatches each folder from a worker thread of its own. The watchers of a thread share one FSEvents stream, built again whenever one of them starts or stops, and what is written from about 16 ms before to 10 ms after is never reported. In the app’s engine process, starting or stopping the preview, or opening or closing another project, hid an agent’s writes from an open project until the listing every 15 s: with a file written every millisecond, 467 writes of 5786 over 10 previews started and stopped, 336 of 4297 over 10 projects opened and closed (packages/engine/bench/watch-rebuild.ts). A worker has its own event loop, hence its own stream: none lost since. The worker is plain JavaScript evaluated from a string (nothing to resolve in an app bundle; it runs in Electron’s utility process). Costs: a thread and about 10 MB per open project; the folder’s watcher reports after 38 ms instead of 11 (median), so a project opens about 25 ms later on macOS. Linux (watchFolders) and Windows are unchanged; so is the Store contract. - The journal’s watcher stays in the engine’s thread: what another Elabora tool journals it also writes in the folder, whose watcher reports it, and the journal is read before the files. The preview’s watchers stay there too: a change written while another watcher starts or stops does not rebuild the preview, the next one does.
- Tests: a store reports every write while a preview’s worth of watchers start and stop in the process, and a session sees every write of an agent while other projects open and close, without the periodic listing (before, the store missed 10 to 20 % of the writes, the session about 40 %).
Unreleased — a project is watched once open (2026-10-07)
openProjectresolves once the folder’s watcher reports what is written from then on, and the session’s first listing of the folder comes after it: a file an agent writes as soon as the app has opened the project is journaled at once, instead of at the next listing, 15 s later (the engine tests turned that listing off and failed once in about twenty runs ofnpm test). On macOS the watchers of a process share one FSEvents stream, built again whenever one starts or stops, and writes from about 16 ms before to 10 ms after are never reported; with sessions kept open in one process, a write right after opening was missed in 5 opens of 120, and in none of 240 since (packages/engine/bench/watch-ready.ts).- Store contract:
watch(onChange, onReady?)(@elabora/coreproject/store.ts): a store callsonReadyonce what is written from then on is reported, at once when it cannot tell, or after a bound.FsStoreon macOS writes a probe file in the project’s.elabora/every 25 ms until its watcher reports it, then removes it (about 12 ms; 2 s at most, for a volume without FSEvents); inotify and ReadDirectoryChangesW watch from the call. The session starts the journal’s watcher before the folder’s and waits 3 s at most. Open time on macOS: median 7 → 14 ms in the measure. Stores that wrap another store’swatchpassonReadyon. - Tests: the engine tests open projects with
openProjectagain (the probe helperopenWatchedis gone); new tests write as soon as a store is ready and as soon as a project is open, with other folders watched.
Unreleased — automatic versions in any process (2026-10-07)
- An edit no longer hangs when the worker of its automatic version cannot start. Edits through
@elabora/nodeedit(CLI, MCP server) take the version in a worker thread and wait for it; a process started withnode --input-type=module -e "…"passed--input-typeto that worker, which failed before running anything, and the edit, holding the project lock, waited 10 minutes without a word. The worker now does without the flags of the main entry (--input-type,-e/--eval,-p/--print; it inherits every flag when the others include one a worker refuses, such as--max-old-space-size), and starts from an evaluated bootstrap that wakes the edit in every case: a task module that cannot load, a thread that stops before finishing (an uncaught error,process.exit) or cannot be created end the wait at once with anerrorin the result’sbackup, and the edit goes on, as when a version cannot be taken. The CLI was not affected.
Unreleased — faster free-object gates (2026-10-07)
elabora object test (MCP elabora_test_object) had become the slowest check agents run: about 230 s for the 3D chart table of medieval-charts and 22 s for diagnostic-reseau on a Mac.
- Where the time went (Linux, 4 cores, no GPU: chart table 326 to 328 s,
diagnostic-reseau8.3 s). Nearly all of it was Chrome drawing the objects’ frames in software (SwiftShader, which the gates keep: snapshots need the same pixels on every machine, and the boot budget speaks for machines without a GPU): 40 to 75 ms a frame for the chart table, about 5,600 frames a run, of which its snapshots look at 10. Astepreturned once its frames were queued, so their drawing was paid by whatever came next: a later step (up to 24 s), closing the page (72 s in all, for frames nobody would see) or a snapshot, which hit Playwright’s 30 s timeout: the chart table failed its gates there, every run (« Three voyages complete the table »; with the stall guard’s 60 s thatmaingave snapshots since, it passes in 309 s). Pages (0.1 s per test), axe (0.2 s), PNG comparisons (0.1 s) were negligible; mounting takes 0.3 to 8 s a test (its first frame uploads textures of 1,536 to 4,096 px), and stays: it is the boot time the gate measures. - Dry frames. Frames no snapshot shows run dry: the object’s code runs every frame (its state, events, uploads, shaders and their errors), its draws, clears and blits are skipped (
@elabora/playerdry-frames.ts, frame methodobject/test-dry, test mode only). What a skipped operation would have drawn is tracked as stale until drawn again; a dry run decides only when it passes and saw nothing that depended on it (a canvas still stale at a snapshot, a draw on a stale framebuffer or sampling a stale texture,readPixels, copies and blits of a stale drawing, a WebGL canvas drawn into a 2D canvas, uploaded or turned into an image, a query orgetErrorafter a skipped draw, transform feedback). Otherwise, and whenever it fails or a snapshot differs, the run is done again with every frame drawn, and that run decides; when only the drawing of a render target was reused, it is first tried again with render targets drawn and only the canvas dry, as are the object’s next tests (orbital-rendezvousbakes its Earth into textures over the first frames: 134 s fully dry and drawn again, 48 s so). Runs that record baselines draw every frame. The 300 frames of the smoke gate, tests without snapshots and the steps after a test’s last snapshot cost nothing to draw; a step before a snapshot draws its last frame only.testObject(…, { allFrames: true })draws everything. - A step waits for its frames (a WebGL 2 fence:
gl.finishis a flush in Chrome), so that every call waits for its own drawing and the 60 s stall limit applies to it; a snapshot no longer times out behind queued frames. - Snapshots through the DevTools protocol: a PNG Chrome encodes for speed when it is only compared (lossless; the same pixels as Playwright’s element screenshot, without the text caret as there; about 50 ms instead of 0.4 to 1 s), with the default compression when it is kept as a baseline: baselines recorded now are byte for byte those recorded before. Existing baselines stay valid: the chart table’s ten and
diagnostic-reseau’s two compare equal. - After, on the same machine, against the gate runner of
main(which since gives snapshots the stall guard’s 60 s): chart table 309 s → 36 to 45 s,orbital-rendezvousphasing lab 288 s → 37 s,diagnostic-reseau8.5 s → 4.2 to 4.9 s, every gate as before and every baseline equal (those ofmainincluded). Drawing every frame (allFrames), the chart table takes 322 and 331 s: the time goes away with the frames not drawn. What is left is mostly mounting, which the boot gate measures. - How each run was drawn is in the result (
gates.frames:dry,dry on the canvas, orevery frame (why):records baselines,to confirm a failure,readPixels of a drawing a dry frame skipped…), so that a slow test explains itself; it is not part ofobject.gates.json. - Boot times are measured as before (the mount and its first frame, drawn). On this machine they come in steps of about 2.2 s whatever the code (chart table, medians of 9 from 0.7 to 4.5 s over runs of the same object, with or without dry frames; the slow-boot warning comes and goes): the time is in the object’s first frame, which uploads its canvas textures through SwiftShader. Not changed here.
- Evaluated and left out: running behaviour tests in a GPU browser (the frames it would move are the ones now skipped; frames before snapshots must stay in software; without a GPU, as here and on CI, both browsers are SwiftShader), several pages at once (SwiftShader already uses every core: two runs together took 93 s against 112 s one after the other, and concurrent mounts would skew the boot samples), fewer page loads (0.1 s each), dropping the queued frames when a page closes (
Page.crashhangs Playwright’s close). - Docs and skills: docs/free-object-contract.md (protocol, §6 Frames drawn), docs/architecture.md (Rendering),
elabora-free-objects(draw every frame in tests, pick by raycasting rather thanreadPixels).
Unreleased — free-object gates on a loaded machine (2026-10-07)
- Event expectations no longer read after a fixed 20 ms (
elabora object test, gate 3). Eachexpect-eventandexpect-no-eventfirst takes a round trip to the frame (messages between two windows keep their order), so the events of the earlier steps are all in, those answering a notification (set-props,set-view…) included; anexpect-eventnot yet met reads the events again until its event arrives or 5 s have passed (an object that emits once an asset has arrived). Virtual time does not move meanwhile, and consecutive expectations still share the events of an action. Under a load average of 30 to 70, an event could arrive after the 20 ms and fail its test. - Snapshots have the stall guard’s 60 s (gate 5) instead of Playwright’s 30 s, which software WebGL with large textures exceeded under load (
medieval-charts, chart table); a capture that never ends fails its test as a stuck call does.
Unreleased — fonts for Central European languages (2026-10-07)
The vetted fonts held the latin subset only: a course in Polish, Czech, Romanian or Turkish had its ą, ř, ș or ğ drawn by the fallback fonts of the stack, in another typeface than the letters around them.
unicodeRangeontheme.fontsentries (format 0.2, additive): the characters a font file holds, in CSSunicode-rangesyntax (code points only, checked by the schema). A face split into subset files is one entry per file; the player writes one@font-facerule per file with itsunicode-range, so a browser downloads a file only for the pages that use one of its letters. The compiled course data carries it, and free objects get each face with its range.theme.fontsholds 24 entries instead of 12.@elabora/fontsships the latin-ext subset of 24 families (every one but UnifrakturMaguntia, which has none), each file with its range from Fontsource’sunicode.json: the list goes from 1.87 MB to 3.42 MB (latin 1.76 MB, latin-ext 1.56 MB, licences 0.11 MB). The catalogue iselabora-fonts/2:subsets(latin; latin-ext: az, bs, cs, cy, dsb, eo, et, haw, hr, hsb, hu, lt, lv, mi, mt, pl, ro, se, sk, sl, tk, tr, uz) replacessubsetandlanguages, families list theirsubsets, files theirsubsetandunicodeRange;languageSubsets(language)says what a language needs, andselectFontFilestakessubsets. Pangrams in Czech, Hungarian, Polish, Romanian and Turkish.elabora font addtakes latin-ext when a language of the course needs it: the language ofcourse.jsonor of a translation (translations/<language>.xlf). Western European courses get the latin files as before, without ranges.--subsets latin,latin-ext(MCPelabora_add_fontsubsets, engineadd_font) chooses; with--from, any subset of the Fontsource package (cyrillic,greek,vietnamese…), with the ranges of itsunicode.json(a package without one is taken one subset at a time; scripts Fontsource splits into numbered slices, Japanese, Chinese, Korean, are refused: take a font file). Notes say which languages keep letters no embedded file holds; the output names the subsets.elabora font listandelabora_list_fontsgive the subsets.- Tests: the catalogue’s ranges hold the letters of every pangram; in Chrome, a Polish word is drawn by the embedded face alone (the DevTools protocol’s platform fonts) while a family added without latin-ext leaves its letters to another font, and only the latin-ext file of the faces a page uses is fetched.
- Docs and skills: docs/design.md Fonts, D22 in docs/decisions.md (revised: the size, the new mark at about 4 MB), SPEC.md, docs/architecture.md, packages/fonts/README.md,
elabora-visual-design.
Unreleased — looking fast while designing (2026-10-07)
Agents building courses reported a slow look-and-fix loop: each visual check was a build plus a headless browser, about 12 s, and the in-app browser of their app did not show 3D objects, so they wrote throwaway Playwright scripts.
- Where the time went (one screen,
elabora screenshot, cold): starting Node and building were under 1.5 s; most of the rest was Chrome drawing in software. Every browser Elabora started forced SwiftShader for WebGL (--use-angle=swiftshader), which also composites every page on the CPU: waiting for fonts, images and objects took 2 to 9 s on designed pages, a full-page PNG 1 to 4 s, and a free object 1.5 s more of fixed wait. Measured on the samples, cold, before and after alternated (other test runs loaded the machine): a capture of the 3D screen ofdiagnostic-reseautook 10.3 s, ofmedieval-charts37 s, of a longmedieval-chartspage 20 s;elabora fit23 to 48 s and 160 to 250 s. - Captures draw with the GPU.
launchBrowsertakesrendering: 'software' | 'gpu'; captures (elabora screenshot, component previews),elabora fit,elabora object replayand contact sheets use the machine’s GPU (SwiftShader where there is none). Free-object gates and the LMS emulator stay in software: visual baselines need the same pixels everywhere. The same captures now take 4.0 s, 7 s and 4.5 s.elabora fitreads the pixels behind text from a PNG Chrome encodes for speed (lossless; about 0.1 s instead of 0.6 s a window): 9 s and 33 s.elabora test(Moodle, iframe) 30 → 22 s and 121 → 59 s through its fit pass;elabora object testis unchanged (software, 22 s and 230 s). elabora look [dir] [--screen <id>,…|all] [--size …] [--state answered] [--step <n>] [--layer <id>] [--viewport] [--json] | --url | --stop(MCPelabora_look): a screen in well under a second while designing. A look server kept for the course (@elabora/nodelook.ts: one canvas host and one GPU browser, started in the background by the first look, stopped after 15 minutes without one) renders the screen from the files as they are, without a build, as the editor canvas does, with the player’s header and footer; the page updates in place, so a free object an edit did not touch keeps running, and one whose code or assets changed starts again. It writes a JPEG (PNG with--out x.png; PNG took seconds to encode on textured pages), says whether a slide fits at that size or how long a page is, the objects and console errors, and with--jsonwhere every block is.--urlprints a page that follows the looks and every saved file, for a browser pane. A look after an edit takes 0.3 to 0.4 s from the command to the image on the 3D screen ofdiagnostic-reseau(0.14 s in the server), 0.4 to 0.5 s on a four-windowmedieval-chartspage, 0.7 s on its 3D page (the object below the window is scrolled to and drawn); the first look of a session 2 to 2.6 s (the server and the browser start).elabora lookstarts without loading the rest of the command line (bin/elabora.ts;@elabora/nodelook-client.ts): 0.1 s instead of 0.5 s. The full build and its checks stay the final word.- Free objects show in the in-app browsers of agent apps. Claude’s browser pane refuses frames sandboxed without
allow-same-origin(an opaque origin, what packages use for free objects) withERR_BLOCKED_BY_CLIENT, and what they would load; the course showed « Chargement de l’activité… » for ever.elabora previewand the look page now serve the same files on a second port of their server (startServer({ frames: true })) and load object frames from there withallow-same-origin: another origin, so the object still cannot reach the page, but the same site (same process, one site approval). The player reads the port from<meta name="elabora-object-frames">(added by the preview to the page it serves), the canvas from?frames=<port>(@elabora/playerframes.ts); built packages are unchanged. elabora previewpages reload after each rebuild, on the screen they showed (server-sent events from the preview server, in the page it serves).- Full-page captures with free objects below the window lay the capture of each object over its frame in the page, then capture the page once, instead of composing the whole page again in a second page.
- Canvas page options:
chrome=playershows the player’s header and footer, inert (the player and the canvas share@elabora/playershell.ts), so a slide has the room it has in a package;frames=<port>. - Docs and skills: docs/architecture.md (Browser for measures, captures and tests: rendering, looking while designing, frames in browser panes), docs/design.md §6,
elabora-visual-design,elabora-free-objects,elabora-course-authoring,elabora-components, the courseAGENTS.mdtemplate and the MCP instructions: look after every visual change, check before handing over, no Playwright scripts to see a screen or an object.
Unreleased — lists and material colours in free objects (2026-10-07)
The agent that built examples/medieval-charts had to number a repeated setting (voyage1From, voyage2From…) and to add a theme token for every shade of its 3D scene, down to the brass of the dividers. Both now have a sanctioned form.
- List props (format 0.2, object manifest, additive): a prop of
type: "array"withx-control: "list"anditemsthat are a simple value or an object whose fields are simple values, each with atypeand atitle(schemaslistItem,listObject,listField;minItems,maxItems,required, adefaultper field for new items). Props take atitletoo, the label editors show. Items take no field the manifest does not declare. A block’s list,set-object-props, the default and now the props of object tests (testpropsandset-props) are checked item by item (E_OBJECT_PROP_VALUEnames the item:/1/to;E_OBJECT_PROP_DEFAULT). Fields markedx-translatableare translation units (block.<id>.props.<prop>.<item id or index>.<field>); a list a block leaves to the manifest default is written whole into the translated screen. Item fields named like an annotation (description,default) are no longer stripped before validation. - Material colours: a colour prop (
format: "color",x-control: "color") may keep a literal default with no theme token: the brass of an instrument, a wood, a hull. Colours that carry the course identity still bind to a token withx-token. Fixes ofW_OBJECT_LITERAL_COLORandE_OBJECT_TOKENsay so. - Contrast of the text an object draws:
x-text-on(format 0.2, additive). A colour prop the object draws text in names the colour that text is read on, another colour prop or a theme token;E_OBJECT_TEXT_CONTRAST(error, F1) reports a pair under 4.5:1 with the manifest defaults, a block’s values or an event’sset-object-props. W_OBJECT_PROP_CONTROL: anx-controlthat does not fit its prop (listorvec3-gizmoon something else than a list,selectwithout an enum or an integer range,sliderwithout minimum and maximum,colorwithoutformat: "color", a field of a list with a control of another type).- Sample
medieval-charts: the chart table (object version 2.0.0) takes onevoyageslist (one to six voyages, three by default) instead ofvoyage1From…voyage3To, and colour props for the brass, boxwood, steel and hull of its tools and ship, applied live onupdate(); its words for the end of the voyages follow their number. A new object test lists two other voyages and recolours the brass. The block’s props are unchanged (it uses the defaults). - Sample
orbital-rendezvous: the phasing lab (object version 3.0.0) takes onerequirementslist (one to six requirements, each a distance to the client, the most phasing orbits allowed and a propellant budget; the three of before by default) instead ofreq1Gap…req3Budget. Its state, events and score follow the number of requirements (state arrays sized to six), as do the dots and the eyebrow of the mission card; the end of the lab says « Every requirement is met » (« Toutes les exigences sont remplies » infr.xlf) instead of « All three requirements met ». The block ofscreens/lab.jsonlists its three requirements, with the same values. A new object test runs two requirements in another order; per-system baselines of its snapshots for Linux. - Docs and skills: docs/free-object-contract.md (Lists, Colours), SPEC.md, docs/design.md,
elabora-free-objects(lists instead of numbered props, material colours),elabora-visual-design.
Unreleased — blocks added by hand (2026-10-07)
add_blockwithout an id (engine command, MCPelabora_add_block): the block gets a free id,<screen>-<type>then-2,-3…, returned asblock. An id already used in the course is now refused (Id "…" is already used) instead of leaving a duplicate for the validator.- Videos and sounds are added before their alternatives (format 0.2, relaxed and additive):
captionsandtranscriptare no longer required by the video and audio schemas, so a video or a sound can be placed (by the app, by hand) and completed afterwards; the validator reports what is missing, as for an image’s alt (errors, rule F3, they block the build):E_VIDEO_CAPTIONS_MISSING(a video played with its sound: the media probe says whether its file has a sound track; a file not probed counts as having one),E_MEDIA_TRANSCRIPT_MISSING(a video played by the learner, or a sound; a silent video may givealtinstead),E_VIDEO_ALT_MISSING(a loop). - Looping videos (
video.loop, withvideo.alt): a short silent animation plays like an animated image, muted, in a loop, starting by itself unless the learner prefers reduced motion, with a button that pauses it (WCAG 2.2.2; labelspauseAnimation,playAnimationin the player’s seven languages); itsaltnames it as an image.decorative: trueon a video (played as a loop, hidden from assistive technologies) or on a sound (an ambiance, music: no transcript) says it carries no information. A video played by the learner without a transcript but withaltis described by it. - The pause button of a loop acts on what it shows. Pressed while the loop was still loading (the video not playing yet), « Mettre en pause l’animation » started it; it now keeps it still, also once loaded. A loop below the window starts when it comes into it (Chrome holds a muted video that plays by itself until then) and stops when it leaves; the button follows.
set_captions(engine command): a video’s captions from the text of a WebVTT file, or of a SubRip one (.srt, its timings converted), written underassets/captions/(named after the block; the same text is reused) and named by the block, as one undoable change.- A preview shows a course that is not deliverable yet.
elabora preview(and the app’s Aperçu) built like a delivery, so an image without alt or a video without captions, placed and not yet completed, left it on a server error (ENOENT … .elabora/preview/index.html); a package over the LMS size limit did too.build({ preview: true })(@elabora/node) now stops only on errors of the course itself (format, references, components, objects: codes without a rule); errors under a rule of the guardrails or of the delivery checklist (accessibility, plan coverage, package limits) are reported and the course is shown. When it cannot be built at all, the preview page lists the errors that stop it and shows the course again once a change builds.elabora previewprints the errors left either way. Deliveries andelabora buildare unchanged.
Unreleased — vetted fonts (2026-10-07)
- New package
@elabora/fonts: 25 free font families vetted for courses, shipped with Elabora so they are there offline (the desktop app, the CLI, new projects): EB Garamond, Libre Baskerville, Lora, Source Serif 4, Merriweather, Playfair Display, Fraunces; Inter, Roboto, Open Sans, Source Sans 3, Lato, Atkinson Hyperlegible Next, IBM Plex Sans, Space Grotesk, Montserrat, Poppins, Figtree, Nunito; JetBrains Mono; Oswald, Bebas Neue, Caveat, Cinzel, UnifrakturMaguntia. All under the SIL OFL 1.1, as WOFF2 files of the latin subset (variable fonts: the weight axis, one file per style) next to their licence, 1.87 MB in all.catalogue.jsonrecords for each family its category and style, use, CSS stack, default tokens, pairings, specimens, weights, styles, files with sizes and hashes, licence and copyright, and where the files came from (npm package, version, tarball and integrity, upstream version);import { FONTS, findFont } from '@elabora/fonts'gives it as data to the app.npm run fontsrefreshes the files fromsources.json(--check,--update); packages/fonts/README.md lists them. elabora font list [dir] [--category <c>] [--json](MCPelabora_list_fonts) andelabora font add [dir] <family> [--weights …] [--styles …] [--for body,heading|mono|display|none](MCPelabora_add_font, engine commandadd_font): copies a font and its licence intotheme/fonts/<id>/, declares its faces incourse.jsontheme.fontsand gives its stack to the tokens (font.body,font.heading, or a custom tokenfont.<name>), as one undoable change; adding a family again replaces its faces. Any other font goes the same way with its licence:--from <Fontsource package folder or npm pack .tgz>, or--file <a.woff2,…> --license <file> [--family <name>](weights and style read from the files).- The build carries font licences: the licence files (
OFL.txt,LICENSE…,EULA…) in the folder of each packaged font go into the package next to it; the build report lists them (design.fontLicenses). Packages of courses that kept their licence next to their fonts (the samples) now hold that file too. - New warning
W_FONT_LICENSE_MISSING: a font file of the course without a licence file in its folder (one finding per folder). - The player sets
code,kbd,sampandpreinvar(--t-font-mono), the custom tokenfont.mono(elabora font add . "JetBrains Mono"sets it), with the system monospace fonts as fallback. - Docs and skills: docs/design.md Fonts, docs/architecture.md (packages), D22 in docs/decisions.md,
elabora-visual-design, the courseAGENTS.mdtemplate, NOTICE.
Unreleased — stable tests (2026-10-07)
elabora fitgives the same answer every run on scroll pages with transitions. Its second pass (animations on) hides the text to capture what is behind it, then shows it again: that started the colour transitions of the theme back (.t-btn:color .2s), and the next window read buttons still fading in, at 70 to 99 % of their colour depending on timing (medieval-charts: « “Submit” reaches 4.41:1 … as the page scrolls » one run in two). CSS transitions are now settled before each window is read (those of the screen just shown too); scroll-driven and other animations are left as they are.
Unreleased — texts the author points at on the canvas (2026-10-07)
- Fields on the canvas: rendered with the new
fieldsoption (renderScreen,createRenderContext; the engine’s canvas always), templates mark an element whose whole content is one text prop withdata-t-field(its path below props:prompt,options.1.label), built-in blocks included, and the canvas layout gives each block itsfieldswith their boxes. The app opens its text editor on the text under the pointer. Packages and captures are never marked.
Unreleased — retouches of validated texts (2026-10-07)
- Retouches in the module (rule A5): the wording of a locked text changes at once on its block, without a change request: the block keeps
textRefand getsretouch: { text, date, by? }(format 0.2, additive, on text, callout and heading), which the module shows until the client’s next review. The plan is not touched and stays approved;I_TEXT_RETOUCHEDsays it. Commandsretouch_textandrestore_validated_text(CLIelabora plan retouch|restore, MCPelabora_retouch_text,elabora_restore_validated_text), and the human-onlyapprove_retouches(elabora plan approve-retouches --by): the plan takes the approved wording, stays approved, follows with its hash and lists them inapproval.retouches. - Verbatim texts:
lockedTexts[].verbatim: truemarks a text the client wants as signed (a legal notice, a quote): never retouched (E_RETOUCH_VERBATIM); a retouch on a block withouttextRefisE_RETOUCH_WITHOUT_TEXTREF. - The engine’s
listFilesrequest lists the files of the project, or of one folder (changes/,assets/), with their kind and size.
Unreleased — Rise-like steps, replay, contact sheets (2026-10-07)
- Continue in the page (scroll presentation): the next step of a page opens with a Continue button where the shown content ends, like the continue dividers of Rise; Next, in the footer, goes to the next page. With
navigation.lockUntilCompletethe button waits for the activities shown above it and says why. Slides keep Next relabelled Continue. Visible change for scroll courses with steps: the footer no longer reveals them. - Steps that open once answered: screen
steps(format 0.2, additive) sets, by step number,opens(continue, the default, oranswered: by itself once every activity of the steps above is answered, right or wrong, without taking the focus) andlabel(the text of its Continue button, a translation unitscreen.<id>.step.<n>.label). A step that opens once answered needs an activity above it (E_STEP_UNREACHABLE, which also counts such steps as reached on screens withcontinueRevealsSteps: false); settings for a step the screen does not have, or for its first, are the new warningW_STEP_SETTING_UNUSED. navigation.footer: "end"(format 0.2, additive): Previous and Next at the end of the page instead of a bar at the bottom of the window; in scroll presentation they appear once the last step is shown.- Labels follow the presentation: in scroll presentation the player says page (
Page 2 of 6, the locked and explore messages) in its seven languages;course.uistill rewords them. - Headings that follow the page in templates:
data-t-headingon an<h2>–<h6>of a component template renders it one level under the heading before the instance in reading order (the others marked so keep their distance), in the package and inE_HEADING_ORDER(@elabora/corecomponents/headings.ts, shared by the validator and the compiler). It goes on headings only (E_COMPONENT_HOOK). elabora object replay [dir] <id> [--test <name>] [--block <id>] [--size 1600x900] [--out sheet.png](MCPelabora_replay_object): a test ofobject.tests.jsonreplayed in the built course, on the page that places the object, scrolled into view, in real time; a contact sheet of its snapshots and the end, and the expectations that failed (they wait a moment for an object a little behind).elabora assets --sheet <file.png>(MCPelabora_build_assetssheet): a contact sheet of every asset the generators wrote, on the course’s surface colour, with names and sizes.elabora fiton scroll pages runs a second pass with animations on when a stylesheet animates with the scroll: text that a scroll-driven animation never lets be readable in the middle of the window (20 to 80 % of its height, sampled every quarter of a window) is reported “as the page scrolls”. Measured contrast now counts the opacity of the text’s ancestors.- Object tests settle CSS: in test mode the frame ends the CSS transitions and animations of the object’s DOM at once (time there is virtual), so snapshots and the accessibility gate never catch a panel half-faded.
elabora buildwrites again a package folder an earlier build left in the output folder (elabora testwrites one), sodist/never holds a folder older than the ZIP next to it.E_DUPLICATE_IDsays that sections share one namespace with screens, blocks, layers and events.- The LMS emulator presses Continue buttons in the page.
- Sample
medieval-charts: its scroll-driven animations start later, so the learner sees them happen; Continue buttons and steps that open once a check is answered; Previous and Next at the end of each page; headings of its components follow the page; covered bytest/matrix/medieval.test.ts(emulator with axe, fit with and without motion, the gates of its chart table).
Unreleased — what an agent did, and Codex by name (2026-10-07)
- The engine’s
historyrequest takesops: true: eachHistoryItemthen carries the change’sops(asChangeView.opsinchangedevents). A window opened after an agent’s work can say what it did (« 1 bloc modifié, 1 ajouté ») and not only that a screen changed. Without the param the response is unchanged. - Codex is named in the journal: the CLI journals a
elaboracommand run by Codex as{ kind: 'agent', name: 'codex' }. Codex (the CLI and the desktop app, since February 2026) setsCODEX_THREAD_IDin the shells it runs, as Claude Code setsCLAUDECODE; such commands used to be journaled as an unnamed agent.ELABORA_AUTHORstill wins.
Unreleased — scroll pages, Rise-like courses (2026-10-07)
presentation.mode already chose between scroll (the default: a screen is a page, a lesson) and slides. Building a long scrolling sample (examples/medieval-charts) showed where the tools still thought in slides; they now follow the mode.
elabora fitin scroll presentation no longer prints a ✘ and “below the fold” for every long page: it gives the length of each page in windows and measures the contrast of its text down the whole page, window by window (it used to read the first window only), at the reference size unless sizes are given. Text that has scrolled under the sticky footer is left out (in slides too), and text shadows now count as what letters are read against (a halo behind a label).FitResultscreens havewindows.elabora testmeasures contrast on scroll courses too (it ran the fit for slides only); the build report’stests.fitstays for slides. The MCPelabora_check_fitdescription says so.W_TEXT_DENSITYfollows the presentation. Slides: 250 words a screen, as before. Scroll pages: the longest stretch of reading with nothing to do (an activity, a component with controls, an accordion, a button, media) over about 450 words, or a page over about 1,500 words (two lessons rather than one). The message names the first and last block of the stretch.- Free objects can follow the scroll:
ctx.viewandhost.onView(docs/free-object-contract.md, View),{ visible, ratio, progress, entry, exit }measured on the frame like a CSSview()timeline, sent by the player when it changes (scroll, resize, a step revealed) at most once a frame, and at initialize. New protocol notificationobject/view; hosts that do not measure (slides, object tests) leave the frame wholly in view. Object tests (format 0.2,object-testsschema, additive):viewon a test and the stepset-view. elabora screenshot --scroll 0,25%,50%,1800(MCPelabora_preview_screenshotscroll): a filmstrip down a page, the window scrolled to each position (px or % of the page), animations on, on one contact sheet: scroll-driven animations, sticky figures and objects that follow the scroll, which a full-page capture cannot show.- Full-page captures show free objects below the window. Their frames run in their own process, which browsers neither run out of view nor paint in a capture beyond the window: each one is now scrolled into view, given time to draw, captured and laid in place.
- LMS emulator: a first screen whose activity must be done before Next (navigation locked) is answered before leaving it, as a learner would; the run used to time out.
W_STYLE_LITERAL_COLORignores the colours of masks (mask,mask-image,-webkit-mask-image): only their alpha counts.- Docs and skills: docs/design.md Scroll pages (what each tool does in scroll presentation; scroll-driven design as progressive enhancement:
@supports (animation-timeline: view()),prefers-reduced-motion, the final state as base style;timeline-scopefor a sticky figure that builds up);elabora-visual-design,elabora-free-objects,elabora-course-authoring; the courseAGENTS.mdtemplate. - New sample
examples/medieval-charts(English, 4 scroll pages): a folio layout with a turning wind rose, glosses in the margin, a T-O map that assembles as it scrolls into view, a diptych that opens, a scrolling workshop where a portolan chart builds up stage by stage, a wind rose of tabs, and a 3D chart table (free objectchart-table) whose vellum unrolls and inks itself as the learner scrolls to it, then three voyages to lay with ruler and dividers; assessment with five validated questions. Geometry shared by the 3D object and the figures (objects/chart-table/chart.js).
Unreleased — found by the desktop app (2026-10-07)
- The engine builds packages: a
buildrequest (session.build({ mediaQuality?, release?, profiles?, name? }); docs/architecture.md, Engine). The app used to call@elabora/nodebuild()beside the session: the state keys it wrote toelabora.lock.jsoncame back from the watcher as anexternalchange, shown as an agent’s work. The build now runs in the session’s write queue (outside the project lock, so CLI writers do not wait for renditions being encoded); the keys it adds are journaled as onesystemchange (commandbuild), and a successful build takes apublishversion named after the day in the course language (« Publié le 7 octobre 2026 »). Result:ok,zip,size,report,summary,errors,change,version; renditions encoded meanwhile come asmediaJobevents. - A new course in French starts with French texts.
elabora init,createProjectand the library wrote the placeholders of the plan and the intro screen in English whatever the course language (“Replace with the introduction validated in the storyboard.”); they follow it now (French and English; another language gets English): « À remplacer par l’introduction validée dans le storyboard. » - A results block failed to show while the course status was « not attempted »: its status took the SCORM value as a class name, and a class name cannot hold a space. The editor canvas and thumbnails of a results screen (nothing answered there) showed the previous screen instead; a player would fail the same way on a results screen reached before the course set its status. The class is now
t-status--not-attempted.
Unreleased — Linux and Windows, found by CI (2026-10-07)
- Project lock on Windows: a lock file another process is deleting refuses to open or be created (EPERM) for a few milliseconds; the lock read that as an error and the writer failed. It now counts as a lock being released and tries again (writers in five processes, 100 increments, none lost).
- Media on Windows: a conversion’s change lacked the removal of the original. Windows reports the folder of a file that changes, too. A reported folder brings its files to be read again, and the original, gone as soon as the job was done, was journaled as removed by someone else before the job’s change. Files a media job writes or replaces are now left to the job there as everywhere else, so undoing the conversion brings the original back in one step. A store that reports folders like Windows reproduces it on every system.
- Watching on Linux: Node’s recursive watcher watches each file by its inode and loses a file once it is replaced by a rename (atomic writes, the engine’s own included), so an agent’s next write to it went unseen until the rescan.
FsStorewatches each folder there (watchFolders). - Free-object visual baselines are per system (
<test>--<step>-darwin.png,-win32,-linux): fonts and WebGL draw 2 to 3 % of a 3D frame differently on another system, which failed gate 5 for an object made on a Mac. A system’s first run records its baselines; baselines without suffix were made on macOS and stay macOS’s. - Planetary sample: the phone-width column of the
figure-splitlayout isminmax(0, 1fr), so content can no longer widen it. On Linux, where maths are drawn with a wider fallback font, the practice screen was 386 px wide at 375 px. - CI:
.github/workflows/ci.ymlruns on Linux for each push tomainand each pull request; on Windows it runs the typecheck and the unit suite (the browser suites pass on a Windows 11 machine, but three canvas tests time out on GitHub’s Windows runners); macOS runs weekly..gitattributeskeeps LF line endings on Windows checkouts.
Unreleased — rendition cache pruned (2026-10-07)
pruneProjectData(run by automatic versions and once per engine session) also removes the renditions of<data>/cache/media/that no package can use any more: their master was replaced or deleted, or the preset’s parameters changed. They go once a week old, so a deletion undone meanwhile finds its rendition again. Its result hasrenditions: { removed, bytesFreed }.
Unreleased — resume on a renamed screen (2026-10-07)
cmi.core.lesson_locationnow holds the screen’s key fromelabora.lock.json, not its id.elabora renamecarries the key to the new id, so a learner who left on a screen renamed since resumes on it in the new version (they used to start again at the first screen, with their progress). The player still reads a location saved as an id by an older package.elabora testaccepts either in its close check; the update report no longer warns about renamed screens.
Unreleased — TypeScript packages (2026-10-06)
The core is now TypeScript in strict mode, in six npm workspace packages (@elabora/format, @elabora/player, @elabora/core, @elabora/node, @elabora/engine, @elabora/cli; docs/architecture.md), run directly by Node 24 with no build step (decision D12, revised). The entries below this one describe what the migration added; this one is what changes for people using the core.
- Node 24 or later is required (type stripping).
node bin/elabora.jsand theelaboracommand work as before. - Code that imported
src/…modules imports packages instead:@elabora/core(logic without I/O: a project is loaded from a files snapshot,loadProject(files);validateProject(project)),@elabora/node(loadProjectFromDir,validateDir,build, the emulator, media, backups, library…),@elabora/format(schemas and generated types). - Builds of the samples are byte-identical to 0.1 except where an entry below says otherwise (the player bundles,
"use strict", the format version, the cascade-layer fix). An oracle compared validation, projections and every built file before and after each step. - A typical module (87 screens, 288 MB of video): validation 16 ms (one screen re-validated: 0.2 ms); build 1.1–1.4 s and under 190 MB of memory, instead of 4.2 s and 845 MB more (videos were read whole and recompressed at every build, preview included);
elabora export0.4 s,elabora import1.1 s.
Unreleased — cascade layers in the documented order (2026-10-06)
- Visible change for existing courses: the course theme now overrides component stylesheets, as documented (
elabora.base<elabora.components<elabora.theme, docs/design.md).elabora/player.cssstill declared the prototype’s order,@layer elabora.base, elabora.layout, elabora.theme;, and loads first, soelabora.componentscame last: in packages and in the editor canvas a component or layout rule beat a theme rule on the same property whatever their specificity. Every stylesheet that names the layers (elabora/player.css,elabora/theme.css, the canvas) now starts with the same statement,@layer elabora.base, elabora.components, elabora.theme;(LAYER_ORDERin@elabora/corerender/styles.ts). - What to check in a course: rules of
components/*/component.cssthat set a property the theme also sets on the same element, typically a layout’s own screen title (.t-layout--hero .t-screen-title) against the theme’s.t-screen-title, or a component’s.t-textagainst the theme’s.t-text. Those now lose to the theme. Capture the course before and after updating (elabora screenshot . --screen all --size 1600x900,1280x720 --state initial,answered --out-dir shots/) and compare; where the component’s value was meant, write it in the theme, scoped to the layout or component (the theme has the last word on what it styles). - Samples: the titles of the
heroandstagelayouts and the object frame ofstage(planetary-gearset), the titles of thecosmos-hero,missionandsplitlayouts and the line height of the stat card’s sentence (orbital-rendezvous) moved from their component stylesheets to the theme. Every capture is unchanged.
Unreleased — broken projects, catalogue (2026-10-07)
- New warning
W_OBJECT_UNUSEDfor a free object placed on no screen (and not the course backdrop), with its own fix; it was reported asW_SCREEN_FILE_UNUSED, whose fix is about screens. The message is unchanged. - Broken projects are reported, never a crash: loading, validation, compilation, screen rendering, the projections, translation units, the LESSON.md export and every command read projects whose JSON parses but breaks the schema (agents’ half-written files). A document that is not a JSON object (
course.jsonholdingnull, a screen holding a list…) givesE_SCHEMAand is left out like a file that does not parse; it used to be silently ignored or to crash. Commands refuse to edit a part of a file that breaks the schema (EditError, for instancescreens/intro.json: "layers" is not a list);migrate_componentsrefuses to overwrite aelabora.lock.jsonthat does not read. A block of unknown type or a free object without manifest compiles to an empty block. Seeded mutation tests check every reader and command. W_UI_LABELS_LANGUAGEno longer points tosrc/runtime/i18n.js.
Unreleased — the editing engine (2026-10-06)
@elabora/engine(docs/architecture.md, Engine): the editing session the desktop app sits on.openProject(dir)keeps the project in memory (files snapshot, loaded project, findings per screen and per project check) and runs commands,undo,undoBatch(« Annuler le travail de l’IA ») andmark(« Demander à l’IA ») one at a time as journaled changes under the project lock, given its snapshot: what an edit wrote updates it without reading the folder, and only what the change can affect is validated again (on an 87-screen course: about 5 ms from a command to its diagnostics).- Other writers: changes of the CLI and the MCP server are read from the journal and applied, never journaled again; files written by agents’ own tools are journaled as one
externalchange (bursts debounced, the session’s own writes recognised by content); a JSON file being written keeps its last valid content, withE_JSON_PARSE, until it parses. - Batches and versions: AI batches close after 5 minutes without AI changes, at a human change or a mark, with an
after-aiversion; a mark keeps abefore-aiversion;autoversions every 5 minutes of activity, acloseversion, the project’s data pruned once per session. Restores are journaled as onesystemchange, so they can be undone. - Media in the session: the project’s media queue runs from opening; job results are journaled (a conversion with its references rewritten, undoable back to the original); media dropped into
assets/that are not masters are converted; the media status follows the jobs (I_MEDIA_PROCESSING). - Rendering and the canvas:
renderScreenthrough render contexts kept while shared files are unchanged, memoised per screen and view;openCanvas,canvasView,closeCanvasdrive the canvas host, and the screen shown is pushed again after each change that affects it, the change’s sequence number as render id (about 10 ms from a command to the canvas DOM in Chrome). - Older formats: option
migrateask(the summary says so, themigraterequest writes it, an edit migrates first),auto(on open, after the open version) ornever(writes refused). - Message protocol (
@elabora/engine/protocol): requests with ids, typed results and errors, events (changed,diagnostics,mediaJob,backupTaken,batchClosed,error,closed), withsession.handleandsession.subscribe, andProtocolClientfor the app. - Journal: a change may list previous contents that no store kept (
unavailable: a binary file overwritten by an agent’s own tools before any version); it is journaled all the same, and undo skips that part. Undo’s reason for contents not kept is nowunavailable(it wasmissing, which stays for targets that are gone; MCPelabora_undoskipped). @elabora/coreproject/store.ts: theStoreinterface;@elabora/nodestore/:FsStore(async reads, atomic writes with retries on Windows sharing violations, recursive watching) andreadSnapshot.@elabora/nodeedits takefiles,validate: falseandautoVersion: falsefor callers that hold the project in memory, and returnwritten. The component registry is kept per set of components (a project with one screen replaced keeps it); generator imports are parsed once per source text.
Unreleased — media: import, masters, renditions, streaming packages (2026-10-06)
elabora media import [dir] <file…> [--to <folder>] [--name <name>] [--no-wait] [--json](MCPelabora_media_import) brings video, audio and images intoassets/as web masters at thehighpreset: MP4 (H.264, AAC, index first), M4A, WebP (JPEG or PNG when ffmpeg has no WebP encoder), SVG as is. Files already right are copied or rewrapped without re-encoding; only the streams that need it are re-encoded; metadata (GPS positions of phone videos and photos included) is removed, image orientation and colour profile kept. The original is never copied. Names are reserved at once under the project lock; the conversion runs as a background job (--no-wait: a detachedelabora media workergoes on).elabora media optimize [dir] [<path>…|--all]converts media ofassets/that are not masters next to themselves, then rewrites the references and removes the old files in one undoable change (optimize_media).elabora media status [--wait](MCPelabora_media_status),elabora media cancel <job>,elabora media worker.- New codes:
W_MEDIA_NOT_OPTIMIZED(a media file ofassets/that is not a master; a GPS position is reported as a privacy matter),I_MEDIA_PROCESSING(a reference to a file a media job is writing, instead ofE_ASSET_MISSING),E_MEDIA_UNREADABLE,I_MEDIA_TOOLS_MISSING(no ffprobe: media not checked),E_MEDIA_RENDITION(a rendition could not be made).E_PACKAGE_TOO_LARGEgives the size of the package at each media quality and the one that fits. - Media quality of packages:
elabora build --media-quality high|medium|low(MCPelabora_buildmediaQuality), elsecourse.jsondelivery.mediaQuality(format 0.2), elsehigh. Belowhigh, packages ship renditions encoded once from the masters and cached; a rendition under another extension (MP3 → M4A) takes that name and the course points at it.elabora buildwaits for media jobs and shows their progress on stderr;--no-waitrefuses to build while they run. The build report has amediasection (quality, files, renditions) when the course ships media. - Packages are streamed: media are never read whole; the ZIP is planned first (exact size before writing) and written as a stream; folder builds clone media. Packages without media keep their bytes. A 300 MB module builds in under 2 s with under 300 MB of memory.
- Preview: media are served from the project folder with HTTP range requests (videos seek, Safari plays), never copied into the preview; media still being converted show a placeholder. The LMS emulator serves package files as streams with range requests too.
@elabora/coremedia/(presets, conformance, plans, size estimates); the files snapshot gives media files ofassets/their probe and verdict (BinaryFile.media), probing only new or changed files, andProject.media(jobs under way, unreadable files, ffprobe missing).@elabora/nodemedia/(tools, probe, lossless image metadata removal, encoding, jobs, import, optimisation, renditions),packaging/media.ts,http/files.ts.
Unreleased — prototype format 0.2, format migrations, rename (2026-10-06)
- Prototype format 0.2 (never published: the format restarted at
elabora/0.1on 13 October, D39):course.jsondelivery.mediaQuality(highby default,medium,low: the media preset of the package; read by the media pipeline once it lands in the build; set withupdate_course, MCPelabora_update_course, checked against the schema), thea11ywording of free-object blocks is part of the format, quizshuffleis removed (it was never implemented). Generated types and the registry follow 0.2 (MediaQualitytype). - Migrations: an older course validates and builds as it is, read migrated in memory, with the new info
I_FORMAT_OUTDATED(whatelabora migratewould change;elabora validateshows it).elabora migrate [dir] [--dry-run] [--json]writes it as one change after a backup version (elabora undoreverts it). Edits through Elabora (CLI and MCP) migrate an outdated course first, as its own change (the edit result carriesformatMigration), and library imports migrate archives and folders. Migrations keep each file’s formatting and change only the migrated values; the gate results of a free object (object.gates.json) carry over when the object changed by its format strings only. A course in a newer format gives the new errorE_FORMAT_NEWERand is neither read further nor written. 0.1 → 0.2 changes the format and$schemastrings, drops quizshuffle(blocks, forks, component examples and fallbacks) and updates the two lines ofAGENTS.mdthat name the format and renaming. elabora rename [dir] <old-id> <new-id>and MCPelabora_rename(from,to; 46 tools with the library tools): a section, screen, layer, block or event gets another id in one undoable change. Sections, events, translation unit ids, theme and component stylesheet selectors andelabora.lock.jsonfollow, a screen file takes the new name, so the course validates as before and learners keep their progress; plan waivers that name the id are proposed in a change request. Refused when the new id is in use or not a slug.elabora difflists renamed elements (learnerState.renamed) instead of removed and added ones.- Examples, templates and LMS profiles are in 0.2;
test/fixtures/format-0.1/keeps a course in 0.1 for the migration tests. AGENTS.md (template), the MCP instructions and theelabora-course-authoringskill tell agents to rename withelabora renameand about format versions. - Node API:
migrateProject,migrateIfOutdated(for the engine on open; both take the executor’s optionsfiles,validate,autoVersion, and returnwrittenwithfiles),edit.rename; core:checkFormat,FORMAT_STEPS,renameId;project.migrationon loaded projects.
Unreleased — the editor canvas (2026-10-06)
renderScreen(@elabora/corerender/screen.ts): one screen as the player shows it, without building the course: the course data reduced to the screen, the view (state,step,layer), the design stylesheets in their cascade layers and the project files it references;languagerenders a translation.createRenderContextcomputes once what screens share; a screen edited since renders with it. The build gets its course data andelabora/theme.cssfrom the same functions (packages are byte-identical).- Canvas (
elabora/canvas.js,@elabora/playercanvas.ts): the player mounted for one screen without SCORM, navigation chrome or saving, updated in place by render data (postMessageorwindow.__elaboraCanvas.receive): unchanged blocks keep their nodes, so free objects keep running while the author types; scroll, step and open layer stay; every block’s box is reported (elabora/layout). Files carry the hash of their content: an image replaced at the same path or an editedobject.jsloads again. The screen rendering of the player moves toscreen-view.ts, shared by both. - Canvas host (
@elabora/nodestartCanvasHost,createCanvasHandler): serves the canvas page, the player bundles and the project files renders list (media streamed from the project folder with range requests), and pushes render data to the pages as server-sent events.
Unreleased — project library and archives (2026-10-06)
- Project library (
@elabora/nodelibrary/, docs/architecture.md, Project library and archives): the app keeps projects as plain folders named by their project id in a library of its own (ELABORA_LIBRARY, else thelibraryPathsetting, else the local application data), with a trash.elabora projects list [--json] [--validate]lists them newest first (title, course id, language, last change, size, validation summary, any error, folder);elabora projects path [<id>]prints the library’s folder or a project’s (by id or its first characters), and--set <dir> [--move|--copy]changes the library’s folder, moving the projects by renaming, or by copying and checking every file;elabora projects create <course-id> --title <t> [--language fr],duplicate <id> [--title <t>],rename <id> --title <t>(an undoable edit ofcourse.json),trash <id>,trash list|restore <entry>|empty [--older-than <days>]. - Project archives (
elabora-archive/1, documented in docs/architecture.md and pointed to from SPEC.md §14):elabora export [dir] [--out <file>] [--history] [--json]writes<course-id>.elabora(by default in the project’sdist/), a ZIP of the project’s files with a manifest of their sizes and SHA-256, and with--historythe journal, the backup versions and their contents.elabora import <archive|folder> [--replace [<id>] | --copy] [--json]checks the whole archive (names, sizes, hashes, history) before anything appears in the library; when the library holds the course already it imports nothing, names the projects and exits with 1 until--replace(same id and folder, replaced files to the trash, history kept, a backup version taken first) or--copyis given. A folder is imported as a copy. Long exports and imports report their progress on stderr;--jsonresults go to stdout. - MCP: read-only
elabora_projects_list(optionalcourseId) andelabora_projects_path(optionalid) let agents find a project’s folder; no tool changes the library. @elabora/nodepackaging/zip-reader.tsreads ZIP files as streams (one entry at a time through one reused buffer, checked against its CRC and size, inflating stopped at the declared size);writeZiptakesstoreand a known CRC and size for files already read, and reports its progress.
Unreleased — command line and MCP server: undo, history, batches, strict flags (2026-10-06)
elabora history [dir] [--limit 20] [--json]shows the changes grouped by batch of AI work: when, who (Claude Code, an MCP client, a person, edits made outside Elabora), the request that started the batch, each change in a few words with its sequence number, and what is undone;--jsongives the batches (historyBatches).elabora undoundoes the last change;--change <id|seq>one change (an undo too, which redoes it;--seqstill works);--batch [<id>]a batch of AI work, by default the last one. Whatever was edited since is kept and listed (kept: screens/a.json title: changed since), and the command still succeeds.elabora mark [dir] "<request>"labels the next batch of AI work with the request that starts it. In a Claude CodeUserPromptSubmithook,elabora mark --stdinreads the prompt from the hook’s JSON and prints nothing.- MCP: writes are journaled as
{ kind: "agent", name: <MCP client name> }and every write result carries itschange(id,seq,batch).elabora_historyreturns the batches (limit);elabora_undotakeschange(id or seq) orbatch(id, ortruefor the last batch of AI work) instead ofseq, and returnsundone,change,files,skipped(file,path,reason,message) anddiagnostics. - MCP resources: the server lists and serves the shipped skills again (
elabora://skills/<name>); since the move intopackages/cliit listed none. - Flags are checked: every command declares its flags; an unknown flag, a value given to a flag that takes none, or a missing value stops the command with exit code 2 and its section of the help. They were ignored before, and a flag without value took the next argument (
elabora validate --json coursevalidated the current folder).elabora <command> --helpprints the section of the command;elabora --versionprints the version.elabora changewith an unknown subcommand prints its usage. Command words come first. elabora component new --leveltakesblockorscreen;elabora component add|publishwithout--libraryprint their usage.- Code:
packages/cli/src/commands/(one module per group of the help) andsrc/mcp/(one module per group of tools, typed tool definitions whose arguments ajv checks).
Unreleased — edits as operations, journal and undo (2026-10-06)
- Every edit is a journaled change: the commands of the CLI and the MCP server (
add_block,move_block,create_screen…) are pure functions in@elabora/corecommands/;@elabora/nodeedit/runs them under a project lock, as operations addressed by id (@elabora/coreops/), with their inverse, their author (ELABORA_AUTHOR, Claude Code, agent, human) and their batch, injournal/<yyyy-mm>.jsonlof the project’s data folder. Edits returnchange(id,seq,batch);undois the change’s sequence number in the journal. The edit history of the prototype (.elabora/history/) is no longer read. - Undo keeps later edits: undoing a change applies the inverse operations whose targets still hold what the change wrote and reports the others (
skipped), instead of refusing when a file changed since.undoBatchundoes a batch of AI work (by default the last),historyBatcheslists batches with the request that opened them,appendMarkrecords that request; an undo can be undone. A hand-formatted JSON file comes back exactly as it was. - Automatic versions: an edit takes a backup version just before writing when the newest one is older than 5 minutes, then prunes the journal (30 days) and the backups (
pruneProjectData). - New errors for refused edits:
E_LOCKED(another writer holds the project),E_CONFLICT(a file changed on disk while the edit was computed; computed again twice first),E_BLOB_MISSING(a binary content is not kept), andE_OP_TARGET_MISSING,E_OP_DUPLICATE,E_OP_NOT_KEYED,E_OP_NOT_JSON,E_OP_INVALIDfor operations that cannot apply. Edit errors carry them incodeanddiagnostics.
Unreleased — the core without I/O (2026-10-06)
- Projects load from a files snapshot:
@elabora/corereads a project only throughProjectFiles(text files with their text, binary files with their size and hash) andloadProject(files); it has no Node import, noprocess, noBuffer, so the same validation and compilation can run in a browser or on a server.@elabora/nodereads course folders (readProjectFiles,loadProjectFromDir,validateDir); media are hashed on demand, through.elabora/cache/hashes.json. - New warning
W_SYNC_CONFLICT_COPY: conflict copies made by OneDrive, Dropbox, iCloud or Google Drive (intro (conflicted copy).json,intro-DESKTOP-4F2K9.json…) and iCloud placeholders are reported and no longer loaded as content. - Asset and font references are matched with the exact case of the file name, as LMS servers do (macOS used to find
Logo.svgforlogo.svg).
Unreleased — explore in any order (2026-10-06)
- Named controls: a component button can carry a name (
data-t-click="engine", or bound to a prop: one marker per item of a list), and a click event can listen to that name only (on.name). Tabs and disclosures of components fireclicktoo, with their key as name. One block can now hold every marker of a figure. Names are checked on the rendered instance (E_EVENT_TARGETlists the names a block has);W_DEAD_BUTTONnames the button left without an event. - Visited state: every control (buttons, tabs, disclosures) is visited once the learner has used it, a tab or disclosure once its content is shown. The player sets
data-t-visited(style a check, a dimmed marker), saves it with the progress (a lock key per control; suspend dataw, a few characters each, counted in the budget) and restores it on return or relaunch. - Visited condition:
{ "visited": "<block id>" }holds once every control of the block is visited,"name"for one; any screen’s block. The visit is recorded before the click’s events run, so “all visited → complete-screen” works on the last one: Storyline’s “click each item, then Next”. New checkE_VISITED_TARGET.
Unreleased — click to reveal (2026-10-06)
- Event action
reveal-step({ "type": "reveal-step", "step": 3 }): a click in the slide (a marker over a figure, a button) reveals the steps of the screen up tostep, as Continue would; selected again, it brings that step back into focus. Checked against the steps of the screen (E_ACTION_TARGET). Steps that are skipped fade in too. - Blocks of the latest step shown carry
data-t-current: the marker to select next can call for a click. - Continue per screen:
continueRevealsSteps: falseleaves the steps of a screen to its own triggers (the button reads Next and, withlockUntilComplete, waits for the last step: “Explore this screen to continue”, a new player label in the 7 languages).E_STEP_UNREACHABLEwhen a step is then reached by noreveal-step. - Resume where the learner left: a screen left part-way opens again at the step reached (Previous, outline, relaunch), saved in the suspend data (
p, counted in the budget); a screen seen to the end shows every step. Before, any visited screen showed every step. - Answer room measured again once fonts are loaded and shortly after the screen shows (feedback-only glyphs or fonts arriving late left it short under load), and by
ready()before fit checks and captures, which also wait for fonts. - Faster tests: the emulator runs its launches four at a time (
elabora testtoo;paralleloption); e2e files run side by side; the multi-LMS test runs Chrome’s two unload settings for one LMS only. A run that ended withoutonRunhung the emulator (fixed before release). - Example
orbital-rendezvous: “Lower is faster” is built like a Storyline slide. Only marker 1 shows at first, pulsing; selecting it flies the Space Station for one hour around the Earth (its trail draws behind it), then its card appears and marker 2 starts pulsing; after 3, the rule. Course componentshotspotandorbit-flight, layoutsplitwithfigure: "stack", the figure generated in layers.
Unreleased — what the space sample taught (2026-10-05)
Features decided after building examples/orbital-rendezvous, and the bugs it surfaced. No format version change while prototyping.
- Nothing moves when the learner answers (slides): every activity reserves the room of its tallest outcome (behaviours render their outcomes in a dry run, measured on hidden copies; templates opt out with
data-t-reserve="off"); blocks concealed by events keep their place (data-t-concealed); layers can open as a panel over part of the screen ("presentation": "panel"), the slide staying usable.elabora fitmeasures the answered slide and reports what still moves (W_SLIDE_SHIFT,I_SLIDE_SHIFT_MINIMUM). - Captures for agents:
elabora screenshottakes any number of shots in one build and one browser (--screen all,--size a,b,--state initial,answered,--out-dir), filmstrips of animations on one contact sheet (--at 0,2000,4000),--motion; MCPelabora_preview_screenshotgainsstate,at,motion. Full-page captures now paint free objects below the first viewport (sandboxed frames kept in process) and keep the footer at the end of the page; the preview banner is left out. - Measured contrast:
elabora fitcompares text with the pixels behind it (backdrops, images, gradients) at the reference size (W_TEXT_CONTRAST_MEASURED). - Backdrop (
theme.backdrop): CSS layers behind every screen that follow the learner (data-screen,data-section,data-step,data-variant), variants picked per screen (backdrop, ornone), and decorative free objects (pedagogicalRole: "decoration") mounted once, given the course context (ctx.context,host.onContext), still under reduced motion and frozen below 24 frames per second. ChecksE_BACKDROP_OBJECT,E_BACKDROP_VARIANT,E_OBJECT_DECORATION. - Safe area: layouts declare the part of a free object they cover (
--t-safe-top|right|bottom|left); objects get it in px (ctx.safeArea,host.onSafeArea,object/safe-area); tests setsafeAreaorset-safe-area. - Translation: one package per language;
translations/<lang>.xlf(XLIFF 2.0) in the source course;elabora translate export|status|build|approve; stale units when the source changes; checks of maths, links, placeholders and schema limits; the translated course regenerates its figures; the translated plan has its own approval (translationOf). Object words: manifeststrings(ICU messages) read withctx.t(), reworded per block (props.strings,props.a11y),E_OBJECT_STRINGS,W_OBJECT_STRINGS_LANGUAGE,W_OBJECT_STRING_UNDECLARED. Player labels in fr, en, de, es, it, nl and pt with ICU plurals;W_UI_LABELS_LANGUAGEelsewhere. The proof dossier is fully worded in the course language. - Free objects: a loading indicator after 400 ms (machines without a GPU); boot time judged on the median of every mount of the run, over the budget a warning (
W_GATE_BOOT_SLOW), over 10 s an error;elabora object test --only <name>; a full run is recorded inobject.gates.jsonand the proof dossier reports it (passed, failed, stale, never run). - Fixes: XML namespace names (
http://www.w3.org/2000/svg) are not external resources; a “ResizeObserver loop” warning no longer fails an object, errors after load are logged instead of “could not load”, an object answering after the 15 s timeout recovers, each error is reported once; the WebGL budget counts WebGL contexts, not canvases; consecutiveexpect-eventsteps share the events of the previous actions;x-tokenaccepts custom tokens; themeurl()may point anywhere in the course; reduced motion stops::before/::afteranimations; punctuation stays on the line of inline maths;component new|forkprint only their own findings; full-bleed slides are edge to edge; the contract saysmount()must return fast. Test runs no longer hang on a page that stops answering: an emulator run with no progress for 90 s fails with the step it stopped at, and an object call unanswered for 60 s fails the smoke gate (the other gates are reported not run).elabora testprints each run as it ends. - AGENTS.md (template and examples) leads with the command line; MCP tools are named alongside.
- Example
orbital-rendezvous: space course in slides with a backdrop, a frameless 3D phasing lab using the safe area and manifest strings, a panel debrief, answered slides that fit 1600×900 and 1280×720, and a French translation intranslations/fr.xlf.
Unreleased — size classes, slides that fit, steps (2026-10-05)
- Size classes: sizes in rem, and 1rem follows the window. Reference 1600×900 (1rem = the learner’s default, 16 px); medium ≥ 1360×760 (15 px); compact, such as 1280×720 (14 px); phones and portrait tablets (15 px). The player, the free-object UI kit and the sample are in rem;
contentWidthandwideWidth(px at the reference size) are emitted in rem. Text fields never go under 16 px. Free-object frames receive the page’s 1rem (rootSizeat initialize,object/set-root-sizewhen the window changes class). New warningW_STYLE_FIXED_FONT_SIZEfor px font sizes in course and component stylesheets. - Slides that fit:
presentation.fit(reference1600x900,minimum1280x720 by default).elabora fit(MCPelabora_check_fit) measures every screen with all its steps in Chrome: overflow in px and the blocks below the fold.elabora testruns it for slides (W_SLIDE_OVERFLOWat the reference size,I_SLIDE_OVERFLOW_MINIMUMat the minimum) and the proof dossier reports it. Slide padding reduced. - Steps:
stepon the blocks of a screen; Next becomes Continue while steps remain. Pending steps take no room in scroll presentation and keep their place in slides; revealed blocks take focus and fade in. WithlockUntilComplete, the activities of the revealed steps come first. A screen counts as viewed at its last step; visited screens show every step. ChecksW_STEP_ORDER(scroll presentation) andW_STEP_PLACEMENT.elabora screenshot --step, MCPstep. - Screenshots default to 1600×900 and run with reduced motion.
aspectRatio: "fill"objects now get the flexed height of their slot. - Example
planetary-gearsetreworked as slides that fit 1600×900: a compact hero with the goals and the three ratios revealed as a second step; anatomy and the worked example split along the plan items (9 screens); the 3D bench fills the stage, its debrief table opens in a layer; the hoist decision sits next to its figure; one assessment question per slide, the configuration question as four cards in a row. Fixed: the hero figure collapsed to zero width on phones.
Unreleased — components (2026-10-05)
One model for every block type; no format version change while prototyping.
- Components:
components/<id>/(component.json, template.html, component.css, migrations.mjs) define block types and screen layouts. Props schema withx-kind(text, markdown, tex, asset, token), named slots holding blocks (blockson instances), named styles, examples. Templates rendered at build time ({{prop}},data-if,data-each,data-slot,data-title,{{@id}}), parsed with parse5. - Built-in components: the native catalogue is rendered from built-in templates (
src/components/builtin/), except video, audio and free objects (code). - Headless behaviours: forks of quiz, numeric, decision and results keep scoring, saved state (same key), SCORM records, focus and announcements in the player; templates provide
data-t-…hooks. Primitives for any template: click events, disclosure, tabs, player labels. - Forks (
extends,propsExtend), fallback to native blocks (declared or generated; used by LESSON.md export, the client view and detach), duplicate and detach of instances, moves into and out of slots. - Libraries:
elabora component add|publishwith pinned copies (origin and hash), explicit upgrades, local edits reported. - Migrations:
elabora component migraterunsmigrations.mjssteps on every instance, all or nothing; versions recorded inelabora.lock.json. - Layouts are screen-level components (
layouts/is gone;layoutProps); cascade layerelabora.componentsreplaceselabora.layout. - Checks: 21 component codes (templates, hooks, slots, props, fallback, migrations, library copies, usage); heading order through templates and slots; plan references inside component props; quiz forks may add presentation fields to plan question options.
- CLI
elabora component list|new|fork|preview|migrate|add|publish,elabora block duplicate|detach; MCP:elabora_list_components(replaceselabora_list_block_types),elabora_get_component,elabora_create_component,elabora_fork_component,elabora_write_component_file,elabora_duplicate_block,elabora_detach_block,elabora_migrate_components,elabora_add_library_component,elabora_publish_component,elabora_preview_component(42 tools; 43 withelabora_check_fit). - Player: the course outline floats over the screen under the header instead of pushing it down; it closes on Escape, a click outside or when focus leaves it. No scroll bounce, pull-to-refresh or scroll chaining into the LMS page (
overscroll-behavior: none). - Example
planetary-gearset: layouts as screen components, aratio-cardcomponent replacing a CSS convention, aconfig-quizfork of quiz with generated configuration diagrams for plan question Q-3.
Unreleased — design layer prototype (2026-10-05)
Agents design freely; content stays in blocks. No format version change while prototyping.
- Theme: free course stylesheet (
theme/theme.css, cascade layerelabora.theme), embedded fonts (also sent to free objects), custom tokens exposed as--t-*variables and bindable withx-token, named styles per block type or screen (style,data-style,t-s-<name>),presentation(scroll/slides, content widths). - Layouts:
layouts/<id>/templates with named slots (layout,slot), full-bleed bands, reading order from the template; checked for forbidden elements, text, undeclared slots, accepted block types. - Maths:
$…$/$$…$$in Markdown and amathblock, LaTeX rendered to MathML at build time with temml (E_MATH_PARSE). - Numeric answers:
numericblock and numeric plan questions (tolerance, unit, targeted feedback per misconception, locale-aware input,numericSCORM interactions, suspend-data budget). - Responsive free objects:
aspectRatioauto/filland more ratios,narrowAspectRatio,minHeight/maxHeight,host.requestHeight(), theme variables, fonts and a UI kit inside frames, test framesize. - Asset generators:
generators/*.mjs,elabora assets(MCPelabora_build_assets), provenance with stale / hand-edited detection, listed in the build report. - Checks: words in SVG figures, CSS and templates; literal colours and external URLs in stylesheets; phone-width overflow in
elabora test; axe findings now name the elements (object gate 6 and emulator). - Fixes: no focus ring on the screen title, English labels without the French space before colons, styled tables, an object frame that never answers now shows its fallback after 15 s,
elabora screenshotwaits for images and objects (--width,--height,--viewport,--layer), function parameters named like globals no longer flagged by the object analyzer, props read in imported files count as used. - MCP:
elabora_write_design_file,elabora_build_assets; layouts, slots and styles in screen and block edits; screenshots of layers and at other sizes. - Example
examples/planetary-gearset(English): hero, split and stage layouts, theme with Inter and Space Grotesk (OFL), generated figures without words, numeric questions, typeset equations, responsive 3D test bench.
0.1.0 — 2026-10-05
First version of the open core (Stage 1).
- Format
elabora/0.1: JSON Schemas for course, plan (contract layer with approval hash and change requests), screens, 12 native blocks, events, variables, theme tokens, free-object manifest and tests, LMS profiles, diagnostics. - Validator with stable diagnostic codes: format, plan conformity, item writing and feedback, accessibility (alt text, headings, contrast), events, free-object static analysis, suspend-data budget per LMS profile, pedagogical waivers.
- Deterministic SCORM 1.2 compiler with build report and proof dossier.
- Accessible player: layers, events, scoring, status machine without regression, session end without
unload, sandboxed free objects. - Free-object contract 0.1 with three.js r186 vendored and automated gates 2–6.
- 11 executable LMS profiles and an emulator (iframe, popup, popup chain, nested iframe; abrupt close, resume, status, axe-core).
- MCP server (30 tools, journaled and undoable edits), four skills, AGENTS.md template.
- LESSON.md 1.9 export/import for simple blocks;
elabora inspectfor any SCORM ZIP;elabora difffor updates.