scenescout 3.14.1 → 3.16.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 (59) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +90 -18
  3. package/dist/browsers.js +28 -0
  4. package/dist/check-run.js +211 -24
  5. package/dist/ci-run.js +374 -62
  6. package/dist/cli.js +221 -47
  7. package/dist/commands.js +42 -4
  8. package/dist/engine/baseline.js +377 -0
  9. package/dist/engine/brief.js +16 -7
  10. package/dist/engine/browser.js +1660 -386
  11. package/dist/engine/calibration.js +61 -30
  12. package/dist/engine/capture.js +164 -0
  13. package/dist/engine/check.js +258 -68
  14. package/dist/engine/ci-lanes.js +215 -0
  15. package/dist/engine/ci.js +233 -18
  16. package/dist/engine/claims.js +234 -12
  17. package/dist/engine/collector.js +851 -78
  18. package/dist/engine/crawl.js +49 -0
  19. package/dist/engine/dedup.js +329 -9
  20. package/dist/engine/design.js +355 -43
  21. package/dist/engine/export.js +877 -0
  22. package/dist/engine/fingerprint.js +92 -4
  23. package/dist/engine/flow.js +18 -6
  24. package/dist/engine/forms.js +198 -18
  25. package/dist/engine/journey.js +29 -1
  26. package/dist/engine/lane.js +13 -3
  27. package/dist/engine/launch.js +45 -6
  28. package/dist/engine/limits.js +7 -0
  29. package/dist/engine/live-page.js +49 -2
  30. package/dist/engine/live.js +4 -1
  31. package/dist/engine/memory.js +668 -70
  32. package/dist/engine/open.js +118 -0
  33. package/dist/engine/oracles.js +167 -2
  34. package/dist/engine/plain.js +268 -0
  35. package/dist/engine/png.js +127 -0
  36. package/dist/engine/policy.js +379 -9
  37. package/dist/engine/probes.js +3 -2
  38. package/dist/engine/profiles.js +45 -9
  39. package/dist/engine/project-folder.js +191 -0
  40. package/dist/engine/provider.js +4 -3
  41. package/dist/engine/refresh.js +338 -25
  42. package/dist/engine/replay.js +63 -10
  43. package/dist/engine/report.js +248 -40
  44. package/dist/engine/request.js +317 -23
  45. package/dist/engine/sarif.js +120 -0
  46. package/dist/engine/scripted-login.js +342 -62
  47. package/dist/engine/settle.js +67 -0
  48. package/dist/engine/signed-in.js +256 -0
  49. package/dist/engine/tickets.js +671 -0
  50. package/dist/engine/unload.js +3 -2
  51. package/dist/export-run.js +633 -0
  52. package/dist/first-run.js +581 -0
  53. package/dist/installer.js +267 -3
  54. package/dist/intake.js +104 -0
  55. package/dist/login-run.js +386 -71
  56. package/dist/mcp-server.js +637 -68
  57. package/dist/playbook.js +5 -0
  58. package/package.json +6 -3
  59. package/skills/scenescout/SKILL.md +51 -18
