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