staysfixed 0.1.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 (57) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/LICENSE +21 -0
  3. package/README.md +529 -0
  4. package/bin/staysfixed.js +18 -0
  5. package/examples/guards/the-sidebar-still-collapses.js +91 -0
  6. package/examples/staysfixed.config.electron.js +172 -0
  7. package/examples/staysfixed.config.web.js +277 -0
  8. package/package.json +61 -0
  9. package/src/cli/approve.js +126 -0
  10. package/src/cli/check.js +73 -0
  11. package/src/cli/doctor.js +379 -0
  12. package/src/cli/flake.js +61 -0
  13. package/src/cli/index.js +519 -0
  14. package/src/cli/init.js +564 -0
  15. package/src/cli/mark.js +69 -0
  16. package/src/cli/status.js +19 -0
  17. package/src/cli/trace.js +73 -0
  18. package/src/cli/walk.js +57 -0
  19. package/src/core/config.js +226 -0
  20. package/src/core/errors.js +48 -0
  21. package/src/core/git.js +90 -0
  22. package/src/core/hash.js +32 -0
  23. package/src/core/history.js +173 -0
  24. package/src/core/log.js +144 -0
  25. package/src/core/paths.js +135 -0
  26. package/src/drive/browser.js +540 -0
  27. package/src/drive/cdp.js +382 -0
  28. package/src/drive/electron.js +326 -0
  29. package/src/drive/find.js +331 -0
  30. package/src/drive/launch.js +263 -0
  31. package/src/drive/page.js +1042 -0
  32. package/src/freeze/clock.js +213 -0
  33. package/src/freeze/fonts.js +243 -0
  34. package/src/freeze/index.js +234 -0
  35. package/src/freeze/mask.js +187 -0
  36. package/src/freeze/motion.js +206 -0
  37. package/src/freeze/network.js +455 -0
  38. package/src/freeze/random.js +87 -0
  39. package/src/freeze/settle.js +178 -0
  40. package/src/guard/api.js +197 -0
  41. package/src/guard/load.js +324 -0
  42. package/src/guard/name.js +327 -0
  43. package/src/guard/run.js +224 -0
  44. package/src/index.js +61 -0
  45. package/src/marker/mark.js +260 -0
  46. package/src/marker/trace.js +293 -0
  47. package/src/mcp/server.js +377 -0
  48. package/src/mcp/tools.js +978 -0
  49. package/src/picture/capture.js +276 -0
  50. package/src/picture/compare.js +103 -0
  51. package/src/picture/run.js +284 -0
  52. package/src/picture/store.js +208 -0
  53. package/src/report/console.js +540 -0
  54. package/src/report/html.js +579 -0
  55. package/src/run.js +614 -0
  56. package/src/types.js +471 -0
  57. package/src/walk/run.js +541 -0