@@ -173,9 +173,17 @@ export const LIVE_PAGE = `<!doctype html>
173
173
  #report .doc details { margin: 8px 0; }
174
174
  #report .doc summary { cursor: pointer; color: var(--muted); }
175
175
  #report .doc .unset { color: var(--muted); font-style: italic; }
176
+ #report .doc figure.picture { margin: 8px 0 14px; max-width: min(100%, 720px); }
177
+ #report .doc figure.picture img { display: block; max-width: 100%; height: auto; border: 1px solid var(--line); border-radius: 6px; }
178
+ #report .doc figure.picture figcaption { margin-top: 4px; font-size: 12px; color: var(--muted); }
179
+ #report .doc figure.picture .gone-note { display: none; padding: 12px; border: 1px dashed var(--line); border-radius: 6px; color: var(--muted); }
180
+ #report .doc figure.picture.gone img { display: none; }
181
+ #report .doc figure.picture.gone .gone-note { display: block; }
176
182
  #report .doc details.evidence { margin: 10px 0 16px; padding: 8px 12px; background: var(--panel); border: 1px solid var(--line); border-radius: 6px; }
177
183
  #report .doc details.evidence[open] summary { margin-bottom: 10px; }
178
184
  #report .doc details.evidence figure { margin: 0 0 12px; }
185
+ #report .doc figure.picture { margin: 8px 0 14px; max-width: 640px; }
186
+ #report .doc figure.picture img { display: block; width: auto; max-width: 100%; max-height: 360px; border: 1px solid var(--line); border-radius: 6px; }
179
187
  #report .doc details.evidence .shots { display: flex; flex-wrap: wrap; gap: 14px; }
180
188
  #report .doc details.evidence figure { flex: 1 1 320px; max-width: 440px; }
181
189
  #report .doc details.evidence img { display: block; width: 100%; max-height: 280px; object-fit: cover; object-position: top;
@@ -494,7 +502,28 @@ export const LIVE_PAGE = `<!doctype html>
494
502
  function evidenceFor(id) {
495
503
  var found = null;
496
504
  reportEvidence.forEach(function (e) { if (e.id === id) found = e; });
497
- if (!found || !found.frames.length) return null;
505
+ if (!found) return null;
506
+ // The picture taken when it was filed comes first, open: it is what a reader looks for.
507
+ var picture = null;
508
+ if (found.picture) {
509
+ picture = el('figure', 'picture');
510
+ picture.setAttribute('data-testid', 'live-report-picture-' + id);
511
+ var link = el('a');
512
+ link.href = 'record/' + found.picture.file;
513
+ link.target = '_blank';
514
+ link.rel = 'noreferrer';
515
+ link.setAttribute('data-testid', 'live-report-picture-open');
516
+ var pic = el('img');
517
+ pic.loading = 'lazy';
518
+ pic.src = 'record/' + found.picture.file;
519
+ pic.alt = 'What the finding is about, when it was filed';
520
+ pic.addEventListener('error', function () { picture.classList.add('gone'); });
521
+ link.appendChild(pic);
522
+ picture.appendChild(link);
523
+ picture.appendChild(el('figcaption', '', found.picture.caption));
524
+ picture.appendChild(el('p', 'gone-note', 'This picture is no longer in ' + found.picture.file.replace(/\\/[^/]*$/, '') + '.'));
525
+ }
526
+ if (!found.frames.length) return picture;
498
527
  var box = el('details', 'evidence');
499
528
  box.setAttribute('data-testid', 'live-report-evidence-' + id);
500
529
  box.appendChild(el('summary', '', found.frames.length + (found.frames.length === 1 ? ' screenshot' : ' screenshots') + ' from around this finding'));
@@ -514,7 +543,11 @@ export const LIVE_PAGE = `<!doctype html>
514
543
  fig.appendChild(el('p', 'gone-note', 'This frame is no longer in ' + f.frame.replace(/\\/[^/]*$/, '') + '.'));
515
544
  shots.appendChild(fig);
516
545
  });
517
- return box;
546
+ if (!picture) return box;
547
+ var both = el('div');
548
+ both.appendChild(picture);
549
+ both.appendChild(box);
550
+ return both;
518
551
  }
519
552
  function renderMarkdown(root, md) {
520
553
  root.textContent = '';
@@ -567,6 +600,20 @@ export const LIVE_PAGE = `<!doctype html>
567
600
  inline(h, m[2]);
568
601
  container.appendChild(h);
569
602
  i += 1;
603
+ } else if ((m = /^!\\[([^\\]]*)\\]\\((recordings\\/[A-Za-z0-9._\\/-]+)\\)$/.exec(line)) && m[2].indexOf('..') < 0) {
604
+ // A finding's picture from the run's recording, served by the frame
605
+ // route. Any other picture is a file this view cannot serve.
606
+ flush();
607
+ var fig = el('figure', 'picture');
608
+ var pic = el('img');
609
+ pic.loading = 'lazy';
610
+ pic.src = 'record/' + m[2];
611
+ pic.alt = m[1];
612
+ fig.appendChild(pic);
613
+ container.appendChild(fig);
614
+ i += 1;
615
+ } else if (/^!\\[[^\\]]*\\]\\([^)]*\\)$/.test(line)) {
616
+ i += 1;
570
617
  } else if ((m = /^<details><summary>(.*)<\\/summary>$/.exec(line))) {
571
618
  flush();
572
619
  var details = el('details');
@@ -27,6 +27,7 @@ import fs from "node:fs";
27
27
  import http from "node:http";
28
28
  import path from "node:path";
29
29
  import { LIVE_PAGE } from "./live-page.js";
30
+ import { isPng } from "./png.js";
30
31
  import { JOURNEY_END, JOURNEY_START, TASK_SET } from "./memory.js";
31
32
  /** Holds the live view's token, next to status.json. Written owner-only; removed when the engine shuts down. */
32
33
  export const LIVE_TOKEN_FILE = "live-token";
@@ -477,7 +478,9 @@ export class LiveServer {
477
478
  const jpeg = await this.provider.frame(asked);
478
479
  if (!jpeg)
479
480
  return this.send(res, 404, "no such frame");
480
- return this.send(res, 200, jpeg, "image/jpeg", { "Content-Length": jpeg.length, "Cache-Control": "private, max-age=3600" });
481
+ // Frames are JPEG; a finding's picture is PNG (capture.ts), and is labelled as one.
482
+ const type = isPng(jpeg) ? "image/png" : "image/jpeg";
483
+ return this.send(res, 200, jpeg, type, { "Content-Length": jpeg.length, "Cache-Control": "private, max-age=3600" });
481
484
  }
482
485
  if (route === "events" && !name) {
483
486
  const wanted = this.param(req, "sessions");