versioncam 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +196 -46
  2. package/README.md +486 -243
  3. package/bin/versioncam.js +1 -1
  4. package/dist/app-server.js +2 -2
  5. package/dist/app-server.js.map +1 -1
  6. package/dist/cli/commands/doctor.js +41 -4
  7. package/dist/cli/commands/doctor.js.map +1 -1
  8. package/dist/cli/commands/init.js +26 -1
  9. package/dist/cli/commands/init.js.map +1 -1
  10. package/dist/cli/commands/preview.js +10 -1
  11. package/dist/cli/commands/preview.js.map +1 -1
  12. package/dist/cli/commands/record.js +3 -2
  13. package/dist/cli/commands/record.js.map +1 -1
  14. package/dist/cli/commands/render.d.ts +17 -0
  15. package/dist/cli/commands/render.js +79 -22
  16. package/dist/cli/commands/render.js.map +1 -1
  17. package/dist/cli/commands/stability.js +4 -4
  18. package/dist/cli/commands/stability.js.map +1 -1
  19. package/dist/cli/cwd.d.ts +46 -0
  20. package/dist/cli/cwd.js +102 -0
  21. package/dist/cli/cwd.js.map +1 -0
  22. package/dist/cli/main.js +11 -2
  23. package/dist/cli/main.js.map +1 -1
  24. package/dist/cli/usage.d.ts +10 -5
  25. package/dist/cli/usage.js +42 -24
  26. package/dist/cli/usage.js.map +1 -1
  27. package/dist/config.d.ts +24 -0
  28. package/dist/config.js +25 -0
  29. package/dist/config.js.map +1 -1
  30. package/dist/core/camera-track.d.ts +63 -0
  31. package/dist/core/camera-track.js +230 -0
  32. package/dist/core/camera-track.js.map +1 -0
  33. package/dist/core/camera.d.ts +14 -1
  34. package/dist/core/camera.js +18 -2
  35. package/dist/core/camera.js.map +1 -1
  36. package/dist/core/follow.d.ts +129 -0
  37. package/dist/core/follow.js +151 -0
  38. package/dist/core/follow.js.map +1 -0
  39. package/dist/core/motion-defaults.d.ts +29 -0
  40. package/dist/core/motion-defaults.js +29 -0
  41. package/dist/core/motion-defaults.js.map +1 -1
  42. package/dist/core/timeline.d.ts +32 -2
  43. package/dist/core/timeline.js +10 -0
  44. package/dist/core/timeline.js.map +1 -1
  45. package/dist/driver/clip.d.ts +4 -1
  46. package/dist/driver/clip.js +9 -1
  47. package/dist/driver/clip.js.map +1 -1
  48. package/dist/driver/session.d.ts +35 -1
  49. package/dist/driver/session.js +81 -12
  50. package/dist/driver/session.js.map +1 -1
  51. package/dist/driver/settle.js +2 -2
  52. package/dist/driver/settle.js.map +1 -1
  53. package/dist/index.d.ts +2 -2
  54. package/dist/index.js +1 -1
  55. package/dist/index.js.map +1 -1
  56. package/dist/inspect/inspect.js +1 -1
  57. package/dist/loader.d.ts +7 -1
  58. package/dist/loader.js +23 -8
  59. package/dist/loader.js.map +1 -1
  60. package/dist/page/assets/index-fZtacWyp.js +2 -0
  61. package/dist/page/index.html +2 -2
  62. package/dist/render/encode.d.ts +12 -1
  63. package/dist/render/encode.js +94 -35
  64. package/dist/render/encode.js.map +1 -1
  65. package/dist/render/presentation.d.ts +12 -0
  66. package/dist/render/presentation.js +15 -0
  67. package/dist/render/presentation.js.map +1 -1
  68. package/dist/render/render.js +19 -2
  69. package/dist/render/render.js.map +1 -1
  70. package/dist/render/sampling.d.ts +13 -0
  71. package/dist/render/sampling.js +48 -20
  72. package/dist/render/sampling.js.map +1 -1
  73. package/dist/render/scenes.d.ts +16 -0
  74. package/dist/render/scenes.js +108 -0
  75. package/dist/render/scenes.js.map +1 -0
  76. package/dist/render/sequence.d.ts +18 -0
  77. package/dist/render/sequence.js +67 -13
  78. package/dist/render/sequence.js.map +1 -1
  79. package/dist/review/review.js +48 -23
  80. package/dist/review/review.js.map +1 -1
  81. package/dsl.md +267 -92
  82. package/package.json +2 -2
  83. package/plugin/README.md +58 -47
  84. package/plugin/agents/versioncam-reviewer.md +3 -2
  85. package/plugin/skills/versioncam/SKILL.md +60 -14
  86. package/plugin/skills/versioncam/authoring.md +6 -1
  87. package/plugin/skills/versioncam/onboarding.md +17 -17
  88. package/dist/page/assets/index-DUom5amc.js +0 -1
package/README.md CHANGED
@@ -1,47 +1,52 @@
1
1
  # versioncam
2
2
 
3
- Record demo clips of a real web app: scripted, deterministic, regenerated on
4
- deploy.
3
+ Record demo clips of a real web app: scripted, deterministic, re-recorded on
4
+ every push.
5
5
 
6
- A clip is a script in your repository. It drives the shipped UI through its own
7
- controls, captures it, composites a cursor and a camera over the result, and
8
- encodes it. Run it again after every deploy. When the UI changes in a way the
9
- clip depends on, the clip *fails* — naming the control it could not find and
10
- the second it gave up — instead of quietly producing a video that is wrong.
6
+ A clip is a script in your repository. It drives your app through its own
7
+ controls in a real browser and captures what the page shows. Then it draws a
8
+ cursor and a camera over the frames and encodes a video. Record it again on
9
+ every push. When the app changes under a clip, the clip *fails*: it names the
10
+ control it could not find and the second it gave up. It never turns into a
11
+ video that is quietly wrong.
11
12
 
12
13
  ## Install
13
14
 
14
- Three commands, then a sentence:
15
+ Three commands, then one sentence to your agent:
15
16
 
16
17
  ```bash
17
18
  npm i -D versioncam
18
19
  npx versioncam install # the Chromium this version records with
19
- npx versioncam init # installs the authoring skill into .claude/
20
+ npx versioncam init # the authoring skill, into .claude/
20
21
  ```
21
22
 
22
23
  ```
23
24
  /versioncam "Show a user filtering the list down to one shipment"
24
25
  ```
25
26
 
26
- The first run of the skill sets the repository up: it writes
27
+ The first run of the skill sets the repository up. It writes
27
28
  `versioncam.config.ts` from what it finds, proves the app starts and records
28
29
  the same way twice, and only then writes the clip. See *Authoring*.
29
30
 
30
- `npx versioncam doctor` checks the rest — Node 22 or later, Chromium, ffmpeg,
31
- your config, and whether the app starts. ffmpeg and ffprobe come from the
32
- system (`brew install ffmpeg`, `apt install ffmpeg`); the optional
33
- `ffmpeg-static` covers images that have neither, but it does not include
34
- ffprobe. It is tested on macOS and Linux; Windows is untested.
31
+ In a monorepo, run all three in the app's own directory. The skill goes
32
+ there, and so does the config its first run writes.
35
33
 
