# Free-object contract 0.1

A free object is custom code (a three.js simulation, a mini-game, an interaction outside the catalogue) that stays **testable, editable without code, sandboxed and tracked**. It derives from an earlier free-scene contract (v0.1), renamed "free object" as in the app (the counterpart of a Storyline Web Object, without colliding with Storyline "scenes").

## 1. Files (`objects/<id>/`)

| File | Role | Who reads it |
|---|---|---|
| `object.manifest.json` | Machine contract: props, objects, state, events, tracking, budgets, a11y, sandbox | Validator, editor, host, tests |
| `object.js` | ES module exporting `mount` | Build, sandbox |
| `object.tests.json` | Behaviour sequences and snapshots | `elabora object test` |
| `object.notes.md` | Pedagogical role in 2–3 sentences | Client review (the client sees props and notes, not code) |
| `__snapshots__/*.png` | Visual baselines (gate 5), one per system (`<test>--<step>-darwin.png`, `-win32`, `-linux`; without suffix: made on macOS) | `elabora object test` |

The build bundles `object.js` and what it imports into one minified classic script. Code from elsewhere keeps its licence in a legal comment (`/*! … */`, or one with `@license` or `@preserve`): comments are dropped, legal comments are gathered at the end of the built `object.js`, so the notice ships with every package.

## 2. Manifest essentials
See [`object-manifest.schema.json`](../packages/format/schemas/0.1.10/object-manifest.schema.json).

