staysfixed 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +534 -402
  2. package/package.json +8 -3
  3. package/src/cli/index.js +14 -0
  4. package/src/v2/adapters/android-driver.js +1705 -0
  5. package/src/v2/adapters/android.js +1117 -0
  6. package/src/v2/adapters/contract.js +565 -0
  7. package/src/v2/adapters/electron.js +1594 -0
  8. package/src/v2/adapters/http.js +733 -0
  9. package/src/v2/adapters/ios-driver.js +1551 -0
  10. package/src/v2/adapters/ios.js +989 -0
  11. package/src/v2/adapters/isolate.js +739 -0
  12. package/src/v2/adapters/process.js +920 -0
  13. package/src/v2/adapters/source.js +1241 -0
  14. package/src/v2/adapters/web-driver.js +1532 -0
  15. package/src/v2/adapters/web.js +1009 -0
  16. package/src/v2/adapters/windows.js +1329 -0
  17. package/src/v2/browsers.js +1203 -0
  18. package/src/v2/cause.js +364 -0
  19. package/src/v2/check.js +1331 -0
  20. package/src/v2/ci.js +1209 -0
  21. package/src/v2/cli.js +657 -0
  22. package/src/v2/cluster.js +372 -0
  23. package/src/v2/coverage.js +1116 -0
  24. package/src/v2/detect.js +1199 -0
  25. package/src/v2/doctor.js +1690 -0
  26. package/src/v2/escalate.js +679 -0
  27. package/src/v2/init.js +1394 -0
  28. package/src/v2/intent.js +659 -0
  29. package/src/v2/journeys/from-routes.js +498 -0
  30. package/src/v2/journeys/from-suite.js +988 -0
  31. package/src/v2/journeys/index.js +651 -0
  32. package/src/v2/journeys/record.js +516 -0
  33. package/src/v2/mcp/server.js +374 -0
  34. package/src/v2/mcp/tools.js +1571 -0
  35. package/src/v2/normalise.js +783 -0
  36. package/src/v2/observation.js +877 -0
  37. package/src/v2/rank.js +672 -0
  38. package/src/v2/reference.js +1051 -0
  39. package/src/v2/remote.js +911 -0
  40. package/src/v2/run.js +964 -0
  41. package/src/v2/sealed.js +564 -0
  42. package/src/v2/selfcheck.js +564 -0
  43. package/src/v2/ship.js +684 -0
  44. package/src/v2/store.js +703 -0
  45. package/src/v2/types.js +503 -0
  46. package/src/v2/waiver.js +511 -0
  47. package/src/watch/panel.js +73 -44
