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.
- package/CHANGELOG.md +196 -46
- package/README.md +486 -243
- 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 +41 -4
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/init.js +26 -1
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/preview.js +10 -1
- package/dist/cli/commands/preview.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.d.ts +17 -0
- package/dist/cli/commands/render.js +79 -22
- 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/cwd.d.ts +46 -0
- package/dist/cli/cwd.js +102 -0
- package/dist/cli/cwd.js.map +1 -0
- package/dist/cli/main.js +11 -2
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/usage.d.ts +10 -5
- package/dist/cli/usage.js +42 -24
- package/dist/cli/usage.js.map +1 -1
- package/dist/config.d.ts +24 -0
- package/dist/config.js +25 -0
- package/dist/config.js.map +1 -1
- package/dist/core/camera-track.d.ts +63 -0
- package/dist/core/camera-track.js +230 -0
- package/dist/core/camera-track.js.map +1 -0
- package/dist/core/camera.d.ts +14 -1
- package/dist/core/camera.js +18 -2
- package/dist/core/camera.js.map +1 -1
- package/dist/core/follow.d.ts +129 -0
- package/dist/core/follow.js +151 -0
- package/dist/core/follow.js.map +1 -0
- package/dist/core/motion-defaults.d.ts +29 -0
- package/dist/core/motion-defaults.js +29 -0
- package/dist/core/motion-defaults.js.map +1 -1
- package/dist/core/timeline.d.ts +32 -2
- 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 +35 -1
- package/dist/driver/session.js +81 -12
- 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/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/inspect/inspect.js +1 -1
- package/dist/loader.d.ts +7 -1
- package/dist/loader.js +23 -8
- package/dist/loader.js.map +1 -1
- package/dist/page/assets/index-fZtacWyp.js +2 -0
- package/dist/page/index.html +2 -2
- package/dist/render/encode.d.ts +12 -1
- package/dist/render/encode.js +94 -35
- package/dist/render/encode.js.map +1 -1
- package/dist/render/presentation.d.ts +12 -0
- package/dist/render/presentation.js +15 -0
- package/dist/render/presentation.js.map +1 -1
- package/dist/render/render.js +19 -2
- package/dist/render/render.js.map +1 -1
- package/dist/render/sampling.d.ts +13 -0
- package/dist/render/sampling.js +48 -20
- package/dist/render/sampling.js.map +1 -1
- package/dist/render/scenes.d.ts +16 -0
- package/dist/render/scenes.js +108 -0
- package/dist/render/scenes.js.map +1 -0
- package/dist/render/sequence.d.ts +18 -0
- package/dist/render/sequence.js +67 -13
- package/dist/render/sequence.js.map +1 -1
- package/dist/review/review.js +48 -23
- package/dist/review/review.js.map +1 -1
- package/dsl.md +267 -92
- package/package.json +2 -2
- package/plugin/README.md +58 -47
- package/plugin/agents/versioncam-reviewer.md +3 -2
- package/plugin/skills/versioncam/SKILL.md +60 -14
- package/plugin/skills/versioncam/authoring.md +6 -1
- package/plugin/skills/versioncam/onboarding.md +17 -17
- 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,
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
`
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
44
|
-
|
|
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
|
|
56
|
-
runs `render` adds `sudo apt-get install -y ffmpeg`.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
`
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
`
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
is
|
|
111
|
-
|
|
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
|
|
115
|
-
clip that opens
|
|
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.
|
|
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.
|
|
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
|
|
142
|
-
for a straight one, `s.lasso` for a closed loop) are
|
|
143
|
-
target's box for the same reason
|
|
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
|
|
147
|
-
the session
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
|
162
|
-
`[placeholder]`, `[text]` or `[name]
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
and
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
`
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
239
|
-
press states
|
|
240
|
-
data. The glyph, the press and the ripple are drawn
|
|
241
|
-
resolution,
|
|
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
|
|
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
|
|
249
|
-
|
|
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
|
-
|
|
253
|
-
device frame and padding are available
|
|
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
|
|
259
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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**
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
|
335
|
-
|
|
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
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
reviewer
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
same
|
|
374
|
-
|
|
375
|
-
|
|
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
|
|
380
|
-
that had not finished loading. Find the cause
|
|
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
|
|
385
|
-
|
|
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
|
|
389
|
-
|
|
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
|
|
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
|
|
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
|
|
403
|
-
differs is usually obvious
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
|
634
|
+
browser scheduled the layout. The clips that typed diverged. The clips that
|
|
415
635
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|