- **props**: every setting an author may change; `default` and `description` required, `title` (a short label in the editor) recommended. `x-group` (panel group), `x-advanced` (a setting authors rarely change: kept under « More settings » while it has its default), `x-control` (`text`, `textarea`, `slider`, `range`, `number`, `toggle`, `select`, `color`, `vec2`, `vec3`, `vec3-gizmo`, `asset`, `list`), `x-token` (theme token used by default, e.g. `color.accent`), `x-text-on` (below), `x-object-id` (clicking that object in the preview opens this prop), `x-editable-by` (`author` or `developer`), `x-translatable`, `x-enum-labels` (below). The control fits the type (`W_OBJECT_PROP_CONTROL`): `select` needs an enum or an integer range, `slider` a minimum and a maximum, `color` a string of `format: "color"`.
- **Controls of numbers**: `slider` for one number on a scale (`minimum`, `maximum`, a `multipleOf` for its step), `number` when there is no scale. `range` for a low and a high value on the same scale (a window of time, the band a value must fall in): `{ "type": "array", "items": { "type": "number", "minimum": 0, "maximum": 24, "multipleOf": 0.5 }, "minItems": 2, "maxItems": 2, "default": [8, 18], "x-control": "range" }`, low first; the editor shows one slider with two handles. `vec2` and `vec3` for a point or a size of two or three numbers (`[x, y]`, `[x, y, z]`, `minItems` and `maxItems` 2 or 3): X, Y and Z on one row. `vec3-gizmo` is a `vec3` the object can also move in its preview. Two or three number fields named `x`, `y` and `z` that follow each other (in a list's items) are shown on one row the same way, without a control.
- **Choices in words**: an enum holds values for the code (`parefeu`, `30-78`); `x-enum-labels` says how each reads in the editor, in the manifest's language: `"failingDevice": { "type": "string", "enum": ["commutateur", "routeur", "parefeu", "serveur"], "x-enum-labels": { "parefeu": "Pare-feu", "serveur": "Serveur d'impression" }, … }`. Keys are values of the enum (numbers written as text, `E_OBJECT_ENUM_LABELS`); a value without words shows capitalised (`Commutateur`). It works on a prop, on the items of a list of simple values and on a field of a list's items. A block rewords them with `props.enumLabels` (`{ "failingDevice": { "parefeu": "Firewall" } }`, `"voyages.from"` for a field of a list), and translations write there (units `block.<id>.enumLabels.<prop>.<value>`): authors of the translated course read them in its language. They are the editor's words: the object receives values, never these labels, and « Rechercher » does not find them.
- **Settings in the course's language**: the settings' `title`, `description` and `x-group` and the events' `description` of an object a block places are what authors read in the inspector and the events: the course's translation covers them (units `object.<id>.…`, written in the manifest of the translated course). A backdrop's object has no settings in the editor: its manifest stays as written.
- **Lists**: a setting that repeats (the voyages of a chart, the stations of a route) is one prop of `type: "array"` with `x-control: "list"`, never numbered props. `items` is a simple value (`{ "type": "string", "enum": […] }`) or an object whose fields are simple values, each with a `type` and a `title` (and `enum`, `minimum`, `maximum`, `maxLength`, `pattern`, `format: "color"`, `x-control`, `x-translatable`, a `default` for new items); `required` lists the fields every item has; `minItems` and `maxItems` bound the list. Items take no field the manifest does not declare. A block's list, an event's `set-object-props`, a test's props and the default are checked item by item (`E_OBJECT_PROP_VALUE`, `E_OBJECT_PROP_DEFAULT`); fields marked `x-translatable` are translation units (`block.<id>.props.<prop>.<item id or index>.<field>`). The object receives the list as an array in `ctx.props` and `update()`.
- **Colours**: a colour an author may change is a prop (`type: "string"`, `format: "color"`, `x-control: "color"`), never a literal in the code (`W_OBJECT_LITERAL_COLOR`). Bind it to a theme token (`x-token`) when it carries the course identity (accent, inks of the charter): a charter change follows. A **material colour** of the scene (the brass of a pair of dividers, a wood, a hull) keeps a literal default and no token: authors change it in the block, the object stays portable, and the course theme is not filled with a token per shade. A colour the object draws **text** in names the colour that text is read on with `x-text-on` (another colour prop, or a token such as `color.surface`): the validator checks they reach 4.5:1 with the defaults and with every block's or event's values (`E_OBJECT_TEXT_CONTRAST`). Text of the object's DOM is also checked by axe at gate 6.
- **objects**: parts with stable ids; the code sets the same id (`userData.id` and `host.registerObject`).
- **state** (schema of `getState()`) and **persistState** (`enabled`, `maxLength`): the state is saved in the learner's suspend data after each declared event.
- **events**: only declared events are accepted by the host; `tracking.interaction` writes `cmi.interactions` (objective from `planRefs`), `tracking.score` gives a 0–1 score, `tracking.completion` completes the object.
- **planRefs**, **pedagogicalRole**: what the object serves in the plan (its objectives and items, which the plan defines). `pedagogicalRole: "decoration"` makes a backdrop object (below): no planRefs, no events.
- **strings** and **language**: every word the object shows that is not a prop (buttons, status lines, units), with a description for translators, in ICU message syntax: `{gap}`, `{n, plural, one {# orbit} other {# orbits}}`, `{kind, select, a {…} other {…}}`. The code reads them with `ctx.t(key, values)`; a block rewords them with `props.strings` (same placeholders, `E_OBJECT_STRINGS`), and translations write there. A manifest in another language than the course needs its strings worded by the block (`W_OBJECT_STRINGS_LANGUAGE`); keys read in the code must be declared (`W_OBJECT_STRING_UNDECLARED`). The a11y description and keyboard text can be worded per block too (`props.a11y`).
- **budgets** (draw calls, triangles, boot time, WebGL contexts), **a11y** (description, keyboard, drag alternative, motion), **sandbox** (`network: "none"`).

## 3. Module interface

```js
import * as THREE from 'three';            // mapped to the vendored r186 build
import * as d3 from 'd3';                  // D3 7.9, bundled with its notice (charts, D60)
import { play, createFocus } from '@elabora/charts';   // the kit Elabora's charts move with (charts.md)

export function mount(container, ctx) {
  // ctx.props, ctx.tokens, ctx.objects, ctx.locale, ctx.reducedMotion, ctx.testMode, ctx.savedState
  // ctx.safeArea {top, right, bottom, left}: px of the frame covered by the course layout
  // ctx.t(key, values), ctx.strings                        (words: manifest strings, as the block words them)
  // ctx.context {screen, section, step, variant, screenIndex, screenCount, sectionIndex, sectionCount, direction}
  //                                                        (decorations of the backdrop, §4c)
  // ctx.view {visible, ratio, progress, entry, exit}       (where the frame is in the window)
  // ctx.host.onFrame((dt, frame) => …)  → unsubscribe     (all animation and time)
  // ctx.host.random(), ctx.host.now()                      (determinism)
  // ctx.host.emit(name, payload)                           (declared events only)
  // ctx.host.asset(id) → Promise<ArrayBuffer>              (declared assets, no network)
  // ctx.host.registerRenderer(r) / registerCamera(c) / registerObject(id, obj3d)
  // ctx.host.requestHeight(px)                             (blocks with aspectRatio "auto")
  // ctx.host.onSafeArea((area) => …)  → unsubscribe        (the layout covers another part)
  // ctx.host.onContext((context) => …) → unsubscribe       (decorations: the learner moved)
  // ctx.host.onView((view) => …) → unsubscribe             (the learner scrolled: entrances, parallax)
  // ctx.host.log(...values)                               (a line in the page's console, as information; an error thrown
  //                                                        reaches it as an error, which look and screenshot list)
  return {
    update(nextProps, transition) {},   // apply props without remounting; transition: the block carries on from another screen (§4e)
    getState() {},          // matches manifest.state
    setState(snapshot) {},  // resume
    pause() {}, resume() {},// optional
    dispose() {}            // free geometries, textures, renderer
  };
}
```

`mount` must return quickly. The player waits up to 15 s for the frame to answer `object/initialize` (font loading included), then shows the text version; boot time (budget `bootMsMax`, 3 s by default) is measured up to the return of `mount`. Learners on virtual desktops and locked-down laptops often have no GPU: WebGL runs in software, many times slower. Build the scene, render a first frame, and spread heavy work (procedural textures, large geometries) over the next frames with `host.onFrame`. An object that answers after the timeout recovers, and errors raised once the object runs are logged without hiding it.

## 4. Host ⇄ object protocol (JSON-RPC 2.0 over postMessage)

| Direction | Method | Purpose |
|---|---|---|
| object → host | `object/ready` | Frame loaded; the host may initialise |
| host → object | `object/initialize` | props, seed, locale, reducedMotion, tokens, objects, testMode, savedState, fonts (ArrayBuffers of the course fonts), rootSize (px of 1rem in the course page: the frame sets its own 1rem to it) |
| host → object | `object/context` | context of a decoration (`screen`, `section`, `step`, `variant` and their ranks, §4c), when the learner moves |
| host → object | `object/safe-area` | safeArea `{top, right, bottom, left}` in px, when the part of the frame covered by the layout changes |
| host → object | `object/set-root-size` | rootSize, when the course window changes size class (1rem of 16, 15 or 14 px): text in rem follows |
| host → object | `object/view` | view `{visible, ratio, progress, entry, exit}`: where the frame is in the window, when it changes (at most once a frame; also in initialize) |
| host → object | `object/update-props` | props; with `transition` (§4e), also the arriving block's `strings` and `safeArea`, applied before `update(props, transition)` is called |
| host → object | `object/pause`, `object/resume`, `object/get-state`, `object/set-state`, `object/step`, `object/dispose` | Lifecycle and tests |
| host → object | `object/perf`, `object/test-click`, `object/test-key`, `object/test-dry` | Budgets and test input; frames dry or drawn (test mode, see §6) |
| object → host | `object/event` | `{name, payload}`, checked against the manifest (name and payload schema) |
| object → host | `object/asset` (request) | Declared asset, answered with an ArrayBuffer |
| object → host | `object/resize` | `{height}` wanted by the object (used when the block shape is `auto`) |
| object → host | `object/error`, `object/log` | Diagnostics |

The frame has an opaque origin: messages use target origin `*` and are authenticated by `event.source` (the host only talks to its own iframe; the frame only to its parent).

## 4b. Size, theme and UI kit
- **Size.** The block chooses the frame shape: `aspectRatio` (`16:9`, `21:9`, `3:2`, `4:3`, `1:1`, `3:4`, `2:3`, `9:16`), `auto` (the object calls `host.requestHeight(px)`; the host clamps to `minHeight`/`maxHeight`) or `fill` (height of the layout slot), and `narrowAspectRatio` when the block is narrower than 640 px. A responsive object watches its container (`ResizeObserver`), switches to a stacked layout under 640 px and requests the height it needs.
- **Theme.** Before `mount`, the frame receives the course tokens as CSS variables (`--t-primary`, `--t-color-<custom>`…, same names as the player) and the embedded fonts, registered with `FontFace` from ArrayBuffers sent by the host (the sandbox has no network and an opaque origin, so fonts cannot be fetched by URL).
- **UI kit.** Classes that look like the player and follow the theme: `t-ui` (root), `t-ui-panel`, `t-ui-card`, `t-ui-eyebrow`, `t-ui-title`, `t-ui-muted`, `t-ui-btn` (`--primary`, `--ghost`), `t-ui-seg` + `t-ui-seg-options` (radio group as segmented buttons), `t-ui-meters` + `t-ui-meter` (`small`, `b`, `em`), `t-ui-status` (`is-ok`, `is-ko`), `t-ui-badge`. Focus rings and reduced motion are handled.
- **Safe area.** A layout may float its own content over a stage (a title over a 3D scene). It declares the covered part of the frame in CSS, on the slot or the block, with `--t-safe-top`, `--t-safe-right`, `--t-safe-bottom` and `--t-safe-left` (any length, for example `--t-safe-left: 30rem`, set back to `0` where the layout stacks). The host resolves them to px and gives them to the object as `ctx.safeArea`, then through `host.onSafeArea` whenever they change. The object keeps key content (the subject of the scene, labels, controls) out of it; backgrounds may run under it. Best effort, checked by snapshots: tests set it with `safeArea` or the step `set-safe-area`.
- **View (scroll presentation).** On a page longer than the window, an object can follow the scroll: `ctx.view` and `host.onView` give where its frame is, measured like a CSS `view()` timeline on the frame's box: `visible` (some of it is in the window), `ratio` (the share of its height in the window), `progress` (0 when its top reaches the bottom of the window, 1 when its bottom leaves the top), `entry` (0 to 1 while it comes in from below; 1 once it is wholly in the window, or fills it) and `exit` (0 to 1 while it leaves at the top). Use `entry` for an entrance as the learner scrolls to the object (an unrolling, a drawing), `progress` for a slow parallax. Keep entrances one way (what has appeared stays when the learner scrolls back), skip them under `ctx.reducedMotion`, and draw only when visible or when something changed: browsers stop running frames out of view, and the last picture stays. Without a host that measures it (slides, object tests), the frame is wholly in view.
- **Tests.** Time is virtual in tests (frames advance on `step`): CSS transitions and animations of the object's DOM end at once (zero duration), so snapshots and the accessibility gate never catch a panel half-faded. Each test may set the frame `size: [width, height]` (default `[960, 600]`): test the narrow layout with `[375, 860]`. A test may also set `safeArea`, `context`, `view`, `strings` (rewording) and `locale`; steps `set-safe-area`, `set-context` and `set-view` change them on the way (`set-view` keeps the values it leaves out: `{ "action": "set-view", "view": { "entry": 0.4, "progress": 0.25 } }`). Event expectations read the events once the frame has answered the gate, so every event the earlier steps emitted at once is in, those emitted in answer to `set-props`, `set-view` and the other notifications included; an expected event that comes a moment later (once an asset has arrived) is awaited up to 5 s, virtual time standing still; `expect-no-event` does not wait for events still to come. A snapshot has the stall guard's 60 s.

## 4c. Decorations (course backdrop)
A free object with `pedagogicalRole: "decoration"` is mounted once behind the course by `theme.backdrop.object` (see design.md, Backdrop), never in a block. It is hidden from screen readers and out of the tab order; it has no planRefs, no events (`events: []`), no saved state and no focusable element (gate 6 checks it); its default budgets are smaller (30 draw calls, 60,000 triangles). `elabora object new . <id> --decoration` writes one to start from: a 2D canvas that follows the course, out of the player's bars, which lands at once when frames are still.

It receives `ctx.context` and `host.onContext` to follow the course: `screen`, `section`, `step` (from 1), `variant`, and where they stand: `screenIndex` (from 0) of `screenCount`, `sectionIndex` (from 0, `null` outside a section) of `sectionCount`, and `direction`, `forward` or `back` in the course order (`null` on the first screen shown, or a session resumed: be there at once). Its safe area (`ctx.safeArea`, `host.onSafeArea`) is the room the player's bars cover, top and bottom; it changes with them. `elabora look` and the editor's canvas show one screen at a time, at rest: the context counts its ranks in the whole course and the safe area is their bars', but `direction` stays `null` (each screen is shown as reached at once), and an edit of the object's files starts it again with them; `elabora object replay` shows the passages, and `elabora screenshot --screen <id> --from <id> --at 0,300,900` films one, from the screen before to that one.

Under reduced motion, and on a machine that renders below 24 frames per second, the frame runs it in still mode: frames run for 1.5 s after mount and 1 s after each change of context, props or safe area, then nothing. `host.still` says it (true from mount under reduced motion), and `host.onStill(callback)` is called once if the machine freezes it later: a movement then lands where it goes in the frames that follow, rather than stopping on its way. What it draws outside a frame shows too: a canvas redrawn in the `host.onContext` callback is on screen at once, whether frames run or not (still frames only stop `host.onFrame`).

To fade the drawing at the edge of the bars rather than cut it there, erase a band along the safe area once the drawing is done. `destination-out` only reads the alpha of what it paints, so black (`#000`, greys are not reported as literal colours) and `transparent` are enough:

```js
const fade = 48; // px of fading under the top bar
const g = paint.createLinearGradient(0, area.top, 0, area.top + fade);
g.addColorStop(0, '#000'); g.addColorStop(1, 'transparent');
paint.globalCompositeOperation = 'destination-out';
paint.fillStyle = '#000'; paint.fillRect(0, 0, width, area.top);   // nothing under the bar
paint.fillStyle = g; paint.fillRect(0, area.top, width, fade);     // then a soft edge
paint.globalCompositeOperation = 'source-over';
```

In tests, `context` takes the same fields; `step` starts at 1. Its gates' snapshots are taken in a frame of the test's `size`, without the course: `elabora object replay` shows it behind the course. A baseline recorded in a single colour (the object drew nothing) is reported (`W_GATE_VISUAL_BLANK`).

## 4d. Replay in the course
`elabora object replay . <id> [--test <name>] [--block <id>] [--size 1600x900]` (MCP `elabora_replay_object`) replays one test of `object.tests.json` where learners meet the object: the built course, on the page that places it, scrolled into view, in real time, with the course around it. `step` waits its frames in real time, `click`, `key`, `set-props` and `set-state` drive the object, `set-view` scrolls the page to that view, `snapshot` captures the window; expectations are checked as they come, waiting a moment for an object a little behind (software WebGL). The result is a contact sheet of the snapshots and the end, with the expectations that failed. It shows; the gates stay `elabora object test`.

## 4e. Continuity between screens
**What persists from one screen to the next.** A free object's frame goes with its screen and is mounted again on the next one that places it, unless its block carries on (this section: same `continuity` key, the frame stays and keeps running). The backdrop's decoration (§4c) is mounted once for the whole session. Everything else of a screen goes when the learner leaves it; its saved state (`persistState`) comes back with the object.

Blocks of different screens that share a `continuity` key ([D65](decisions/D65.md)) are one object to the learner: `{ "id": "lab-scene", "type": "free-object", "continuity": { "key": "lock", "duration": 900 }, "props": { "object": "canal-lock", … } }` on each screen, the same object everywhere. Going from one of those screens to another (Next, Previous, the outline, an event), the frame never leaves the page: the object keeps running, and receives the arriving block's props with a second argument:

```js
update(nextProps, transition) {
  // transition: { from: { screen, block }, to: { screen, block }, direction: 'forward' | 'back',
  //               duration (ms, 0 under less motion), reducedMotion }
  // ctx.strings and ctx.safeArea are already the arriving block's (host.onSafeArea was called).
}
```

Move to the new settings over `transition.duration` with `host.onFrame`: take what the last frame drew as the start (camera, positions, colours), ease toward what the new props ask, and let the live state run meanwhile (blend from the start to the live value, so that a simulation already moving ends where it should). Glide only what moves that way in the world: the camera, a level, a gauge, a gate that swings. An element whose way from one state to the other would be unrealistic (a vehicle that would cross a wall or the water, a boat that would turn round in a lock) fades out where it was over the first part of the passage and fades in at its new place over the last, never seen on its way; an element that stays where it was stays. Hide the controls of the departing mode at once and show the arriving ones once the scene is mostly there. With `duration` 0 apply everything at once. An object that ignores `transition` applies the props as for any `update`: the frame still carries on, the change is a cut. The frame glides from the departing block's box to the arriving one's on the same curve (`cubic-bezier(.77, 0, .175, 1)`).

- **State**: the chain has one saved state (`ctx.savedState` when it mounts again), saved after a declared event of any of its blocks. The object decides what its modes share: keep what a learner did in one mode (a lab) in a field of the state when another mode replays something, and find it again when the learner comes back. Completion stays each block's: send the completion event again for the arriving block when its goal is reached.
- **Events** are the arriving block's from the moment it arrives (the screen's `object.event` target, interactions, completion). So are the frame's title, its description and its text version.
- **Mounted again**: a screen without the key stops the object; the next screen with it mounts it with the chain's state and no transition, as does a session resumed in the middle of the chain.
- **The layout**: the frame lives in a keep layer of the screen, over the block's own frame. Text the layout lays over the object needs a `z-index` to stay above it (a positioned element), as a title over a stage usually has; the layout's backgrounds stay behind. The block's ancestors let clicks through to the object; their other children keep their own behaviour.
- **Tests**: `set-props` with `transition` (`{ "action": "set-props", "props": { … }, "transition": { "duration": 1200, "safeArea": { "top": 128 } } }`) gives the props as a passage does; the steps that follow advance through it frame by frame, so a `snapshot` at frame 30 shows the passage on its way. `elabora object replay` plays it in the course page, in real time.