@@ -0,0 +1,503 @@
1
+ /**
2
+ * Stays Fixed v2 — the whole contract, in one file.
3
+ *
4
+ * v1 photographed screens and compared pixels. v2 is a difference machine: it walks the
5
+ * product, writes down what it observed as `path -> value` facts, and reports only the
6
+ * facts that changed. Pictures become the seventh channel and the last one.
7
+ *
8
+ * Everything here is a JSDoc typedef. Nothing runs. It exists so the pieces built in
9
+ * parallel fit together, and so `npm run typecheck` proves it before anything is wired up.
10
+ *
11
+ * THE ONE IDEA WORTH READING TWICE: every platform flattens to the same shape. A button on
12
+ * an iPhone screen, an HTTP status, an exit code and an IPC channel are all one addressable
13
+ * fact. That is why one comparison engine can serve seven platforms, and why the path
14
+ * grammar below is the most load-bearing thing in the repository.
15
+ */
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Channels — the seven ways we can observe a product
19
+ // ---------------------------------------------------------------------------
20
+
21
+ /**
22
+ * The seven observation channels, in the order we trust them.
23
+ *
24
+ * - `meaning` What the interface says a control is and does: role, name, state.
25
+ * Never the raw DOM — the DOM changes when nothing did.
26
+ * - `effects` What went out: network calls, files written, processes spawned,
27
+ * storage writes. Recorded at the CALL boundary, so an irreversible
28
+ * effect is observed without being performed.
29
+ * - `complaints` Console messages, stderr, crashes, exit codes.
30
+ * - `results` What came back: stdout, HTTP bodies, the exported API surface.
31
+ * - `contract` Read statically out of the source: routes, exports, IPC channels.
32
+ * Free, exact, and it sees doors no walkthrough ever opens.
33
+ * - `counters` Coarse counts and timing. Deliberately coarse: fine timing is wobble.
34
+ * - `pixels` What it looked like. Evidence for a finding another channel already
35
+ * made — never the accusation itself.
36
+ *
37
+ * @typedef {'meaning'|'effects'|'complaints'|'results'|'contract'|'counters'|'pixels'} Channel
38
+ */
39
+
40
+ /**
41
+ * Where an observation came from, when it matters which platform produced it.
42
+ * @typedef {'cli'|'library'|'server'|'web'|'electron'|'android'|'ios'|'windows'} Surface
43
+ */
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Observations — one fact about the product at one moment
47
+ // ---------------------------------------------------------------------------
48
+
49
+ /**
50
+ * A value we can write down, compare and store.
51
+ *
52
+ * `undefined` is deliberately not in this list. A fact we do not have is an ABSENT PATH,
53
+ * not a path holding nothing — and telling those two apart is how appeared/vanished works.
54
+ *
55
+ * @typedef {string|number|boolean|null|any[]|Record<string, any>} ObservedValue
56
+ */
57
+
58
+ /**
59
+ * Anything extra that helps a human understand a difference, and nothing that is compared.
60
+ *
61
+ * Values in here are NEVER part of the comparison — they are context. Put the fact in
62
+ * `value`; put the story in `meta`.
63
+ *
64
+ * @typedef {object} ObservationMeta
65
+ * @property {string} [describe] One plain sentence a person can read.
66
+ * @property {string} [source] Concretely where it came from: a file, a URL, a channel name.
67
+ * @property {number} [line] Line in `source`, for the contract channel.
68
+ * @property {Surface} [surface]
69
+ * @property {string} [journey] Journey that produced it, when observations get mixed.
70
+ * @property {string} [step] Step within the journey.
71
+ * @property {string} [evidence] Path to a picture, a log, a HAR — proof, not comparison.
72
+ * @property {boolean} [refused] True when we stopped at the call boundary on purpose
73
+ * (money, a message, data loss). A refusal is missing
74
+ * coverage, never a pass.
75
+ * @property {string} [refusedWhy] Plain English: what we would have had to do.
76
+ */
77
+
78
+ /**
79
+ * One fact about the product at one moment.
80
+ *
81
+ * @typedef {object} Observation
82
+ * @property {string} path Stable dotted address. See PATH_RULES in observation.js.
83
+ * @property {Channel} channel
84
+ * @property {ObservedValue} value
85
+ * @property {number} [at] Milliseconds since the capture started. Never compared —
86
+ * it is a clock, and clocks are pure wobble.
87
+ * @property {ObservationMeta} [meta]
88
+ */
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Journeys — the named sequences that produce observations
92
+ // ---------------------------------------------------------------------------
93
+
94
+ /**
95
+ * Where a journey came from, and it matters.
96
+ *
97
+ * - `code` Read out of the source: routes, exports, IPC channels. Free and exact.
98
+ * - `suite` The project's own existing tests, run under instrumentation.
99
+ * - `recorded` A real session someone actually performed, frozen into a file.
100
+ * - `explored` The agent opened one named gap and froze what it found.
101
+ *
102
+ * Ranked by how much we trust them, best first. A finding from a `code` journey is a fact
103
+ * about the product; a finding from an `explored` journey is a fact about one path an agent
104
+ * happened to take, and it is reported that way.
105
+ *
106
+ * @typedef {'code'|'suite'|'recorded'|'explored'} JourneySource
107
+ */
108
+
109
+ /**
110
+ * One step of a journey. The driving lanes each extend this with their own keys —
111
+ * a CLI step carries argv, a web step carries a selector — so the shape stays open.
112
+ * @typedef {{act: string, note?: string} & Record<string, unknown>} JourneyStep
113
+ */
114
+
115
+ /**
116
+ * A named sequence of steps that produces observations.
117
+ *
118
+ * @typedef {object} Journey
119
+ * @property {string} name File-safe id. It becomes a folder name and the head of
120
+ * every path this journey produces.
121
+ * @property {string} describe One plain sentence: what this journey does.
122
+ * @property {JourneySource} source
123
+ * @property {Surface} surface
124
+ * @property {string} [from] Concretely where it came from — a test file, a recording,
125
+ * the source file a route was read out of.
126
+ * @property {JourneyStep[]} [steps]
127
+ * @property {Channel[]} [channels] Channels this journey actually collects. Anything not
128
+ * listed is not claimed, and shows up in Coverage as a gap.
129
+ * @property {boolean} [irreversible] A step here would spend money, send a message or destroy
130
+ * data. Observed at the call boundary, refused at the effect.
131
+ * @property {string} [skip] Why it is switched off. Skipped is missing coverage.
132
+ * @property {number} [timeoutMs]
133
+ */
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // Captures — one run of one journey against one build
137
+ // ---------------------------------------------------------------------------
138
+
139
+ /**
140
+ * Which build a capture ran against.
141
+ *
142
+ * `id` is content-addressed against the build artifact wherever that is possible — the
143
+ * point being that two captures with the same `id` really did run the same bytes. Computing
144
+ * it belongs to the build lane; this is only the shape it hands over.
145
+ *
146
+ * @typedef {object} BuildFingerprint
147
+ * @property {string} id File-safe. Content hash where we have one, else a run id.
148
+ * @property {string} product Which product this build is of. One repo can make five.
149
+ * @property {Surface} [surface]
150
+ * @property {string} [version]
151
+ * @property {string|null} [gitSha]
152
+ * @property {string|null} [branch]
153
+ * @property {boolean} [dirty] Working tree had uncommitted changes.
154
+ * @property {string} [artifact] Path to the built thing. NOT stored here — see store.js.
155
+ * @property {string} [artifactSha256]
156
+ * @property {string} [builtAt] ISO timestamp.
157
+ * @property {string} [platform] e.g. 'darwin-arm64'. Comparing across platforms warns.
158
+ * @property {string} [tool] Stays Fixed version that captured it.
159
+ */
160
+
161
+ /**
162
+ * Which of the two runs of the same build this is.
163
+ *
164
+ * We run the new build TWICE, so `a` and `b` are the same bytes minutes apart, and anything
165
+ * that disagrees between them is the product arguing with itself. `single` is one run with
166
+ * no wobble measurement — honest, but weaker, and it has to say so.
167
+ *
168
+ * @typedef {'a'|'b'|'single'} CaptureRun
169
+ */
170
+
171
+ /**
172
+ * One run of one journey against one build.
173
+ *
174
+ * @typedef {object} Capture
175
+ * @property {string} id Sortable: '20260829-013245-a'.
176
+ * @property {string} journey Journey name.
177
+ * @property {JourneySource} [source] Copied off the journey so a stored capture explains itself.
178
+ * @property {BuildFingerprint} build
179
+ * @property {CaptureRun} run
180
+ * @property {string} startedAt ISO timestamp.
181
+ * @property {number} durationMs
182
+ * @property {Observation[]} observations
183
+ * @property {Coverage} [coverage] What this capture did NOT manage to look at.
184
+ * @property {boolean} [complete] False when the file was read back torn — see store.js.
185
+ * @property {string} [note]
186
+ * @property {string} [rules] Id of the normalisation rule set applied, if any.
187
+ */
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // Wobble — what a build disagrees with itself about
191
+ // ---------------------------------------------------------------------------
192
+
193
+ /**
194
+ * One path that would not sit still between two runs of the SAME build.
195
+ *
196
+ * @typedef {object} WobbleEntry
197
+ * @property {string} path
198
+ * @property {Channel} channel
199
+ * @property {DifferenceKind} kind `appeared` / `vanished` is the worst kind: the product
200
+ * does not agree with itself about what exists.
201
+ * @property {ObservedValue} [a] Value in the first run, when it had one.
202
+ * @property {ObservedValue} [b] Value in the second run, when it had one.
203
+ * @property {number} distance Rough size of the disagreement, 0..1. For ranking and for
204
+ * reading. NEVER a threshold — v2 has no tolerances.
205
+ */
206
+
207
+ /**
208
+ * What differed between two runs of the same build: the product's own noise, measured
209
+ * rather than guessed.
210
+ *
211
+ * @typedef {object} Wobble
212
+ * @property {string} buildId
213
+ * @property {string} journey
214
+ * @property {[string, string]} runs The two capture ids that were compared.
215
+ * @property {WobbleEntry[]} entries Sorted by path.
216
+ * @property {string[]} unstable Just the paths, for fast set arithmetic.
217
+ * @property {number} steady How many paths agreed. The denominator that makes the
218
+ * unstable count mean something.
219
+ * @property {boolean} measured False when only one run exists, so nothing was measured
220
+ * and the run must say so out loud.
221
+ */
222
+
223
+ /**
224
+ * What came out of subtracting the wobble from the differences.
225
+ *
226
+ * @typedef {object} WobbleSubtraction
227
+ * @property {Difference[]} real Differences at paths the build holds steady. These are
228
+ * the only ones worth an agent's tokens.
229
+ * @property {Difference[]} noise Differences at paths that wobble anyway.
230
+ * @property {WobbleEntry[]} newlyUnstable
231
+ * Steady in the reference, wobbling now. Nobody else's tool
232
+ * catches this: the change made something unpredictable,
233
+ * which is a bug even though no value is "wrong".
234
+ * @property {boolean} couldTellNewlyUnstable
235
+ * False when we had no stability record for the reference,
236
+ * so `newlyUnstable` is empty for lack of evidence rather
237
+ * than because nothing became unstable.
238
+ * @property {string} note One plain sentence stating exactly that.
239
+ */
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // Differences — reference against candidate
243
+ // ---------------------------------------------------------------------------
244
+
245
+ /**
246
+ * `appeared` and `vanished` are the important ones. A path that stopped existing is a door
247
+ * that closed, and no pixel comparison has ever noticed one.
248
+ * @typedef {'changed'|'appeared'|'vanished'} DifferenceKind
249
+ */
250
+
251
+ /**
252
+ * One path that differs between the reference and the candidate.
253
+ *
254
+ * @typedef {object} Difference
255
+ * @property {string} path
256
+ * @property {Channel} channel
257
+ * @property {DifferenceKind} kind
258
+ * @property {ObservedValue} [reference] Absent when the path appeared.
259
+ * @property {ObservedValue} [candidate] Absent when the path vanished.
260
+ * @property {number} distance Rough size, 0..1. Ranking only.
261
+ * @property {string} [journey]
262
+ * @property {boolean} [real] Survived the wobble floor. Set by subtractWobble.
263
+ * @property {boolean} [wobbling] This path does not sit still in this build anyway.
264
+ * @property {boolean} [proven] Re-checked against the old build booted live, and it
265
+ * survived. Cheap suspicion, expensive proof.
266
+ * @property {string} [describe] One plain sentence, carried from the observation.
267
+ * @property {string} [evidence] A picture or log that shows it, for a human.
268
+ */
269
+
270
+ // ---------------------------------------------------------------------------
271
+ // Findings — differences clustered into something worth acting on
272
+ // ---------------------------------------------------------------------------
273
+
274
+ /**
275
+ * The classes an agent may never wave through on its own. Anything in one of these goes to a
276
+ * person, whatever the agent believes it meant to change.
277
+ * @typedef {'money'|'sign-in'|'data-loss'|'crash'|'guard'|'ordinary'} FindingClass
278
+ */
279
+
280
+ /**
281
+ * A cluster of differences with one likely cause, written for someone to act on.
282
+ *
283
+ * The whole point of clustering: one missing stylesheet is one finding, not four hundred
284
+ * differences. An agent should be able to read the title and know what to go and look at.
285
+ *
286
+ * @typedef {object} Finding
287
+ * @property {string} id Stable across runs while the cause persists, so the same
288
+ * finding is not reported as new every time.
289
+ * @property {string} title Plain English, no jargon, no test ids: "Saving a session
290
+ * no longer writes the file."
291
+ * @property {string} why The likely cause, said plainly, and hedged when it is a guess.
292
+ * @property {FindingClass} class
293
+ * @property {Difference[]} differences
294
+ * @property {number} rank Higher is more urgent. Distance from the changed code is
295
+ * the biggest term: a break far from the edit is the very
296
+ * definition of a side effect.
297
+ * @property {string} [signature] What the cluster was grouped on.
298
+ * @property {string[]} [nearFiles] Source files the cluster points at, nearest first.
299
+ * @property {boolean} [sealed] In an unwaivable class. Goes to a person, full stop.
300
+ * @property {string} [evidence] A picture, a log, a diff — for the human, at the end.
301
+ * @property {number} [count] How many differences this one finding stands for. Five hundred
302
+ * differences are not five hundred findings, and the count is
303
+ * what stops a cluster hiding its own size.
304
+ * @property {string} [summary] One line, for a list. `title` is the headline; this is the
305
+ * sentence under it.
306
+ * @property {string[]} [paths] The addresses involved, for an agent that wants to look.
307
+ * @property {Difference} [sample] One representative difference, so a reader sees the shape
308
+ * without being handed all of them.
309
+ * @property {number} [distance] How far from the code that changed. Bigger is more suspicious:
310
+ * a break far from the edit is the definition of a side effect.
311
+ */
312
+
313
+ // ---------------------------------------------------------------------------
314
+ // Coverage — and mostly, what was NOT checked
315
+ // ---------------------------------------------------------------------------
316
+
317
+ /**
318
+ * One thing we did not look at, and what it would take to look at it.
319
+ *
320
+ * This shape carries the self-description requirement: an agent installing the tool reads
321
+ * `unlockedBy` and knows exactly what to install, start or supply. Nobody should have to read
322
+ * documentation to wire this up.
323
+ *
324
+ * @typedef {object} CoverageGap
325
+ * @property {string} what What is not covered, in plain English.
326
+ * @property {string} why Why not: missing runtime, refused effect, no snapshot,
327
+ * old build will not compile.
328
+ * @property {string} [unlockedBy] The concrete thing that would fix it: "install a Java
329
+ * runtime", "add an SSH host", "supply a database snapshot".
330
+ * @property {Channel} [channel]
331
+ * @property {Surface} [surface]
332
+ * @property {number} [doors] How many addressable things this gap hides, when countable.
333
+ */
334
+
335
+ /**
336
+ * What was checked and, much more importantly, what was not.
337
+ *
338
+ * A tool that reports "nothing changed" is indistinguishable from a broken tool. Coverage is
339
+ * how the difference is made visible instead of pretended away.
340
+ *
341
+ * @typedef {object} Coverage
342
+ * @property {number} paths Addresses observed.
343
+ * @property {number} journeys Journeys walked.
344
+ * @property {Partial<Record<Channel, number>>} byChannel Paths observed per channel.
345
+ * @property {number} [doorsKnown] Doors the contract channel found in the source.
346
+ * @property {number} [doorsWalked] How many of those any journey actually opened.
347
+ * @property {CoverageGap[]} gaps Everything we could not see. Never empty on a real run.
348
+ */
349
+
350
+ // ---------------------------------------------------------------------------
351
+ // Verdict — what a whole run concluded
352
+ // ---------------------------------------------------------------------------
353
+
354
+ /**
355
+ * How the reference was obtained, because it changes how much the answer is worth.
356
+ *
357
+ * - `paired` The old build was booted live on this machine, in this minute.
358
+ * - `stored-record` Compared against observations stored the last time the old build ran.
359
+ * Genuinely weaker: it lets back in every difference that comes from the
360
+ * day being different. It must announce itself in those words, every run.
361
+ *
362
+ * @typedef {'paired'|'stored-record'} ReferenceMode
363
+ */
364
+
365
+ /**
366
+ * What a whole run concluded.
367
+ *
368
+ * @typedef {object} Verdict
369
+ * @property {string} runId
370
+ * @property {string} product
371
+ * @property {boolean} ok Nothing unintended survived the wobble floor.
372
+ * @property {ReferenceMode} mode
373
+ * @property {string} [modeWarning] Present whenever `mode` is 'stored-record'.
374
+ * @property {BuildFingerprint} reference
375
+ * @property {BuildFingerprint} candidate
376
+ * @property {Finding[]} findings Ranked, worst first. The only thing an agent should read.
377
+ * @property {number} differencesReal
378
+ * @property {number} differencesNoise
379
+ * @property {WobbleEntry[]} newlyUnstable
380
+ * @property {Coverage} coverage
381
+ * @property {string} summary One paragraph of plain English. What changed, what did not,
382
+ * what was not looked at.
383
+ * @property {number} durationMs
384
+ * @property {string} startedAt
385
+ * @property {string} [tool]
386
+ */
387
+
388
+ export {};
389
+
390
+ // ---------------------------------------------------------------------------
391
+ // Normalisation — the rules that decide what is a difference and what is churn
392
+ // ---------------------------------------------------------------------------
393
+
394
+ /**
395
+ * What a rule does to a value.
396
+ *
397
+ * - `replace` Rewrite text that matches a pattern. The workhorse.
398
+ * - `round` Cut a float back to a sane number of digits.
399
+ * - `sort` Put an unordered collection in a fixed order.
400
+ * - `drop` Remove a piece of the value entirely. The dangerous one — it does not
401
+ * normalise a difference, it deletes the ability to see one. Ships unused.
402
+ *
403
+ * @typedef {'replace'|'round'|'sort'|'drop'} RuleKind
404
+ */
405
+
406
+ /**
407
+ * One normalisation rule.
408
+ *
409
+ * Rules are DATA, not code: every field here survives `JSON.stringify`, so a project keeps
410
+ * its own rules in git beside its config, reviews them in a pull request, and can see exactly
411
+ * what its tool is choosing not to look at.
412
+ *
413
+ * `wouldHide` is required by convention rather than by the type system, and no rule should
414
+ * ship without one. A rule set nobody can audit is how a difference machine goes quiet.
415
+ *
416
+ * @typedef {object} NormaliseRule
417
+ * @property {string} id Stable, dotted: 'clock.iso', 'id.uuid'.
418
+ * @property {RuleKind} kind
419
+ * @property {string} what Plain English: what this rewrites.
420
+ * @property {string} why Plain English: why it churns without this.
421
+ * @property {string} wouldHide Plain English: the real change this would wrongly hide.
422
+ * @property {string} [pattern] replace: a regular expression, as a string.
423
+ * @property {string} [flags] replace: default 'g'.
424
+ * @property {string} [with] replace: what to put in its place. '$1' works.
425
+ * @property {boolean} [numbers] replace: also test numeric values, whole-value only.
426
+ * @property {boolean} [keys] replace: also rewrite object keys. Can merge two entries
427
+ * into one — off unless a rule says otherwise.
428
+ * @property {number} [digits] round: significant digits to keep.
429
+ * @property {string[]} [paths] Path globs this rule applies to. Default: every path.
430
+ * @property {Channel[]} [channels] Channels this rule applies to. Default: every channel.
431
+ * @property {string[]} [at] Globs over the position INSIDE the value, written
432
+ * '$.items.3.name'. Used by sort, round and drop.
433
+ * @property {boolean} [off] Shipped, documented, and not switched on.
434
+ * @property {string} [whyOff] Why it is not on by default.
435
+ */
436
+
437
+ /**
438
+ * One thing a rule actually changed, so a difference hidden by normalisation can be audited.
439
+ * @typedef {object} Replacement
440
+ * @property {string} ruleId
441
+ * @property {string} what
442
+ * @property {string} why
443
+ * @property {string} wouldHide
444
+ * @property {string} at Where inside the value: '$', '$.items.3.id'.
445
+ * @property {string} before
446
+ * @property {string} after
447
+ */
448
+
449
+ /**
450
+ * @typedef {object} Explanation
451
+ * @property {ObservedValue} value The value after normalising.
452
+ * @property {Replacement[]} replacements Every change, in the order they were made.
453
+ * @property {string} summary One plain sentence for a report.
454
+ */
455
+
456
+ // ---------------------------------------------------------------------------
457
+ // Store — where observations live on disk
458
+ // ---------------------------------------------------------------------------
459
+
460
+ /**
461
+ * @typedef {object} Store
462
+ * @property {string} root Project root.
463
+ * @property {string} dir The v2 folder: <root>/.staysfixed/v2.
464
+ * @property {string} buildsDir <dir>/builds — one folder per build fingerprint.
465
+ * @property {string} referencesFile <dir>/references.json — which build is 'working', per product.
466
+ */
467
+
468
+ /**
469
+ * Enough to find one stored capture again.
470
+ * @typedef {object} CaptureRef
471
+ * @property {string} buildId
472
+ * @property {string} journey
473
+ * @property {string} captureId
474
+ * @property {string} file Absolute path to the JSONL file.
475
+ */
476
+
477
+ /**
478
+ * What the store remembers about a build. The build ARTIFACT is not kept here — a paired
479
+ * system that stored every binary would run to tens of gigabytes a year. Artifacts are kept
480
+ * only at markers, by another part of the tool; this record just says where one was.
481
+ *
482
+ * @typedef {object} BuildRecord
483
+ * @property {BuildFingerprint} fingerprint
484
+ * @property {string} firstSeenAt
485
+ * @property {string} lastSeenAt
486
+ * @property {number} captures How many capture files are stored for it.
487
+ * @property {string[]} journeys Journey names captured against it.
488
+ * @property {boolean} [isReference] Filled in by referenceFor / listBuilds.
489
+ */
490
+
491
+ /**
492
+ * Which build a product currently calls 'working'.
493
+ *
494
+ * Cut by an act Asad already performs — saying ship — never by an agent, and never by the
495
+ * tool deciding on its own that a run looked fine.
496
+ *
497
+ * @typedef {object} ReferencePointer
498
+ * @property {string} product
499
+ * @property {string} buildId
500
+ * @property {string} setAt
501
+ * @property {string} [setBy] 'ship-everywhere', a person, a command.
502
+ * @property {string} [note]
503
+ */