versioncam 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/LICENSE.md +105 -0
  3. package/README.md +463 -0
  4. package/bin/versioncam.js +29 -0
  5. package/dist/.types-render/render-page/draw.d.ts +28 -0
  6. package/dist/.types-render/render-page/main.d.ts +24 -0
  7. package/dist/.types-render/render-page/theme.d.ts +37 -0
  8. package/dist/app-server.d.ts +59 -0
  9. package/dist/app-server.js +328 -0
  10. package/dist/app-server.js.map +1 -0
  11. package/dist/cli/app.d.ts +13 -0
  12. package/dist/cli/app.js +21 -0
  13. package/dist/cli/app.js.map +1 -0
  14. package/dist/cli/commands/check.d.ts +8 -0
  15. package/dist/cli/commands/check.js +51 -0
  16. package/dist/cli/commands/check.js.map +1 -0
  17. package/dist/cli/commands/doctor.d.ts +8 -0
  18. package/dist/cli/commands/doctor.js +130 -0
  19. package/dist/cli/commands/doctor.js.map +1 -0
  20. package/dist/cli/commands/dsl.d.ts +16 -0
  21. package/dist/cli/commands/dsl.js +22 -0
  22. package/dist/cli/commands/dsl.js.map +1 -0
  23. package/dist/cli/commands/frame.d.ts +8 -0
  24. package/dist/cli/commands/frame.js +72 -0
  25. package/dist/cli/commands/frame.js.map +1 -0
  26. package/dist/cli/commands/init.d.ts +23 -0
  27. package/dist/cli/commands/init.js +109 -0
  28. package/dist/cli/commands/init.js.map +1 -0
  29. package/dist/cli/commands/inspect.d.ts +1 -0
  30. package/dist/cli/commands/inspect.js +32 -0
  31. package/dist/cli/commands/inspect.js.map +1 -0
  32. package/dist/cli/commands/install.d.ts +33 -0
  33. package/dist/cli/commands/install.js +66 -0
  34. package/dist/cli/commands/install.js.map +1 -0
  35. package/dist/cli/commands/login.d.ts +10 -0
  36. package/dist/cli/commands/login.js +49 -0
  37. package/dist/cli/commands/login.js.map +1 -0
  38. package/dist/cli/commands/measure.d.ts +1 -0
  39. package/dist/cli/commands/measure.js +36 -0
  40. package/dist/cli/commands/measure.js.map +1 -0
  41. package/dist/cli/commands/open-app.d.ts +14 -0
  42. package/dist/cli/commands/open-app.js +45 -0
  43. package/dist/cli/commands/open-app.js.map +1 -0
  44. package/dist/cli/commands/preview.d.ts +8 -0
  45. package/dist/cli/commands/preview.js +55 -0
  46. package/dist/cli/commands/preview.js.map +1 -0
  47. package/dist/cli/commands/record.d.ts +10 -0
  48. package/dist/cli/commands/record.js +86 -0
  49. package/dist/cli/commands/record.js.map +1 -0
  50. package/dist/cli/commands/render.d.ts +1 -0
  51. package/dist/cli/commands/render.js +93 -0
  52. package/dist/cli/commands/render.js.map +1 -0
  53. package/dist/cli/commands/review.d.ts +6 -0
  54. package/dist/cli/commands/review.js +89 -0
  55. package/dist/cli/commands/review.js.map +1 -0
  56. package/dist/cli/commands/sheet.d.ts +1 -0
  57. package/dist/cli/commands/sheet.js +48 -0
  58. package/dist/cli/commands/sheet.js.map +1 -0
  59. package/dist/cli/commands/stability.d.ts +14 -0
  60. package/dist/cli/commands/stability.js +110 -0
  61. package/dist/cli/commands/stability.js.map +1 -0
  62. package/dist/cli/main.d.ts +2 -0
  63. package/dist/cli/main.js +69 -0
  64. package/dist/cli/main.js.map +1 -0
  65. package/dist/cli/usage.d.ts +10 -0
  66. package/dist/cli/usage.js +46 -0
  67. package/dist/cli/usage.js.map +1 -0
  68. package/dist/config.d.ts +250 -0
  69. package/dist/config.js +154 -0
  70. package/dist/config.js.map +1 -0
  71. package/dist/core/camera.d.ts +30 -0
  72. package/dist/core/camera.js +94 -0
  73. package/dist/core/camera.js.map +1 -0
  74. package/dist/core/compose.d.ts +38 -0
  75. package/dist/core/compose.js +81 -0
  76. package/dist/core/compose.js.map +1 -0
  77. package/dist/core/cursor.d.ts +38 -0
  78. package/dist/core/cursor.js +103 -0
  79. package/dist/core/cursor.js.map +1 -0
  80. package/dist/core/easing.d.ts +15 -0
  81. package/dist/core/easing.js +33 -0
  82. package/dist/core/easing.js.map +1 -0
  83. package/dist/core/loop.d.ts +30 -0
  84. package/dist/core/loop.js +96 -0
  85. package/dist/core/loop.js.map +1 -0
  86. package/dist/core/motion-defaults.d.ts +61 -0
  87. package/dist/core/motion-defaults.js +62 -0
  88. package/dist/core/motion-defaults.js.map +1 -0
  89. package/dist/core/rng.d.ts +13 -0
  90. package/dist/core/rng.js +27 -0
  91. package/dist/core/rng.js.map +1 -0
  92. package/dist/core/sse.d.ts +15 -0
  93. package/dist/core/sse.js +16 -0
  94. package/dist/core/sse.js.map +1 -0
  95. package/dist/core/timeline.d.ts +146 -0
  96. package/dist/core/timeline.js +81 -0
  97. package/dist/core/timeline.js.map +1 -0
  98. package/dist/core/timing.d.ts +31 -0
  99. package/dist/core/timing.js +29 -0
  100. package/dist/core/timing.js.map +1 -0
  101. package/dist/core/typing.d.ts +12 -0
  102. package/dist/core/typing.js +35 -0
  103. package/dist/core/typing.js.map +1 -0
  104. package/dist/driver/clip.d.ts +71 -0
  105. package/dist/driver/clip.js +120 -0
  106. package/dist/driver/clip.js.map +1 -0
  107. package/dist/driver/compare.d.ts +34 -0
  108. package/dist/driver/compare.js +40 -0
  109. package/dist/driver/compare.js.map +1 -0
  110. package/dist/driver/gate.d.ts +36 -0
  111. package/dist/driver/gate.js +27 -0
  112. package/dist/driver/gate.js.map +1 -0
  113. package/dist/driver/launch.d.ts +43 -0
  114. package/dist/driver/launch.js +47 -0
  115. package/dist/driver/launch.js.map +1 -0
  116. package/dist/driver/page-hooks.d.ts +72 -0
  117. package/dist/driver/page-hooks.js +129 -0
  118. package/dist/driver/page-hooks.js.map +1 -0
  119. package/dist/driver/reports.d.ts +34 -0
  120. package/dist/driver/reports.js +42 -0
  121. package/dist/driver/reports.js.map +1 -0
  122. package/dist/driver/session.d.ts +285 -0
  123. package/dist/driver/session.js +773 -0
  124. package/dist/driver/session.js.map +1 -0
  125. package/dist/driver/settle.d.ts +41 -0
  126. package/dist/driver/settle.js +82 -0
  127. package/dist/driver/settle.js.map +1 -0
  128. package/dist/env.d.ts +11 -0
  129. package/dist/env.js +41 -0
  130. package/dist/env.js.map +1 -0
  131. package/dist/fixtures.d.ts +13 -0
  132. package/dist/fixtures.js +13 -0
  133. package/dist/fixtures.js.map +1 -0
  134. package/dist/index.d.ts +30 -0
  135. package/dist/index.js +16 -0
  136. package/dist/index.js.map +1 -0
  137. package/dist/inspect/inspect.d.ts +70 -0
  138. package/dist/inspect/inspect.js +176 -0
  139. package/dist/inspect/inspect.js.map +1 -0
  140. package/dist/inspect/measure.d.ts +40 -0
  141. package/dist/inspect/measure.js +107 -0
  142. package/dist/inspect/measure.js.map +1 -0
  143. package/dist/loader.d.ts +28 -0
  144. package/dist/loader.js +143 -0
  145. package/dist/loader.js.map +1 -0
  146. package/dist/page/assets/index-DUom5amc.js +1 -0
  147. package/dist/page/index.html +18 -0
  148. package/dist/render/encode.d.ts +40 -0
  149. package/dist/render/encode.js +183 -0
  150. package/dist/render/encode.js.map +1 -0
  151. package/dist/render/ffmpeg.d.ts +13 -0
  152. package/dist/render/ffmpeg.js +72 -0
  153. package/dist/render/ffmpeg.js.map +1 -0
  154. package/dist/render/presentation.d.ts +19 -0
  155. package/dist/render/presentation.js +27 -0
  156. package/dist/render/presentation.js.map +1 -0
  157. package/dist/render/render.d.ts +59 -0
  158. package/dist/render/render.js +144 -0
  159. package/dist/render/render.js.map +1 -0
  160. package/dist/render/sampling.d.ts +47 -0
  161. package/dist/render/sampling.js +129 -0
  162. package/dist/render/sampling.js.map +1 -0
  163. package/dist/render/sequence.d.ts +24 -0
  164. package/dist/render/sequence.js +105 -0
  165. package/dist/render/sequence.js.map +1 -0
  166. package/dist/render/serve.d.ts +35 -0
  167. package/dist/render/serve.js +124 -0
  168. package/dist/render/serve.js.map +1 -0
  169. package/dist/review/review.d.ts +54 -0
  170. package/dist/review/review.js +229 -0
  171. package/dist/review/review.js.map +1 -0
  172. package/dist/scene.d.ts +23 -0
  173. package/dist/scene.js +2 -0
  174. package/dist/scene.js.map +1 -0
  175. package/dsl.md +119 -0
  176. package/package.json +76 -0
  177. package/plugin/.claude-plugin/plugin.json +9 -0
  178. package/plugin/README.md +105 -0
  179. package/plugin/agents/versioncam-reviewer.md +63 -0
  180. package/plugin/skills/versioncam/SKILL.md +235 -0
  181. package/plugin/skills/versioncam/authoring.md +226 -0
  182. package/plugin/skills/versioncam/onboarding.md +199 -0