## 5. Sandbox
`<iframe sandbox="allow-scripts" allow="" referrerpolicy="no-referrer">` (no `allow-same-origin`; `allow-pointer-lock` only if declared) and a frame CSP `default-src 'none'; script-src 'self'; connect-src 'none'; …`. The object cannot reach the course page, the LMS API, cookies, storage or the network. Only the host talks to the LMS. Objects are compiled into classic scripts because module scripts would need CORS headers from the LMS server.

## 6. Gates

| # | Gate | Automated by | Blocking |
|---|---|---|---|
| 1 | Manifest: schema, coherence of objects/props/events/tokens, `planRefs` in the plan, static analysis of the code | `elabora validate` | yes |
| 2 | Smoke: mount, 300 frames, `getState()` matches the state schema, dispose, no console error; every call answered within 60 s (an object stuck in a loop fails here, the other gates are not run) | `elabora object test` | yes |
| 3 | Behaviour: `object.tests.json` replayed (clicks on declared objects, keys, props, state, expected events) | `elabora object test` | yes |
| 4 | Budgets: draw calls, triangles, WebGL contexts, boot time (median of the mounts of the run, the smoke test's and one per test; over the budget a warning, `W_GATE_BOOT_SLOW`, as learners without a GPU see the loading indicator; over 10 s an error) | `elabora object test` | yes |
| 5 | Visual: snapshots against the baselines of the system running the gates (0.1 % pixel tolerance; fonts and WebGL differ between systems); a system's first run records its baselines, and says so (`recorded`: nothing was compared; with another browser engine's baselines only, `I_GATE_VISUAL_OTHER_ENGINE` names it). The gate catches a drawing that changed, not a shade: each pixel may differ a little (anti-aliasing, raster noise between runs), so a colour made a little lighter can pass. After changing a colour on purpose, look at it (`elabora object replay`, `elabora look`) and record the baselines again (`--update-snapshots`). A test whose WebGL context was lost before a snapshot (the browser's GPU process giving way, most often on a loaded machine) is run again rather than compared, twice at most, then reported as `E_GATE_CONTEXT_LOST` | `elabora object test` | yes |
| 6 | Accessibility: axe-core (WCAG 2.2 A/AA) on the object DOM, keyboard reachability | `elabora object test` | yes |
| 7 | Signal: multimodal judge with a checklist | not in 0.1 | never alone |
| 8 | Human: pedagogical review and code review | proof dossier checklist | yes |

**Frames drawn.** The gates draw WebGL in software, as on machines without a GPU and with the same pixels on every machine, and drawing is nearly all their time; yet a test only looks at the last frame before each snapshot. The others run **dry**: the object's code runs every frame (its state, events, uploads, shaders and their errors), its draws, clears and blits are skipped (`object/test-dry`, `@elabora/player` `dry-frames.ts`). What a skipped operation would have drawn is tracked as stale until drawn again, and a run decides only if nothing it saw depended on it: a canvas still showing a stale drawing at a snapshot, a draw on a stale framebuffer or sampling a stale texture, a read of a stale drawing (`readPixels`, a copy or blit, a WebGL canvas drawn into a 2D canvas, uploaded or turned into an image), a query or `getError` after a skipped draw, transform feedback. A dry run that saw one of these, or that fails, or whose snapshot differs, is done again with every frame drawn, and that run decides; when only the drawing of a render target was reused (a texture baked over the first frames), it is first tried again with render targets drawn and only the canvas dry, as are the object's next tests. Runs that record baselines (`--update-snapshots`, a system's first run) draw every frame. So verdicts are those of every frame drawn; a test that draws only when something changed, or reads its drawing back, is just slower. In test mode (`ctx.testMode`) draw every frame, and pick with raycasting rather than by reading pixels back. A step waits until its frames are drawn.

A full run of `elabora object test` is recorded in `objects/<id>/object.gates.json` (date, Elabora version, gates, boot time, fingerprint of the object folder); the proof dossier reports it as passed, failed, stale (the object changed since) or never run. `--only <name>` runs the tests whose name contains it, without recording. The behaviour tests run side by side in several pages; their mounts run one at a time, so that each boot time is measured alone.

An object unchanged since its last full run on this computer is not run again: its result comes back at once, `unchanged since <date>, not run again` (`reused`). Unchanged means the object folder (its fingerprint) and what the gates take from the course and the computer: the theme tokens, the fonts and their files, the language, the Elabora code and what it runs the object with (esbuild, three.js, axe-core, the snapshot comparison), the browser and its version, the system. That run is kept in the course's machine-local `.elabora/cache/objects/`, unless it wrote baselines or an object stuck in a call or a boot near the timeout failed it (a loaded computer may cause those). `--force` (MCP `force`) runs the gates anyway; `--update-snapshots` always runs them.

`--update-snapshots` also removes the baselines of states no test names any more (a test or a snapshot step renamed or removed), for every engine and system. It keeps, and lists, the baselines of the states that exist made by another engine or on another system (Chrome's `-darwin.png` next to the app's Electron `-electron-darwin.png`): runs on that browser or system still compare with them.

## 7. Lifecycle
Semver on the manifest: removing or renaming a prop, state field or event is a major change and needs a migration of the values stored in screens. Authors change **values** (block `props.props`); behaviour changes go through an agent as a localized diff and replay gates 2–6. An object reused in three modules or more is a candidate for promotion into the native catalogue.
