staysfixed 0.3.1 → 0.6.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 (48) hide show
  1. package/CHANGELOG.md +159 -3
  2. package/README.md +611 -402
  3. package/package.json +8 -3
  4. package/src/cli/index.js +14 -0
  5. package/src/v2/adapters/android-driver.js +1705 -0
  6. package/src/v2/adapters/android.js +1117 -0
  7. package/src/v2/adapters/contract.js +643 -0
  8. package/src/v2/adapters/electron.js +1594 -0
  9. package/src/v2/adapters/http.js +734 -0
  10. package/src/v2/adapters/ios-driver.js +1551 -0
  11. package/src/v2/adapters/ios.js +989 -0
  12. package/src/v2/adapters/isolate.js +739 -0
  13. package/src/v2/adapters/process.js +931 -0
  14. package/src/v2/adapters/source.js +1292 -0
  15. package/src/v2/adapters/web-driver.js +1532 -0
  16. package/src/v2/adapters/web.js +1009 -0
  17. package/src/v2/adapters/windows.js +1329 -0
  18. package/src/v2/browsers.js +1203 -0
  19. package/src/v2/cause.js +371 -0
  20. package/src/v2/check.js +1429 -0
  21. package/src/v2/ci.js +1209 -0
  22. package/src/v2/cli.js +670 -0
  23. package/src/v2/cluster.js +372 -0
  24. package/src/v2/coverage.js +1124 -0
  25. package/src/v2/detect.js +1199 -0
  26. package/src/v2/doctor.js +1702 -0
  27. package/src/v2/escalate.js +679 -0
  28. package/src/v2/init.js +1394 -0
  29. package/src/v2/intent.js +659 -0
  30. package/src/v2/journeys/from-routes.js +500 -0
  31. package/src/v2/journeys/from-suite.js +988 -0
  32. package/src/v2/journeys/index.js +651 -0
  33. package/src/v2/journeys/record.js +516 -0
  34. package/src/v2/mcp/server.js +374 -0
  35. package/src/v2/mcp/tools.js +1571 -0
  36. package/src/v2/normalise.js +783 -0
  37. package/src/v2/observation.js +938 -0
  38. package/src/v2/rank.js +672 -0
  39. package/src/v2/reference.js +1051 -0
  40. package/src/v2/remote.js +910 -0
  41. package/src/v2/run.js +1080 -0
  42. package/src/v2/sealed.js +568 -0
  43. package/src/v2/selfcheck.js +729 -0
  44. package/src/v2/ship.js +684 -0
  45. package/src/v2/store.js +703 -0
  46. package/src/v2/types.js +509 -0
  47. package/src/v2/waiver.js +511 -0
  48. package/src/v2/watch/focus.js +215 -0
