mechanism · diff

git:20261018.e7c68c2 to git:20261019.40933f4

1 added, 1 removed. Audit A to A.

---
name: mechanism
description: Build an inspectable model of a real object from the evidence (spec sheets, teardowns, official photos, published test vectors), take it apart, and render a film with designed sound, an interactive viewer and an STL/GLB. Every number is tagged SPEC, MEASURED, DERIVED or ESTIMATED, and a spec whose numbers do not add up is not rendered. Use for exploded views, teardowns, comparisons and explainers of real products or machines (gear trains, key switches, cipher machines, phones, boards). Triggers on "exploded view of X", "model X exactly", "compare X and Y", "make a video of how X works", "/mechanism".
---
# Mechanism
Model a real object (hardware or software) from evidence, check the model against what is published, and film it. The point is not a pretty render: it is that every number says where it came from and that a model whose numbers do not close is not rendered.
## Quick start (the eight steps)
1. **Research.** Open the maker's sheet, a teardown, a standard, photos. Keep every URL and the date you opened it. Note what a page refused to show.
2. **Start the object in the user's project** (never in this folder): `node scripts/new.mjs <name> --dir <work> --title "..." --fig "Fig N"` (for example `--dir ./mechanism-work`).
3. **Write `<work>/<name>/spec.js`.** Copy the example closest to the object (`examples/hub`, `switch`, `lock`, `rtx4090`, `winrar`) and tag every number SPEC, MEASURED, DERIVED or ESTIMATED.
4. **Gate:** `node scripts/check-spec.mjs <work>/<name>/spec.js` until it says PASSES and the verdict says VERIFIED. Prefer checks that measure the drawn model or cross two sources; a unit conversion is reported but does not count (see Verdicts).
5. **Lint the film, then look:** `node scripts/lint-film.mjs <work>/<name>/index.html` renders one draft frame per phase and per tour stop and says EMPTY (nothing in the shot: a see-through plate, an orbiting part, an object off frame; exit code 1), LOOK (little in the middle: open the PNG it names and judge it) or SMALL (raise `director.zoom`). Fix every EMPTY and judge every LOOK, then open `?explore`. A tour stop aims where the part was assembled, so a part that orbits or moves (a planet gear, a lifted pin) needs `tourDim` or no stop. Then run the preflight on both cuts: `node scripts/preflight.mjs <work>/<name>/index.html --length short` and again without `--length`. It steps through the whole timeline without rendering an MP4 and reports geometry and text: FAIL for a camera inside a part, WARN for geometry inside the camera's near plane, a tour part or the object mostly out of the frame or spilling past both sides, and texts that overlap or run past the edge. Fix every FAIL. For each WARN open the frame (`node scripts/render.mjs <page> --length short --still <t> --out frame.png`) and decide: a real defect (fix it), a deliberate choice (a close-up, a part that flies out, a panel that slides in) or a false positive. Keep the ones you leave and say so in the final report. It exits 0 unless `--strict`; do not tune the spec only to silence a warning. Fixing a defect you can see in the frame (a dark-on-dark part you cannot read, a window that is not cut) is not tuning: say what you saw and fix it. The gate checks numbers, not shape; the preflight also checks that flat parts (`plate`, `profile`) cover their outline minus their holes (`GEOMETRY_AREA_MISMATCH`: FAIL above 5 %), but a mesh can be wrong in ways no check sees, so open a frame of every part you cut holes in. A part with `cutaway` fades while the mechanism runs: anything that sits on it (a dial, a pointer, a label) needs the same `cutaway` or it floats alone in the air when the part fades; no check can tell that from a part that is meant to stay, so look at a frame from the phase where the shell fades.
6. **Recognisable?** Run the stranger test (see Recognisable) and record the answer, even if it fails. If the judges do not name the object, the film does not explain it: see "Explain what it is first".
7. **Render:** the short cut first (`--length short`: a 30 s film that takes 1.5 to 4 minutes of wall time on a laptop GPU; run it in the background), then the normal one, then `--export glb`. Put everything next to the spec.
8. **Report** what you did in the format under "Final report". Say what is estimated and what failed.
## Setup
This skill is a toolkit as well as instructions, so **run everything from the folder that contains this SKILL.md**: `cd` to it first (cloned as a skill, that is `~/.claude/skills/mechanism`; installed as a plugin, it is `${CLAUDE_PLUGIN_ROOT}`). It needs Node 22+, Edge or Chrome, and `ffmpeg` (plus `uv` only to measure photos). There is no install step. **Work in the user's project, never inside this folder** (an agent cannot write under `.claude/skills/`): pick a folder such as `./mechanism-work` (call it `<work>`). A new object starts with `node scripts/new.mjs <name> --dir <work> --title "..." --fig "Fig N"`, which creates `<work>/<name>/` (page, `run.js` and a starter `spec.js` that FAILS the gate until every TODO is replaced). Renders go next to it, for example `--out <work>/<name>/<name>.mp4`. The scripts themselves are run from this folder.
Output for one subject: an MP4 (4:5, 9:16 or 1:1, 1080 wide, stereo, in the `dossier` look), as a short cut of about 30 s (share this one), a normal cut of 90 to 140 s and an extended one; an interactive `index.html?explore`, an STL/GLB of the model, and the data files that justify every number. A normal cut renders in about 2 to 3 seconds per second of film on a machine with a discrete GPU (the scripts ask for it); the short cut takes about a minute.
Two routes. **The declarative route is the default**: write one `spec.js` (parts with real dimensions, evidence, mechanism) and the engine does the scene, the kinematics, the camera, the sound and the film. Use the hand-built route (`examples/phones/scene.js`) only for products that need bespoke look, such as phone back covers with measured camera islands.
## Core rules
1. **Search before you model. Never invent a shape you could have looked up.** The user will notice a generic model immediately. Two phones that share one template are a failure.
2. **Look at the pictures.** `WebFetch` returns text only, but you can `curl` image files and open them with `Read`. Official sources worth checking, in this order:
- the maker's spare-parts site (Xiaomi: `spare-parts.mi.com/products/<model>-<part>`) for photos of the real back cover, display module with frame, boards, cameras, speakers;
- GSMArena `pictures` pages (`fdn2.gsmarena.com/vv/pics/<brand>/<slug>-N.jpg`: front, back and side views aligned) and review photos (`fdn.gsmarena.com/imgroot/reviews/...`);
- teardown articles and videos' hero photos, the maker's spec page and FAQ (battery cells, construction), phone-case fit guides.
3. **Measure, do not eyeball.** Use the published body size as the scale (`mm per px = published size / silhouette size`), check that x and y scales agree (within ~2%), then measure with OpenCV: silhouette, corner radius fit, Hough circles for lenses, rectangles for boards. Scripts live in `data/phones/ref/measure*.py`, run with `uv run --with opencv-python-headless --with numpy python ...`.
4. **Cross-check every derived number against a second published one.** Examples that already work: display area from diagonal + resolution aspect, checked against the published area and the ppi; sensor size from MP x pixel pitch, checked against the optical format (16 mm per inch-type for large sensors, 18 mm for small ones); entrance pupil = real focal length / f-number, real focal = 35 mm equivalent x sensor diagonal / 43.27.
5. **Tag everything.** SPEC (published), MEASURED (measured on an official photo, say the error), DERIVED (formula plus cross-check), ESTIMATED (nobody publishes it: draw it dashed and say so). Never present an estimate as fact. Say when sources conflict and which one you used.
6. **When two objects are compared, model each one as itself.** Corner radius, frame profile, back material, camera island, buttons, ports, display technology and interior all come from that object's own evidence. Same scale, different design.
7. **Say what is missing.** If there is no interior photo, say so in the video and keep that interior estimated. Ask the user for a photo only after you have searched.
## The declarative route (spec -> film)
1. **Research first, into the spec.** Search, download and look at pictures, measure (rules above). Then write `<work>/<name>/spec.js` as `window.SPEC = {...}`; copy `examples/hub/spec.js` (a three-speed planetary hub) as the template. Fields: `research.sources` (title, url, fetched date, note about what the page refused), `research.claims` (text, `conf`, `src`, and `cross` for DERIVED, `error` for MEASURED, `why` for ESTIMATED), `parts` (kind, real dimensions in mm, material, position, explode vector, `body`, `orbit`, `claims`, `info`, `cutaway`), `mechanism` (links between bodies, `drive.modes` with the ratio the maker publishes), `director` (optional tour order, durations, views).
2. **Gate: `node scripts/check-spec.mjs <work>/<name>/spec.js`.** It fails (exit 1) when a number has no source, a DERIVED claim has no cross-check, a MEASURED one no error, an ESTIMATED one no reason; when two meshing gears do not sit at the centre distance their module and teeth demand; when the teeth do not interleave; or when a mode of operation does not give the ratio the maker publishes. Fix the model or the claim, never the check. Do not render a spec that does not pass.
3. **Build the page**: `node scripts/new.mjs <name> --dir <work>` creates `<work>/<name>/` (page, `run.js`, starter `spec.js`); the script list is fixed and `spec.js` is the only thing you edit.
4. **Look before rendering**: `node scripts/probe.mjs "<work>/<name>/index.html?explore" <work>/<name>/probe` (mouse simulation) and `node scripts/render.mjs <page> --still 9 --blur 1` at the assembly, power-path, tour and panel times (`window.__specInfo().phases` lists them).
5. **Render**: `node scripts/render.mjs <work>/<name>/index.html --blur 3 --out <work>/<name>/<name>.mp4` (duration comes from the spec). Variants: `--theme night`, `--aspect 9:16`, a clip with `--from 13 --to 21`, and the model with `--export stl --e 1` or `--export glb` (`--e` is the explode factor, 0..1).
What the director builds by itself: staggered assembly from back to front, a cutaway (parts marked `cutaway` fade) while the mechanism runs, one mode of operation at a time with its measured ratio, its published number and a live rpm readout, the exploded view, a tour with a callout per part, re-assembly, a ratios table, an evidence table with sources, and a sources panel. Sound follows the physics: each part's impact uses its material and size, and the tooth ticks are the real tooth passes of the running gears.
**One tooth count per body.** In a gear train a `body` has a single tooth count: two gears of different size on one shaft each need their own `body`, joined by a link `{ t: 'same', a: '<body>', b: '<body>' }`. The gate fails with that advice when a body mixes tooth counts. Parts library (`kit/parts.js`, real dimensions): `gear` (involute, module, teeth, pressure angle, backlash, lightening web), `ring` (internal teeth), `sprocket` (chain pitch and roller), `shaft`, `pin`, `disc`, `flange` (spoke holes), `tube`, `plate` (`holes`: `[x, y, r]` round or `[x, y, width, height]` rectangular), `profile` (`pts` outline, `holes`: a list of polygons cut out of it, so no zero-width bridges), `bearing` (balls and cage), `spring` (helix), `nut`, `bolt`. Also `box` (hollow housing with optional floor), `block` and `cross` (stem). Engine parts: `crankshaft` (throws at any angle), `conrod`, `piston` (ring grooves, compression height), `engblock` (block, head or pan with through bores), `camshaft`, `valve`. Puzzle part: `cubie` (a cube with stickers). Two kinds of mechanism: **gear trains** (`mechanism.links` + `drive.modes` with the published ratio) and **presses** (`mechanism.press = { travel, actuation, force: [[mm, cN], ...] }` + `drive.modes` as phases `{ from, to }`; parts take `slide: { k }` to follow the stroke and `compress: { free }` for springs; the director plots the force curve against what a linear spring through the first two points would give, and the gate demands a `press.note` when the published points do not add up). `up: 'z'` stands the object on its z axis (switches, towers). `orient: [rx, ry, rz]` (degrees) turns the whole object (the Enigma stack lies along x). **Engines**: `mechanism.engine = { cylinders, bore, stroke, rod, deck, ch, pitch, throws, firing, valves: { intake: { open, close, lift }, exhaust: {...} } }` is a crank-slider with the exact kinematics (piston height r cos a + sqrt(l^2 - r^2 sin^2 a)); parts take `engine: { role: 'crank' | 'cam' | 'piston' | 'rod' | 'valve', cyl, valve }` and move with it. The gate computes the displacement from the bore and stroke, demands that the published firing order is possible with the crank throws (1-2-3-4 fails on a flat-plane four) and that the piston crown reaches the deck. Cam lobes are placed from the valve windows. `late: true` makes a part arrive last (shells) and `cutaway: 0.8` sets how transparent it gets while running. **Calculated numbers**: `mechanism.calc = { vars: { name: { v, label, unit, claim } } }` with `drive.modes` of `{ expr, unit, digits, formula, publish: { value, tol, claim }, show: [part ids] }` evaluates each published figure that comes out of others (TFLOPS = cores x 2 x clock, bandwidth = bus x data rate / 8) from variables that each carry their own claim, so an estimated assumption is drawn in a different colour; parts take `spin: rad/s` (fans) and the kinds `fan` and `fins` exist. This is the cheap route for any product whose datasheet publishes derived figures (GPUs, CPUs, drives). **Puzzles**: `mechanism.cube = { pieces, pitch }` runs the Rubik cube as 26 cubies that really turn (`kit/cube.js`, no tables); modes take `alg`, `repeat`, `back` and a `metric` (`order`, `superflip`, `positions`) that the gate compares with the published number. **Ciphers**: `mechanism.enigma = { rotors, reflector, rings, start, plugs, tests: [...] }` runs the real machine logic (`kit/enigma.js`); every published test (`plain` -> `expect`, or the sequence of `windows`, or `decrypt: true` for reciprocity) is simulated and the spec fails if the machine disagrees. Parts of kind `rotor` take `role: 'R' | 'M' | 'L' | 'UKW' | 'ETW'` and get their wiring and notch from the machine config, so what is drawn is what is computed. `drive.modes` become typed phases (`start`, `text` or `'@prev'`, `publish: { text, windows }`); the film shows the rotor windows, the lampboard and the signal path through the contacts, and the viewer lets you type on the keyboard. `director.views` merges partial views (`{ iso: { yaw, pitch } }`) onto the defaults. Kinematics (`kit/kin.js`): one-degree-of-freedom gear trains by the Willis relations, external and internal meshes, carriers, fixed members, tooth phase solved so the teeth really interleave (residual reported), centre-distance and assembly checks.
**Look.** The signature look is `dossier` (blueprint grid, ruler, monospaced type, a data block counting SPEC / DERIVED / ESTIMATED): it is the default, and a film should keep it so mechanism does not look like every other explainer. Three alternatives live in `MX.styles` (`kit/core.js`): `paper`, `night` and `studio` (black set with the maker's accent). Choose with `--style` on render or `?style=` on a viewer, or `style: 'studio'` in the spec; `brand: { accent: '#76b900' }` gives the studio look its colour. A style is one table entry (palette, title font, card layout, stage: background, grid, shadow, bloom, rim light). Use `--draft` for a fast preview render.
**Presentation methods.** When requested, select `--method studio|workflow|paper` independently of `--background graphite|blueprint|porcelain|ivory` (all twelve combinations). Studio names the normal 3D director; Workflow follows actual `mechanism.steps.label(i)` data, or the drive modes in their declared order; Paper explains the tour parts using real mesh edges/silhouettes, numbered callouts and projected detail views. The same options work as URL parameters or `presentation: { method, background }` in the spec. Do not confuse the legacy `--style paper|studio` palettes with the new methods. No method/background option means the existing film is preserved. With `?explore`, only the new background is applied to the existing viewer.
**Independent engines, not palette variants.** Studio uses the product WebGL renderer (1.5x supersampling, `stage.renderScale` range 1–2). Workflow draws its own Canvas2D process scene without building the Studio object: with `workflowScene: { kind: 'reconcile', before, after, updated }` (see `examples/react`) it shows the real page, the state, both trees, the node-by-node comparison and the one DOM change; otherwise a line of stations with a carrier. Paper draws vector sheets without shaded images, lighting or shadows (flat material colour coding is allowed, as on a coloured drafting sheet): with `paperScene: { kind: 'pin-section', ... }` (see `examples/lock`) a side section driven by the published table; otherwise orthographic sheets from the model meshes, used as data. Neither 2D engine may create a WebGL context or composite Studio frames. Which kit modules each engine needs is declared once in `kit/manifest.js`, read by `boot.js`, `scripts/new.mjs` and `tests/manifest.test.mjs`. `boot.js` decides the engine just before the compiler, from the URL or else `spec.presentation`, so Workflow (either way) loads no 3D code at all (no three.js, `gl.js`, `figure.js`, `parts.js`, `paper-engine.js` or `paper-mesh.js`) and Flow runs without the 3D stack. Paper is two modules: `paper-engine.js` draws (no three.js) and `paper-mesh.js` turns the compiled meshes into the plain view set it draws. A spec whose `paperScene.kind` is one Paper draws natively (`kit/manifest.js` `paperScenes`, today `pin-section`) goes straight to `paper-engine.js`: no three.js, no 3D figure, no mesh adapter; any other spec (or an unknown kind) uses the meshes. The gate runs the same either way (it reads the spec, not the meshes). The preflight's `GEOMETRY_AREA_MISMATCH` runs only where meshes are compiled (Studio, Paper without a vector scene). Where none are compiled (Workflow, Paper with a vector scene, `--skip shape`) the page publishes `window.__checks.geometryArea = { run: false, reason }` and the preflight prints `NOT RUN GEOMETRY_AREA <reason>` (in `--json`: `notRun`); that is neither a finding nor a pass, so check the model's flat parts with a Studio preflight. A check that throws, or an omission the engine does not explain, is `GEOMETRY_AREA_NOT_RUN` (FAIL); a page that does not report it at all (hand-built) is `GEOMETRY_AREA_UNREPORTED` (WARN). A new kit module must be added to the manifest or the manifest test fails. Motion in both uses `P.motion` (`kit/presentation.js`: closed-form spring, expo, log-space zoom, masked word rise), all pure functions of time. Studio exports STL/GLB; the 2D engines export films/stills. Studio has the orbit viewer; Workflow/Paper retain their own playback canvas with `?explore`.
Use `--length short --aspect 9:16` first: Workflow/Paper are 30 seconds (normal 60, extended 90); Studio keeps its duration. The 2D engines use CPU Canvas2D and deterministic time-based state, not temporal blur. A station's progress is illustrative, not measured latency. Changing appearance never changes the evidence verdict. Paper views are not to scale; displayed numeric parameters must already be backed by the spec. Estimates stay marked/dashed. Inspect first/middle/final frames of every step/detail and run preflight with matching method/background/length/aspect. Camera checks are inapplicable to 2D scenes; text checks and Paper's geometry-area checks still apply. Run `node --test tests/presentation.test.mjs tests/paper.test.mjs tests/paper-contract.test.mjs` after engine changes: they reject WebGL creation in Workflow/Paper, and any gradient, shadow, filter or lighting blend in a Paper frame (Paper is strokes, hatching and flat fills).
**Software.** The object can be software. Pick what the program writes or computes (a file format, a message, a result), make the real tool produce a sample, write a pure model in `kit/` from the published description, and let the spec ask the model for numbers the tool itself wrote: CRCs, sizes, and above all `rebuild the whole sample byte for byte` and `have the real tool accept what the model built`. Keep the evidence next to the spec in `data.js` (written by a script that runs the tool, so anyone can reproduce it); `check-spec.mjs` loads it. Calculated values may be strings (hex, bytes): `mechanism.calc` compares them to `publish.text`. See `examples/lock` (a pin tumbler lock with a KW1 key: the published table is cross-checked, the right key aligns every split on the shear line and a wrong key does not; `kind: profile` draws the key from a polygon, `motion(step, t, phase)` animates a "what if" phase), `examples/winrar` (software doing its job: a DEFLATE stream WinRAR wrote, unpacked token by token by `kit/deflate.js`, written from RFC 1951; the books and belt are the icon), `examples/rar5` and `kit/rar5.js`.
**Explain what it is first.** Someone who has never heard of the object has to understand what it is and what it does in the first seconds, with no help. Put its name on screen (a nameplate, or the object's own printed text), say in plain words what they are looking at (`director.what`, the step labels and the mode formulas), and use no internal jargon without explaining it. Answer the question a newcomer would ask ("what does a click do to the page?"), not the most interesting internal detail ("which list items does the reconciler move?"): keep the detail for a second film. For software, show the real input (the code on a card), the state before and after, and the real output. The stranger test is the check: if the judges describe a generic diagram instead of naming the thing, the film does not explain it yet, so rewrite the captions and the scene and test again. `examples/react` went from "a tile-sorting puzzle" (55 %, not React) to "a React 18 explainer" (90 %) with exactly that change.
**Closing card.** Every film ends on a card with the object name and the gate verdict, and nothing about this toolkit: no name, no URL, no credit. Sign it yourself if you want to with `director.outro: { title, sub, what, line, url }` (all optional), or drop the card with `director.outro: false`.
- **Lengths.** Render the short cut (`--length short`, about 30 s) for sharing, the normal one for the full argument and `--length extended` when someone wants the whole teardown. Name the files `<name>-short.mp4` / `<name>.mp4`. Choose what the short cut keeps with `director.short: { modes: [ids], tour: [ids] }` (the steps phase and the first published checks by default).
+ **Lengths.** Render the short cut (`--length short`, about 30 s) for sharing, the normal one for the full argument and `--length extended` when someone wants the whole teardown. Name the files `<name>-short.mp4` / `<name>.mp4`. Choose what the short cut keeps with `director.short: { modes: [ids], tour: [ids] }` (the steps phase and the first published checks by default). In the normal and extended cuts the whole exploded object is held still for `director.explodedHold` seconds (0.9 by default) before the camera flies to the first part; the short cut has no hold.
**Provenance per dimension.** When a part mixes sourced and estimated sizes, add `params: { w: { value, unit, conf, claim, tol }, wall: { conf: 'estimated', why } }`: the gate compares each drawn size with its source value and reports the part as mixed, and the card shows each size with its class. (`node scripts/registry.mjs` refreshes the register of the skill's own examples; it is for contributors, not for your object.)
Confidence is not size verification: cards explicitly say `SIZE NOT COMPARED` or how many numeric dimensions have a sourced comparator. The register records both compared and unchecked dimensions per part. Read warnings for displayed calculations with no published comparator, tooth counts outside the kinematic train, and unknown spec/part keys. A warning discloses an untested quantity; it does not prove that quantity wrong. Keep the existing VERIFIED/CONSISTENT rules, including the independent-source requirement, and never invent a comparator to remove a warning.
**Sources that are scans.** A primary source (a standard, a datasheet) can be a PDF with no text layer. Do not skip it and do not quote it from memory: render its pages to images (`uv run --with pymupdf python -c "import fitz; d = fitz.open('C:/path/file.pdf'); d[0].get_pixmap(dpi=130).save('C:/path/page-0.png')"`, with `C:/...` paths: Python on Windows rejects `/c/...`), open the images and read the numbers off them. Say in the source note that it was read by eye from page images (a digit can be misread), cross-check each figure against a second source or a computed relation, and say which reading you followed when the drawing and the printed value disagree.
**Processes.** A render takes minutes: start it with your tool's own background mode (not `nohup ... &` inside a one-shot shell: on Windows it can die with the shell) and poll its log. **Never stop processes by name** (`taskkill /IM node.exe`, `pkill node`, `killall`): the user's machine runs other Node programs (dev servers, MCP servers) and that kills them all. Keep the PID of what you started and stop only that one (`taskkill /PID <pid> /T`). Do not start a second render of the same file while the first is running; wait for it or stop that PID. A foreground call is cut off after about 10 minutes and the render then looks frozen, so start every render in the background from the beginning. The last line of a render says both numbers: `listo: <file> (film 28.5 s, rendered in 217 s of wall time)`; the second one is the time it took, not the length of the film.
**Encoders.** `--encoder browser` uses software WebCodecs H.264, with automatic JPEG fallback when unsupported; `--encoder jpeg` is the compatibility default. Do not select hardware encoding without a quality comparison. The bitrate scales with dimensions, fps and part count; override with `--bitrate <bits/s>` only after comparing the thin text in every panel. Both pipelines use limited-range BT.709 output with sRGB transfer metadata. A frame/operation stalled for 30 seconds is stopped by its owned browser PID tree and retried once; `--timeout <seconds>` adjusts the render watchdog. Browser tools allocate port zero, so `CDP_PORT` is obsolete. Render validation belongs in the user's work folder, never in `docs/media`.
**Trust.** A spec is code. Never run `check-spec.mjs`, `registry.mjs` or a render on a spec taken from the web or a pull request without reading it; write only your own specs. Keep `--out` paths inside your work folder. See SECURITY.md.
**Recognisable.** A correct model nobody recognises is not finished. Add `recognize: { object, iconic: [{ text, parts | partsPrefix }] }` (2 or more features, each built from parts that exist) and, before delivering, run `node scripts/stranger.mjs <work>/<name>/index.html` (for an example of this repo, its name), which saves a label-free still under a neutral name in a temporary folder, and show that still, giving the judge only its path and never the object's name, to a model with no context ("what object is this?") and record the answer in `recognize.blind: { answer, judge, date, ok }`. If it is not recognised, fix the model (the iconic shapes, the livery, the proportions), not the caption. For software, research the real thing on the web first (logo, icon, UI) and keep the local tool as optional evidence.
**Verdicts.** The gate prints VERIFIED only if a published figure was compared, none is pending and at least one of the figures comes from a source independent of the model's inputs; CONSISTENT if everything matched but every figure shares its source with the inputs; NOTHING CHECKED / INCOMPLETE / DOES NOT PASS otherwise. **A check that only converts units** (inches to mm, pounds to kg, anything times 25.4) cannot fail, so it is reported but not counted: a spec with nothing but conversions says ONLY CONVERSIONS. Mark a real conversion `kind: 'convert'` (the gate also recognises the usual unit factors). **VERIFIED does not mean the source is right.** If the figure a model is compared with comes from the same source that fed the model, the check shows only that the two agree, so the gate classifies every published-figure check as independent, same source, or with no sourced inputs (the `provenance:` line) and the state is CONSISTENT, not VERIFIED, when none is independent. Prefer a published figure from another source than the inputs; when a source copies another (a wiki quoting the maker), say so with `derives: ['<source id>']` on it and it counts as the same. Numbers you type in a part card or the lead text of the film (a decimal, or a number with a unit) must be traceable to a claim, a dimension or a published value, or the gate warns that they can drift from the model: put the derivation in a claim. Strong checks are: measuring the drawn model (a spacing from the drawn pins, a displacement from the drawn bore), crossing two sources (a diagonal against the area and the resolution), an identity the sheet states in another way, or rebuilding and testing the real thing. In `mechanism.calc` expressions you can use `sqrt`, `abs`, `min`, `max`, `pow`, `round`, `hypot`, `PI` and the trigonometric functions without the `Math.` prefix. Templates from `scripts/new.mjs` are drafts that fail until every TODO is replaced. Never put a placeholder source, a calc variable without a claim or an ESTIMATED part without a reason in a spec. `node --test tests/gate.test.mjs` must stay green.
**Final report.** End the job with a short report the user can trust: (1) what you modelled and which variant (the sheet may cover several: say which one); (2) the sources you used and the ones that failed (403, empty page, a mirror instead of the maker's copy); (3) what the gate checked against what, in plain words, and which checks are conversions; (4) what is ESTIMATED (list it, it is drawn dashed); (5) the stranger test: what the judge said, which model judged, and whether it read text on the model instead of recognising the shape; (6) the lengths and the files (film, short cut, viewer, STL, GLB); (7) anything you did not check, including how the film plays beyond the stills you looked at. Do not round a weak result up.
**Verdict first.** `director.cold: { question, dur }` opens the film with the question and the published answers being checked row by row (the gate's own comparisons), every part's card shows its source and fetch date, and the film ends with a receipt panel (VERIFIED or DOES NOT PASS). Prefer it for any object whose point is a number someone could doubt.
**Brand identity.** An object has to look like itself: the maker's wordmark, its colours, where it is printed. Record the livery as a claim (SPEC when an opened source shows it, otherwise ESTIMATED with the reason) and draw it with `decals` on the part's faces: `decals: [{ face: '+z', at: [x, y, z], w, h, rot, glow, items: [{ t: 'text', s: 'HONDA', size, weight, spacing, color }, { t: 'rect' | 'circle' | 'line', ... }] }]`. Use type and colour only, never logo files. Parts also take `rot: [rx, ry, rz]` (degrees) and the livery materials `graphite`, `honda-red`, `nv-green`. Give the finished object a beauty shot before it is opened with `director.hold` (seconds): the assembled, lettered object turns slowly, then the cutaway starts.
## Workflow (hand-built route)
1. **Dossier** (`data/<subject>/<subject>.json`): body size, display, SoC, battery, cameras (MP, pixel pitch, format, f-number, equivalent focal length), materials, ports, buttons, sources with URLs. Keep conflicts in the file.
2. **Reference photos** (`data/<subject>/ref/`): download, view, measure. Record the result as a `layout` block in the dossier, in a stated coordinate frame (back view: x from the left edge, y from the top), with a `conf` tag and the photo it came from.
3. **Derive** (`scripts/derive-<subject>.mjs`): compute everything derivable and print the cross-checks. If a check fails, fix the model or say so. Generates `examples/<subject>/model.js`.
4. **Build the scene** on `kit/figure.js`: one `part()` per physical piece, with its own `info` (title, lines, conf tags), explode offset and material. Display stacks, boards, batteries and camera modules are separate parts so they can explode.
5. **Script the film**: `F.run({ duration, keys, script, audio })`. Camera keys `[t, view, ease]` with `dof`, per-part `arc`, `spin`, `tilt`, staggered `explodeK`, a moving key light, tour stops that focus one part and show a callout, then 2D panels (structure, interior, sensors to scale, processors, comparison, outro). Every panel shows its sources.
6. **Design the sound** with `kit/sound.js` (see below). Never reuse another figure's cue list.
7. **Check stills before rendering**: `node scripts/render.mjs <page> --still <t> --blur 1`. Look at the assembled view, an exploded tour stop, each panel, and compare the rear view with the real photos side by side.
8. **Render**: `node scripts/render.mjs <page> --duration <s> --fps 30 --blur 3 --out out/<name>.mp4` (about 6 s of wall time per second of video). Verify duration, stereo, loudness, contact sheet. Test `?explore` with `scripts/probe.mjs` (simulated mouse).
## The engine
- `kit/vendor/three.bundle.js`: three.js 0.186 plus EffectComposer, GTAO, Bokeh, Bloom, OutputPass, RoomEnvironment. Rebuild with `scripts/build-vendor.mjs` (`tools/vendor-build`). Works from `file://`.
- `kit/gl.js`: perspective camera (fov 22; `zoom` = px per mm at the target), managed colour, Neutral tone mapping, studio environment, soft shadows, MSAA 4x, GTAO, depth of field via `view.dof`, bloom. `G.mat` (physical material), `G.glass` (real transmission), `G.canvasTex`, `G.rbox`. Traps: `visible = null` does not hide (use booleans), `position`/`rotation` are read-only (copy), transmission needs `transparent: false`.
- `kit/figure.js`: `MX.figure({ w, h, axes, stage, card })`, `F.add(part)`, `F.finish()`, `F.camera`, `F.shot`, `F.drift`, `F.overlay`, `F.run`, `F.explore` (orbit, wheel zoom, explode slider, view presets, layer toggles, measure between two points, tap a part). HUD helpers in `F.hud`: `dim`, `tag`, `panel`, `info`.
- `kit/core.js`: easing (`MX.E.bezier`, `outBack`, `outElastic`, `smooth`, `snap`), `MX.card`, `MX.callout`, `MX.run` (frame-exact capture with motion blur by subframes, stereo WAV).
- `kit/sound.js`: modal synthesis per material (glass, aluminum, steel, plastic, pcb, ceramic, pouch, rubber; pitch falls with the size of the part, decay differs by material), `click`, `whoosh`, `servo`, `charge`, `blip`, `tone`, `boom`, stereo pan per object, reverb, generative music bed with its own key and tempo. Map events to the physics: a part landing = impact of its own material and size; CPU cores = notes quantized to a scale by clock speed; each lens = a steel ring sized like the lens. `scripts/soundtest.mjs` measures that materials and sizes really sound different.
## Look and feel
Paper background, ink text, teal for measured and spec, ember for estimated, one accent only. Glass screens are emissive. Metals reflect the studio. Everything that is not the subject of the current shot is dimmed and defocused. Labels sit at fixed screen positions per shot. Text at 18 px or more.
## Requirements
Node 22+, Edge or Chrome, `ffmpeg`, `uv` (Python tools for measuring photos). No npm install is needed to render; `npm install` in `tools/vendor-build` only to rebuild the three.js bundle.
## Limits
For toolkit changes, run `npm test` and `node scripts/visual-regression.mjs`. Golden frames are sampled regression evidence, not approval of every frame. Regenerate explicitly with `node scripts/visual-regression.mjs --update` only after reviewing the change; never regenerate to make a defect disappear. Stage initialization uses seeded denoiser noise. Full-resolution GPU edge samples can retain tiny bounded rounding differences even when geometry and time are identical. See `docs/perf-quality.md` for measurements and limits.
- It cannot get millimetres from a single photo without a scale: use a published size, a coin or ruler in the picture, or the datasheet. A catalog collage scales every piece to fit its slot, so even ratios between objects are not real.
- Interiors need photos. If none exist, the interior is estimated and says so.
- Rendering is heavy: about 2 to 3 seconds of wall time per second of film on a discrete GPU, much more on an integrated one (the scripts force the high-performance GPU; check `UNMASKED_RENDERER_WEBGL` if it is slow). Also checked on Ubuntu 24.04 (install, gates, stills, lint); a machine with no GPU renders a frame in about 7 s, so there use stills and the lint, not a film. Not tried on macOS.
## Examples
`examples/lock` (a pin tumbler lock with a KW1 key: the published table must close and a wrong key must block the plug), `examples/winrar` (WinRAR unpacking, from RFC 1951), `examples/react` (the real React DOM recorded in jsdom; a model written from the reconciliation algorithm must predict which list items move), `examples/switch` (a Cherry MX2A Red key switch: the three published force points cannot come from one linear spring, and it says so), `examples/enigma` (the Enigma I rotor stack: wiring, notches and stepping are checked against the published tests AAAAA > BDZGO, rings BBB > EWTYX and the ADU > ADV > AEW > BFX double step; type on your keyboard in the viewer), `examples/k20a` (Honda K20A: a crank-slider engine with 16 valves; the displacement is computed from bore and stroke and the 1-3-4-2 order is checked against the crank), `examples/rubik` (the cube: the 20-move superflip must flip the 12 edges, the longest period must be 1,260, the positions 43,252,003,274,489,856,000), `examples/rar5` (software: the 87 bytes of a RAR5 archive WinRAR 7.23 writes, rebuilt byte for byte by a model written from the published format and accepted by the real UnRAR), `examples/mos6502` (the 6502 in its DIP-40: pins counted on the pinout against the published bus widths, leads measured against the DIP standard), `examples/rtx4090` (GeForce RTX 4090: TFLOPS, fillrates, bandwidth and memory capacity are computed from the published cores, clocks, bus and data rate and must match; the inside of the card is estimated), `examples/hub` (a three-speed planetary hub, fully declarative: the tooth counts are DERIVED from a forum post and cross-checked against the published 75 / 100 / 133 %, the sizes are ESTIMATED and dashed), `examples/phones` (POCO X8 Pro Max vs Galaxy S26 Ultra, measured on official photos), `examples/rpi4` (Raspberry Pi 4B from the official datasheet drawing), `examples/backpressure` (the first, a 2D machine for an idea).