scenescout 3.22.0 → 3.23.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.
package/dist/engine/ci.js CHANGED
@@ -16,6 +16,7 @@ import { markdownCell } from "./check.js";
16
16
  import { parseLimitFlag } from "./limits.js";
17
17
  import { isWorthALook, redactSecrets } from "./memory.js";
18
18
  import { SARIF_ANCHOR_FALLBACKS, checkSarifAnchor, sarifLocation } from "./sarif.js";
19
+ import { fromRunLine, resolveFromRun } from "./from-run.js";
19
20
  // ── options ─────────────────────────────────────────────────────────────────
20
21
  export const PROVIDERS = ["anthropic", "openai"];
21
22
  /** Where each provider's key is read from. Nothing else is: no flag, no file, no input. */
@@ -93,6 +94,9 @@ export const CI_OPTION_NAMES = [
93
94
  "show",
94
95
  "compare-url",
95
96
  "dedup",
97
+ "from-run",
98
+ "from-run-mode",
99
+ "from-run-turns-per-page",
96
100
  "sarif-file-anchor",
97
101
  ];
98
102
  /** The longest --show description: it becomes a line of the model's prompt. */
@@ -115,7 +119,8 @@ export function checkBaseUrl(raw) {
115
119
  return { ok: true, url: u.toString().replace(/\/+$/, "") };
116
120
  return { ok: false, error: "--base-url must be https (plain http only to 127.0.0.1 or localhost): the API key is sent to it" };
117
121
  }
118
- export function parseCiArgs(args, cwd) {
122
+ /** `env` is read for the earlier run alone (SCENESCOUT_FROM_RUN, SCENESCOUT_FROM_RUN_MODE), since a flag wins over it. */
123
+ export function parseCiArgs(args, cwd, env = {}) {
119
124
  const positional = [];
120
125
  const flags = new Map();
121
126
  for (let i = 0; i < args.length; i++) {
@@ -271,6 +276,13 @@ export function parseCiArgs(args, cwd) {
271
276
  const anchor = flags.has("sarif-file-anchor") ? checkSarifAnchor(flags.get("sarif-file-anchor")) : undefined;
272
277
  if (anchor && !anchor.ok)
273
278
  return anchor;
279
+ const runFlags = { path: flags.get("from-run"), mode: flags.get("from-run-mode"), turnsPerPage: flags.get("from-run-turns-per-page") };
280
+ if (show && (runFlags.path !== undefined || runFlags.mode !== undefined || runFlags.turnsPerPage !== undefined))
281
+ return { ok: false, error: "--from-run starts an exploration from an earlier run, and --show explores nothing: give one or the other" };
282
+ // A run asked to show an element explores nothing, so an earlier run named in the environment does not apply to it.
283
+ const fromRun = show ? { ok: true } : resolveFromRun(runFlags, env);
284
+ if (!fromRun.ok)
285
+ return fromRun;
274
286
  const resolve = (p) => (p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p) ? p : `${cwd.replace(/[\\/]$/, "")}/${p}`);
275
287
  return {
276
288
  ok: true,
@@ -296,6 +308,7 @@ export function parseCiArgs(args, cwd) {
296
308
  ...(navTimeout.value !== undefined ? { navTimeoutMs: navTimeout.value } : {}),
297
309
  dedup: dedup,
298
310
  ...(anchor ? { sarifFileAnchor: anchor.value } : {}),
311
+ ...(fromRun.fromRun ? { fromRun: { ...fromRun.fromRun, path: resolve(fromRun.fromRun.path), given: fromRun.fromRun.path } } : {}),
299
312
  },
300
313
  };
301
314
  }
@@ -691,6 +704,7 @@ export function ciKickoff(o) {
691
704
  return [
692
705
  `Run an exploratory test session following the method.`,
693
706
  `Target: ${o.url}`,
707
+ ...(o.fromRunLines ?? []),
694
708
  `Project directory (for scout_scan): ${o.projectDir}`,
695
709
  `Level: ${o.level}`,
696
710
  `Write mode: ${o.mode}`,
@@ -760,6 +774,7 @@ export function ciSummaryMarkdown(r, secrets = []) {
760
774
  `| Model | ${r.provider} ${cell(r.model, secrets)}, effort ${r.effort} |`,
761
775
  ...(r.lanes ? [`| Lanes | ${lanesCell(r.lanes, secrets)} |`] : []),
762
776
  ...(r.dedup ? [`| Finding dedup | ${dedupLine(r.dedup)} |`] : []),
777
+ ...(r.fromRun ? [`| Started from | ${cell(fromRunLine(r.fromRun), secrets)} |`] : []),
763
778
  `| Usage | ${usageLine(r.spend, r.model, r.endedAt, r.price)} |`,
764
779
  ``,
765
780
  ];
@@ -785,6 +800,27 @@ export function ciSummaryMarkdown(r, secrets = []) {
785
800
  lines.push(r.capture ? `The pictures are in shots/.` : `The full report, with repro steps and the gap ledger, is report.md.`, ``);
786
801
  return lines.join("\n");
787
802
  }
803
+ /** A run record with every text a page or a person wrote in it passed through `clean`, as everything else ci.json holds is. */
804
+ function cleanRecord(r, clean) {
805
+ const all = (xs) => xs.map(clean);
806
+ return {
807
+ ...r,
808
+ knownRoutes: all(r.knownRoutes),
809
+ visited: all(r.visited),
810
+ lanes: r.lanes.map((l) => ({ session: l.session, routes: all(l.routes) })),
811
+ steps: r.steps.map((s) => ({ ...s, route: clean(s.route), ...(s.target !== undefined ? { target: clean(s.target) } : {}) })),
812
+ left: r.left.map((l) => ({
813
+ ...l,
814
+ route: clean(l.route),
815
+ unexercised: all(l.unexercised),
816
+ forms: all(l.forms),
817
+ unchosen: l.unchosen.map((d) => ({ key: clean(d.key), options: all(d.options) })),
818
+ })),
819
+ gaps: all(r.gaps),
820
+ ...(r.assigned ? { assigned: all(r.assigned) } : {}),
821
+ ...(r.prefixes ? { prefixes: r.prefixes.map((n) => ({ ...n, target: clean(n.target), ...(n.why !== undefined ? { why: clean(n.why) } : {}) })) } : {}),
822
+ };
823
+ }
788
824
  function lanesCell(l, secrets) {
789
825
  if (l.oneLoop)
790
826
  return `${l.asked} asked; explored in one loop: ${cell(l.oneLoop, secrets)}`;
@@ -876,6 +912,8 @@ export function ciSummaryJson(r, version, secrets = []) {
876
912
  })),
877
913
  ...(r.capture ? { capture: cleanCapture(r.capture, clean) } : {}),
878
914
  ...(r.lanes ? { lanes: lanesJson(r.lanes, clean) } : {}),
915
+ ...(r.fromRun ? { fromRun: { mode: r.fromRun.mode, source: clean(r.fromRun.source), runId: r.fromRun.runId, recordAt: r.fromRun.recordAt } } : {}),
916
+ ...(r.record ? { record: cleanRecord(r.record, clean) } : {}),
879
917
  };
880
918
  }
881
919
  /** The lanes as ci.json holds them. Module paths and lane names come from the app's routes: redacted like the rest. */