@@ -0,0 +1,509 @@
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 {boolean} [couldNotTell] True when the wobble measurement was too big to be a
239
+ * measurement — the same build answered differently at
240
+ * most of its own addresses, so subtracting it subtracts
241
+ * the answer. A run in this state has no verdict, and it
242
+ * must never be reported as a clean one.
243
+ * @property {string} [couldNotTellWhy] Said plainly, with the numbers in it.
244
+ * @property {string} note One plain sentence stating exactly that.
245
+ */
246
+
247
+ // ---------------------------------------------------------------------------
248
+ // Differences — reference against candidate
249
+ // ---------------------------------------------------------------------------
250
+
251
+ /**
252
+ * `appeared` and `vanished` are the important ones. A path that stopped existing is a door
253
+ * that closed, and no pixel comparison has ever noticed one.
254
+ * @typedef {'changed'|'appeared'|'vanished'} DifferenceKind
255
+ */
256
+
257
+ /**
258
+ * One path that differs between the reference and the candidate.
259
+ *
260
+ * @typedef {object} Difference
261
+ * @property {string} path
262
+ * @property {Channel} channel
263
+ * @property {DifferenceKind} kind
264
+ * @property {ObservedValue} [reference] Absent when the path appeared.
265
+ * @property {ObservedValue} [candidate] Absent when the path vanished.
266
+ * @property {number} distance Rough size, 0..1. Ranking only.
267
+ * @property {string} [journey]
268
+ * @property {boolean} [real] Survived the wobble floor. Set by subtractWobble.
269
+ * @property {boolean} [wobbling] This path does not sit still in this build anyway.
270
+ * @property {boolean} [proven] Re-checked against the old build booted live, and it
271
+ * survived. Cheap suspicion, expensive proof.
272
+ * @property {string} [describe] One plain sentence, carried from the observation.
273
+ * @property {string} [evidence] A picture or log that shows it, for a human.
274
+ */
275
+
276
+ // ---------------------------------------------------------------------------
277
+ // Findings — differences clustered into something worth acting on
278
+ // ---------------------------------------------------------------------------
279
+
280
+ /**
281
+ * The classes an agent may never wave through on its own. Anything in one of these goes to a
282
+ * person, whatever the agent believes it meant to change.
283
+ * @typedef {'money'|'sign-in'|'data-loss'|'crash'|'guard'|'ordinary'} FindingClass
284
+ */
285
+
286
+ /**
287
+ * A cluster of differences with one likely cause, written for someone to act on.
288
+ *
289
+ * The whole point of clustering: one missing stylesheet is one finding, not four hundred
290
+ * differences. An agent should be able to read the title and know what to go and look at.
291
+ *
292
+ * @typedef {object} Finding
293
+ * @property {string} id Stable across runs while the cause persists, so the same
294
+ * finding is not reported as new every time.
295
+ * @property {string} title Plain English, no jargon, no test ids: "Saving a session
296
+ * no longer writes the file."
297
+ * @property {string} why The likely cause, said plainly, and hedged when it is a guess.
298
+ * @property {FindingClass} class
299
+ * @property {Difference[]} differences
300
+ * @property {number} rank Higher is more urgent. Distance from the changed code is
301
+ * the biggest term: a break far from the edit is the very
302
+ * definition of a side effect.
303
+ * @property {string} [signature] What the cluster was grouped on.
304
+ * @property {string[]} [nearFiles] Source files the cluster points at, nearest first.
305
+ * @property {boolean} [sealed] In an unwaivable class. Goes to a person, full stop.
306
+ * @property {string} [evidence] A picture, a log, a diff — for the human, at the end.
307
+ * @property {number} [count] How many differences this one finding stands for. Five hundred
308
+ * differences are not five hundred findings, and the count is
309
+ * what stops a cluster hiding its own size.
310
+ * @property {string} [summary] One line, for a list. `title` is the headline; this is the
311
+ * sentence under it.
312
+ * @property {string[]} [paths] The addresses involved, for an agent that wants to look.
313
+ * @property {Difference} [sample] One representative difference, so a reader sees the shape
314
+ * without being handed all of them.
315
+ * @property {number} [distance] How far from the code that changed. Bigger is more suspicious:
316
+ * a break far from the edit is the definition of a side effect.
317
+ */
318
+
319
+ // ---------------------------------------------------------------------------
320
+ // Coverage — and mostly, what was NOT checked
321
+ // ---------------------------------------------------------------------------
322
+
323
+ /**
324
+ * One thing we did not look at, and what it would take to look at it.
325
+ *
326
+ * This shape carries the self-description requirement: an agent installing the tool reads
327
+ * `unlockedBy` and knows exactly what to install, start or supply. Nobody should have to read
328
+ * documentation to wire this up.
329
+ *
330
+ * @typedef {object} CoverageGap
331
+ * @property {string} what What is not covered, in plain English.
332
+ * @property {string} why Why not: missing runtime, refused effect, no snapshot,
333
+ * old build will not compile.
334
+ * @property {string} [unlockedBy] The concrete thing that would fix it: "install a Java
335
+ * runtime", "add an SSH host", "supply a database snapshot".
336
+ * @property {Channel} [channel]
337
+ * @property {Surface} [surface]
338
+ * @property {number} [doors] How many addressable things this gap hides, when countable.
339
+ */
340
+
341
+ /**
342
+ * What was checked and, much more importantly, what was not.
343
+ *
344
+ * A tool that reports "nothing changed" is indistinguishable from a broken tool. Coverage is
345
+ * how the difference is made visible instead of pretended away.
346
+ *
347
+ * @typedef {object} Coverage
348
+ * @property {number} paths Addresses observed.
349
+ * @property {number} journeys Journeys walked.
350
+ * @property {Partial<Record<Channel, number>>} byChannel Paths observed per channel.
351
+ * @property {number} [doorsKnown] Doors the contract channel found in the source.
352
+ * @property {number} [doorsWalked] How many of those any journey actually opened.
353
+ * @property {CoverageGap[]} gaps Everything we could not see. Never empty on a real run.
354
+ */
355
+
356
+ // ---------------------------------------------------------------------------
357
+ // Verdict — what a whole run concluded
358
+ // ---------------------------------------------------------------------------
359
+
360
+ /**
361
+ * How the reference was obtained, because it changes how much the answer is worth.
362
+ *
363
+ * - `paired` The old build was booted live on this machine, in this minute.
364
+ * - `stored-record` Compared against observations stored the last time the old build ran.
365
+ * Genuinely weaker: it lets back in every difference that comes from the
366
+ * day being different. It must announce itself in those words, every run.
367
+ *
368
+ * @typedef {'paired'|'stored-record'} ReferenceMode
369
+ */
370
+
371
+ /**
372
+ * What a whole run concluded.
373
+ *
374
+ * @typedef {object} Verdict
375
+ * @property {string} runId
376
+ * @property {string} product
377
+ * @property {boolean} ok Nothing unintended survived the wobble floor.
378
+ * @property {ReferenceMode} mode
379
+ * @property {string} [modeWarning] Present whenever `mode` is 'stored-record'.
380
+ * @property {BuildFingerprint} reference
381
+ * @property {BuildFingerprint} candidate
382
+ * @property {Finding[]} findings Ranked, worst first. The only thing an agent should read.
383
+ * @property {number} differencesReal
384
+ * @property {number} differencesNoise
385
+ * @property {WobbleEntry[]} newlyUnstable
386
+ * @property {Coverage} coverage
387
+ * @property {string} summary One paragraph of plain English. What changed, what did not,
388
+ * what was not looked at.
389
+ * @property {number} durationMs
390
+ * @property {string} startedAt
391
+ * @property {string} [tool]
392
+ */
393
+
394
+ export {};
395
+
396
+ // ---------------------------------------------------------------------------
397
+ // Normalisation — the rules that decide what is a difference and what is churn
398
+ // ---------------------------------------------------------------------------
399
+
400
+ /**
401
+ * What a rule does to a value.
402
+ *
403
+ * - `replace` Rewrite text that matches a pattern. The workhorse.
404
+ * - `round` Cut a float back to a sane number of digits.
405
+ * - `sort` Put an unordered collection in a fixed order.
406
+ * - `drop` Remove a piece of the value entirely. The dangerous one — it does not
407
+ * normalise a difference, it deletes the ability to see one. Ships unused.
408
+ *
409
+ * @typedef {'replace'|'round'|'sort'|'drop'} RuleKind
410
+ */
411
+
412
+ /**
413
+ * One normalisation rule.
414
+ *
415
+ * Rules are DATA, not code: every field here survives `JSON.stringify`, so a project keeps
416
+ * its own rules in git beside its config, reviews them in a pull request, and can see exactly
417
+ * what its tool is choosing not to look at.
418
+ *
419
+ * `wouldHide` is required by convention rather than by the type system, and no rule should
420
+ * ship without one. A rule set nobody can audit is how a difference machine goes quiet.
421
+ *
422
+ * @typedef {object} NormaliseRule
423
+ * @property {string} id Stable, dotted: 'clock.iso', 'id.uuid'.
424
+ * @property {RuleKind} kind
425
+ * @property {string} what Plain English: what this rewrites.
426
+ * @property {string} why Plain English: why it churns without this.
427
+ * @property {string} wouldHide Plain English: the real change this would wrongly hide.
428
+ * @property {string} [pattern] replace: a regular expression, as a string.
429
+ * @property {string} [flags] replace: default 'g'.
430
+ * @property {string} [with] replace: what to put in its place. '$1' works.
431
+ * @property {boolean} [numbers] replace: also test numeric values, whole-value only.
432
+ * @property {boolean} [keys] replace: also rewrite object keys. Can merge two entries
433
+ * into one — off unless a rule says otherwise.
434
+ * @property {number} [digits] round: significant digits to keep.
435
+ * @property {string[]} [paths] Path globs this rule applies to. Default: every path.
436
+ * @property {Channel[]} [channels] Channels this rule applies to. Default: every channel.
437
+ * @property {string[]} [at] Globs over the position INSIDE the value, written
438
+ * '$.items.3.name'. Used by sort, round and drop.
439
+ * @property {boolean} [off] Shipped, documented, and not switched on.
440
+ * @property {string} [whyOff] Why it is not on by default.
441
+ */
442
+
443
+ /**
444
+ * One thing a rule actually changed, so a difference hidden by normalisation can be audited.
445
+ * @typedef {object} Replacement
446
+ * @property {string} ruleId
447
+ * @property {string} what
448
+ * @property {string} why
449
+ * @property {string} wouldHide
450
+ * @property {string} at Where inside the value: '$', '$.items.3.id'.
451
+ * @property {string} before
452
+ * @property {string} after
453
+ */
454
+
455
+ /**
456
+ * @typedef {object} Explanation
457
+ * @property {ObservedValue} value The value after normalising.
458
+ * @property {Replacement[]} replacements Every change, in the order they were made.
459
+ * @property {string} summary One plain sentence for a report.
460
+ */
461
+
462
+ // ---------------------------------------------------------------------------
463
+ // Store — where observations live on disk
464
+ // ---------------------------------------------------------------------------
465
+
466
+ /**
467
+ * @typedef {object} Store
468
+ * @property {string} root Project root.
469
+ * @property {string} dir The v2 folder: <root>/.staysfixed/v2.
470
+ * @property {string} buildsDir <dir>/builds — one folder per build fingerprint.
471
+ * @property {string} referencesFile <dir>/references.json — which build is 'working', per product.
472
+ */
473
+
474
+ /**
475
+ * Enough to find one stored capture again.
476
+ * @typedef {object} CaptureRef
477
+ * @property {string} buildId
478
+ * @property {string} journey
479
+ * @property {string} captureId
480
+ * @property {string} file Absolute path to the JSONL file.
481
+ */
482
+
483
+ /**
484
+ * What the store remembers about a build. The build ARTIFACT is not kept here — a paired
485
+ * system that stored every binary would run to tens of gigabytes a year. Artifacts are kept
486
+ * only at markers, by another part of the tool; this record just says where one was.
487
+ *
488
+ * @typedef {object} BuildRecord
489
+ * @property {BuildFingerprint} fingerprint
490
+ * @property {string} firstSeenAt
491
+ * @property {string} lastSeenAt
492
+ * @property {number} captures How many capture files are stored for it.
493
+ * @property {string[]} journeys Journey names captured against it.
494
+ * @property {boolean} [isReference] Filled in by referenceFor / listBuilds.
495
+ */
496
+
497
+ /**
498
+ * Which build a product currently calls 'working'.
499
+ *
500
+ * Cut by an act Asad already performs — saying ship — never by an agent, and never by the
501
+ * tool deciding on its own that a run looked fine.
502
+ *
503
+ * @typedef {object} ReferencePointer
504
+ * @property {string} product
505
+ * @property {string} buildId
506
+ * @property {string} setAt
507
+ * @property {string} [setBy] 'ship-everywhere', a person, a command.
508
+ * @property {string} [note]
509
+ */