36
- `install` rather than `npx playwright install`, because versioncam pins its
37
- Playwright exactly — a new Chromium changes every captured pixel — and your
38
- app may have a Playwright of its own, at another version, whose Chromium is
39
- the one `npx playwright install` fetches.
34
+ `npx versioncam doctor` checks the rest: Node 22 or later, Chromium, ffmpeg,
35
+ your config and the scenes module it names, if any, and whether the app
36
+ starts. ffmpeg and ffprobe come from the
37
+ system (`brew install ffmpeg`, `apt install ffmpeg`). The optional
38
+ `ffmpeg-static` package covers a machine that has neither, but it has no
39
+ ffprobe. versioncam is tested on macOS and Linux. Windows is untested.
40
+
41
+ Use `versioncam install`, not `npx playwright install`. versioncam pins its
42
+ Playwright exactly, because a new Chromium changes every captured pixel. Your
43
+ app may have a Playwright of its own at another version, and
44
+ `npx playwright install` fetches that one's Chromium.
40
45
 
41
46
  ### In CI
42
47
 
43
- The same install, then `check`, which records every clip and fails the job on
44
- the first one that no longer matches the app:
48
+ The same install, then `check`. It records every clip and fails the job on the
49
+ first one that no longer matches the app.
45
50
 
46
51
  ```yaml
47
52
  - uses: actions/setup-node@v4
@@ -52,16 +57,19 @@ the first one that no longer matches the app:
52
57
  - run: npx versioncam check
53
58
  ```
54
59
 
55
- `check` records and does not render, so it needs no ffmpeg; a job that also
56
- runs `render` adds `sudo apt-get install -y ffmpeg`. Two runs on one runner
57
- image produce the same bytes. To pin everything the pixels depend on — the
58
- browser build, the fonts, the system libraries — run the job in the Playwright
59
- image for the version versioncam pins, `mcr.microsoft.com/playwright:v1.59.1-noble`,
60
- where Chromium and its libraries are already installed.
60
+ `check` records and does not render, so it needs no ffmpeg. A job that also
61
+ runs `render` adds `sudo apt-get install -y ffmpeg`.
62
+
63
+ Two runs on one runner image produce the same bytes. To pin everything the
64
+ pixels depend on (the browser build, the fonts, the system libraries), run the
65
+ job in the Playwright image for the version versioncam pins,
66
+ `mcr.microsoft.com/playwright:v1.59.1-noble`. Chromium and its libraries are
67
+ already installed there.
61
68
 
62
69
  ## Configure
63
70
 
64
- `versioncam.config.ts`, beside your clips:
71
+ `versioncam.config.ts` sits beside your clips. It is the one place your app
72
+ describes itself to the recorder.
65
73
 
66
74
  ```ts
67
75
  import { defineRecorder } from "versioncam";
@@ -86,36 +94,100 @@ export default defineRecorder({
86
94
  });
87
95
  ```
88
96
 