@@ -0,0 +1,226 @@
1
+ # Writing and repairing a clip
2
+
3
+ You write one clip of a running web app and get it recording cleanly. The
4
+ clip is a TypeScript file in the app's repository that drives the shipped UI
5
+ through its own controls. Someone else decides whether it is a good demo; your
6
+ job is that it runs, it satisfies the intent, and a person watching it can see
7
+ each thing it claims to show.
8
+
9
+ You have: the intent, the clip id, the clips directory, and the output of
10
+ `npx versioncam inspect /` for the start page — whether you are the session
11
+ running the skill or a subagent it handed this brief to. Work from the
12
+ directory that holds `versioncam.config.ts`.
13
+
14
+ ## Look before you write
15
+
16
+ Three things, in this order, before the first line of the clip:
17
+
18
+ 1. **`npx versioncam dsl`.** The whole authoring surface, held to the code by
19
+ a test — the only list of methods worth trusting. Prose about this DSL has
20
+ been wrong before, and a clip written against a method that did not exist
21
+ quietly did something else for months. If a method is not in that output,
22
+ it does not exist, however plausible it sounds.
23
+ 2. **The existing clips in the clips directory.** Read them. They are the
24
+ house style, and they show which patterns survive contact with this app —
25
+ how long a spinner is held, when the camera moves, what a caption says.
26
+ 3. **`npx versioncam inspect <path>`** for every page the intent touches
27
+ beyond the start page. If the intent names a screen you have not
28
+ inspected, navigate to it and inspect it before scripting anything there.
29
+
30
+ Do not read the app's source to find a control. Clips written from source
31
+ reach for things that are conditional, renamed, or not rendered at all;
32
+ `inspect` reports what is actually on the page and what it is called. The
33
+ bracket after each name says where the name came from, which is which
34
+ locator finds it: `[aria-label]` and `[label]` are what `byLabel` finds,
35
+ `[placeholder]` is what `byPlaceholder` finds, `[text]` is a control's own
36
+ words — `byRole("button", { name })` — and `[name]` or `[none]` means use the
37
+ test id. Use `npx versioncam measure <path> <css>` when you need to know where
38
+ matches are.
39
+
40
+ `measure` prints what each match says, too. **Read a status line before
41
+ promising it in a beat**: `npx versioncam measure / '[data-testid="status"]'`
42
+ answers `text="12 shipments"` and where it sits — including when that is
43
+ below the fold, where no frame will show it.
44
+
45
+ ## Rules
46
+
47
+ - **Target by test id or role, never by coordinates.** `s.byTestId(…)`,
48
+ `s.byRole(…)`, `s.byLabel(…)`. A clip built from locators survives a layout
49
+ change; one built from pixels is broken by the next padding tweak and does
50
+ not say so. Geometry belongs only inside a stroke authored as fractions of a
51
+ target's own box.
52
+ - **Nine to eleven seconds**, unless `review` in the config says otherwise —
53
+ `versioncam review` enforces whatever it says. Shorter reads as a fragment,
54
+ longer loses the viewer. Adjust holds to hit it; never drop a beat to save
55
+ time.
56
+ - **A caption.** A clip with no caption fails review. One caption, unless the
57
+ beats genuinely change subject — and if you write a second, look at the
58
+ contact sheet, because two captions on screen at once is the failure that
59
+ hides best.
60
+ - **Every beat ends in `await s.settle({ label })`, then a `hold`.** The
61
+ settle advances the page's clock without capturing, so the app's own waiting
62
+ never reaches the video. The hold after it is what makes the result
63
+ *visible*: settle by itself moves straight on to the next action and the
64
+ thing you waited for flashes past in one frame.
65
+ - **A camera, highlight or caption write costs no time.** It writes to a track
66
+ at the current instant, and only the frames captured after it show the
67
+ result. So anything you write needs frames after it or it never renders. The
68
+ usual victim is a `s.camera.reset()` on the last line: the reset is in the
69
+ timeline, no frame follows it, and the video ends zoomed in. Give it a hold.
70
+ - **A canvas, a WebGL map or a video needs `forceCaptureEvery: 1`.** The
71
+ recorder skips the screenshot when nothing observable changed, and what it
72
+ watches is the DOM, the element under the cursor and the running animations
73
+ — none of which a surface that redraws itself ever touches. The symptom is
74
+ what you will see on the contact sheet: the map, the chart or the video is
75
+ *identical* in every tile while the cursor and the caption move normally,
76
+ and nothing in the `record` or `review` output mentions it, because as far
77
+ as either is concerned nothing changed. If the subject of the clip is such a
78
+ surface, say so in the clip's options and pay full price:
79
+ `clip("<id>", { title: "…", capture: { forceCaptureEvery: 1 } }, …)`.
80
+ - **End with nothing open.** No dialog, no menu, no tooltip, no focused
81
+ dropdown, and the cursor clear of whatever the last frame is about.
82
+ - **Start the cursor clear of the controls.** Where it rests in frame 0 is
83
+ `cursorStart` in the config, fixed before a clip's first line runs; the
84
+ default was tuned for one app and lands on a row or a field in others. Look
85
+ at frame 0 (`npx versioncam frame <id> 0`) before the sheet does; if the
86
+ cursor sits on a control, the fix is `cursorStart` — the one change to the
87
+ config allowed below.
88
+ - **If `open()` lands on a sign-in screen, stop.** Say so and go no further.
89
+ Signing the recorder in is the repository owner's job, through `auth` in
90
+ `versioncam.config.ts`, and `versioncam login` is interactive so you cannot
91
+ run it. Do not type credentials, do not go looking for any in the repository
92
+ or the environment, and do not script a way around the screen. A clip that
93
+ signs itself in is a clip nobody can run.
94
+ - **Do not run `git`.** You do not need it. Someone else reads your work and
95
+ commits it.
96
+ - **Do not edit the app, and leave `versioncam.config.ts` alone — with one
97
+ exception.** The app is not yours to change, and the config is every
98
+ clip's: a setting changed to make this clip pass changes all the others. The
99
+ exception is `cursorStart`, when a reviewer fails the cursor in the first
100
+ frame — frame 0 is taken before a clip's first line runs, so nothing in the
101
+ clip can move it, and the config is the only fix. Change it between
102
+ recordings, never during one (a save the dev server sees reloads the page
103
+ being recorded), and say so in what you write down.
104
+
105
+ ## The shape of a clip
106
+
107
+ Whenever the clips directory has clips in it, they are better models than this
108
+ — they are the ones that already work against this app. This is the minimum,
109
+ for a repository whose first clip you are writing:
110
+
111
+ ```ts
112
+ import { clip } from "versioncam";
113
+
114
+ export default clip(
115
+ "<id>",
116
+ { title: "<a short title>", seed: 11 },
117
+ async (s) => {
118
+ await s.open("/");
119
+ s.caption("<what this clip shows>");
120
+ await s.hold(400);
121
+
122
+ await s.click(s.byTestId("<control>"));
123
+ await s.settle({ label: "<beat label>" });
124
+ await s.hold(700);
125
+ },
126
+ );
127
+ ```
128
+
129
+ `seed` fixes the motion, so two runs of the clip produce the same frames. Pick
130
+ one and leave it alone.
131
+
132
+ ## Write the beat sheet before you record
133
+
134
+ `.versioncam/author/<id>/beats.json`, an array of `{ label, expect }`:
135
+
136
+ ```json
137
+ [
138
+ { "label": "shipment created", "expect": "a new row for Mira Halloran at the top of the shipment list" },
139
+ { "label": "cancelled", "expect": "the list status line reads one fewer shipment" }
140
+ ]
141
+ ```
142
+
143
+ `label` is exactly the string you pass to `settle({ label })` — `versioncam
144
+ review` matches them and fails on any beat with no mark. `expect` is one
145
+ sentence about what a person should *see* once the beat has landed, not what
146
+ the code did — and if it quotes the screen, it quotes what `measure` read.
147
+
148
+ Write it before the first recording. Written afterwards it describes what you
149
+ got instead of what you meant, which is precisely the check it exists to be.
150
+
151
+ ## The round
152
+
153
+ ```
154
+ inspect → write the clip → npx versioncam record --draft <id>
155
+ → npx versioncam review <id> --beats .versioncam/author/<id>/beats.json
156
+ → fix → record again
157
+ ```
158
+
159
+ The clip goes in the clips directory as `<id>.clip.ts`, and the id inside
160
+ `clip("<id>", …)` must match the id you were given — that is what `record`
161
+ looks up.
162
+
163
+ Record in `--draft` while you are working. It is the same page at the same
164
+ viewport, captured cheaply, and it is what the loop is for; the full recording
165
+ is taken once the reviewer passes the clip.
166
+
167
+ When `record --draft` fails it names the step, the locator and the frame, and
168
+ writes `failure.png` and `failure.json` into the recording directory. **Read
169
+ `failure.png`.** One look at the moment it gave up is almost always faster
170
+ than reasoning about the DOM from the message. To look at a beat that did
171
+ record, `npx versioncam frame <id> --at "<label>"` renders the frame the
172
+ contact sheet will show for it.
173
+
174
+ **At most three `record` calls, then the sheet, whether or not review has
175
+ passed.** If the third has not passed review, stop there and write down which
176
+ check still fails, what you tried, and what you think is in the way. A fourth
177
+ attempt on a stuck clip has not helped in this loop. The round after this one
178
+ arrives with a picture of the clip and a reviewer's verdict, which is
179
+ information you do not have.
180
+
181
+ In a later round you have that verdict. Fix what it names — the items that
182
+ failed, read as written — and nothing else; then record again, in draft, and
183
+ run `review`.
184
+
185
+ ## Failure → repair
186
+
187
+ Every row of this happened while the recorder was being built, and two of them
188
+ happened again the first time an agent used it.
189
+
190
+ | Failure | Repair |
191
+ |---|---|
192
+ | locator not found at step N | `inspect` at that moment; pick a control that exists; if none does, the beat is wrong |
193
+ | beat produced no new state | missing `settle`, wrong target, or effect off-screen; check cuts and states |
194
+ | settle timed out on `X` | add a `ready` hook, raise the timeout, or the source is live — record a HAR |
195
+ | document reloaded mid-clip | something navigated; find the trigger and avoid it |
196
+ | drag or lasso selected nothing | `measure`, re-author geometry as fractions of the real box |
197
+ | duration drift | adjust holds, never the beats |
198
+ | tooltip / menu open in a frame it should not be | move the cursor start; add a hold before the next action |
199
+
200
+ Two more, from the tools rather than the app:
201
+
202
+ | Failure | Repair |
203
+ |---|---|
204
+ | a locator "matched N elements" | the text appears in more than one place — use `byRole` with a name, or a test id |
205
+ | review warns that a track never rendered | you wrote a camera, highlight or caption with no frames after it; add a hold |
206
+
207
+ ## What you write down
208
+
209
+ Four things, at the end of every round — in `round-N/author.md`, or as your
210
+ reply if you are a subagent — and the fourth matters as much as the rest:
211
+
212
+ 1. The path of the clip file you wrote.
213
+ 2. The path of `beats.json`.
214
+ 3. The last `versioncam record` summary line, verbatim, and the
215
+ `versioncam review` verdict.
216
+ 4. **One paragraph on what was awkward about the tools.** Where a message
217
+ misled you, where you had to guess, what you looked for and could not find,
218
+ what took three attempts that should have taken one. Be blunt and specific;
219
+ naming the command and what you expected is worth more than a judgement
220
+ about it. This paragraph is kept and read — it is how the next fix to the
221
+ recorder gets found, and the last agent to write one produced the list that
222
+ became a work package.
223
+
224
+ If you stopped without passing review, say so plainly in the first line. A
225
+ failure that names the obstacle is useful. A claim that it passed when it did
226
+ not wastes a whole round.
@@ -0,0 +1,199 @@
1
+ # The first run: setting a repository up
2
+
3
+ There is no `versioncam.config.ts` here, so before any clip can be written the
4
+ repository has to be taught three things: how its app starts, whether it signs
5
+ in, and what "the page is done" means. Then one question has to be answered
6
+ before anything is authored: **does this app record the same way twice?** A
7
+ clip is only as deterministic as the app it films, and one written against an
8
+ app that is not fails CI for reasons nobody wrote.
9
+
10
+ You do not run the app yourself — not with `npm run dev`, not in the
11
+ background, not ever in this phase. You write what you learn into the config,
12
+ and `versioncam` starts the app from it, every time, and stops it again. If
13
+ you find yourself wanting to start a server by hand, the config is wrong;
14
+ fix the config.
15
+
16
+ Work in the directory the skill was run from; the config goes here. If there
17
+ is no config here but `Glob` finds a `versioncam.config.*` further down, stop
18
+ and say to run the skill from that directory instead: every `versioncam`
19
+ command reads the config from the directory it runs in, and a second config
20
+ would split the repository in two.
21
+
22
+ ## 1. How the app runs
23
+
24
+ Look, in this order, and stop when the answer is clear:
25
+
26
+ - `package.json` `scripts` — `dev`, then `start`, then `serve`. A script's own
27
+ `--port` flag is the port. In a monorepo, the package whose scripts start
28
+ the web app, which may be a directory below this one.
29
+ - The framework's config for the port — `vite.config.*` (`server.port`),
30
+ `next.config.*`, `angular.json`, `astro.config.*` — and, if it says nothing,
31
+ the framework's default: Vite and SvelteKit 5173, Next and Remix 3000,
32
+ Astro 4321, Angular 4200.
33
+ - `Makefile`, `Procfile`, `docker-compose.yml`, and the README's
34
+ getting-started section, for anything that needs more than one process.
35
+
36
+ From that, the config's `baseUrl` and `webServer`:
37
+
38
+ ```ts
39
+ baseUrl: "http://localhost:<port>",
40
+ webServer: { command: "<the command a person would type>", url: "http://localhost:<port>" },
41
+ ```
42
+
43
+ `command` runs through a shell from the config's directory; add `cwd` when it
44
+ has to run from somewhere else (`cwd: "../.."` for a repository root two levels
45
+ up). A stack of several services started by one command — a `make start`, a
46
+ `docker compose up` — needs `url` pointed at the service that is ready *last*,
47
+ usually a backend's health endpoint, not at the page: a dev server answers
48
+ seconds before the API behind it, and a clip that opens then meets a sign-in
49
+ form with nothing to sign in to. Give such a stack `timeoutMs: 180_000`.
50
+
51
+ **If the port comes from an environment variable, or there are several
52
+ plausible commands, ask** — with `AskUserQuestion`, listing the candidates
53
+ you found and what each would start. If nothing plausible turned up, ask what
54
+ starts the app. If you cannot ask (no one is there: the question tool is not
55
+ available or errors), take the most specific candidate, and say in your final
56
+ report which one you took and why.
57
+
58
+ **Never read a value out of any `.env` file.** A port, a URL, a key: if it is
59
+ only in a `.env`, it is a question for the person, not a file for you to open.
60
+
61
+ ## 2. Whether it signs in
62
+
63
+ Look for sign-in routes, a login component, a redirect on `/` in the router.
64
+ Step 5 below will show it for certain — `inspect /` of an app behind a
65
+ sign-in lists a login form — so this is a first look, not the verdict.
66
+
67
+ If it does sign in, the choice is the person's, asked once:
68
+
69
+ - **`versioncam login`** — a browser opens, they sign in the way they always
70
+ do, and the session is saved to a file that stays out of git. The right
71
+ default when a person is there. Config: `auth: { storageState:
72
+ ".versioncam/auth.json" }`, and they run `npx versioncam login` themselves;
73
+ you cannot, because it waits for them.
74
+ - **A `login` hook** reading named variables from `.env.recording` — the right
75
+ one for CI. You write the hook with the variable *names*
76
+ (`process.env.RECORDING_USER`, say); they put the values in
77
+ `.env.recording`, which is gitignored.
78
+
79
+ Never write a secret anywhere — not into the config, not into a clip, not into
80
+ your report — and never go looking for one. With no one to ask, stop here and
81
+ say what the person needs to do: nothing below works on a sign-in screen.
82
+
83
+ ## 3. Write the config
84
+
85
+ `versioncam.config.ts` in this directory, with only what the app needs:
86
+
87
+ ```ts
88
+ import { defineRecorder } from "versioncam";
89
+
90
+ export default defineRecorder({
91
+ baseUrl: "http://localhost:5173",
92
+ webServer: { command: "npm run dev", url: "http://localhost:5173" },
93
+ viewport: { width: 1440, height: 900 },
94
+ clips: "demos/clips/**/*.clip.ts",
95
+ });
96
+ ```
97
+
98
+ Plus `auth` if step 2 said so, and `cursorStart` if step 7 says so. Not
99
+ `settle`, not `review`, not `theme`: every default is there because it is right
100
+ for most apps, and a field nobody needed is a question the next person has to
101
+ answer. Say in a comment above each field you did write why it has the value
102
+ it has — the port came from the dev script, the command from the README.
103
+
104
+ Then: create `demos/clips/`, and make sure `.versioncam/` is ignored —
105
+ recordings, renders and a saved sign-in go there. If a `.gitignore` here, or
106
+ in a directory above this one up to the repository root, already says so,
107
+ leave it; otherwise add the line to the nearest one, creating one here if
108
+ there is none, and leave the rest of the file alone.
109
+
110
+ ## 4. `npx versioncam doctor`
111
+
112
+ It checks the machine, loads the config, and starts the app through
113
+ `webServer` exactly as every later command will, then stops it. **It must
114
+ pass.** If the app check fails, the reason is in its last lines and in
115
+ `.versioncam/webserver.log`: the wrong command, the wrong port, a port already
116
+ taken. Fix the config and run it again. Do not go on until it passes.
117
+
118
+ ## 5. `npx versioncam inspect /`
119
+
120
+ What is on the start page, as a clip will see it. Three things to look for:
121
+
122
+ - **A sign-in form.** Step 2 was wrong; go back to it.
123
+ - **A loading screen, or a warning that a settle timed out.** The app needs
124
+ `settle.ready`: a function that says when the page is really done. Find what
125
+ "loaded" means here — a map's `loaded()`, a canvas that has drawn, a
126
+ spinner's test id gone — add it to the config, and inspect again.
127
+ - **An empty page.** The app may need a path other than `/`, or data it does
128
+ not have; say so.
129
+
130
+ ## 6. Stability
131
+
132
+ Write `demos/clips/00-open.clip.ts` — open the start page, one caption, hold
133
+ three seconds:
134
+
135
+ ```ts
136
+ import { clip } from "versioncam";
137
+
138
+ export default clip("00-open", { title: "The app opens", seed: 1 }, async (s) => {
139
+ await s.open("/");
140
+ s.caption("<the app's name>");
141
+ await s.hold(3000);
142
+ });
143
+ ```
144
+
145
+ Then:
146
+
147
+ ```bash
148
+ npx versioncam stability 00-open
149
+ ```
150
+
151
+ It records the clip twice and compares every frame. `stable` is the answer
152
+ that lets you go on. `unstable` prints the first frame that differs and saves
153
+ the two pictures behind it; **read both** and say what differs.
154
+
155
+ What you may fix, in the config:
156
+
157
+ - Something still animating in when the clip starts — a fade, a skeleton
158
+ screen: a `settle.ready` that waits for it to finish.
159
+ - Time rendered from the server rather than the page: the page's own clock is
160
+ already fixed (`clock.time`), so a timestamp that still moves came from an
161
+ API, and that is the next item.
162
+
163
+ What you may not: **live data** — a feed, a count that changes, a model's
164
+ answer. That needs recorded network replay, which versioncam does not have
165
+ yet. Say so, and ask whether to go on regardless.
166
+
167
+ **Never author on an unstable app without saying so** — in the report now,
168
+ and in `summary.json` at the end, as `"stable": false`. A clip written against
169
+ an app that does not reproduce will fail CI, and whoever reads the failure
170
+ deserves to know it was known.
171
+
172
+ ## 7. The first frame
173
+
174
+ ```bash
175
+ npx versioncam record --draft 00-open
176
+ npx versioncam frame 00-open 0
177
+ ```
178
+
179
+ Look at where the cursor is. Every clip starts it there, and it is taken
180
+ before a clip's first line runs, so no clip can move it. The default,
181
+ `(0.72, 0.28)` of the viewport, was picked for one app's open map and lands on
182
+ a list row or a field in others — and a reviewer fails a clip whose first
183
+ frame has the cursor on a control, a row or a field. If it is on one, set
184
+ `cursorStart` in the config to open space — a margin, the gap below a panel —
185
+ as fractions of the viewport, with a comment saying what is there, and look
186
+ at frame 0 again.
187
+
188
+ ## 8. Report
189
+
190
+ Before going on to the clip, say — in a few lines:
191
+
192
+ - what you found (the command, the port, and where each came from);
193
+ - what you asked, and what the answer was — or what you assumed and why;
194
+ - that `stability` passed on `00-open`, or what differed;
195
+ - the files you created or changed — the config, `demos/clips/00-open.clip.ts`,
196
+ `.gitignore` — and that **nothing was committed**: reviewing and committing
197
+ them is the person's job.
198
+
199
+ Then go back to the skill and continue with the clip.