package/src/types.js ADDED
@@ -0,0 +1,471 @@
1
+ /**
2
+ * Stays Fixed — the shared contract.
3
+ *
4
+ * This file holds only JSDoc typedefs. Every module in `src/` types itself against
5
+ * these shapes, so the pieces fit without importing each other's internals.
6
+ *
7
+ * Nothing here runs. It exists so `npm run typecheck` can prove the seams line up.
8
+ */
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Config — what a project writes in staysfixed.config.js / .json
12
+ // ---------------------------------------------------------------------------
13
+
14
+ /**
15
+ * @typedef {object} AppConfig
16
+ * @property {'web'|'electron'} kind What we are opening.
17
+ * @property {string} [url] web: the address to open (also the base for relative screen urls).
18
+ * @property {string} [start] web: shell command that starts the app (optional; we wait for `url`).
19
+ * @property {string} [cwd] Working directory for `start` / `binary`.
20
+ * @property {string} [binary] electron: path to the executable (or a .app bundle).
21
+ * @property {string[]} [args] electron: extra argv.
22
+ * @property {Record<string,string>} [env] Extra environment for the launched process.
23
+ * @property {number} [startTimeoutMs] How long to wait for the app to answer. Default 60000.
24
+ * @property {number} [debugPort] CDP port to use. Default: a free one we pick.
25
+ * @property {string} [attach] Attach to an already-running CDP endpoint instead of launching, e.g. "http://127.0.0.1:9333".
26
+ * @property {string} [browser] web: path to a Chrome/Chromium/Edge binary. Default: found on the system.
27
+ * @property {boolean} [headless] web: run the browser headless. Default true.
28
+ * @property {string} [windowMatch] electron: only drive the window whose title/url contains this.
29
+ */
30
+
31
+ /**
32
+ * @typedef {object} ViewportConfig
33
+ * @property {number} width CSS pixels. Default 1440.
34
+ * @property {number} height CSS pixels. Default 900.
35
+ * @property {number} [deviceScaleFactor] Default 2 (retina-sharp, still deterministic).
36
+ * @property {boolean} [mobile] Emulate a touch device. Default false.
37
+ */
38
+
39
+ /**
40
+ * @typedef {object} SettleConfig
41
+ * @property {number} [frames] Consecutive identical frames required. Default 2.
42
+ * @property {number} [intervalMs] Gap between frames. Default 250.
43
+ * @property {number} [timeoutMs] Give up after this. Default 10000.
44
+ * @property {number} [maxDriftPixels] Pixels allowed to differ and still count as "identical". Default 0.
45
+ */
46
+
47
+ /**
48
+ * @typedef {object} FreezeConfig
49
+ * @property {string|false} [clock] ISO time the app always believes it is. Default '2026-01-01T12:00:00.000Z'. false = leave the clock alone.
50
+ * @property {string} [timezone] IANA zone forced on the page. Default 'UTC'.
51
+ * @property {string} [locale] BCP-47 locale forced on the page. Default 'en-US'.
52
+ * @property {boolean} [motion] Kill animations, transitions, carets, smooth scroll. Default true.
53
+ * @property {'seeded'|'off'} [random] Seed Math.random and crypto randomness. Default 'seeded'.
54
+ * @property {number} [seed] The seed. Default 20260101.
55
+ * @property {boolean} [fonts] Wait for document.fonts.ready and pin text rendering. Default true.
56
+ * @property {'replay'|'block-external'|'live'} [network] How to handle requests. Default 'block-external'.
57
+ * @property {string[]} [networkAllow] Globs always allowed through, even in 'block-external'.
58
+ * @property {SettleConfig} [settle]
59
+ * @property {boolean} [hideScrollbars] Default true.
60
+ * @property {boolean} [hideCaret] Default true.
61
+ */
62
+
63
+ /**
64
+ * @typedef {object} ToleranceConfig
65
+ * @property {number} [pixels] Share of pixels allowed to differ, 0..1. Default 0.0005.
66
+ * @property {number} [threshold] Per-pixel colour sensitivity, 0..1 (lower = stricter). Default 0.12.
67
+ * @property {boolean} [antialiasing] Ignore anti-aliasing noise. Default true.
68
+ * @property {number} [maxPixels] Hard cap on differing pixels, overrides `pixels` when set.
69
+ */
70
+
71
+ /**
72
+ * A rectangle painted over before comparing, in CSS pixels of the captured page.
73
+ * @typedef {object} MaskRect
74
+ * @property {number} x
75
+ * @property {number} y
76
+ * @property {number} width
77
+ * @property {number} height
78
+ */
79
+
80
+ /** @typedef {string|MaskRect} Mask A CSS selector (every match is painted over) or an explicit rectangle. */
81
+
82
+ /**
83
+ * One declarative step. Exactly one action key should be set.
84
+ * @typedef {object} Step
85
+ * @property {string} [goto] Navigate. Relative paths resolve against app.url.
86
+ * @property {string} [click] CSS selector to click.
87
+ * @property {string} [type] CSS selector to type into (pair with `text`).
88
+ * @property {string} [text] Text for `type`.
89
+ * @property {string} [press] Key to press, e.g. 'Enter', 'Escape'.
90
+ * @property {string} [hover] CSS selector to hover.
91
+ * @property {string} [waitFor] CSS selector to wait for.
92
+ * @property {string} [waitForGone] CSS selector to wait to disappear.
93
+ * @property {number} [wait] Milliseconds to wait (last resort; settle usually beats this).
94
+ * @property {string} [scrollTo] CSS selector to scroll into view.
95
+ * @property {string} [evaluate] JavaScript to run in the page.
96
+ * @property {string} [note] Human note, shown in reports.
97
+ */
98
+
99
+ /**
100
+ * @typedef {object} ScreenConfig
101
+ * @property {string} name File-safe id, e.g. 'sessions-empty'.
102
+ * @property {string} [describe] Plain-language description shown to humans.
103
+ * @property {string} [url] Shorthand for a single `goto` step.
104
+ * @property {Step[]} [steps] Declarative steps run before the shutter.
105
+ * @property {Step[]} [after] Steps run AFTER the picture, to put the app back.
106
+ * Needed when a screen changes something the app SAVES —
107
+ * a reload gives back the screen, not the app's memory.
108
+ * @property {(page: PageApi) => Promise<void>} [do] Or code, when the config is JS.
109
+ * @property {Mask[]} [masks] Extra masks for this screen only.
110
+ * @property {ToleranceConfig} [tolerance] Override tolerance for this screen only.
111
+ * @property {ViewportConfig} [viewport] Override viewport for this screen only.
112
+ * @property {string} [clip] Capture only this element instead of the whole page.
113
+ * @property {boolean} [fullPage] Capture the full scrollable page. Default false.
114
+ * @property {boolean} [skip] Temporarily leave this screen out.
115
+ * @property {FreezeConfig} [freeze] Per-screen freeze overrides.
116
+ */
117
+
118
+ /**
119
+ * @typedef {object} WalkConfig
120
+ * @property {string} [describe]
121
+ * @property {ScreenConfig[]} [steps] Screens walked in order before a release. Defaults to `screens`.
122
+ */
123
+
124
+ /**
125
+ * @typedef {object} McpConfig
126
+ * @property {boolean} [allowApprove] Let an agent approve pictures. Default FALSE, on purpose.
127
+ * @property {boolean} [allowMark] Let an agent write known-good markers. Default false.
128
+ */
129
+
130
+ /**
131
+ * @typedef {object} StaysFixedConfig
132
+ * @property {AppConfig} app
133
+ * @property {ViewportConfig} [viewport]
134
+ * @property {FreezeConfig} [freeze]
135
+ * @property {ToleranceConfig} [tolerance]
136
+ * @property {Mask[]} [masks] Masks applied to every screen.
137
+ * @property {ScreenConfig[]} [screens]
138
+ * @property {string} [guards] Folder holding guard files. Default '.staysfixed/guards'.
139
+ * @property {WalkConfig} [walk]
140
+ * @property {McpConfig} [mcp]
141
+ * @property {string} [dir] Where state lives. Default '.staysfixed'.
142
+ * @property {number} [flakeLimit] Flakes before a check is condemned. Default 2.
143
+ * @property {number} [retries] Re-captures on a failing screen before calling it a real change. Default 1.
144
+ * @property {number} [concurrency] Screens captured at once. Default 1 (determinism first).
145
+ */
146
+
147
+ /** @typedef {StaysFixedConfig & Required<Pick<StaysFixedConfig,'viewport'|'freeze'|'tolerance'|'masks'|'screens'|'guards'|'dir'|'flakeLimit'|'retries'|'concurrency'|'mcp'>>} ResolvedConfig */
148
+
149
+ // ---------------------------------------------------------------------------
150
+ // Project — resolved paths and loaded config
151
+ // ---------------------------------------------------------------------------
152
+
153
+ /**
154
+ * @typedef {object} ProjectPaths
155
+ * @property {string} root Project root (folder holding the config).
156
+ * @property {string} dir Absolute .staysfixed folder.
157
+ * @property {string} approved Approved pictures (committed).
158
+ * @property {string} results Latest run output (ignored by git).
159
+ * @property {string} diffs Diff images for the latest run.
160
+ * @property {string} markers Known-good markers.
161
+ * @property {string} guards Guard files.
162
+ * @property {string} fixtures Recorded network replies.
163
+ * @property {string} historyFile Flake register.
164
+ * @property {string} reportFile Self-contained HTML report.
165
+ * @property {string} configFile The config that was loaded.
166
+ */
167
+
168
+ /**
169
+ * @typedef {object} Project
170
+ * @property {ResolvedConfig} config
171
+ * @property {ProjectPaths} paths
172
+ */
173
+
174
+ // ---------------------------------------------------------------------------
175
+ // Driving — CDP, browser, page
176
+ // ---------------------------------------------------------------------------
177
+
178
+ /**
179
+ * @typedef {object} CdpSession
180
+ * @property {(method: string, params?: Record<string, unknown>, sessionId?: string) => Promise<any>} send
181
+ * @property {(event: string, handler: (params: any, sessionId?: string) => void) => () => void} on
182
+ * @property {() => Promise<void>} close
183
+ * @property {() => boolean} isOpen
184
+ */
185
+
186
+ /**
187
+ * @typedef {object} LaunchedApp
188
+ * @property {CdpSession} cdp
189
+ * @property {PageHandle} page
190
+ * @property {() => Promise<void>} close Stop everything we started. Never touches anything we attached to.
191
+ * @property {string} endpoint The CDP endpoint in use.
192
+ * @property {number|null} pid The process we spawned, if any.
193
+ * @property {'web'|'electron'} kind
194
+ */
195
+
196
+ /**
197
+ * The surface a screen recipe or a guard is handed.
198
+ * @typedef {object} PageApi
199
+ * @property {(url: string) => Promise<void>} goto
200
+ * @property {(selector: string, opts?: {timeoutMs?: number}) => Promise<void>} click
201
+ * @property {(selector: string, text: string) => Promise<void>} type
202
+ * @property {(key: string) => Promise<void>} press
203
+ * @property {(selector: string) => Promise<void>} hover
204
+ * @property {() => Promise<void>} moveMouseAway Park the pointer in the far corner so nothing is left hovered.
205
+ * @property {(selector: string, opts?: {timeoutMs?: number}) => Promise<void>} waitFor
206
+ * @property {(selector: string, opts?: {timeoutMs?: number}) => Promise<void>} waitForGone
207
+ * @property {(selector: string) => Promise<void>} scrollTo
208
+ * @property {(ms: number) => Promise<void>} wait
209
+ * @property {(js: string) => Promise<any>} evaluate
210
+ * @property {(selector: string) => Promise<boolean>} visible
211
+ * @property {(selector: string) => Promise<boolean>} exists
212
+ * @property {(selector: string) => Promise<string>} textOf
213
+ * @property {(selector: string) => Promise<number>} count
214
+ * @property {(selector: string) => Promise<MaskRect|null>} boxOf
215
+ * @property {() => Promise<string>} url
216
+ * @property {() => Promise<string>} title
217
+ * @property {(opts?: CaptureOptions) => Promise<Buffer>} shoot Raw screenshot, no settle, no compare.
218
+ * @property {(v: ViewportConfig) => Promise<void>} setViewport
219
+ * @property {() => string[]} consoleErrors
220
+ */
221
+
222
+ /**
223
+ * @typedef {object} CaptureOptions
224
+ * @property {boolean} [fullPage]
225
+ * @property {string} [clip] Selector to clip to.
226
+ * @property {MaskRect} [rect] Explicit rectangle to clip to.
227
+ */
228
+
229
+ // ---------------------------------------------------------------------------
230
+ // Pictures
231
+ // ---------------------------------------------------------------------------
232
+
233
+ /**
234
+ * @typedef {object} PictureMeta
235
+ * @property {string} name
236
+ * @property {string} [describe]
237
+ * @property {number} width
238
+ * @property {number} height
239
+ * @property {number} deviceScaleFactor
240
+ * @property {string} sha256
241
+ * @property {string} approvedAt ISO timestamp.
242
+ * @property {string} approvedBy git user.name <email>, or 'unknown'.
243
+ * @property {string} [gitSha] Commit the approval was made against.
244
+ * @property {string} tool Stays Fixed version that took it.
245
+ * @property {string} [platform] e.g. 'darwin-arm64'. Pictures are platform-tagged; comparing across platforms warns.
246
+ */
247
+
248
+ /**
249
+ * @typedef {'passed'|'changed'|'new'|'missing'|'failed'|'skipped'|'flaky'} CheckStatus
250
+ */
251
+
252
+ /**
253
+ * @typedef {object} PictureResult
254
+ * @property {string} name
255
+ * @property {string} [describe]
256
+ * @property {CheckStatus} status
257
+ * @property {number} [diffPixels]
258
+ * @property {number} [diffRatio]
259
+ * @property {string} [approvedPath]
260
+ * @property {string} [actualPath]
261
+ * @property {string} [diffPath]
262
+ * @property {string} [message] Plain-language explanation.
263
+ * @property {number} durationMs
264
+ * @property {number} [attempts]
265
+ * @property {string[]} [consoleErrors]
266
+ * @property {{width:number,height:number}} [size]
267
+ * @property {{width:number,height:number}} [approvedSize]
268
+ */
269
+
270
+ // ---------------------------------------------------------------------------
271
+ // Guards
272
+ // ---------------------------------------------------------------------------
273
+
274
+ /**
275
+ * @typedef {object} GuardApi
276
+ * @property {PageApi} page Full page control.
277
+ * @property {(path: string) => Promise<void>} open Shorthand for page.goto.
278
+ * @property {(selector: string) => Promise<void>} click
279
+ * @property {(what: string, check: () => unknown | Promise<unknown>) => Promise<void>} expect
280
+ * Assert in plain language: expect('the sidebar is hidden', () => ...).
281
+ * @property {(cmd: string, opts?: {cwd?: string, timeoutMs?: number}) => Promise<{code: number, stdout: string, stderr: string}>} run
282
+ * Run a shell command, for guards that are not about the screen.
283
+ * @property {(file: string) => Promise<string>} read Read a project file.
284
+ * @property {Project} project
285
+ */
286
+
287
+ /**
288
+ * @typedef {object} Guard
289
+ * @property {string} name Plain language. "the sidebar still collapses".
290
+ * @property {string} [fixed] When the bug was fixed (free text or a date).
291
+ * @property {string} [because] Why this guard exists — the story of the bug.
292
+ * @property {string} [link] Issue / commit / session note.
293
+ * @property {boolean} [skip]
294
+ * @property {number} [timeoutMs] Default 30000.
295
+ * @property {(app: GuardApi) => Promise<void>} run
296
+ * @property {string} [file] Filled in by the loader.
297
+ */
298
+
299
+ /**
300
+ * @typedef {object} GuardResult
301
+ * @property {string} name
302
+ * @property {CheckStatus} status
303
+ * @property {string} [message]
304
+ * @property {string} [failedAt] The plain-language expectation that failed.
305
+ * @property {string} [file]
306
+ * @property {string} [because]
307
+ * @property {number} durationMs
308
+ * @property {number} [attempts]
309
+ */
310
+
311
+ // ---------------------------------------------------------------------------
312
+ // Runs, markers, history
313
+ // ---------------------------------------------------------------------------
314
+
315
+ /**
316
+ * @typedef {object} RunSummary
317
+ * @property {string} id Run id, sortable: '20260829-013245'.
318
+ * @property {string} startedAt
319
+ * @property {number} durationMs
320
+ * @property {PictureResult[]} pictures
321
+ * @property {GuardResult[]} guards
322
+ * @property {{passed:number,changed:number,new:number,failed:number,missing:number,skipped:number}} totals
323
+ * @property {boolean} ok
324
+ * @property {GitInfo} git
325
+ * @property {string} tool
326
+ * @property {string} platform
327
+ * @property {string[]} [condemned] Names of checks that have flaked past the limit.
328
+ */
329
+
330
+ /**
331
+ * @typedef {object} GitInfo
332
+ * @property {string|null} sha
333
+ * @property {string|null} shortSha
334
+ * @property {string|null} branch
335
+ * @property {boolean} dirty
336
+ * @property {string|null} user
337
+ */
338
+
339
+ /**
340
+ * @typedef {object} Marker
341
+ * @property {string} label 'v0.15.0', 'before-the-store-work', anything.
342
+ * @property {string} at ISO timestamp.
343
+ * @property {string} [note]
344
+ * @property {GitInfo} git
345
+ * @property {Record<string,string>} pictures screen name -> sha256 of the approved picture.
346
+ * @property {Record<string,CheckStatus>} guards
347
+ * @property {string} tool
348
+ * @property {string} platform
349
+ */
350
+
351
+ /**
352
+ * @typedef {object} HistoryEntry
353
+ * @property {string} name
354
+ * @property {'picture'|'guard'} kind
355
+ * @property {number} runs
356
+ * @property {number} flakes Times it changed its mind without the code changing.
357
+ * @property {CheckStatus[]} recent Last N statuses, newest last.
358
+ * @property {string} [lastFlakeAt]
359
+ * @property {string} [lastFlakeGitSha]
360
+ * @property {boolean} [condemned] Past the flake limit — fix it or delete it.
361
+ */
362
+
363
+ /** @typedef {Record<string, HistoryEntry>} History */
364
+
365
+ export {};
366
+
367
+ // ---------------------------------------------------------------------------
368
+ // Extra shapes the modules pass between themselves
369
+ // ---------------------------------------------------------------------------
370
+
371
+ /**
372
+ * What `createPage` actually returns: the public PageApi plus the plumbing the
373
+ * freeze layer and the capture loop need. Screens and guards only ever see PageApi.
374
+ * @typedef {PageApi & {
375
+ * send: (method: string, params?: Record<string, unknown>) => Promise<any>,
376
+ * on: (event: string, handler: (params: any) => void) => () => void,
377
+ * sessionId: string,
378
+ * targetId: string,
379
+ * addInitScript: (source: string) => Promise<string>,
380
+ * removeInitScript: (id: string) => Promise<void>,
381
+ * insertCss: (css: string) => Promise<string>,
382
+ * removeCss: (id: string) => Promise<void>,
383
+ * baseUrl: string|null,
384
+ * clearConsole: () => void,
385
+ * }} PageHandle
386
+ */
387
+
388
+ /**
389
+ * @typedef {object} FreezeHandle
390
+ * @property {() => Promise<void>} release Undo everything that can be undone.
391
+ * @property {() => FreezeStats} stats
392
+ */
393
+
394
+ /**
395
+ * @typedef {object} FreezeStats
396
+ * @property {number} requestsAllowed
397
+ * @property {number} requestsBlocked
398
+ * @property {number} requestsReplayed
399
+ * @property {number} requestsRecorded
400
+ * @property {string[]} blockedUrls
401
+ */
402
+
403
+ /**
404
+ * @typedef {object} SettleReport
405
+ * @property {boolean} settled
406
+ * @property {number} attempts
407
+ * @property {number} lastDriftPixels
408
+ * @property {number} waitedMs
409
+ */
410
+
411
+ /**
412
+ * @typedef {object} CaptureReport
413
+ * @property {Buffer} png
414
+ * @property {number} width
415
+ * @property {number} height
416
+ * @property {SettleReport} settle
417
+ * @property {string[]} consoleErrors
418
+ * @property {FreezeStats} [freeze]
419
+ */
420
+
421
+ /**
422
+ * @typedef {object} CompareReport
423
+ * @property {boolean} equal
424
+ * @property {number} diffPixels
425
+ * @property {number} diffRatio
426
+ * @property {Buffer|null} diffPng
427
+ * @property {boolean} sizeMismatch
428
+ * @property {{width:number,height:number}} size
429
+ * @property {{width:number,height:number}} approvedSize
430
+ */
431
+
432
+ /**
433
+ * @typedef {object} WalkStep
434
+ * @property {number} index
435
+ * @property {string} name
436
+ * @property {string} [describe]
437
+ * @property {string} file Absolute path to the photo of this step.
438
+ * @property {string} [url]
439
+ * @property {string} [title]
440
+ * @property {number} durationMs
441
+ * @property {string[]} [consoleErrors]
442
+ * @property {string} [error]
443
+ */
444
+
445
+ /**
446
+ * @typedef {object} WalkReport
447
+ * @property {string} id
448
+ * @property {string} dir
449
+ * @property {WalkStep[]} steps
450
+ * @property {boolean} ok
451
+ * @property {GitInfo} git
452
+ * @property {string} [reportFile]
453
+ */
454
+
455
+ /**
456
+ * @typedef {object} TraceFinding
457
+ * @property {string} name
458
+ * @property {'unchanged'|'changed'|'unknown'} verdict
459
+ * @property {Marker} [lastGood] Newest marker where this screen looked like it does now.
460
+ * @property {Marker} [firstBad] Oldest marker after that where it did not.
461
+ * @property {{sha: string, shortSha: string, subject: string, author: string, date: string}[]} [commits]
462
+ * @property {string[]} [files]
463
+ * @property {string} [message]
464
+ */
465
+
466
+ /**
467
+ * @typedef {object} TraceReport
468
+ * @property {TraceFinding[]} findings
469
+ * @property {number} markersSearched
470
+ * @property {string} [message]
471
+ */