89
- Everything app-specific goes through this object. If the recorder needs to know
90
- something about your app that this cannot express, that is a missing field, not
91
- a reason to fork.
92
-
93
- **The app starts itself.** `webServer` is Playwright's, field for field:
94
- `command`, `url` (default `baseUrl`), `cwd` (default the config's directory),
95
- `timeoutMs` (default two minutes), `reuseExistingServer`, `env`. Every command
96
- that drives the app — `record`, `check`, `stability`, `inspect`, `measure`,
97
- `login`, `doctor` — first asks `url` whether anything is serving. Any answer
98
- counts, a 401 included, and a server that answers is used as it is and left
99
- running afterwards; with `CI` set it is refused instead, because on a runner a
100
- server that was up before the job started is not the build under test
101
- (`reuseExistingServer: true` allows it). Silence means the command starts
102
- `command` through a shell, with the shell's environment plus `env` — not what
103
- the recorder loaded from `.env.recording` for itself — waits for an answer,
104
- and writes what it prints to `.versioncam/webserver.log`. Whatever it started,
105
- it stops when it finishes — normally, on an error, or on Ctrl-C — by
106
- signalling the command's whole process group: SIGTERM, five seconds' grace,
107
- then SIGKILL, and the log says which it took. Something the start command
108
- hands off to outside that group, such as a container runtime, is out of its
109
- reach — the command warns when the port still answers after the stop — and so
110
- is worth starting yourself. So is any stack with a slow cold start: every
111
- command reuses what is already running.
97
+ If the recorder needs to know something about your app that this object
98
+ cannot say, that is a missing field. It is not a reason to fork.
99
+
100
+ | Field | Default | What it sets |
101
+ |---|---|---|
102
+ | `baseUrl` | required | Where the app is served. |
103
+ | `webServer` | none | How to start the app when nothing is serving it. See *Starting the app*. |
104
+ | `clips` | `"demos/**/*.clip.ts"` | Where the clips are: a glob, or a list of them, relative to the config. |
105
+ | `viewport` | `{ width: 1440, height: 900 }` | The page's size. The video has the same shape. |
106
+ | `dpr` | `2` | Device pixel ratio, from 1 to 3. At 2, a zoom shows real pixels. |
107
+ | `fps` | `60` | Frames per second, from 10 to 120. |
108
+ | `output` | `{ width: 1920 }` | The video's width. Its height follows the viewport. |
109
+ | `auth` | none | How the app signs in: a saved session, `{ storageState }`, or a hook, `{ login }`. |
110
+ | `settle` | `quietMs: 150`, `timeoutMs: 15000`, `yieldMs: 20` | When a page counts as done. `ready` adds a condition of your own. |
111
+ | `capture` | `gate: true`, `forceCaptureEvery: 30` | Which frames get a screenshot. See *Speed*. |
112
+ | `review` | `minSeconds: 9`, `maxSeconds: 11` | How long a clip should run. `versioncam review` holds clips to it. |
113
+ | `cursorStart` | `{ x: 0.72, y: 0.28 }` | Where the cursor rests in the first frame, as fractions of the viewport. |
114
+ | `clock` | `time: "2026-09-01T09:00:00Z"`, `timezoneId: "UTC"`, `locale: "en-GB"` | The instant, time zone and locale the app sees. |
115
+ | `theme` | `accent: "#2f6fed"`, `cursorScale: 2.1` | The colour of ripples and rings, the cursor's size, and an optional `presentation`. See *Presentation*. |
116
+ | `motion` | `MOTION` | Dwells, strokes and camera timing. `MOTION` is exported, with the reason for each number beside it. |
117
+ | `outDir` | `".versioncam"` | Where everything the recorder writes goes. Gitignore it. |
118
+ | `scenes` | none | A module whose default export is `Record<string, Scene>`, keyed by scene id: `export default { [intro.id]: intro }`. A named export is not read, and a module with no default export is refused. |
119
+ | `sequence` | none | Clip ids, in order, for `versioncam render --sequence`. |
120
+
121
+ ### Starting the app
122
+
123
+ `webServer` is Playwright's, field for field:
124
+
125
+ | Field | Default | What it sets |
126
+ |---|---|---|
127
+ | `command` | required | Run through a shell to start the app. |
128
+ | `url` | `baseUrl` | What to ask whether the app is up. |
129
+ | `cwd` | the config's directory | Where `command` runs. |
130
+ | `timeoutMs` | `120000` | How long to wait for `url` to answer. |
131
+ | `reuseExistingServer` | `true`, and `false` when `CI` is set | Use a server that already answers instead of starting one. |
132
+ | `env` | `{}` | Added to the shell's environment, for the server only. |
133
+
134
+ Every command that drives the app (`record`, `check`, `stability`, `inspect`,
135
+ `measure`, `login` and `doctor`) goes through the same steps:
136
+
137
+ 1. It asks `url` whether anything is serving. Any answer counts, a 401
138
+ included.
139
+ 2. If something answers, it uses that server as it is and leaves it running.
140
+ With `CI` set it refuses instead: on a runner, a server that was up before
141
+ the job started is not the build under test. `reuseExistingServer: true`
142
+ allows it.
143
+ 3. If nothing answers, it runs `command` with the shell's environment plus
144
+ `env`. It does not pass on what the recorder loaded from `.env.recording`
145
+ for itself. It waits for `url` to answer and writes the server's output to
146
+ `.versioncam/webserver.log`.
147
+ 4. When it finishes, normally, on an error or on Ctrl-C, it stops what it
148
+ started. It signals the command's whole process group: SIGTERM, then
149
+ SIGKILL after five seconds. The log says which one it took.
150
+
151
+ A process the start command hands off to outside its group, such as a
152
+ container runtime, is out of reach. The command warns when the port still
153
+ answers after the stop. Start a stack like that yourself, and any stack with a
154
+ slow cold start: every command reuses what is already running.
112
155
 
113
156
  For a stack of several services, point `url` at the one that is ready last,
114
- not at the page: a dev server answers seconds before the API behind it, and a
115
- clip that opens then meets a sign-in form with nothing to sign in to.
157
+ not at the page. A dev server answers seconds before the API behind it, and a
158
+ clip that opens too early meets a sign-in form with nothing to sign in to.
159
+
160
+ ### Presentation
161
+
162
+ `theme.presentation` is what surrounds the app in the video. A backdrop, a
163
+ device frame and padding are off by default, because the product should
164
+ record the product. They are there for a clip that should look like a
165
+ product shot, on a landing page say. A caption is drawn over the app on a
166
+ soft scrim unless `caption` says otherwise.
167
+
168
+ ```ts
169
+ theme: {
170
+ presentation: { captionInset: { right: 40 } },
171
+ },
172
+ ```
173
+
174
+ | Field | Default | What it sets |
175
+ |---|---|---|
176
+ | `backdrop` | none | Behind the app: two colours for a diagonal gradient, or one flat colour. |
177
+ | `frame` | `false` | Rounded corners, a hairline and a drop shadow around the app. |
178
+ | `padding` | `0` | How far in the app is drawn, as a fraction of the video's shorter side, up to 0.3. |
179
+ | `caption` | `"auto"` | Where captions go: `"below"` the app, `"overlay"` on it, or `"none"`. `"auto"` is below when padding makes room, and over the app when it does not. |
180
+ | `captionInset` | `{ left: 40 }` | Where a caption over the app sits: `{ left: n }` or `{ right: n }`, pixels of the video in from that side. `right` puts it in the bottom-right corner, for an app whose own controls are at the bottom left. |
181
+
182
+ A caption inset is in the video's pixels. The video is `output.width` wide,
183
+ 1920 unless the config says otherwise, so with a page 1440 wide, four of the
184
+ video's pixels are three of the page's.
116
185
 
117
186
  ## Write a clip
118
187
 
188
+ A clip is a file that default-exports `clip(id, options, body)`. The body
189
+ drives the app through `s`, its session.
190
+
119
191
  ```ts
120
192
  import { clip } from "versioncam";
121
193
 
@@ -133,169 +205,304 @@ export default clip("02-find-a-shipment", { title: "Find a shipment" }, async (s
133
205
  });
134
206
  ```
135
207
 
136
- Every `await` advances the clip's time and captures as it goes. `s.camera.*`,
137
- `s.highlight` and `s.caption` write to tracks at the current instant and cost
138
- no time. `s.offCamera(page => …)` runs raw Playwright with nothing captured —
139
- for setup that should not appear, such as mocking an API.
208
+ Every `await` advances the clip's time and captures frames as it goes.
209
+ `s.camera.*`, `s.highlight` and `s.caption` write to tracks at the current
210
+ instant and cost no time. `s.offCamera(page => …)` runs raw Playwright with
211
+ nothing captured, for setup that should not appear, such as mocking an API.
140
212
 
141
- Target elements by role or test id, never by coordinates. Strokes (`s.drag`
142
- for a straight one, `s.lasso` for a closed loop) are authored in fractions of a
143
- target's box for the same reason: a clip built from locators survives a layout
213
+ Target elements by test id or role, never by coordinates. Strokes (`s.drag`
214
+ for a straight one, `s.lasso` for a closed loop) are written in fractions of a
215
+ target's box for the same reason. A clip built from locators survives a layout
144
216
  change.
145
217
 
146
- `npx versioncam dsl` prints the whole surface, and a test asserts that list and
147
- the session agree. That is worth more than it sounds:
148
- this README promised an `s.drag` for months while there was none, and a clip
149
- that needed one clicked instead — on a d3 brush, where a click sets a selection
150
- of zero width and so *clears* the filter. It recorded cleanly and showed
151
- nothing, for months, and no check caught it.
218
+ `npx versioncam dsl` prints every method with its options and defaults, and a
219
+ test holds that list to the session's code. The same reference is
220
+ *The clip DSL*.
221
+
222
+ | Option | Default | What it sets |
223
+ |---|---|---|
224
+ | `title` | required | The clip's name, kept in its timeline. It is not drawn: write a caption with `s.caption`. |
225
+ | `seed` | `1` | Seeds the jitter in cursor paths, dwells and typing. A new seed is a new take of the same script. |
226
+ | `viewport`, `dpr`, `fps` | the config's | For a clip that needs a different shape. |
227
+ | `capture` | the config's | A clip that films a canvas sets `{ forceCaptureEvery: 1 }` here. |
228
+
229
+ ### The camera
230
+
231
+ The camera crops the recorded page as the video plays. There are two ways
232
+ to point it.
233
+
234
+ - **`s.camera.zoom(target, scale)`** frames one thing and stays there: a form
235
+ being filled, an answer to read. `scale` is a ceiling, so a small target
236
+ is framed with room around it.
237
+ - **`s.camera.follow()`** follows the cursor at one scale until the next
238
+ camera line. Use it when a clip moves around the page, in place of a zoom
239
+ before each thing the cursor uses, which zooms in and out and pans in fits
240
+ and starts.
241
+
242
+ ```ts
243
+ s.camera.follow({ scale: 1.35 });
244
+ await s.typeInto(s.byTestId("recipient"), "Mira Halloran");
245
+ await s.click(s.byTestId("create-shipment"));
246
+ await s.click(s.byTestId("cancel-3")); // across the page, and the camera goes too
247
+ s.camera.reset();
248
+ await s.hold(1200);
249
+ ```
250
+
251
+ A camera that follows keeps the cursor inside a dead zone in the middle of
252
+ the frame, and moves only when the cursor leaves it: a move to the next field
253
+ of a form does not move the picture. It never shows beyond the page's edge,
254
+ and it moves smoothly, without overshooting. Its path is worked out from the
255
+ whole cursor track once the clip has run, so it sets off with the cursor
256
+ instead of trailing behind it. `s.camera.release()` stops it where it is.
257
+
258
+ Every camera line starts from wherever the camera is, still or moving, and
259
+ eases into its new framing over its `duration`. A zoom written while the last
260
+ one is still moving takes over from it without a stop. `s.camera.reset()`
261
+ goes back to the whole page.
262
+
263
+ `versioncam review` warns about a click that lands outside what the camera
264
+ shows. *The clip DSL* has every option and its default.
152
265
 
153
- **Look before you write.** Clips written from an app's source reach for
154
- controls that are conditional, renamed, or not rendered at all:
266
+ ### Look before you write
267
+
268
+ A clip written from an app's source reaches for controls that are
269
+ conditional, renamed, or not rendered at all. Ask the page instead:
155
270
 
156
271
  ```bash
157
272
  npx versioncam inspect /shipments # what is on the page and what it is called
158
273
  npx versioncam measure / '.marker' # where the matches are, as fractions
159
274
  ```
160
275
 
161
- `inspect` says where each name came from — `[aria-label]`, `[label]`,
162
- `[placeholder]`, `[text]` or `[name]` — because that decides the locator:
163
- `byLabel` finds the first two and not a placeholder, which `byPlaceholder`
164
- finds instead. `measure` prints what each match says, so a beat's `expect`
165
- can quote the status line it is waiting for rather than guess at it.
276
+ `inspect` says where each name came from: `[aria-label]`, `[label]`,
277
+ `[placeholder]`, `[text]` or `[name]`. That decides the locator. `byLabel`
278
+ finds the first two but not a placeholder; `byPlaceholder` finds that.
279
+ `measure` prints what each match says, so a beat's `expect` can quote the
280
+ status line it waits for instead of guessing at it.
166
281
 
167
282
  ## Commands
168
283
 
284
+ Every command works in the directory that holds `versioncam.config.ts`: run
285
+ it from there, or name that directory with `--cwd`. `npx versioncam --help`
286
+ prints the same list.
287
+
288
+ ### Set up
289
+
290
+ Once per machine, and once per repository.
291
+
292
+ ```
293
+ versioncam install the Chromium this version of versioncam records with
294
+ --with-deps and its system libraries (Linux CI)
295
+ versioncam init install the authoring skill into .claude/
296
+ --into <dir> into <dir>/versioncam/ instead, for another host
297
+ --force overwrite files that are already there
298
+ versioncam doctor check this machine can record, and that the app starts
299
+ versioncam login sign in once and save the browser session
300
+ ```
301
+
302
+ ### Write
303
+
304
+ Look at the page before writing against it.
305
+
306
+ ```
307
+ versioncam inspect <path> what a clip can click, and where each name came from
308
+ --json machine-readable
309
+ versioncam measure <path> <sel> where a selector's matches are, and what they say
310
+ versioncam dsl the clip DSL: every method, its options and defaults
311
+ ```
312
+
313
+ ### Record
314
+
315
+ `check` is the CI command. It does not render: a clip that no longer matches
316
+ the app fails while it records, at the step that no longer exists. A video of
317
+ the wreckage would help nobody.
318
+
169
319
  ```
170
320
  versioncam record [id…] drive the app and capture frames
171
321
  --draft 15fps at CSS scale: cheap, for writing clips
172
- versioncam render [id…] composite and encode what has been recorded
173
- --scene <id> render a designed scene (repeatable)
174
- --sequence stitch the results into one piece
175
- versioncam check [id…] record everything; non-zero if any clip breaks
322
+ versioncam check [id…] record every clip; exit 1 if any breaks
176
323
  versioncam stability [id…] record each clip twice the same way; stable or not
177
324
  --runs <n> how many times (default 2)
178
325
  --full at full quality rather than draft
326
+ ```
327
+
328
+ ### Review
329
+
330
+ Look at what a recording shows, and check what needs no eyes.
331
+
332
+ ```
179
333
  versioncam sheet <id> an eight-frame contact sheet
180
334
  --at-marks sample the beats instead of even spacing
181
335
  versioncam frame <id> <n> one frame at output size, to a png
182
336
  --t <seconds> pick the frame by time instead
183
337
  --at <label> or by the beat it shows
338
+ versioncam review <id> the checks that need no eyes; PASS/FAIL, exit 0/1
339
+ --beats <file> [{ label, expect }] the clip promised
340
+ --min <s> --max <s> duration bounds (default 9 and 11)
184
341
  versioncam preview <id> open the scrubber in a real browser
185
342
  --scene <id> preview a scene instead
186
343
  --frame <n> start at this frame
187
- versioncam review <id> the checks that need no eyes; PASS/FAIL, exit 0/1
188
- --beats <file> [{ label, expect }] the clip promised
189
- --min <s> --max <s> duration bounds (default 9–11)
190
- versioncam inspect <path> what a clip can click, and where each name came from
191
- --json machine-readable
192
- versioncam measure <path> <sel> where the matches of a selector are, and what they say
193
- versioncam dsl the clip DSL, every method, one line each
194
- versioncam login sign in once and save the browser session
195
- versioncam doctor check this machine can record, and that the app starts
196
- versioncam install the Chromium this version of versioncam records with
197
- --with-deps and its system libraries (Linux CI)
198
- versioncam init install the authoring skill into .claude/
199
- --into <dir> into <dir>/versioncam/ instead, for another host
200
- --force overwrite files that are already there
201
344
  ```
202
345
 
203
- `versioncam check` is the CI command. It does not render: a demo that no longer
204
- matches the app fails while being recorded, at the step that no longer exists,
205
- and encoding a video of the wreckage helps nobody.
346
+ ### Output
347
+
348
+ Turn recordings and scenes into video.
349
+
350
+ With no ids, `render` renders every recording in `.versioncam/recordings/`,
351
+ and stale ones are recordings too: a draft or a trial run, or a clip since
352
+ renamed or deleted, stays there until its directory is deleted. It renders
353
+ every scene as well. Ids choose among recordings and scenes alike, by
354
+ substring, as they choose clips for `record`. `--scene` names a scene
355
+ exactly, and it renders whatever the ids are. Without ids, the scenes it
356
+ names are the only scenes rendered. An id that matches nothing, or a
357
+ `--scene` the scenes module does not define, stops the command before
358
+ anything renders.
359
+
360
+ ```
361
+ versioncam render [id…] composite and encode recordings and scenes
362
+ no ids: every one, stale recordings included
363
+ --scene <id> and this scene, whatever the ids say (repeatable)
364
+ --sequence stitch the results into one piece
365
+ ```
366
+
367
+ ### From another directory
368
+
369
+ In a monorepo the app is one directory among many. A command run from
370
+ somewhere else, the repository root or a subdirectory of the app, can name
371
+ the app's directory. The config, `.env.recording` and `.env`, `.versioncam/`
372
+ and any relative path are then that directory's. `--cwd` can go anywhere on
373
+ the line.
374
+
375
+ `npx` runs the versioncam it finds in the directory it is run from, or in the
376
+ nearest one above it, never in one below. From above an app that has
377
+ versioncam in its own `node_modules`, it runs another copy, or downloads the
378
+ latest. So `--cwd` refuses a directory whose own versioncam is not the one
379
+ running, and says to `cd` into it instead.
380
+
381
+ ```
382
+ versioncam <command> --cwd <dir> as if run in <dir>, which holds versioncam.config.ts
383
+ ```
206
384
 
207
385
  ## What a recording leaves behind
208
386
 
209
- In `.versioncam/recordings/<id>/`: `timeline.json`, the `states/` it indexes, and
210
- `settles.json` — every wait, what it was still blocked on, and how long it
211
- really took. A clip that fails also leaves `failure.png`, the page at the
212
- moment it gave up, and `failure.json` with the message, the frame and the
213
- authored second.
214
-
215
- The timeline's `marks` are the clip's beats: one per labelled `settle()`, at
216
- the first frame that shows what the wait was for. `versioncam sheet --at-marks`
217
- samples those frames instead of eight even ones, which is the difference
218
- between a sheet that shows what the clip is about and one that happens to miss
219
- it.
220
-
221
- `versioncam review <id> --beats beats.json` is the half of reviewing a clip that
222
- needs no eyes, and no browser: is it the right length, did a wait give up, does
223
- every beat in the sheet have a mark, did the screen actually change between one
224
- beat and the next, and did the clip end on a camera move with no frames left to
225
- show it. It prints a line per clause and exits 0 or 1. What needs eyes — is
226
- this legible, is the cursor in the way, does the frame show what the beat
227
- claimed — is what the contact sheet and `versioncam frame` are for.
387
+ `versioncam record` writes each clip to `.versioncam/recordings/<id>/`.
388
+
389
+ | File | What it holds |
390
+ |---|---|
391
+ | `timeline.json` | Everything the renderer needs, and everything a reviewer can check. |
392
+ | `states/` | The page states the timeline indexes, one image each. |
393
+ | `settles.json` | Every wait: what it was still blocked on, and how long it really took. |
394
+ | `failure.png` | Only after a failure: the page at the moment the clip gave up. |
395
+ | `failure.json` | Only after a failure: the message, the frame and the authored second. |
396
+
397
+ The timeline's `frames` say which state each frame shows. Beside them are the
398
+ tracks the renderer draws: `cursor`, `pointer`, `camera`, `highlights` and
399
+ `captions`. A camera that follows the cursor, or eases from one move into
400
+ another, is kept as a key a frame. Three more fields are there for reading:
401
+
402
+ - **`marks`** are the clip's beats: one per labelled `settle()`, at the first
403
+ frame that shows what the wait was for. `versioncam sheet --at-marks`
404
+ samples those frames instead of eight even ones. That is the difference
405
+ between a sheet that shows what the clip is about and one that happens to
406
+ miss it.
407
+ - **`steps`** are the script's calls, in order, each with the frame it began
408
+ on: `{ "frame": 212, "call": "typeInto" }`. There is one per line of the
409
+ script. `typeInto` is one step, not also the click it makes, and a locator
410
+ is an argument, not a step. A track write spends no time, so it shares a
411
+ frame with the line after it. New in 0.1.3.
412
+ - **`meta`** says where the recording came from: `title`, `appCommit`,
413
+ `baseUrl` and `recordedAt`. `recorder` is the versioncam version that made
414
+ it (new in 0.1.3). `captured` is how many frames got a screenshot, and
415
+ `draft` is set on a draft.
416
+
417
+ ### Checks that need no eyes
418
+
419
+ `versioncam review <id> --beats beats.json` is the half of reviewing a clip
420
+ that needs no eyes and no browser. It prints a line per check and exits 0 or 1:
421
+
422
+ - **duration**: the clip runs as long as `review` in the config says.
423
+ - **settles**: no wait gave up.
424
+ - **beats**: every beat in the beat sheet has a mark.
425
+ - **progress**: the screen changed between one beat and the next.
426
+ - **rendered**: nothing was written after the last frame, where no frame
427
+ would show it. A warning, not a failure.
428
+ - **onscreen**: every highlight and every click is inside what the camera
429
+ shows. Also a warning.
430
+
431
+ What needs eyes is what the contact sheet and `versioncam frame` are for. Is
432
+ it legible? Is the cursor in the way? Does the frame show what the beat
433
+ claimed?
228
434
 
229
435
  ## How it works
230
436
 
231
- **Time is authored, not measured.** The script declares the timeline; the
232
- driver steps the app through it one output frame at a time, advancing the
233
- page's fake clock by exactly 1/fps and capturing. Anything slow — a query, a
234
- tile, a model — happens inside a `settle()` that advances the clock but
235
- captures nothing, so it never reaches the video.
437
+ Five decisions make a recording repeatable.
438
+
439
+ **Time is authored, not measured.** The script declares the timeline. The
440
+ driver steps the app through it one output frame at a time: it advances the
441
+ page's fake clock by exactly 1/fps, then captures. Anything slow, such as a
442
+ query, a map tile or a model, happens inside a `settle()`. A settle advances
443
+ the clock but captures nothing, so the wait never reaches the video.
236
444
 
237
445
  **The cursor is real input with a synthetic glyph.** Headless Chrome draws no
238
- pointer, so the driver moves the *real* mouse along a planned path — hover and
239
- press states genuinely fire in the captured frames — and records that path as
240
- data. The glyph, the press and the ripple are drawn afterwards at output
241
- resolution, which means the look can change without re-recording.
446
+ pointer. So the driver moves the *real* mouse along a planned path, and hover
447
+ and press states really fire in the captured frames. It records that path as
448
+ data. The glyph, the press and the ripple are drawn later at output
449
+ resolution, so the look can change without recording again.
242
450
 
243
451
  **Motion is modelled.** Minimum-jerk profiles, slight Bézier arcs, Fitts-law
244
- durations, a dwell before each click, all seeded — so it never looks
245
- metronomic and never differs between runs.
452
+ durations and a dwell before each click. All of it is seeded, so it never
453
+ looks metronomic and never differs between runs.
246
454
 
247
455
  **Rendering needs nothing running.** The page, the recording, the config and
248
- your scenes are served from an in-memory route table, so `versioncam render` works
249
- from a recording on disk and a browser.
456
+ your scenes are served from an in-memory route table. `versioncam render`
457
+ needs only a recording on disk and a browser.
250
458
 
251
- **Output follows the viewport.** Record at 1440×900 and the video is 16:10;
252
- want 16:9, record at 1600×900. Nothing is letterboxed or cropped. A backdrop,
253
- device frame and padding are available via `theme.presentation`, off by
254
- default.
459
+ **Output follows the viewport.** Record at 1440×900 and the video is 16:10.
460
+ For 16:9, record at 1600×900. Nothing is letterboxed or cropped. A backdrop, a
461
+ device frame and padding are available through `theme.presentation`, and are
462
+ off by default.
255
463
 
256
464
  ## Speed
257
465
 
258
- Writing a clip means running it, looking at it, and changing a line — so what
259
- matters is how long that loop takes.
466
+ Writing a clip is a loop: run it, look at it, change a line. What matters is
467
+ how long that loop takes.
260
468
 
261
469
  **Most frames are not photographed.** A clip is mostly frames in which nothing
262
- moved, and the screenshot is by far the most expensive thing a frame does: 25 ms
470
+ moved. The screenshot is by far the most expensive thing a frame does: 25 ms,
263
471
  against half a millisecond for everything else. So the driver takes one only
264
- when the page can have changed — the DOM mutated, the element under the cursor
265
- changed, an animation is running, input was just sent, or `forceCaptureEvery`
266
- frames have gone by. On the example app that skips 81% to 95% of frames, and
267
- the recording is identical either way. A test records every clip both ways on
268
- every push and compares the bytes, because the failure mode is not a crash but
269
- a video that is quietly wrong.
270
-
271
- The one thing it cannot see is a surface that redraws itself: a canvas, a WebGL
272
- view, a video. None of them touches the DOM. `capture.forceCaptureEvery`
273
- (default 30) is the floor under that; a clip whose subject *is* a canvas should
274
- set it to 1 and pay full price:
472
+ when the page can have changed. That is when the DOM mutated, the element
473
+ under the cursor changed, an animation is running, input was just sent, or
474
+ `forceCaptureEvery` frames have gone by. On the example app that skips 81% to
475
+ 95% of frames, and the recording is identical either way. A test records every
476
+ clip both ways on every push and compares the bytes. The failure it guards
477
+ against is not a crash but a video that is quietly wrong.
478
+
479
+ The one thing the gate cannot see is a surface that redraws itself: a canvas,
480
+ a WebGL view, a video. None of them touches the DOM. `forceCaptureEvery`
481
+ (default 30) is the floor under that. A clip whose subject *is* a canvas
482
+ should set it to 1 and pay full price:
275
483
 
276
484
  ```ts
277
485
  export default clip("03-the-map", { title: "The map", capture: { forceCaptureEvery: 1 } }, …);
278
486
  ```
279
487
 
280
488
  **`--draft` for the loop.** Fifteen frames a second, captured at CSS scale
281
- instead of device scale, into the same place a real recording goes.
489
+ instead of device scale, into the same place a full recording goes.
282
490
 
283
491
  ```bash
284
492
  npx versioncam record --draft 02-find-a-shipment # then render and look
285
493
  ```
286
494
 
287
- It deliberately does not change the page: same viewport, same device pixel
288
- ratio, same layout, same code. A draft that reduced the DPR could pass while
289
- the capture it stands in for fails on a media query. It does ignore a clip's
290
- own `fps`, because the point is to be cheap. Its duration can land up to 1/15 s
291
- from the full capture's, so a draft is for looking at, not for comparing
292
- against.
495
+ A draft does not change the page: same viewport, same device pixel ratio,
496
+ same layout, same code. A draft that lowered the DPR could pass while the
497
+ capture it stands in for fails on a media query. It does ignore a clip's own
498
+ `fps`, because the point is to be cheap. Its length can land up to 1/15 s from
499
+ the full capture's, so a draft is for looking at, not for comparing against.
293
500
 
294
- `versioncam record` prints where the time went, which is how you tell whether the
501
+ `versioncam record` prints where the time went. That tells you whether the
295
502
  next second would come from the frames or from somewhere else:
296
503
 
297
504
  ```
298
- 01-create-a-shipment: 425 frames, 63 states, 1 cuts — plays 7.1s · took 6.3s
505
+ 01-create-a-shipment: 425 frames, 63 states, 1 cut · plays 7.1s · took 6.3s
299
506
  open 0.0s · settles 0.5s · frames 5.7s (79 captured, 346 skipped) · write 0.0s
300
507
  ```
301
508
 
@@ -305,10 +512,11 @@ is why both are named.
305
512
 
306
513
  ## Authoring
307
514
 
308
- A clip is a script, and writing one is a loop: look at the page, write, record,
309
- look at what you got, fix. `versioncam` ships that loop as one agent skill.
310
- There is no API key and no SDK: the model is whichever agent session runs the
311
- skill, and it reads contact sheets with the same tool it reads files with.
515
+ A clip is a script, and writing one is a loop: look at the page, write,
516
+ record, look at what you got, fix. versioncam ships that loop as one agent
517
+ skill. There is no API key and no SDK. The model is whichever agent session
518
+ runs the skill, and it reads contact sheets with the same tool it reads files
519
+ with.
312
520
 
313
521
  ```bash
314
522
  npx versioncam init # .claude/skills/versioncam/ and .claude/agents/versioncam-reviewer.md
@@ -317,123 +525,135 @@ claude --plugin-dir node_modules/versioncam/plugin
317
525
  ```
318
526
 
319
527
  `init --into <dir>` puts the skill directory somewhere else, for a host that
320
- reads skills from elsewhere. Then:
528
+ reads skills from another place. Then ask for a clip:
321
529
 
322
530
  ```
323
531
  /versioncam "Find a shipment by filtering the list, and show how many matched"
324
532
  ```
325
533
 
326
- **The first run** — in a directory with no `versioncam.config.ts` — sets the
327
- repository up before anything else: how the app starts and where it is served,
328
- from its scripts and its framework's config; whether it signs in, which it
329
- asks you about; a config with a `webServer`, so nothing has to be running;
330
- `doctor` and `inspect` to prove the app starts and settles; and `stability`
331
- on a three-second clip, so no clip is written against an app that does not
332
- record the same way twice. It commits nothing.
534
+ **The first run** in a repository with no `versioncam.config.ts` sets it up
535
+ before anything else:
536
+
537
+ 1. It works out how the app starts and where it is served, from its scripts
538
+ and its framework's config.
539
+ 2. It asks you whether the app signs in, and how you want that handled.
540
+ 3. It writes a config with a `webServer`, so nothing has to be running.
541
+ 4. It runs `doctor` and `inspect` to prove the app starts and settles.
542
+ 5. It records a three-second clip twice with `stability`, so no clip is
543
+ written against an app that does not record the same way twice.
544
+
545
+ It commits nothing.
546
+
547
+ **In a monorepo** the skill first looks for `versioncam.config.ts` across the
548
+ whole repository, wherever the session started, and then works in the
549
+ directory that holds it. With several apps, it asks which. With none, the
550
+ first run writes the config where `versioncam init` put the skill.
333
551
 
334
- **Every run.** The session inspects the start page and writes the clip itself:
335
- it reads `versioncam dsl` and your existing clips, writes a beat sheet, writes
552
+ **Every run** then goes the same way. The session inspects the start page and
553
+ reads `versioncam dsl` and your existing clips. It writes a beat sheet, then
336
554
  the clip, and records it in draft until `versioncam review` passes, three
337
- recordings at most. Then a contact sheet sampled *at the beats*, and a fresh
338
- reviewer every round — `versioncam-reviewer`, which sees the picture, the
339
- beats and a five-item checklist, and nothing of how the clip was written. A
340
- reviewer that has read the author's reasoning agrees with it. Its verdict comes
341
- back as fixes to make, and the round repeats; six rounds at most, then it stops
342
- and says why. On a host that cannot register the reviewer as an agent, the same
343
- file is a subagent's prompt, or — with no subagents at all — a checklist the
344
- session applies to its own sheet, and the verdict says which.
345
-
346
- **What you get**, under `.versioncam/author/<id>/`: the beat sheet, a
347
- `summary.json`, the final sheet, and per round what the author found, its
348
- beats and the reviewer's verdict. The clip itself goes in your clips
349
- directory, which is the part worth reading and committing.
555
+ recordings at most. Then comes a contact sheet sampled *at the beats*, and a
556
+ fresh reviewer.
557
+
558
+ **The reviewer** is `versioncam-reviewer`, new every round. It sees the
559
+ picture, the beats and a five-item checklist, and nothing of how the clip was
560
+ written: a reviewer that has read the author's reasoning agrees with it. Its
561
+ verdict comes back as fixes to make, and the round repeats. After six rounds
562
+ at most it stops and says why. On a host that cannot register the reviewer as
563
+ an agent, the same file is a subagent's prompt. On a host with no subagents at
564
+ all, it is a checklist the session applies to its own sheet, and the verdict
565
+ says which.
566
+
567
+ **What you get** is under `.versioncam/author/<id>/`: the beat sheet, a
568
+ `summary.json`, the final sheet, and for each round what the author found,
569
+ its beats and the reviewer's verdict. The clip itself goes in your clips
570
+ directory. That is the part worth reading and committing.
350
571
 
351
572
  **What it costs.** Six to twelve minutes and one or two rounds for a clip on
352
573
  the example app, most of it model latency rather than the recorder. It is not
353
- cheap and it is not instant; it is, however, unattended.
574
+ cheap and it is not instant. It is, however, unattended.
354
575
 
355
- **What it is not.** The skill cannot sign your app in for you — it asks how
356
- you want that handled, and `versioncam login` is yours to run because it waits
357
- for you. It does not run `git`. And `versioncam review` only checks what needs
358
- no eyes: duration, that every beat left a mark, that each beat changed the
359
- screen, that nothing was written after the last frame. Whether the clip is
360
- *good* is the reviewer's job, and after that, yours.
576
+ **What it is not.** The skill cannot sign your app in for you. It asks how you
577
+ want that handled, and `versioncam login` is yours to run because it waits
578
+ for you. It does not run `git`. And `versioncam review` only checks what
579
+ needs no eyes. Whether the clip is *good* is the reviewer's job, and after
580
+ that, yours.
361
581
 
362
582
  ## Determinism
363
583
 
364
584
  Two runs of a clip on one machine produce the same bytes, draft and full. The
365
585
  package's own tests assert it for capture and for rendering on every change,
366
- on macOS and on Linux, and a golden recording pins the capture path against
586
+ on macOS and on Linux. A golden recording pins the capture path against
367
587
  changes nobody meant to make.
368
588
 
369
- Across machines the claim is narrower, and worth stating precisely: the
370
- **timeline** is identical — it is what the driver decided, and nothing in it
371
- depends on the host. The captured **pixels** are identical only where the
372
- rendering environment is: macOS and Linux rasterise text differently, so the
373
- same page in the same state is the same picture and a different file.
374
- Byte-identity is a property of a fixed environment, which in CI means one
375
- runner image — the Playwright container for the version versioncam pins is
376
- the simplest (see *In CI*).
589
+ Across machines the claim is narrower. The **timeline** is identical: it is
590
+ what the driver decided, and nothing in it depends on the host. The captured
591
+ **pixels** are identical only where the rendering environment is. macOS and
592
+ Linux rasterise text differently, so the same page in the same state is the
593
+ same picture and a different file. Byte-identity belongs to a fixed
594
+ environment. In CI that means one runner image, and the Playwright container
595
+ for the version versioncam pins is the simplest (see *In CI*).
377
596
 
378
597
  If two recordings of your clip on one machine differ, something outside the
379
- authored clock reached a frame — an unwarmed resource, a real timer, a font
380
- that had not finished loading. Find the cause; the next section is how.
598
+ authored clock reached a frame: an unwarmed resource, a real timer, a font
599
+ that had not finished loading. Find the cause. The next section is how.
381
600
 
382
601
  ### When a clip will not reproduce
383
602
 
384
- The recorder can only be as deterministic as the app it is filming, and an app
385
- that is *nearly* deterministic is the hard case: it reproduces most runs, so the
603
+ The recorder can only be as deterministic as the app it films. An app that is
604
+ *nearly* deterministic is the hard case: it reproduces on most runs, so the
386
605
  first failure looks like a recorder bug.
387
606
 
388
- Before assuming it is one, record the clip twice the same way — which is
389
- what `stability` does, frame by frame, by what each frame shows:
607
+ Before assuming it is one, record the clip twice the same way. `stability`
608
+ does exactly that, and compares the runs frame by frame, by what each frame
609
+ shows:
390
610
 
391
611
  ```bash
392
612
  npx versioncam stability 02-find-a-shipment # draft; --full for the real thing
393
613
  ```
394
614
 
395
615
  ```
396
- 02-find-a-shipment: run 1 vs run 2 — 268 frames, 264 identical, first difference at frame 131
616
+ 02-find-a-shipment: run 1 vs run 2 · 268 frames, 264 identical, first difference at frame 131
397
617
  .versioncam/stability/02-find-a-shipment/frame-131-run1.png
398
618
  .versioncam/stability/02-find-a-shipment/frame-131-run2.png
399
- unstable — something on this page does not run on authored time
619
+ unstable: something on this page does not run on authored time
400
620
  ```
401
621
 
402
- The two files are the pictures behind the first frame that differs; what
403
- differs is usually obvious once they are side by side. Each run records beside
404
- the clip's own recording, under `recordings/__stability/`, never over it. With
405
- no id it checks every clip, and it exits 0 only when every pair matched —
406
- which makes it the first thing to run in a repository before writing a clip
407
- for it, and what the authoring skill does on a first run.
622
+ The two files are the pictures behind the first frame that differs. Side by
623
+ side, what differs is usually obvious. Each run records beside the clip's own
624
+ recording, under `recordings/__stability/`, never over it. With no id it
625
+ checks every clip, and it exits 0 only when every pair matched. That makes it
626
+ the first thing to run in a repository before writing a clip for it, and it
627
+ is what the authoring skill does on a first run.
408
628
 
409
629
  If the runs differ, something on the page did not run on authored time. Look
410
630
  at the app first, but not only there: of the four causes found so far, the
411
631
  recorder was two. What we have found doing this:
412
632
 
413
633
  - An **autosizing text field** that settles a frame later depending on how the
414
- browser scheduled the layout. The clips that typed diverged; the clips that
634
+ browser scheduled the layout. The clips that typed diverged. The clips that
415
635
  did not, did not.
416
- - A **spinner**, via the recorder's own fault, since fixed: the screenshot was
417
- cancelling the animation the driver had just scrubbed. Worth knowing because
418
- the symptom was six pixels in one frame of three hundred, which reads as
419
- noise rather than as a bug.
420
- - A **typed letter's edge**, via the recorder's own fault, since fixed: a
421
- draft's screenshot has Chromium paint the page again at CSS scale, and it
422
- repainted only the rectangle the page reported as changed, which at that
423
- scale could stop a pixel short of a new glyph's antialiased edge. Only on
424
- Linux, where text has LCD antialiasing: one pixel of one frame, in about half
425
- of all runs. Side by side the two pictures look identical; subtracting one
426
- from the other finds it.
427
- - A **timer firing early**, via the recorder's own fault, since fixed: the
428
- page's fake clock was installed but left running on real time between
429
- frames, so a slow screenshot reached the page as time. One stalled frame on
430
- a CI runner moved the clock 840 ms instead of 67 and the example app's
431
- shipment appeared seven frames early. The clock is paused now and moves only
432
- when the driver moves it.
433
-
434
- If the two recordings match and something else does not, the next question is
435
- what changed between them — the gate (`capture: { gate: false }` turns it off)
436
- or the machine.
636
+ - A **spinner**, the recorder's fault and since fixed. The screenshot was
637
+ cancelling the animation the driver had just scrubbed. The symptom was six
638
+ pixels in one frame of three hundred, which reads as noise rather than as a
639
+ bug.
640
+ - A **typed letter's edge**, the recorder's fault and since fixed. A draft's
641
+ screenshot has Chromium paint the page again at CSS scale. It repainted only
642
+ the rectangle the page reported as changed, which at that scale could stop
643
+ a pixel short of a new glyph's antialiased edge. This happened only on
644
+ Linux, where text has LCD antialiasing: one pixel of one frame, in about
645
+ half of all runs. Side by side the two pictures look identical; subtracting
646
+ one from the other finds it.
647
+ - A **timer firing early**, the recorder's fault and since fixed. The page's
648
+ fake clock was installed but left running on real time between frames, so a
649
+ slow screenshot reached the page as time. One stalled frame on a CI runner
650
+ moved the clock 840 ms instead of 67, and the example app's shipment
651
+ appeared seven frames early. The clock is paused now and moves only when
652
+ the driver moves it.
653
+
654
+ If the two recordings match and something else does not, ask what changed
655
+ between them: the gate (`capture: { gate: false }` turns it off), or the
656
+ machine.
437
657
 
438
658
  ## While recording
439
659
 
@@ -441,23 +661,46 @@ Do not edit files. A dev server pushes a full page reload to every connected
441
661
  client, including the page being recorded. The driver detects it and says so,
442
662
  but the run is lost either way.
443
663
 
444
- Record one clip at a time against a given app session. The CLI already does;
445
- the thing to avoid is leaving a browser tab open on the same session.
664
+ Record one clip at a time against a given app session. The CLI already does.
665
+ The thing to avoid is a browser tab left open on the same session.
666
+
667
+ ## Glossary
668
+
669
+ The words these pages use in a particular sense.
670
+
671
+ | Word | Meaning |
672
+ |---|---|
673
+ | authored time | The clip's own clock. The page's clock moves only when the driver moves it, one frame at a time. |
674
+ | beat | A moment the clip exists to show. A labelled `settle()` marks one. |
675
+ | beat sheet | The beats a clip promises, as `[{ label, expect }]`. `versioncam review --beats` checks a recording against it. |
676
+ | clip | A script, `*.clip.ts`, that drives the app and says what to show. One clip makes one video. |
677
+ | contact sheet | Eight frames of a recording in one image, for judging it at a glance. `versioncam sheet` makes one. |
678
+ | draft | A cheap recording for writing clips: 15 frames a second, captured at CSS scale. |
679
+ | gate | The check that skips a screenshot when nothing on the page can have changed. |
680
+ | mark | Where a beat first shows, in the timeline's `marks`. |
681
+ | recording | What `record` leaves in `.versioncam/recordings/<id>/`: the timeline and the page states it indexes. |
682
+ | render | Drawing the cursor, camera, highlights and captions over a recording, and encoding the video. |
683
+ | reviewer | The fresh agent that judges a contact sheet against the beats and a five-item checklist. |
684
+ | scene | A designed panel: frames drawn in code rather than captured from the app. |
685
+ | settle | A wait that advances the page's clock but captures nothing, so it never reaches the video. |
686
+ | stable | Two recordings of a clip, made the same way on one machine, are identical. |
687
+ | step | One line of the script, as the timeline's `steps` records it. |
688
+ | timeline | `timeline.json`: which state each frame shows, and every track drawn over it. |
689
+ | track write | A call to `camera`, `highlight` or `caption`. It writes at the current instant and spends no time. |
446
690
 
447
691
  ## Help
448
692
 
449
- This README is also the documentation at [version.cam](https://version.cam).
450
693
  Bugs, questions, and apps it cannot record go to the issue tracker at
451
- [github.com/petbul/versioncam-home](https://github.com/petbul/versioncam-home/issues)
452
- — a clip that will not reproduce is easiest to help with alongside the two
694
+ [github.com/petbul/versioncam-home](https://github.com/petbul/versioncam-home/issues).
695
+ A clip that will not reproduce is easiest to help with alongside the two
453
696
  pictures `versioncam stability` saves.
454
697
 
455
698
  ## Licence
456
699
 
457
700
  Versioncam is under the Functional Source License, version 1.1, with Apache
458
- 2.0 as its future licence (`FSL-1.1-ALv2`). Use it on your own apps and in your
459
- own CI, commercial work included, and change it as you need; what the licence
460
- does not allow is making it available to others in a product or service that
461
- competes with it. Two years after each version is released, that version is
462
- also available under Apache 2.0. This is a summary; `LICENSE.md` is the
463
- licence.
701
+ 2.0 as its future licence (`FSL-1.1-ALv2`). Use it on your own apps and in
702
+ your own CI, commercial work included, and change it as you need. What the
703
+ licence does not allow is making it available to others in a product or
704
+ service that competes with it. Two years after each version is released, that
705
+ version is also available under Apache 2.0. This is a summary; `LICENSE.md`
706
+ is the licence.