# D42. The Revue talks to its module through a versioned postMessage bridge, shipped in review builds only (14 Oct 2026)

Step 2 of the first public version is an online Review: authors share a secret link, reviewers comment without accounts on a page at `revue.elabora.studio` that shows the module in an iframe from `*.elabora.page` (decided on 14 October). The page draws pins, outlines and its panel over the module; it needs to know where the reviewer is and where every block is, to take the module to a comment, to pause its narration, and to have its column move aside for a floating panel. The two are on separate origins (a module's code never shares an origin with the page that holds the comments), so they can only speak through `postMessage`; the debug hooks `window.__elabora` are neither reachable across origins nor a contract.
- **A protocol, `elabora-review/1`, versioned in every message.** Module → page: `ready` (build id, course id, version and title, languages, screens with their permanent keys), `layout` (the text size taken, its rules, the column, the widest column, the margin kept, the header's and footer's heights), `location` (screen key, step, layer; time, line and word on a timed screen), `blocks` (each block of what is shown: id, type, built-in kind, a short accessible name in the module's language, its box in the frame, its reading order, hidden or not), and the answers `went`, `measured`, `error`. Page → module: `hello`, `go` (to an anchor), `pause`, `resume`, `layout` (a margin on the right), `measure` (a frame of another size), `scroll` (a wheel passed on). A message may carry an `id`, its answer carries it as `re`. docs/review-bridge.md is the reference; `@elabora/player/review-protocol` has its types and pure functions, for the page too.
- **Origins named at build time, nothing else heard or told.** `elabora build --target web --review-origin <origin>` (repeatable: the production page and development pages; MCP and engine `web.review { origins }`, `build({ target: 'web', web: { review } })`). An origin is `https://host[:port]`, or `http` on this computer (`E_WEB_REVIEW_ORIGIN` otherwise). The module takes messages only from its parent window, only from a named origin, then only from the page's; it posts only to that origin (the embedder's when `location.ancestorOrigins` gives it, else each named origin until the page speaks; never `'*'`), and stays silent embedded elsewhere. No network: the comments travel from the page to the API, never through the module (AGENTS.md rule 2). A list rather than one origin, because the page is developed on `localhost` against real review builds.
- **Shipped in review builds only, as its own file.** The bridge is `elabora/review.js`, loaded after `player.js` by the review build's `index.html`, and the course data's `delivery.review { origins, languages }` says it is one. `player.js` does not change: the bridge drives the player through the hooks of the same build (`go`, `reveal`, `openLayer`, `timeline.at`/`atTime`/`pause`/`play`, the course state) and the DOM, so the protocol is the stable surface and the hooks stay free to change with the player. A build without `--review-origin` is the same bytes as before the bridge existed (checked against `main` on two samples, web and SCORM; the tests compare a review build with a plain one: only `review.js`, `index.html` and `course.js` differ). A review build's defaults are a review's: progress `none`, navigation `free`, `noindex` (each can be given).
- **Hit testing on the page.** The module sends the boxes of its blocks when they change (screen, step, layer, size, scroll, a block that grows; once a frame at most, only when something changed); the page finds the block under the pointer itself (`blockAt`), with no message per pointer move. A free object is one block: its frame is sandboxed and its parts are not reported. The block code is the canvas's (`layout-report.ts`, shared with `canvas.ts`).
- **The anchor**, shared with the comments API: `{ version, language, screen (D4 key), block?, step?, layer?, time?, line?, word? (D28), occurrence?, point? (0..1 in the block's box, or the frame's), frame }`. `occurrence` is added to the contract first proposed: a word said twice in a line (« ni … ni ») needs it, as the format's anchors do. `go` prefers the word, then the line, then the time, so a comment survives a new take; it reports what it no longer found (`went.missing`).
- **The docking rule is measured by the player**: the text size follows the frame (16, 15, 14 px by player.css's media queries, sent as rules), and the widest column is the course's cap at that size or the room left. The page docks its panel when docking changes neither; else it sends a margin, and the module centres its column in the room left **without changing its size** when it fits there (a translation of the column only: header, footer, backdrop and full-bleed screens stay), and does not move otherwise.
*Revisit if:* the page needs parts of free objects (an object event naming the part under a point), comments inside the module's own UI (a mode the module draws), or a second consumer of the bridge (the app's Revue showing a review build) makes the hooks worth a public API of the player.
*Amended on 14 Oct 2026 (the review page's findings):* `location` gives the narration line's text (`said`) and its speaker's name (`speaker`, when the line has one), as the captions show them, and `anchorFor` keeps them in the anchor (`said` at most 280 characters, cut around the word at spaces), so that the page and the author's tools say « quand Marc dit « … » » without the module. A new module message, `key { key: "c" }`, tells the page that the reviewer pressed C while the focus was in the module (no modifier, outside a text field, not taken by the module): keys pressed there never reach the page, whose shortcut for « Commenter » is C. Both live in `review.js` and `review-protocol.ts`: a build without `--review-origin` gains nothing from them.
