@keepsnappy/agent 0.1.0-alpha.19 → 0.1.0-alpha.20

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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // apps/agent/src/cli.ts
4
- import { resolve as resolve8 } from "node:path";
4
+ import { resolve as resolve9 } from "node:path";
5
5
  import { parseArgs as parseArgs4 } from "node:util";
6
6
 
7
7
  // apps/agent/src/config.ts
@@ -3608,6 +3608,7 @@ function occurrence(windows, budgetMs, idleNeutral = false, frameMs, upper = fal
3608
3608
  ...upper ? { timerCarriedMs: carried, renderMsUpper: renderMs === undefined || carried === undefined ? undefined : renderMs + carried } : {}
3609
3609
  };
3610
3610
  }
3611
+ var SAME_COMMIT_MS = 0.01;
3611
3612
  var UPDATERS_KEPT = 5;
3612
3613
  var CPU_FUNCTIONS_KEPT = 10;
3613
3614
  var UNTIMED_COMPONENTS_KEPT = 10;
@@ -3746,7 +3747,9 @@ function profileMetrics(windows, steps, options = {}) {
3746
3747
  const selected = middleBy((active.length ? active : occ).filter((o) => o.window), (o) => o.commitMaxMs, (o) => num2(o.window.raw.id) ?? 0, (o) => num2(o.window.raw.t0) ?? o.window.startWall);
3747
3748
  const identity = options.commitIdentity && metrics.has(`${key}.commit_max_ms`) && COMMIT_IDENTITY_KINDS.includes(kind) && selected?.slowestCommit;
3748
3749
  const openedBy = selected?.window ? options.openedBy?.get(selected.window) : undefined;
3749
- profiles.push({ ...profile, ...identity ? { slowestCommit: identity } : {}, ...openedBy ? { openedBy } : {} });
3750
+ const streamed = identity ? options.commitTops?.get(identity.seq) : undefined;
3751
+ const tops = identity && streamed && Math.abs(streamed.ms - identity.ms) <= SAME_COMMIT_MS ? streamed : undefined;
3752
+ profiles.push({ ...profile, ...identity ? { slowestCommit: { ...identity, ...tops?.top ? { top: tops.top } : {}, ...tops?.updater ? { updater: tops.updater } : {} } } : {}, ...openedBy ? { openedBy } : {} });
3750
3753
  }
3751
3754
  return { metrics, profiles };
3752
3755
  }
@@ -5022,7 +5025,7 @@ function samplerFrameBudgetMs(samples) {
5022
5025
  }
5023
5026
  return hz > 1 ? frameBudgetMs(Math.round(hz)) : undefined;
5024
5027
  }
5025
- function profileOptions(windows, version, processWindows = windows) {
5028
+ function profileOptions(windows, version, processWindows = windows, commitStreams = []) {
5026
5029
  if (version < 26)
5027
5030
  return {};
5028
5031
  const processes = new Set(processWindows.map((w) => w.raw.react?.causes?.process).filter((p) => typeof p === "string"));
@@ -5036,7 +5039,9 @@ function profileOptions(windows, version, processWindows = windows) {
5036
5039
  if (opener)
5037
5040
  openedBy.set(screen, opener.name);
5038
5041
  }
5039
- return { fullRowMs: FULL_ROW_MS, commitIdentity: processes.size <= 1, openedBy };
5042
+ const [only] = processes.size === 1 ? processes : [];
5043
+ const commitTops = new Map(commitStreams.filter((s) => s.process === only).flatMap((s) => s.commits.map((c) => [c.seq, c])));
5044
+ return { fullRowMs: FULL_ROW_MS, commitIdentity: processes.size <= 1, ...commitTops.size ? { commitTops } : {}, openedBy };
5040
5045
  }
5041
5046
  function deriveRun(input) {
5042
5047
  const { run, markers = [], ri, clockOffsetMs, clockErrorMs, samples = [], memWindow } = input;
@@ -5059,7 +5064,7 @@ function deriveRun(input) {
5059
5064
  return { rows, steps, run: final, profiles: [], derivationVersion: version };
5060
5065
  const budget = input.frameBudgetMs ?? (version >= 6 ? samplerFrameBudgetMs(samplesInRun(samples, final.t0, final.t1)) : undefined);
5061
5066
  const compareMs = version >= 20 ? LIMIT_FRAME_MS : budget ?? frameBudgetMs();
5062
- const allComponents = profileOptions(windows, version, input.windows);
5067
+ const allComponents = profileOptions(windows, version, input.windows, input.commitStreams);
5063
5068
  const prof = profileMetrics(windows, steps, {
5064
5069
  ...allComponents,
5065
5070
  frameBudgetMs: version >= 11 ? compareMs : Number(compareMs.toFixed(1)),
@@ -5215,6 +5220,25 @@ var markerEnv = (url, runId) => ({
5215
5220
  var upperFirst = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
5216
5221
  var lowerFirst = (s) => s ? s.charAt(0).toLowerCase() + s.slice(1) : s;
5217
5222
 
5223
+ // packages/present/dist/src/names.js
5224
+ var textPart = (text) => ({ text });
5225
+ var componentPart = (text) => ({ text, component: true });
5226
+ var namePartsText = (parts) => parts.map((p) => p.text).join("");
5227
+ var plainParts = (text) => [textPart(text)];
5228
+ function nameParts(parts) {
5229
+ const out = [];
5230
+ for (const p of parts) {
5231
+ if (!p.text)
5232
+ continue;
5233
+ const last = out.at(-1);
5234
+ if (last && !last.component && !p.component)
5235
+ out[out.length - 1] = textPart(last.text + p.text);
5236
+ else
5237
+ out.push(p);
5238
+ }
5239
+ return out;
5240
+ }
5241
+
5218
5242
  // packages/present/dist/src/placeWords.js
5219
5243
  var GENERIC_GROUPS = new Set(["app", "tabs", "root"]);
5220
5244
  var ORDINAL_SUFFIXES = ["th", "st", "nd", "rd"];
@@ -5250,6 +5274,7 @@ var STATE_WORDS = {
5250
5274
  "no-change": "No change",
5251
5275
  "cant-tell": "Can’t tell"
5252
5276
  };
5277
+ var STATE_ORDER = ["worse", "warning", "cant-tell", "better", "no-change"];
5253
5278
  var INFO_WORDS = { heat: "Phone heated up" };
5254
5279
  var isWorseState = (state) => state === "worse";
5255
5280
  var NOT_BLOCKING = "not blocking";
@@ -5384,12 +5409,18 @@ var screenLeadText = (tier, subject, measure, main, pr) => `${STATE_WORDS[TIER_S
5384
5409
  var FOR_INFORMATION = "For information";
5385
5410
  var informationLeadText = (subject, measure, main, pr) => `${FOR_INFORMATION}: ${leadBody(subject, measure, main, pr)}`;
5386
5411
  function placeShownName(p, o = {}) {
5387
- const target = p.target ? componentShownName(p.target.name, p.target.file) : libraryViewName(p.kind, p.host ?? "");
5388
- const named = p.target && p.parent ? `${target} in ${componentShownName(p.parent.name, p.parent.file)}${p.k ? `, ${ordinal(p.k)}` : ""}` : target;
5389
- return o.onScreen ? `${named} on ${o.onScreen}` : named;
5412
+ return namePartsText(placeShownNameParts(p, o));
5413
+ }
5414
+ function placeShownNameParts(p, o = {}) {
5415
+ const target = p.target ? componentNameParts(p.target.name, p.target.file) : plainParts(libraryViewName(p.kind, p.host ?? ""));
5416
+ const named = p.target && p.parent ? [...target, textPart(" in "), ...componentNameParts(p.parent.name, p.parent.file), textPart(p.k ? `, ${ordinal(p.k)}` : "")] : target;
5417
+ const screen = typeof o.onScreen === "string" ? plainParts(o.onScreen) : o.onScreen;
5418
+ return nameParts(screen?.length && namePartsText(screen) ? [...named, textPart(" on "), ...screen] : named);
5390
5419
  }
5391
5420
  var libraryViewName = (kind, hostType) => `${kind === "drag" ? "Scroll" : "Tap"} outside app components (${hostType})`;
5392
- function componentShownName(name, file) {
5421
+ var componentShownName = (name, file) => componentFileName(name, file);
5422
+ var componentNameParts = (name, file) => [componentPart(componentFileName(name, file))];
5423
+ function componentFileName(name, file) {
5393
5424
  if (name !== "default" || !file)
5394
5425
  return name;
5395
5426
  const path = file.replace(/\.[^.\/]+$/, "");
@@ -5456,21 +5487,36 @@ function foldTwins(rows, placeOf, also = () => "") {
5456
5487
  return { rows: out, folded: rows.length - out.length };
5457
5488
  }
5458
5489
  var foldedTwinsText = (n) => `${n} “${PLACE_KIND_WORDS.after3s}” ${n === 1 ? "twin" : "twins"} folded into ${n === 1 ? "its place" : "their places"}`;
5459
- function placeIdShownName(id, o = {}) {
5490
+ function placeIdShownNameParts(id, o = {}) {
5460
5491
  const p = placeParts(id);
5461
5492
  if (!p)
5462
5493
  return null;
5463
5494
  const component = (part) => ({ name: part.name, file: part.file ?? "" });
5464
- const screen = p.screen.name === "(no screen)" ? "" : p.screen.name === "(outside app components)" ? screenName(p.screen.file ?? "") : componentShownName(p.screen.name, p.screen.file ?? "");
5495
+ const screen = p.screen.name === "(no screen)" ? [] : p.screen.name === "(outside app components)" ? [textPart(screenName(p.screen.file ?? ""))] : componentNameParts(p.screen.name, p.screen.file ?? "");
5465
5496
  if (p.kind === "screen" || p.kind === "launch")
5466
- return screen + (p.kind === "screen" && typeof p.visit === "number" && p.visit > 1 ? ` (${ordinal(p.visit)} visit)` : "");
5467
- return placeShownName({ kind: p.kind, target: p.outsideHost === null ? component(p.target) : null, parent: p.parent ? component(p.parent) : null, k: p.k, ...p.outsideHost === null ? {} : { host: p.outsideHost } }, { onScreen: o.onScreen === false ? undefined : screen });
5497
+ return nameParts([...screen, textPart(p.kind === "screen" && typeof p.visit === "number" && p.visit > 1 ? ` (${ordinal(p.visit)} visit)` : "")]);
5498
+ return placeShownNameParts({ kind: p.kind, target: p.outsideHost === null ? component(p.target) : null, parent: p.parent ? component(p.parent) : null, k: p.k, ...p.outsideHost === null ? {} : { host: p.outsideHost } }, { onScreen: o.onScreen === false ? undefined : screen });
5468
5499
  }
5469
5500
  var isLibraryPlace = (id) => placeParts(id)?.outsideHost != null;
5470
- function stemName(stem, kind) {
5471
- const shown = placeIdShownName(stem);
5501
+ function dropTail(parts, n) {
5502
+ const out = [];
5503
+ for (const p of [...parts].reverse()) {
5504
+ if (n >= p.text.length) {
5505
+ n -= p.text.length;
5506
+ continue;
5507
+ }
5508
+ out.unshift({ ...p, text: p.text.slice(0, p.text.length - n) });
5509
+ n = 0;
5510
+ }
5511
+ return out;
5512
+ }
5513
+ function stemNameParts(stem, kind) {
5514
+ const shown = placeIdShownNameParts(stem);
5472
5515
  if (shown !== null)
5473
- return isLibraryPlace(stem) ? shown : `${KIND_WORD[kind]}: ${shown}`;
5516
+ return nameParts(isLibraryPlace(stem) ? shown : [textPart(`${KIND_WORD[kind]}: `), ...shown]);
5517
+ return plainParts(stemText(stem, kind));
5518
+ }
5519
+ function stemText(stem, kind) {
5474
5520
  if (kind === "journey")
5475
5521
  return PLACE_KIND_WORDS.journey;
5476
5522
  if (kind === "other")
@@ -5486,12 +5532,13 @@ function stemName(stem, kind) {
5486
5532
  const visit = visitMark ? Number(visitMark[2]) : 1;
5487
5533
  return `${PLACE_KIND_WORDS.screen}: ${screenName(visitMark ? visitMark[1] : rest)}${visit > 1 ? ` (${ordinal(visit)} visit)` : ""}`;
5488
5534
  }
5489
- function windowName(id) {
5535
+ var windowName = (id) => namePartsText(windowNameParts(id));
5536
+ function windowNameParts(id) {
5490
5537
  if (id === "flow")
5491
- return PLACE_KIND_WORDS.journey;
5538
+ return plainParts(PLACE_KIND_WORDS.journey);
5492
5539
  const ref = placeRef(id);
5493
- const name = stemName(ref.stem, ref.kind);
5494
- return ref.segment === "after3s" ? `${name} · ${PLACE_KIND_WORDS.after3s}` : name;
5540
+ const name = stemNameParts(ref.stem, ref.kind);
5541
+ return ref.segment === "after3s" ? nameParts([...name, textPart(` · ${PLACE_KIND_WORDS.after3s}`)]) : name;
5495
5542
  }
5496
5543
  function placeKindWord(id) {
5497
5544
  if (id === "flow")
@@ -5499,31 +5546,33 @@ function placeKindWord(id) {
5499
5546
  const { kind } = placeRef(id);
5500
5547
  return kind === "other" ? "Window" : KIND_WORD[kind];
5501
5548
  }
5502
- function windowQualified(id) {
5549
+ var windowQualified = (id) => namePartsText(windowQualifiedParts(id));
5550
+ function windowQualifiedParts(id) {
5503
5551
  const word = placeOpenWord(id);
5504
- const label = placeLabel(id);
5552
+ const label = placeLabelParts(id);
5505
5553
  if (!word)
5506
5554
  return label;
5507
5555
  const after = placeRef(id).segment === "after3s" ? ` · ${PLACE_KIND_WORDS.after3s}` : "";
5508
- return `${after ? label.slice(0, -after.length) : label} (${word})${after}`;
5556
+ return nameParts([...after ? dropTail(label, after.length) : label, textPart(` (${word})${after}`)]);
5509
5557
  }
5510
5558
  var ELEMENT_SEPARATORS = /(?<!\d)\.|\.(?!\d)|[_-]+/g;
5511
5559
  var elementWords = (testId) => testId.replace(ELEMENT_SEPARATORS, " ").replace(/\s+/g, " ").trim();
5512
- function placeLabel(id, screen) {
5560
+ var placeLabel = (id, screen) => namePartsText(placeLabelParts(id, screen));
5561
+ function placeLabelParts(id, screen) {
5513
5562
  if (id === "flow")
5514
- return PLACE_KIND_WORDS.journey;
5563
+ return plainParts(PLACE_KIND_WORDS.journey);
5515
5564
  const ref = placeRef(id);
5516
5565
  const after = ref.segment === "after3s" ? ` · ${PLACE_KIND_WORDS.after3s}` : "";
5517
- const shown = placeIdShownName(ref.stem);
5566
+ const shown = placeIdShownNameParts(ref.stem);
5518
5567
  if (shown !== null)
5519
- return isLibraryPlace(ref.stem) ? `${shown}${after}` : `${KIND_WORD[ref.kind]} · ${shown}${after}`;
5568
+ return nameParts([...isLibraryPlace(ref.stem) ? [] : [textPart(`${KIND_WORD[ref.kind]} · `)], ...shown, textPart(after)]);
5520
5569
  if (ref.kind === "press" || ref.kind === "drag" || ref.kind === "step") {
5521
5570
  const element = elementWords(ref.stem.slice(ref.stem.indexOf(":") + 1).replace(OPEN_MARK, ""));
5522
- return `${KIND_WORD[ref.kind]} · “${element}”${screen ? ` · ${screen}` : ""}${after}`;
5571
+ return plainParts(`${KIND_WORD[ref.kind]} · “${element}”${screen ? ` · ${screen}` : ""}${after}`);
5523
5572
  }
5524
5573
  const name = windowName(ref.stem);
5525
5574
  const colon = name.indexOf(": ");
5526
- return `${colon > 0 && ref.kind !== "other" ? `${name.slice(0, colon)} · ${name.slice(colon + 2)}` : name}${after}`;
5575
+ return plainParts(`${colon > 0 && ref.kind !== "other" ? `${name.slice(0, colon)} · ${name.slice(colon + 2)}` : name}${after}`);
5527
5576
  }
5528
5577
  // packages/present/dist/src/placeNames.js
5529
5578
  function shortestPaths(files) {
@@ -5542,42 +5591,28 @@ function shortestPaths(files) {
5542
5591
  }
5543
5592
  return out;
5544
5593
  }
5545
- // packages/present/dist/src/problems.js
5546
- var FAR_OVER_BUDGET_WORD = "Far over budget";
5547
- var PROBLEM_TAG_WORDS = { new: NEW_IN_PR_CHIP, main: "Also on main", none: NOT_COMPARED_WORD };
5548
- var PROBLEM_TAG_LINE_WORDS = { new: "new in this PR", main: "also on main", none: "not compared with main" };
5549
- var problems = (n) => `${n.toLocaleString("en-US")} ${n === 1 ? "problem" : "problems"}`;
5550
- var places = (n) => `${n.toLocaleString("en-US")} ${n === 1 ? "place" : "places"}`;
5551
- function problemCountsText(c, own) {
5552
- const tags = c.tags;
5553
- const split = tags ? `: ${[`${tags.new} ${PROBLEM_TAG_LINE_WORDS.new}`, ...["main", "none"].filter((t) => tags[t]).map((t) => `${tags[t]} ${PROBLEM_TAG_LINE_WORDS[t]}`)].join(", ")}` : "";
5554
- return `${problems(c.count)} at ${places(c.places)} in ${own} own runs${split}${c.far_count ? `; ${c.far_count} of them ${FAR_OVER_BUDGET_WORD.toLowerCase()}` : ""}`;
5555
- }
5556
- var newProblems = (n) => `${n.toLocaleString("en-US")} new ${n === 1 ? "problem" : "problems"}`;
5557
- var PROBLEMS_BLOCK_WORDS = {
5558
- branch: {
5559
- heading: "New problems in this branch",
5560
- sub: "too slow on their own, whatever main does; they don’t block the merge",
5561
- seeAll: (n) => `See all ${newProblems(n)}`,
5562
- alsoLine: (n) => `${problems(n)} also on main`,
5563
- notComparedLine: (n) => `${problems(n)} not compared with main`,
5564
- choices: { new: PROBLEM_TAG_WORDS.new, also: PROBLEM_TAG_WORDS.main, not_compared: PROBLEM_TAG_WORDS.none },
5565
- panelTitle: "Problems in this branch",
5566
- panelNote: "Each one is too slow in this branch’s own runs, whatever main does. Problems never block.",
5567
- filter: "Which problems"
5568
- },
5569
- main: {
5570
- heading: "New problems in this session",
5571
- sub: "too slow on their own, whatever earlier sessions did",
5572
- seeAll: (n) => `See all ${newProblems(n)}`,
5573
- alsoLine: (n) => `${problems(n)} also in earlier sessions`,
5574
- notComparedLine: (n) => `${problems(n)} not compared with earlier sessions`,
5575
- choices: { new: "New in this session", also: "Also in earlier sessions", not_compared: "Not compared with earlier sessions" },
5576
- panelTitle: "Problems in this session",
5577
- panelNote: "Each one is too slow in this session’s own runs, whatever earlier sessions did. Problems never block.",
5578
- filter: "Which problems"
5579
- }
5594
+ // packages/present/dist/src/words.js
5595
+ var REASON_WORDS = {
5596
+ "no-baseline": "there are no main runs on this phone and journey yet",
5597
+ "too-few-runs": "there are too few valid runs on a side",
5598
+ "judged-measures-missing": "the judged measures are missing on a side",
5599
+ "measured-differently": "the sides were measured differently",
5600
+ "runs-unknown": "a change that would count has no value in some runs, so it cannot be confirmed in every run",
5601
+ "facts-version-mismatch": "the sides were derived with different versions",
5602
+ "not-judged-on-simulators": "timing is not judged on simulators",
5603
+ "platforms-differ": "the sides ran on different platforms (iOS and Android are never compared)",
5604
+ "not-enough-main": "main does not have enough sessions on this phone and journey yet"
5580
5605
  };
5606
+ var waitingWords = (runs) => `${runs} main ${runs === 1 ? "run waits" : "runs wait"} for a reprocess`;
5607
+ var AT_MOST = "≤ ";
5608
+ var atMost = (text, isBound) => isBound ? `${AT_MOST}${text}` : text;
5609
+ var AT_LEAST = "≥ ";
5610
+ var atLeast = (text, isBound) => isBound ? `${AT_LEAST}${text}` : text;
5611
+
5612
+ // packages/present/dist/src/shown.js
5613
+ var feltGainText = (ms) => ms && ms > 0 ? `felt gain −${fieldValueText("jank_ms", ms)}` : null;
5614
+ var feltCostText = (ms) => ms && ms > 0 ? `felt cost +${fieldValueText("jank_ms", ms)}` : null;
5615
+
5581
5616
  // packages/present/dist/src/stability.js
5582
5617
  var UNSTABLE_WORDS = {
5583
5618
  short: "main varies too much here",
@@ -5724,7 +5759,7 @@ var READ_USAGE = {
5724
5759
  page: "--page must be a whole number from 1; use it only with places or compare.",
5725
5760
  all: "--all is for places or compare; do not combine it with --page, --offset or --short.",
5726
5761
  detailFlag: " --detail (places: include recorded run detail; also included by --verbose or component paging)",
5727
- detail: "Include recorded per-run component and window detail.",
5762
+ detail: "Include recorded per-run component and window detail: an opt-in diagnostic that reads the runs' recorded profiles (slower); ranking places never needs it.",
5728
5763
  compareTop: "--top-components must be a whole number from 1 to 200 for compare.",
5729
5764
  top: "--top-components must be a whole number from 0 to 20, or from 1 to 200 with --page or --all.",
5730
5765
  paging: "The API did not advance the component page; stopped without claiming the list is complete.",
@@ -5734,23 +5769,119 @@ var READ_USAGE = {
5734
5769
  var componentListsText = (totals, shown) => `Stored component rows: ${shown.rows} of ${totals.rows} compared; ${shown.added} of ${totals.added} ${READ_WORDS.onlyB.toLowerCase()}; ${shown.removed} of ${totals.removed} ${READ_WORDS.onlyA.toLowerCase()}.`;
5735
5770
  var notMeasuredText = (side) => `${READ_WORDS.notMeasured} in run ${side.toUpperCase()}`;
5736
5771
 
5737
- // packages/present/dist/src/words.js
5738
- var REASON_WORDS = {
5739
- "no-baseline": "there are no main runs on this phone and journey yet",
5740
- "too-few-runs": "there are too few valid runs on a side",
5741
- "judged-measures-missing": "the judged measures are missing on a side",
5742
- "measured-differently": "the sides were measured differently",
5743
- "runs-unknown": "a change that would count has no value in some runs, so it cannot be confirmed in every run",
5744
- "facts-version-mismatch": "the sides were derived with different versions",
5745
- "not-judged-on-simulators": "timing is not judged on simulators",
5746
- "platforms-differ": "the sides ran on different platforms (iOS and Android are never compared)",
5747
- "not-enough-main": "main does not have enough sessions on this phone and journey yet"
5772
+ // packages/present/dist/src/problems.js
5773
+ var FAR_OVER_BUDGET_WORD = "Far over budget";
5774
+ var heaviestWords = (name, value) => [textPart(`${HEAVIEST_WORD}: `), componentPart(name), ...value ? [textPart(` ${value}`)] : []];
5775
+ var HEAVIEST_WORD = "heaviest";
5776
+ var PROBLEM_TAG_WORDS = { new: NEW_IN_PR_CHIP, main: "Also on main", none: NOT_COMPARED_WORD };
5777
+ var PROBLEM_TAG_LINE_WORDS = { new: "new in this PR", main: "also on main", none: "not compared with main" };
5778
+ var problems = (n) => `${n.toLocaleString("en-US")} ${n === 1 ? "problem" : "problems"}`;
5779
+ var places = (n) => `${n.toLocaleString("en-US")} ${n === 1 ? "place" : "places"}`;
5780
+ var problemsAtPlaces = (n, at) => `${problems(n)} at ${places(at)}`;
5781
+ function problemCountsText(c, own) {
5782
+ const tags = c.tags;
5783
+ const split = tags ? `: ${[`${tags.new} ${PROBLEM_TAG_LINE_WORDS.new}`, ...["main", "none"].filter((t) => tags[t]).map((t) => `${tags[t]} ${PROBLEM_TAG_LINE_WORDS[t]}`)].join(", ")}` : "";
5784
+ return `${problemsAtPlaces(c.count, c.places)} in ${own} own runs${split}${c.far_count ? `; ${c.far_count} of them ${FAR_OVER_BUDGET_WORD.toLowerCase()}` : ""}`;
5785
+ }
5786
+ var newProblems = (n) => `${n.toLocaleString("en-US")} new ${n === 1 ? "problem" : "problems"}`;
5787
+ var PROBLEMS_BLOCK_WORDS = {
5788
+ branch: {
5789
+ heading: "New problems in this branch",
5790
+ sub: "too slow on their own, whatever main does; they don’t block the merge",
5791
+ seeAll: (n) => `See all ${newProblems(n)}`,
5792
+ alsoLine: (n, at) => `${PROBLEM_TAG_WORDS.main}: ${problemsAtPlaces(n, at)}`,
5793
+ notComparedLine: (n, at) => `${PROBLEM_TAG_WORDS.none}: ${problemsAtPlaces(n, at)}`,
5794
+ choices: { new: PROBLEM_TAG_WORDS.new, also: PROBLEM_TAG_WORDS.main, not_compared: PROBLEM_TAG_WORDS.none },
5795
+ panelTitle: "Problems in this branch",
5796
+ panelNote: "Each one is too slow in this branch’s own runs, whatever main does. Problems never block.",
5797
+ filter: "Which problems"
5798
+ },
5799
+ main: {
5800
+ heading: "New problems in this session",
5801
+ sub: "too slow on their own, whatever earlier sessions did",
5802
+ seeAll: (n) => `See all ${newProblems(n)}`,
5803
+ alsoLine: (n, at) => `Also in earlier sessions: ${problemsAtPlaces(n, at)}`,
5804
+ notComparedLine: (n, at) => `Not compared with earlier sessions: ${problemsAtPlaces(n, at)}`,
5805
+ choices: { new: "New in this session", also: "Also in earlier sessions", not_compared: "Not compared with earlier sessions" },
5806
+ panelTitle: "Problems in this session",
5807
+ panelNote: "Each one is too slow in this session’s own runs, whatever earlier sessions did. Problems never block.",
5808
+ filter: "Which problems"
5809
+ }
5748
5810
  };
5749
- var waitingWords = (runs) => `${runs} main ${runs === 1 ? "run waits" : "runs wait"} for a reprocess`;
5750
- var AT_MOST = "≤ ";
5751
- var atMost = (text, isBound) => isBound ? `${AT_MOST}${text}` : text;
5752
- var AT_LEAST = "≥ ";
5753
- var atLeast = (text, isBound) => isBound ? `${AT_LEAST}${text}` : text;
5811
+ // packages/present/dist/src/place-tier.js
5812
+ var slowerCountsText = (n, sep = " · ") => [n.worse ? `${n.worse} ${STATE_WORDS.worse.toLowerCase()}` : "", n.slight ? `${n.slight} ${STATE_WORDS.warning.toLowerCase()}` : ""].filter(Boolean).join(sep);
5813
+ // packages/present/dist/src/verdict-section.js
5814
+ var places2 = (n) => `${n.toLocaleString("en-US")} ${n === 1 ? "place" : "places"}`;
5815
+ var of = (n, total) => `${n.toLocaleString("en-US")} of ${places2(total)}`;
5816
+ var runs = (n) => `${n} valid ${n === 1 ? "run" : "runs"}`;
5817
+ var VERDICT_SECTION_WORDS = {
5818
+ slower: (n, total, slight) => `${slight ? "A little slower" : "Slower"} at ${of(n, total)}`,
5819
+ blocksMerge: "blocks the merge",
5820
+ faster: (n, total) => `Faster at ${of(n, total)}, slower at none`,
5821
+ mixed: (slower, faster, total) => `Slower at ${of(slower, total)}, faster at ${faster.toLocaleString("en-US")}`,
5822
+ unpublishedSdk: "ran an unpublished SDK build",
5823
+ noneMoved: (total, judged = total) => judged < total ? `No place moved out of its normal range (${judged.toLocaleString("en-US")} of ${total.toLocaleString("en-US")} judged)` : `No place moved out of its normal range (${places2(total)})`,
5824
+ noneMovedMeasured: (measured, same) => same ? `Nothing moved at ${of(same, same + measured)}; ${measured} measured differently` : `Nothing moved; ${measured} ${measured === 1 ? "place was" : "places were"} measured differently`,
5825
+ tooFewRuns: (valid, needed) => `Not compared: ${runs(valid)} of ${needed} needed`,
5826
+ tooFewMainRuns: (valid, needed) => `Not compared: main has ${runs(valid)} of ${needed} needed`,
5827
+ tooFewBefore: (valid) => `Not compared: the sessions before have only ${runs(valid)}`,
5828
+ noBaseline: { branch: "Nothing to compare with: no main runs on this phone yet", main: "Nothing to compare with: no earlier sessions on this phone" },
5829
+ noisy: "Not judged: main’s own runs vary too much at every place here",
5830
+ versionMismatch: "Not compared: the sides were computed with different analysis versions",
5831
+ unconfirmed: (n) => `Can’t confirm ${places2(n)}: a run has no value there`,
5832
+ notCompared: (reason) => `Not compared: ${reason}`,
5833
+ notJudgedYet: (why) => `Not judged yet: ${why} on this phone`,
5834
+ collecting: (collected, baseline, after) => after ? `Main is collecting a new baseline after ${after}: ${collected} of ${baseline} sessions` : `Main is collecting its baseline: ${collected} of ${baseline} sessions`,
5835
+ doesNotBlock: "doesn’t block",
5836
+ fixes: {
5837
+ branch: {
5838
+ "no-baseline": "Add this journey on this phone to main’s nightly runs.",
5839
+ "too-few-runs": "Run this branch again on this phone.",
5840
+ "judged-measures-missing": "Record the judged measures, then run again.",
5841
+ "runs-unknown": "Run this branch again; a full set of runs decides it.",
5842
+ "facts-version-mismatch": "Recompute both sides in Setup; no new runs are needed.",
5843
+ "platforms-differ": "Add this phone’s platform to main’s nightly runs."
5844
+ },
5845
+ main: {
5846
+ "no-baseline": "Nothing to do: main’s next 3 nightly sessions make a result possible.",
5847
+ "too-few-runs": "Main’s next nightly session decides it.",
5848
+ "runs-unknown": "Main’s next nightly session decides it.",
5849
+ "judged-measures-missing": "Record the judged measures on main; its next nightly session is compared.",
5850
+ "facts-version-mismatch": "Recompute both sides in Setup; no new runs are needed."
5851
+ }
5852
+ },
5853
+ waitForMain: { branch: "Nothing to do on this branch; main’s next nightly session decides it.", main: "Main’s next nightly session decides it." },
5854
+ sides: {
5855
+ branch: { left: "Main", right: "This branch", rightShort: "Branch" },
5856
+ main: { left: "Sessions before", right: "Newest session", rightShort: "Newest" },
5857
+ runSet: { left: "Main", right: "This run set", rightShort: "Run set" }
5858
+ },
5859
+ branchSides: (name) => ({ left: "Main", right: name || "This branch", rightShort: "Branch" }),
5860
+ inProgress: (arrived, expected) => `${arrived} of ${expected} runs arrived; the result comes with the last one`,
5861
+ whatChangedLabel: "What changed",
5862
+ moreSlower: (n) => `+${n.toLocaleString("en-US")} more slower`,
5863
+ slowerPanel: (n) => `${places2(n)} slower`,
5864
+ forInformation: "for information",
5865
+ allFilter: "All",
5866
+ filter: {
5867
+ label: "Show places",
5868
+ choice: (tierWord) => upperFirst(tierWord),
5869
+ showing: (choice, n) => `Showing ${choice.toLowerCase()}: ${n}`
5870
+ },
5871
+ countsLabel: "Places by result",
5872
+ links: {
5873
+ openChart: "Open chart",
5874
+ seeAll: (n) => `See all ${places2(n)}`,
5875
+ whichPlaces: "Which places",
5876
+ whichChanges: "Which changes",
5877
+ howToFix: "How to fix",
5878
+ whatDiffers: "What differs",
5879
+ whatChanged: "What changed",
5880
+ openSetup: "Open setup",
5881
+ openScreen: "Open screen"
5882
+ }
5883
+ };
5884
+
5754
5885
  // packages/present/dist/src/main-history.js
5755
5886
  var MAIN_TIER_WORDS = {
5756
5887
  same: "In norm",
@@ -5832,74 +5963,12 @@ function mainCutWords(v) {
5832
5963
  return parts.length ? parts.join(" ") : "Measures alike; no baseline cut.";
5833
5964
  }
5834
5965
  var BY_RANK = Object.keys(MAIN_TIER_RANK).sort((a, b) => MAIN_TIER_RANK[a] - MAIN_TIER_RANK[b]);
5835
- // packages/present/dist/src/verdict-section.js
5836
- var places2 = (n) => `${n.toLocaleString("en-US")} ${n === 1 ? "place" : "places"}`;
5837
- var of = (n, total) => `${n.toLocaleString("en-US")} of ${places2(total)}`;
5838
- var runs = (n) => `${n} valid ${n === 1 ? "run" : "runs"}`;
5839
- var VERDICT_SECTION_WORDS = {
5840
- slower: (n, total, slight) => `${slight ? "A little slower" : "Slower"} at ${of(n, total)}`,
5841
- blocksMerge: "blocks the merge",
5842
- faster: (n, total) => `Faster at ${of(n, total)}, slower at none`,
5843
- mixed: (slower, faster, total) => `Slower at ${of(slower, total)}, faster at ${faster.toLocaleString("en-US")}`,
5844
- unpublishedSdk: "ran an unpublished SDK build",
5845
- noneMoved: (total, judged = total) => judged < total ? `No place moved out of its normal range (${judged.toLocaleString("en-US")} of ${total.toLocaleString("en-US")} judged)` : `No place moved out of its normal range (${places2(total)})`,
5846
- noneMovedMeasured: (measured, same) => same ? `Nothing moved at ${of(same, same + measured)}; ${measured} measured differently` : `Nothing moved; ${measured} ${measured === 1 ? "place was" : "places were"} measured differently`,
5847
- tooFewRuns: (valid, needed) => `Not compared: ${runs(valid)} of ${needed} needed`,
5848
- tooFewMainRuns: (valid, needed) => `Not compared: main has ${runs(valid)} of ${needed} needed`,
5849
- tooFewBefore: (valid) => `Not compared: the sessions before have only ${runs(valid)}`,
5850
- noBaseline: { branch: "Nothing to compare with: no main runs on this phone yet", main: "Nothing to compare with: no earlier sessions on this phone" },
5851
- noisy: "Not judged: main’s own runs vary too much at every place here",
5852
- versionMismatch: "Not compared: the sides were computed with different analysis versions",
5853
- unconfirmed: (n) => `Can’t confirm ${places2(n)}: a run has no value there`,
5854
- notCompared: (reason) => `Not compared: ${reason}`,
5855
- notJudgedYet: (why) => `Not judged yet: ${why} on this phone`,
5856
- collecting: (collected, baseline, after) => after ? `Main is collecting a new baseline after ${after}: ${collected} of ${baseline} sessions` : `Main is collecting its baseline: ${collected} of ${baseline} sessions`,
5857
- doesNotBlock: "doesn’t block",
5858
- fixes: {
5859
- branch: {
5860
- "no-baseline": "Add this journey on this phone to main’s nightly runs.",
5861
- "too-few-runs": "Run this branch again on this phone.",
5862
- "judged-measures-missing": "Record the judged measures, then run again.",
5863
- "runs-unknown": "Run this branch again; a full set of runs decides it.",
5864
- "facts-version-mismatch": "Recompute both sides in Setup; no new runs are needed.",
5865
- "platforms-differ": "Add this phone’s platform to main’s nightly runs."
5866
- },
5867
- main: {
5868
- "no-baseline": "Nothing to do: main’s next 3 nightly sessions make a result possible.",
5869
- "too-few-runs": "Main’s next nightly session decides it.",
5870
- "runs-unknown": "Main’s next nightly session decides it.",
5871
- "judged-measures-missing": "Record the judged measures on main; its next nightly session is compared.",
5872
- "facts-version-mismatch": "Recompute both sides in Setup; no new runs are needed."
5873
- }
5874
- },
5875
- waitForMain: { branch: "Nothing to do on this branch; main’s next nightly session decides it.", main: "Main’s next nightly session decides it." },
5876
- sides: {
5877
- branch: { left: "Main", right: "This branch", rightShort: "Branch" },
5878
- main: { left: "Sessions before", right: "Newest session", rightShort: "Newest" },
5879
- runSet: { left: "Main", right: "This run set", rightShort: "Run set" }
5880
- },
5881
- branchSides: (name) => ({ left: "Main", right: name || "This branch", rightShort: "Branch" }),
5882
- inProgress: (arrived, expected) => `${arrived} of ${expected} runs arrived; the result comes with the last one`,
5883
- whatChangedLabel: "What changed",
5884
- forInformation: "for information",
5885
- allFilter: "All",
5886
- filter: {
5887
- label: "Show places",
5888
- choice: (tierWord) => upperFirst(tierWord),
5889
- showing: (choice, n) => `Showing ${choice.toLowerCase()}: ${n}`
5890
- },
5891
- countsLabel: "Places by result",
5892
- links: {
5893
- openChart: "Open chart",
5894
- seeAll: (n) => `See all ${places2(n)}`,
5895
- whichPlaces: "Which places",
5896
- whichChanges: "Which changes",
5897
- howToFix: "How to fix",
5898
- whatDiffers: "What differs",
5899
- whatChanged: "What changed",
5900
- openSetup: "Open setup",
5901
- openScreen: "Open screen"
5902
- }
5966
+ var MEMORY_HELD_WORDS = { head: "Memory", rest: "still held after the journey", text: "Memory still held after the journey" };
5967
+ var MAIN_CHART_TITLES = {
5968
+ renders: "Places vs their normal range",
5969
+ rendersTip: (baseline) => `Share of the journey’s places per session, by result against main’s ${baseline} sessions before it. Dashed: not judged yet, while main collects ${baseline} sessions on the setup (after main’s start or a change).`,
5970
+ memory: `${MEMORY_HELD_WORDS.text} · MB`,
5971
+ memoryTip: "Memory kept by the journey: what it still holds after the clean-up at its end. Line: the median of a session; band: the spread inside it."
5903
5972
  };
5904
5973
  // packages/present/dist/src/summary-message.js
5905
5974
  var SUMMARY_WORDS = {
@@ -5916,6 +5985,9 @@ var SUMMARY_WORDS = {
5916
5985
  measuredDifferently: "measured differently",
5917
5986
  namesChanged: (sdk, mainSessions, needed) => `Main: ${mainSessions} of ${needed} sessions on SDK ${sdkTag(sdk)}`,
5918
5987
  morePlaces: (n) => `+${n} more ${n === 1 ? "place" : "places"} changed`,
5988
+ slowerPlaces: (n, split) => `${n} ${n === 1 ? "place" : "places"} slower${split && slowerCountsText(split) ? `: ${slowerCountsText(split, ", ")}` : ""}`,
5989
+ noPlaceSlower: "no place slower",
5990
+ toCheck: (n) => `${n} ${n === 1 ? "component" : "components"} to check`,
5919
5991
  compared: {
5920
5992
  branch: (sessions, days) => `vs main’s last ${sessions}${days}`,
5921
5993
  main: (sessions, days) => `newest session vs main’s ${sessions} before${days}`,
@@ -5932,7 +6004,7 @@ var SUMMARY_WORDS = {
5932
6004
  runSet: "Open the run set"
5933
6005
  }
5934
6006
  };
5935
- var ORDER = ["worse", "warning", "cant-tell", "not-computed", "better", "no-change"];
6007
+ var ORDER = [...STATE_ORDER.slice(0, 3), "not-computed", ...STATE_ORDER.slice(3)];
5936
6008
  var rank = (l) => ORDER.indexOf(l.state);
5937
6009
  var WORD = { ...STATE_WORDS, "not-computed": SUMMARY_WORDS.notComputed };
5938
6010
  var shownLane = (lanes) => [...lanes].sort((a, b) => rank(a) - rank(b) || b.places - a.places)[0];
@@ -5957,6 +6029,17 @@ var changeLine = (c, tier) => {
5957
6029
  const values = c.main !== null && c.now !== null ? `${fromTo(c.main, c.now)}${c.change ? ` (${c.change})` : ""}` : c.change;
5958
6030
  return [c.place ?? upper(c.measure), ...c.place ? [c.measure] : [], values, WORD[c.tier ?? tier]].filter(Boolean).join(" · ");
5959
6031
  };
6032
+ function tierSplit(l) {
6033
+ const worse = new Set, all = new Set;
6034
+ for (const c of l.changes) {
6035
+ const key = c.key ?? c.place ?? c.measure;
6036
+ all.add(key);
6037
+ if ((c.tier ?? l.state) === "worse")
6038
+ worse.add(key);
6039
+ }
6040
+ const split = { worse: worse.size, slight: all.size - worse.size };
6041
+ return l.state === "worse" && !split.slight || l.state === "warning" && !split.worse ? undefined : split;
6042
+ }
5960
6043
  var wordReason = (word) => word?.includes(": ") ? word.slice(word.indexOf(": ") + 2) : null;
5961
6044
  var notJudgedWhy = (l) => l.state === "not-computed" && l.word?.startsWith(`${NOT_JUDGED_WORD}: `) ? upper(l.word.slice(NOT_JUDGED_WORD.length + 2)) : null;
5962
6045
  function why(l) {
@@ -6019,11 +6102,41 @@ function summaryMessage(input) {
6019
6102
  input.pr && input.prNumber ? `${input.branch} (#${input.prNumber})` : input.branch,
6020
6103
  phones === 1 ? l.phone ?? "unknown phone" : SUMMARY_WORDS.phones(phones)
6021
6104
  ].join(" · ");
6105
+ const label = input.pr ? SUMMARY_WORDS.link.comparison : SUMMARY_WORDS.link.session;
6106
+ const link = { label, url: (single ? l.url : null) ?? input.url };
6107
+ if (!input.slack)
6108
+ return { state: l.state, newestSlower, lead, ...withLines(changeLines(l, input.pr, newestSlower)), link };
6022
6109
  const lines = [];
6023
- const basis = compared(l, input.pr, newestSlower);
6110
+ if (l.state === "worse" || l.state === "warning") {
6111
+ const n = l.places || l.changes.length;
6112
+ lines.push(upper([...n ? [SUMMARY_WORDS.slowerPlaces(n, tierSplit(l))] : [], feltCostText(l.feltCostMs)].filter(Boolean).join(" · ") || why(l)));
6113
+ } else if (l.state === "no-change")
6114
+ lines.push(upper(SUMMARY_WORDS.noPlaceSlower));
6115
+ else if (l.state === "cant-tell") {
6116
+ const fix = unblock(l, input.pr);
6117
+ lines.push(fix ? SUMMARY_WORDS.unblock(why(l), fix) : why(l));
6118
+ } else if (l.state === "better")
6119
+ lines.push([why(l), feltGainText(l.feltGainMs)].filter(Boolean).join(" · "));
6120
+ else
6121
+ lines.push(why(l));
6122
+ if (l.toCheckCount)
6123
+ lines.push(upper(SUMMARY_WORDS.toCheck(l.toCheckCount)));
6124
+ const basis = compared(l, input.pr, false);
6024
6125
  const extra = conditions(l, input.pr).join(", ");
6126
+ const heat = l.heatedRuns ? SUMMARY_WORDS.heated(l.heatedRuns, l.runs) : null;
6127
+ const context = [basis ? `${basis}${extra ? `; ${extra}` : ""}` : extra, heat].filter(Boolean).join(" · ");
6128
+ if (context)
6129
+ lines.push(basis ? context : upper(context));
6130
+ return { state: l.state, newestSlower, lead, ...withLines(lines), link };
6131
+ }
6132
+ function changeLines(l, pr, newestSlower) {
6133
+ const lines = [];
6134
+ const basis = compared(l, pr, newestSlower);
6135
+ const extra = conditions(l, pr).join(", ");
6025
6136
  const heat = l.heatedRuns ? upper(SUMMARY_WORDS.heated(l.heatedRuns, l.runs)) : null;
6026
- const tail = [...basis || extra ? [basis ? `${basis}${extra ? `; ${extra}` : ""}` : upper(extra)] : [], ...heat ? [heat] : []];
6137
+ const felt = l.state === "worse" || l.state === "warning" ? feltCostText(l.feltCostMs) : l.state === "better" ? feltGainText(l.feltGainMs) : null;
6138
+ const sums = upper([felt, l.toCheckCount ? SUMMARY_WORDS.toCheck(l.toCheckCount) : null].filter(Boolean).join(" · "));
6139
+ const tail = [...sums ? [sums] : [], ...basis || extra ? [basis ? `${basis}${extra ? `; ${extra}` : ""}` : upper(extra)] : [], ...heat ? [heat] : []];
6027
6140
  if (l.state === "worse" || l.state === "warning" || l.state === "better") {
6028
6141
  const seen = new Set;
6029
6142
  const places = l.changes.filter((c) => {
@@ -6039,12 +6152,11 @@ function summaryMessage(input) {
6039
6152
  if (top.length && more > 0)
6040
6153
  lines.push(SUMMARY_WORDS.morePlaces(more));
6041
6154
  } else {
6042
- const fix = l.state === "cant-tell" ? unblock(l, input.pr) : null;
6155
+ const fix = l.state === "cant-tell" ? unblock(l, pr) : null;
6043
6156
  lines.push(fix ? SUMMARY_WORDS.unblock(why(l), fix) : why(l));
6044
6157
  }
6045
6158
  lines.push(...tail);
6046
- const label = input.pr ? SUMMARY_WORDS.link.comparison : SUMMARY_WORDS.link.session;
6047
- return { state: l.state, newestSlower, lead, ...withLines(lines), link: { label, url: (single ? l.url : null) ?? input.url } };
6159
+ return lines;
6048
6160
  }
6049
6161
  var withLines = (lines) => ({ lines, detail: lines.join(`
6050
6162
  `) });
@@ -6142,13 +6254,16 @@ function laneOf(v, sc, set) {
6142
6254
  namesChanged: sc.context?.namesChanged ?? null,
6143
6255
  url: linkOf(sc, set),
6144
6256
  main: sc.main ?? null,
6145
- reason: sc.headline?.reason ?? sc.headline?.reasons?.[0] ?? null
6257
+ reason: sc.headline?.reason ?? sc.headline?.reasons?.[0] ?? null,
6258
+ feltCostMs: sc.headline?.felt_cost_ms ?? null,
6259
+ feltGainMs: sc.headline?.felt_gain_ms ?? null,
6260
+ toCheckCount: sc.headline?.to_check_count ?? null
6146
6261
  };
6147
6262
  }
6148
6263
  var isPending = (s) => lanesOf(s).every((l) => l.state === "not-computed" && !l.word?.startsWith(`${NOT_JUDGED_WORD}: `));
6149
6264
  var lanesOf = (s) => s.verdicts ? s.verdicts.scenarios.map((sc) => laneOf(s.verdicts, sc, s)) : [];
6150
6265
  var isPr = (s) => !!s.verdicts?.scenarios.some((sc) => sc.role === "comparison");
6151
- var runSetMessage = (s) => summaryMessage({ branch: s.branch, pr: isPr(s), lanes: lanesOf(s), url: linkOf(s) ?? s.dashboard_url, app: s.app ?? null, prNumber: s.pull_request?.number ?? null });
6266
+ var runSetMessage = (s, o = {}) => summaryMessage({ branch: s.branch, pr: isPr(s), lanes: lanesOf(s), url: linkOf(s) ?? s.dashboard_url, app: s.app ?? null, prNumber: s.pull_request?.number ?? null, slack: o.slack });
6152
6267
  var MESSAGE_COLORS = { error: "#D6334A", warn: "#C47A00", better: "#238359", none: "#7C8492" };
6153
6268
  function messageTone(s) {
6154
6269
  const lanes = s ? lanesOf(s) : [];
@@ -6188,9 +6303,9 @@ function resultSlackPayload(s, o = {}) {
6188
6303
  const moved = lanes.some((l) => l.state === "worse" || l.state === "warning");
6189
6304
  const heat = lanes.some((l) => l.heatedRuns > 0);
6190
6305
  const improved = lanes.some((l) => l.state === "better") || s.verdicts.state === "better";
6191
- if (isPr(s) ? !moved && !heat : !moved && !improved && !heat && !o.always)
6306
+ if (!isPr(s) && !moved && !improved && !heat && !o.always)
6192
6307
  return null;
6193
- return slackPayload(runSetMessage(s), messageTone(s));
6308
+ return slackPayload(runSetMessage(s, { slack: true }), messageTone(s));
6194
6309
  }
6195
6310
  var noResultSlackPayload = (branch, reason, url) => slackPayload(noResultMessage(branch, reason, url), { mark: "⚪", color: MESSAGE_COLORS.none });
6196
6311
  var markdown = (m, mark) => `### ${mark} ${m.lead}
@@ -10589,7 +10704,7 @@ async function aa(deps, outDir, opts = {}) {
10589
10704
  }
10590
10705
 
10591
10706
  // apps/agent/src/farm.ts
10592
- import { dirname as dirname3, join as join13, resolve as resolve5 } from "node:path";
10707
+ import { dirname as dirname4, join as join13, resolve as resolve6 } from "node:path";
10593
10708
 
10594
10709
  // packages/appium-driver/dist/src/links.js
10595
10710
  var ALERT_POLL_MS = 250;
@@ -10893,7 +11008,7 @@ import { existsSync, readFileSync, statSync } from "node:fs";
10893
11008
  import { dirname as dirname2, join as join11, relative as relative2, resolve as resolve4 } from "node:path";
10894
11009
  import { fileURLToPath } from "node:url";
10895
11010
  function driverVersion() {
10896
- const built = "0.1.0-alpha.19";
11011
+ const built = "0.1.0-alpha.20";
10897
11012
  if (built)
10898
11013
  return built;
10899
11014
  let dir = dirname2(fileURLToPath(import.meta.url));
@@ -10948,6 +11063,11 @@ var SDK_FLUSH_WORST_CASE_MS = 315000;
10948
11063
  var FLUSH_MARGIN_MS = 30000;
10949
11064
  var FLUSH_WAIT_MS = SDK_FLUSH_WORST_CASE_MS + FLUSH_MARGIN_MS;
10950
11065
  var FLUSH_STATUSES = new Set(["ok", "error", "off"]);
11066
+ // packages/verdict-core/dist/src/version.generated.js
11067
+ var CORE_FINGERPRINT = "1c8a7ed82956dae0";
11068
+
11069
+ // packages/verdict-core/dist/src/version.js
11070
+ var CORE_VERSION = parseInt(CORE_FINGERPRINT.slice(0, 7), 16);
10951
11071
  // packages/verdict-core/dist/src/window-count.js
10952
11072
  var WINDOW_DEPENDENT_JOURNEY_MEASURES = ["sampler.cpu_ms", "sampler.frames.longest_gap_ms"];
10953
11073
  var OFF = new WeakSet;
@@ -11225,7 +11345,7 @@ var DRIVER_RELEASES = [
11225
11345
  { affects: LOADED_MEMORY }
11226
11346
  ],
11227
11347
  uncalibrated: false,
11228
- source: "Drag end points move above the platform bottom system-gesture zone (fix/driver-gesture-zone); start points unchanged; on iOS a shortened drag keeps its duration (slower); on Android it keeps its speed (ends sooner). A drag's end, and the duration of a step whose scroll steps contain such drags, can change. W1 (#306): farm sessions record no video or device logs by default (`--capture off`), so the phone carries less load and every memory measure (mem.*, memory_left_mb, sampler.memory.*, ri.memory.*) is measured differently from capture-on alpha.16 sessions (Elastyczność 2026-10-26 15:45 B1-DIFF; controller Stab 4). The declaration only separates alpha.16 from alpha.17: two alpha.17 sessions with capture on and off still compare unflagged until the run's setup.capture (#344) is judged (follow-up). Screens and taps stay compared (REQ-FARM-35)."
11348
+ source: "Drag end points move above the platform bottom system-gesture zone (fix/driver-gesture-zone); start points unchanged; on iOS a shortened drag keeps its duration (slower); on Android it keeps its speed (ends sooner). A drag's end, and the duration of a step whose scroll steps contain such drags, can change. W1 (#306): farm sessions record no video or device logs by default (`--capture off`), so the phone carries less load and every memory measure (mem.*, memory_left_mb, sampler.memory.*, ri.memory.*) is measured differently from capture-on alpha.16 sessions (Elastyczność 2026-10-26 15:45 B1-DIFF; controller Stab 4). The declaration only separates alpha.16 from alpha.17: two alpha.17 sessions with capture on and off are told apart by the `capture` measured-with aspect (run setup.capture, #344, #426). Screens and taps stay compared (REQ-FARM-35)."
11229
11349
  },
11230
11350
  {
11231
11351
  version: "0.1.0-alpha.18",
@@ -11243,8 +11363,21 @@ var DRIVER_RELEASES = [
11243
11363
  ],
11244
11364
  uncalibrated: false,
11245
11365
  source: "#393 (fix/keepsnappy-clamped-row-drag-travel, appium-driver CHANGELOG 2026-10-27): on iOS a row-to-top drag whose end safeBottomY clamps (chat history on every iPhone with a home indicator) starts 33 pt higher, so it travels 33 pt further in the same 400 ms (a faster release, a longer coast), with one list lookup and two frame reads right before it. Every measure of that drag window can move, React work included (more rows scroll by), and so can the step whose scroll steps contain it. Android and unclamped drags send the same requests as alpha.18 (pinned): declares nothing there (REQ-FARM-35)."
11366
+ },
11367
+ {
11368
+ version: "0.1.0-alpha.20",
11369
+ previous: "0.1.0-alpha.19",
11370
+ declares: [
11371
+ { affects: MEASURES_AT("drag"), places: ["drag"] },
11372
+ { affects: MEASURES_AT("step"), places: ["step"] },
11373
+ { affects: MEASURES_AT("journey"), places: ["journey"] }
11374
+ ],
11375
+ uncalibrated: false,
11376
+ source: '#417 (plan 84 step 2.1, D1, appium-driver CHANGELOG "release alpha.20", 2026-10-27): one gesture definition for every list and screen scroll on both platforms, one touch held 150 ms over 60 % of the list frame (at least 250 dp or pt). Android list and screen flings 12 000 / 3000 dp/s -> that contact (a shorter coast, about 1000-1600 dp in place of about 7000, so fewer momentum frames and possibly one more gesture per page pass); iOS list drags 75 % (end) / 65 % (top) -> 60 %, iOS drawn-row (inverted list) drags 400 -> 150 ms; touches reaching a system zone move whole. Every other request, body and wait is pinned unchanged against alpha.19. The CHANGELOG states "changes Android and iOS list/scroll places; compare only within alpha.20 on both platforms", so both platforms are declared at drag, step and journey places (REQ-FARM-35): the journey holds the whole-run measures the judge reads (memory kept, memory at launch, end and peak, whole-run CPU, the longest freeze), and a shorter coast and one more gesture per page pass move them too.'
11246
11377
  }
11247
11378
  ];
11379
+ var JOURNEY_RELEASES = {};
11380
+ var CAPTURE_MEASURED_WITH = { aspect: "capture", affects: LOADED_MEMORY, marks: true };
11248
11381
  var UNSTABLE_NOISY = {
11249
11382
  reason: "noisy",
11250
11383
  spread: "sessions",
@@ -11258,7 +11391,6 @@ var UNSTABLE_NOISY = {
11258
11391
  uncalibrated: false,
11259
11392
  source: 'Arek 2026-10-18 ~12:00, ~12:20 (noisy on main = unstable, main only, per comparison, hysteresis), 12:50 (within-session spread only: a regression that lands on main moves whole sessions and must stay judged), 13:20 ("zrób na razie wg rekomendacji"; threshold provisional). ' + "Ratio = z99 x the predicted no-change difference SD from the pooled within-session MAD of main (Var(median) = (pi/2) sigma^2 / n) / the smallest warning step at main's median. " + "Replay (noisy/REPORT.md, derivation 22, 1 PR vs 3 main): S23 night 2 -> 1 false worse z 60 (MAD rule 0), sim 0, NI pool 95 -> 64 warnings z 840 (MAD 92); 3 vs 3 0 everywhere; flagged per split S23 17.8, NI 4.9, sim 0.7; judged per split S23 64.5 -> 46.8; flips S23 8 (MAD 21); planted main steps flagged noisy 0 (MAD 5-29 %, with the between-session part 56-99 %). " + "Curve: kept for the judged-rule lookup; the ratio uses the warning step"
11260
11393
  };
11261
-
11262
11394
  class ConfigError2 extends Error {
11263
11395
  problems;
11264
11396
  constructor(problems) {
@@ -11322,7 +11454,13 @@ function withClauseValues(base) {
11322
11454
  const valued = (r) => {
11323
11455
  if (r.id === "memory_retained")
11324
11456
  return { ...r, threshold: [part(memory.value.warn)], uncalibrated: memory.uncalibrated };
11325
- return r.role === "judged" ? judgedByNoise(r) : evidence(r);
11457
+ if (r.role === "judged")
11458
+ return judgedByNoise(r);
11459
+ if (r.id in NOISE_FITS) {
11460
+ const { errorNoise: _error, ...shown } = judgedByNoise(r);
11461
+ return shown;
11462
+ }
11463
+ return evidence(r);
11326
11464
  };
11327
11465
  const rules = base.rules.map(valued);
11328
11466
  const frames = (id) => ({ above: { frames: CLAUSES[id].value.frames }, uncalibrated: CLAUSES[id].uncalibrated });
@@ -11384,9 +11522,20 @@ var MEMORY_RETAINED = {
11384
11522
  uncalibrated: true,
11385
11523
  source: MEMORY_PAIR_SOURCE
11386
11524
  };
11525
+ var FELT_SOURCE = 'Arek 2026-10-27 ~08:00 ("Ok", [ctl-arek-felt-cost], amends decision 0005 / Q-5): component self time explains, never decides; every component past its noise is listed "to check", also on a green verdict';
11387
11526
  var JANK_SOURCE = `W8 option A, Arek 2026-10-26 ("Opcja A jest git", [ctl-arek-jank-time]): jank time (the window's commit times past one 60 Hz frame, added up) is judged in place of the slowest commit, so one spike no longer decides and jitter below a frame counts 0; curve T-NOISE-JANK; deciding since simple-4.26 after the pack v3 replay (A/A Slightly worse 58 -> 46 of 840 on aa-ni, doses 54 of 54)`;
11527
+ var NOISY_WIDEN = {
11528
+ noisyFrom: 1,
11529
+ lineMultiplier: 1,
11530
+ sd: "aa-loo",
11531
+ excludeAbove: null,
11532
+ excludeReturnBelow: null,
11533
+ excludeAboveMs: null,
11534
+ uncalibrated: false,
11535
+ source: `Arek 2026-10-27 ~19:45 [ctl-arek-noisy-widen]: noisy places judged with a widened line, tunable down to the old exclusion (excludeAbove 1.0 / 0.75 with the within-session spread); amendment ~19:40 ("Szum z 3 sesji main powinniśmy liczyć tak samo jak A/A"): one main session against the others, between-session shift included, plus the PR's run-to-run spread; ctl 19:45: ×1, k never raised to pass; start position: never left out (ctl 22:20, the Pixel 7 no longer counts). Pack v4 replay against simple-4.27 without the Pixel 7: A/A false Worse 1 / 1 of 1070 splits, doses caught Reference night 166 / 166, Reference PR 93 / 93, farm iPhone ms 4863 -> 5012 of 6580 (Worse 3107 -> 3167), places left out 9.8 % -> 0 %. Footnote Pixel 7 (21 splits): A/A Worse 0 -> 4`
11536
+ };
11388
11537
  var SHAPE = {
11389
- id: "simple-4.26+noise95-99",
11538
+ id: "simple-4.28+noise95-99",
11390
11539
  minRuns: 2,
11391
11540
  baseline: { sessions: 3, ...GUESS("Q4 / REQ-VER-33: main's newest 3 sessions against one PR session (the quieter setting of the plan 58 simulation)") },
11392
11541
  repeats: { kind: "every-pr-beyond-every-main" },
@@ -11408,6 +11557,7 @@ var SHAPE = {
11408
11557
  uncalibrated: false,
11409
11558
  source: 'Arek 2026-10-16 ("Tak, to też ustalaliśmy"): the SDK, the driver and the journey are the instrument (REQ-VER-35); a release declares the measures it affects (REQ-VER-34); a forced GC before each step is a different instrument (REQ-VER-13, docs/plans/gc-before-action.md)',
11410
11559
  driverReleases: DRIVER_RELEASES,
11560
+ ...Object.keys(JOURNEY_RELEASES).length ? { journeyReleases: JOURNEY_RELEASES } : {},
11411
11561
  unknownDiffers: ["journeyVersion"]
11412
11562
  },
11413
11563
  calibrated: { platforms: ["ios", "android"], uncalibrated: false, source: 'REQ-FARM-26: Android moved in by Arek ("Ok", 2026-10-17) after the S23 A/A, 5 sessions × 3 runs: 0 z 25 false worse under the deciding rule (stats-android/aa/aa-report.md)' },
@@ -11415,7 +11565,7 @@ var SHAPE = {
11415
11565
  { ...COMMIT_MAX_JUDGED, id: "jank", on: { measure: "jank_ms" }, source: JANK_SOURCE },
11416
11566
  evidenceRule("commit_max", { measure: "commit_max_ms" }, JUDGED_WINDOWS, [], "W8 option A: the slowest commit is shown, jank time is judged", { devices: ANY_DEVICE }),
11417
11567
  RENDER_JUDGED,
11418
- COMPONENT_JUDGED,
11568
+ { ...COMPONENT_JUDGED, role: "evidence", source: FELT_SOURCE },
11419
11569
  { ...evidenceRule("commits", { measure: "commits" }, REACT_PLACES, [{ rel: 0.5, abs: 2 }], "render and commit counts are evidence only (ruling a)"), source: COUNT_SOURCE },
11420
11570
  { ...evidenceRule("renders", { measure: "renders" }, REACT_PLACES, [{ rel: 0.5, abs: 10 }], "render and commit counts are evidence only (ruling a)"), source: COUNT_SOURCE },
11421
11571
  evidenceRule("render_upper", { measure: "render_ms_upper" }, WINDOWS, [{ rel: TIER_REL, abs: TIER_UPDATE_MS.warnMs }], "REQ-MEAS-32 timers, option A (Arek 2026-10-17): judged plus timer-carried render time is evidence with the render threshold; render_ms decides"),
@@ -11483,6 +11633,11 @@ var SHAPE = {
11483
11633
  uncalibrated: true,
11484
11634
  source: "ruling d: entry and in-screen mount about 100 ms; an update one frame (16.7 ms on every screen, Arek 2026-10-16)"
11485
11635
  },
11636
+ feltCost: {
11637
+ reads: ["jank", "render"],
11638
+ uncalibrated: false,
11639
+ source: 'Arek 2026-10-27 ~08:00 ("Ok", [ctl-arek-felt-cost]) after giga2-scratch/analysis/2026-10-27-verdict-scale.md option C; B4 replay on the jank pack v3 (676d7453a39c) against simple-4.26: A/A Worse / Slightly worse over 16 pools 2 / 68 -> 1 / 34, doses 52 of 54 (in-screen-mount 1.1x and slow-commit 0.5x lose one case each), golden 10 of 10, corpus 0 moves'
11640
+ },
11486
11641
  freeze: { on: "sampler_longest_gap_ms", uncalibrated: false, source: 'evidence only: the longest freeze covers the whole window, not the transition; "no dropped frames during the transition" (ruling d) needs per-place frame data that is not available' },
11487
11642
  notJudgedLabels: ["launch"],
11488
11643
  unstable: [{
@@ -11491,6 +11646,7 @@ var SHAPE = {
11491
11646
  reads: [COMMIT_MAX_JUDGED, RENDER_JUDGED, COMPONENT_JUDGED, MEMORY_RETAINED],
11492
11647
  source: `${UNSTABLE_NOISY.source}; Arek 2026-10-26 ("A wygląda kusząco. Oddaj do impl.", ctl-arek-met29-own-step): the step is the place's own work kind's (entry and in-screen mount 10 ms, update 5 ms), the smallest only when the kind is unknown; ctl-arek-jank-time: noisiness still reads the slowest commit, so a place is unstable exactly when it was before jank time was judged`
11493
11648
  }],
11649
+ noisy: NOISY_WIDEN,
11494
11650
  judgeFrom: { mainSessions: 3, uncalibrated: false, source: 'Arek 2026-10-18 (REQ-VER-41): a phone is judged only when main has at least 3 sessions on that phone and journey; with fewer the result is "not enough main yet", not judged, not blocking, values shown. Replay (agents/release/replay, d22): 1 PR vs 2 main on the S23 night pool gave 13 z 60 false worse+warning, 1 vs 3 gave 0' },
11495
11651
  measuredWith: [
11496
11652
  { aspect: "renderer", affects: REACT_MEASURES },
@@ -11499,7 +11655,8 @@ var SHAPE = {
11499
11655
  { aspect: "screenWindowModel", affects: WINDOW_MODEL_AFFECTS, places: ["screen"], marks: true },
11500
11656
  { aspect: "cpuSource", affects: ["sampler.cpu_ms"] },
11501
11657
  { aspect: "causeSplit", affects: REACT_MEASURES, places: REACT_PLACES, marks: true },
11502
- { aspect: "endGc", affects: MEMORY_MEASURES }
11658
+ { aspect: "endGc", affects: MEMORY_MEASURES },
11659
+ CAPTURE_MEASURED_WITH
11503
11660
  ],
11504
11661
  tiers: {
11505
11662
  rel: TIER_REL,
@@ -11523,15 +11680,6 @@ var SHAPE = {
11523
11680
  }
11524
11681
  };
11525
11682
  var CONFIG = withClauseValues(SHAPE);
11526
- var P2A_SLIGHT = withClauseValues({
11527
- ...CONFIG,
11528
- id: `${CONFIG.id}+p2a-sw`,
11529
- unstable: CONFIG.unstable.map((u) => ({
11530
- ...u,
11531
- judgeAt: { line: "error", tier: "warning" },
11532
- source: `${u.source}; P2a candidate (plan 81, Arek 2026-10-23 ~15:45): judged only past the error line, as Slightly worse, never blocking`
11533
- }))
11534
- });
11535
11683
  var P4_EVEN = {
11536
11684
  ...CONFIG,
11537
11685
  id: `${CONFIG.id}+p4-even`,
@@ -11542,6 +11690,51 @@ var P3_COMMIT = withClauseValues({
11542
11690
  id: `${CONFIG.id}+p3-commit`,
11543
11691
  commitGrouping: { uncalibrated: true, source: "plan 81 P3; Arek 2026-10-26 [ctl-arek-p81-p3p4]: two windows that share one proven commit (the same slowest commit in every PR run) may be one finding; never merged without it" }
11544
11692
  });
11693
+ // apps/agent/src/flow-version.ts
11694
+ import { createHash as createHash2 } from "node:crypto";
11695
+ import { dirname as dirname3, relative as relative3, resolve as resolve5, sep as sep2 } from "node:path";
11696
+ var REFERENCE = /^\s*(?:-\s*)?(?:runFlow|runScript|file)\s*:\s*['"]?([^'"\s#{}$]+\.(?:ya?ml|js))['"]?\s*$/gm;
11697
+ var commonDir = (paths) => {
11698
+ let dir = dirname3(paths[0]);
11699
+ while (!paths.every((p) => p === dir || p.startsWith(dir.endsWith(sep2) ? dir : dir + sep2)))
11700
+ dir = dirname3(dir);
11701
+ return dir;
11702
+ };
11703
+ async function flowsVersion(files, read, skip = []) {
11704
+ if (files.length === 0)
11705
+ return null;
11706
+ const roots = files.map((f) => resolve5(f));
11707
+ const skipped = new Set(skip.map((s) => resolve5(s)));
11708
+ const seen = new Map;
11709
+ const visit = async (file, root) => {
11710
+ if (seen.has(file) || skipped.has(file))
11711
+ return true;
11712
+ const text = await read(file).then((t) => t.replace(/\r\n?/g, `
11713
+ `), () => null);
11714
+ if (text === null)
11715
+ return !root;
11716
+ seen.set(file, text);
11717
+ for (const m of text.matchAll(REFERENCE))
11718
+ await visit(resolve5(dirname3(file), m[1]), false);
11719
+ return true;
11720
+ };
11721
+ for (const r of roots)
11722
+ if (!await visit(r, true))
11723
+ return null;
11724
+ const base = commonDir(roots);
11725
+ const keyed = [...seen].map(([file, text]) => [relative3(base, file).split(sep2).join("/"), text]);
11726
+ const hash = createHash2("sha256");
11727
+ for (const [key, text] of keyed.sort((a, b) => a[0] < b[0] ? -1 : 1))
11728
+ hash.update(key).update("\x00").update(text).update("\x00");
11729
+ return hash.digest("hex").slice(0, 12);
11730
+ }
11731
+ async function maestroCliVersion(deps) {
11732
+ const r = await deps.spawner.run("maestro", ["--version"], { timeoutMs: 30000, killAfterMs: 5000 });
11733
+ if (r.code !== 0)
11734
+ return;
11735
+ return /^\s*v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\s*$/m.exec(r.stdout)?.[1];
11736
+ }
11737
+
11545
11738
  // apps/agent/src/device-model.ts
11546
11739
  import { tmpdir } from "node:os";
11547
11740
  import { join as join12 } from "node:path";
@@ -11684,6 +11877,19 @@ var maestroLinkFields = (sdkArgs = "") => ({ keepsnappyLinkArgs: linkArgs(sdkArg
11684
11877
 
11685
11878
  // apps/agent/src/farm.ts
11686
11879
  var STATE_VERSION = 1;
11880
+ function plantOf(raw) {
11881
+ let v;
11882
+ try {
11883
+ v = JSON.parse(raw);
11884
+ } catch {
11885
+ return null;
11886
+ }
11887
+ const ok = (p) => {
11888
+ const x = p;
11889
+ return !!x && typeof x.place === "string" && !!x.place && typeof x.kind === "string" && !!x.kind && typeof x.factor === "number" && Number.isFinite(x.factor) && x.factor > 0 && (x.unit === "budget" || x.unit === "MB");
11890
+ };
11891
+ return Array.isArray(v) && v.length > 0 && v.length <= 200 && v.every(ok) ? v : null;
11892
+ }
11687
11893
  var COMPLETION_MARKER = "run:complete";
11688
11894
  var SETUP_FLAGS = [
11689
11895
  ["driver-version", "driver_version"],
@@ -11692,19 +11898,26 @@ var SETUP_FLAGS = [
11692
11898
  ["journey-version", "journey_version"],
11693
11899
  ["flag-set", "flag_set"]
11694
11900
  ];
11695
- function instrumentFlags(f) {
11696
- if (!f["journey-module"] && f.driver !== "appium")
11697
- return {};
11901
+ var LOCAL_PROVIDERS = ["local", "usb", "customer", "simulator", "emulator"];
11902
+ var COMMIT_ID = /^[0-9a-f]{40}$/i;
11903
+ function instrumentFlags(f, maestro = {}) {
11904
+ const given = f["journey-version"]?.trim();
11905
+ const named = given && !COMMIT_ID.test(given) ? given : undefined;
11906
+ if (!f["journey-module"] && f.driver !== "appium") {
11907
+ const driver = f["driver-version"] ?? maestro.maestroVersion;
11908
+ const journey = maestro.flowsVersion ?? named;
11909
+ return { ...driver ? { "driver-version": driver } : {}, ...journey ? { "journey-version": journey } : {} };
11910
+ }
11698
11911
  let hash;
11699
11912
  if (f["journey-module"]) {
11700
11913
  try {
11701
- hash = journeyVersion(resolve5(f["journey-module"]));
11914
+ hash = journeyVersion(resolve6(f["journey-module"]));
11702
11915
  } catch {
11703
11916
  hash = undefined;
11704
11917
  }
11705
11918
  }
11706
11919
  const driver = f["driver-version"] ?? driverVersion() ?? undefined;
11707
- const journey = f["journey-version"] ?? hash;
11920
+ const journey = hash ?? named;
11708
11921
  return { ...driver ? { "driver-version": driver } : {}, ...journey ? { "journey-version": journey } : {} };
11709
11922
  }
11710
11923
  var TOKENS_STUB = "// Replaced by `keepsnappy farm open` for one upload. Never commit real tokens.\noutput.keepsnappyTokens = {};\n";
@@ -11765,7 +11978,8 @@ async function farmJourneys(deps, f, rt, journeys) {
11765
11978
  if (!byName.has(j))
11766
11979
  rt.print(`journey ${j}: no flow file named ${j}.yaml in --config/--flows; sent without a title`);
11767
11980
  }
11768
- return journeysFor(journeys.map((j) => ({ name: j, ...byName.get(j) ?? {} })), { readFile: (p) => deps.fs.readFile(p), root: files.length ? await repoRoot(deps) : null });
11981
+ const info = await journeysFor(journeys.map((j) => ({ name: j, ...byName.get(j) ?? {} })), { readFile: (p) => deps.fs.readFile(p), root: files.length ? await repoRoot(deps) : null });
11982
+ return { journeys: info, files: files.map((x) => x.file) };
11769
11983
  }
11770
11984
  async function farmOpen(deps, f, rt) {
11771
11985
  const problems = [];
@@ -11782,7 +11996,7 @@ async function farmOpen(deps, f, rt) {
11782
11996
  provider = "simulator";
11783
11997
  model = model ? withSimulatorWord(model) : simulator;
11784
11998
  }
11785
- if (!model && ["local", "usb", "customer", "simulator", "emulator"].includes(provider)) {
11999
+ if (!model && LOCAL_PROVIDERS.includes(provider)) {
11786
12000
  model = await detectDeviceModel(deps, { platform: f.platform, deviceId: f["device-id"]?.trim() }) ?? undefined;
11787
12001
  if (model)
11788
12002
  rt.print(`device model read from the device: ${model}`);
@@ -11809,18 +12023,29 @@ async function farmOpen(deps, f, rt) {
11809
12023
  problems.push("--tokens-js <path> is required (the file the suite overlay puts at flows/keepsnappy/tokens.js)");
11810
12024
  if (!f.state)
11811
12025
  problems.push("--state <path> is required (read by keepsnappy farm close)");
12026
+ const plant = f.plant === undefined ? undefined : plantOf(f.plant);
12027
+ if (plant === null)
12028
+ problems.push('--plant must be a JSON list of { place, kind, factor > 0, unit: "budget" | "MB" }');
11812
12029
  if (problems.length)
11813
12030
  throw new ConfigError(problems);
11814
- const statePath = resolve5(f.state);
11815
- const tokensPath = resolve5(f["tokens-js"]);
12031
+ const statePath = resolve6(f.state);
12032
+ const tokensPath = resolve6(f["tokens-js"]);
11816
12033
  if (await deps.fs.exists(statePath)) {
11817
12034
  throw new ConfigError([`${statePath} already exists: close that run set first (keepsnappy farm close --state ${f.state}) or remove the file`]);
11818
12035
  }
11819
12036
  const api = apiTarget(url, f, rt);
11820
12037
  const now = rt.now ?? Date.now;
11821
12038
  const device = { model, os: f["device-os"] ?? null, provider };
11822
- const journeyInfo = await farmJourneys(deps, f, rt, journeys);
11823
- const withInstrument = { ...f, ...instrumentFlags(f) };
12039
+ const { journeys: journeyInfo, files: flowFiles } = await farmJourneys(deps, f, rt, journeys);
12040
+ const { "driver-version": _driver, "journey-version": given, ...rest } = f;
12041
+ const maestro = !f["journey-module"] && f.driver !== "appium";
12042
+ const flows = maestro ? await flowsVersion(flowFiles, (p) => deps.fs.readFile(p), [tokensPath]) : null;
12043
+ const asked = maestro && !f["driver-version"] && LOCAL_PROVIDERS.includes(provider) ? await maestroCliVersion(deps) : undefined;
12044
+ const instrument = instrumentFlags(f, { ...flows ? { flowsVersion: flows } : {}, ...asked ? { maestroVersion: asked } : {} });
12045
+ if (given?.trim() && given.trim() !== instrument["journey-version"]) {
12046
+ rt.print(`--journey-version ${given.trim()} ignored: ${instrument["journey-version"] ? `the journey version is the hash of what runs, ${instrument["journey-version"]}` : "a commit id names the checkout, not the journey; the run records no journey version"}`);
12047
+ }
12048
+ const withInstrument = { ...rest, ...instrument };
11824
12049
  const setup = Object.fromEntries(SETUP_FLAGS.flatMap(([flag, key]) => withInstrument[flag]?.trim() ? [[key, withInstrument[flag].trim().slice(0, 200)]] : []));
11825
12050
  const base = await baseCommitFor(deps, rt.env, { explicit: f["base-commit"], branch: git.branch, commit: git.commit }, rt.print);
11826
12051
  const main = await mainMerges(deps, rt.env, { branch: git.branch, commit: git.commit });
@@ -11837,7 +12062,13 @@ async function farmOpen(deps, f, rt) {
11837
12062
  journeys: journeyInfo,
11838
12063
  repetitions: repeat,
11839
12064
  source: "farm",
11840
- ...f["completion-marker"] || Object.keys(setup).length ? { meta: { ...f["completion-marker"] ? { completion_marker: COMPLETION_MARKER } : {}, ...Object.keys(setup).length ? { setup } : {} } } : {}
12065
+ ...f["completion-marker"] || Object.keys(setup).length || plant ? {
12066
+ meta: {
12067
+ ...f["completion-marker"] ? { completion_marker: COMPLETION_MARKER } : {},
12068
+ ...Object.keys(setup).length ? { setup } : {},
12069
+ ...plant ? { plant } : {}
12070
+ }
12071
+ } : {}
11841
12072
  }).catch(infra2(`cannot open a run set at ${api.url}`));
11842
12073
  const prefix = `${git.commit ? git.commit.slice(0, 12) : "farm"}-${rs.run_set_id.slice(0, 8)}`;
11843
12074
  const runs = [];
@@ -11868,7 +12099,7 @@ async function farmOpen(deps, f, rt) {
11868
12099
  if (rt.env.GITHUB_ACTIONS === "true")
11869
12100
  for (const t of Object.values(tokens).flat())
11870
12101
  rt.print(`::add-mask::${t}`);
11871
- await deps.fs.mkdir(dirname3(tokensPath));
12102
+ await deps.fs.mkdir(dirname4(tokensPath));
11872
12103
  await deps.fs.writeFile(tokensPath, tokensJs(api.url, rs.run_set_id, tokens, maestroLinkFields(f["sdk-args"] ?? "")));
11873
12104
  const state = {
11874
12105
  version: STATE_VERSION,
@@ -11888,7 +12119,7 @@ async function farmOpen(deps, f, rt) {
11888
12119
  closed_at: null,
11889
12120
  result: null
11890
12121
  };
11891
- await deps.fs.mkdir(dirname3(statePath));
12122
+ await deps.fs.mkdir(dirname4(statePath));
11892
12123
  await deps.fs.writeFile(statePath, `${JSON.stringify(state, null, 2)}
11893
12124
  `);
11894
12125
  await githubOutputs(deps, rt, {
@@ -11994,7 +12225,7 @@ async function farmClose(deps, f, rt) {
11994
12225
  throw new ConfigError(["--state <path> is required (written by keepsnappy farm open)"]);
11995
12226
  if (f["fail-on-worse"] && !f.wait)
11996
12227
  throw new ConfigError(["--fail-on-worse needs --wait"]);
11997
- const statePath = resolve5(f.state);
12228
+ const statePath = resolve6(f.state);
11998
12229
  let state;
11999
12230
  try {
12000
12231
  state = JSON.parse(await deps.fs.readFile(statePath));
@@ -12021,10 +12252,10 @@ async function closeRunSet(deps, f, rt, state, statePath) {
12021
12252
  const api = apiTarget(f.api ?? f["ingest-url"] ?? state.api, f, rt);
12022
12253
  const now = rt.now ?? Date.now;
12023
12254
  const sleep = rt.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
12024
- const out = resolve5(f.out ?? dirname3(statePath));
12255
+ const out = resolve6(f.out ?? dirname4(statePath));
12025
12256
  let bsText = null;
12026
12257
  if (f["bs-build"])
12027
- bsText = await deps.fs.readFile(resolve5(f["bs-build"])).catch(() => null);
12258
+ bsText = await deps.fs.readFile(resolve6(f["bs-build"])).catch(() => null);
12028
12259
  const session = sessionOutcome(bsText);
12029
12260
  const where = `${state.branch}${state.commit ? ` @ ${state.commit.slice(0, 7)}` : ""}`;
12030
12261
  rt.print(`KeepSnappy run set ${state.run_set_id} (${where}): closing`);
@@ -12109,7 +12340,7 @@ async function closeRunSet(deps, f, rt, state, statePath) {
12109
12340
  // apps/agent/src/farm-run.ts
12110
12341
  import { existsSync as existsSync2 } from "node:fs";
12111
12342
  import { createRequire } from "node:module";
12112
- import { dirname as dirname4, join as join16, resolve as resolve6 } from "node:path";
12343
+ import { dirname as dirname5, join as join16, resolve as resolve7 } from "node:path";
12113
12344
  import { fileURLToPath as fileURLToPath2 } from "node:url";
12114
12345
 
12115
12346
  // apps/agent/src/app-source.ts
@@ -12382,10 +12613,10 @@ ${r.stderr}`;
12382
12613
  // apps/agent/src/farm-run.ts
12383
12614
  function appiumDriverEntry(o = {}) {
12384
12615
  const exists = o.exists ?? existsSync2;
12385
- const bundled = join16(o.selfDir ?? dirname4(fileURLToPath2(import.meta.url)), "appium-driver.js");
12616
+ const bundled = join16(o.selfDir ?? dirname5(fileURLToPath2(import.meta.url)), "appium-driver.js");
12386
12617
  if (exists(bundled))
12387
12618
  return bundled;
12388
- const pkgDir = dirname4(o.packageJson ? o.packageJson() : createRequire(import.meta.url).resolve("@keepsnappy/appium-driver/package.json"));
12619
+ const pkgDir = dirname5(o.packageJson ? o.packageJson() : createRequire(import.meta.url).resolve("@keepsnappy/appium-driver/package.json"));
12389
12620
  const built = join16(pkgDir, "dist", "src", "cli.js");
12390
12621
  return exists(built) ? built : join16(pkgDir, "src", "cli.ts");
12391
12622
  }
@@ -12484,7 +12715,7 @@ function planFarmRun(f) {
12484
12715
  ports,
12485
12716
  env,
12486
12717
  driverArgs: f["driver-arg"] ?? [],
12487
- out: resolve6(f.out ?? "farm-run"),
12718
+ out: resolve7(f.out ?? "farm-run"),
12488
12719
  timeoutMinutes,
12489
12720
  killAfterSeconds,
12490
12721
  pollSeconds
@@ -12533,7 +12764,7 @@ async function runDriver2(deps, rt, f, p, ports, ctx) {
12533
12764
  device: p.device,
12534
12765
  app: ctx.app,
12535
12766
  ...f["custom-id"] ? { customId: f["custom-id"] } : {},
12536
- suiteDir: resolve6(f["suite-dir"]),
12767
+ suiteDir: resolve7(f["suite-dir"]),
12537
12768
  tokensPath: f["tokens-path"] ?? "flows/keepsnappy/tokens.js",
12538
12769
  tokensJs: ctx.tokensJs,
12539
12770
  execute: f.execute,
@@ -12559,7 +12790,7 @@ async function runDriver2(deps, rt, f, p, ports, ctx) {
12559
12790
  throw new Error("no Appium driver entry: pass --driver-script");
12560
12791
  const args = [
12561
12792
  entry,
12562
- ...f["journey-module"] ? ["--journey-module", resolve6(f["journey-module"])] : [],
12793
+ ...f["journey-module"] ? ["--journey-module", resolve7(f["journey-module"])] : [],
12563
12794
  "--platform",
12564
12795
  p.platform,
12565
12796
  "--device",
@@ -12636,7 +12867,8 @@ async function farmRun(deps, f, rt, ports) {
12636
12867
  driver: p.driver,
12637
12868
  "tokens-js": tokensJs,
12638
12869
  state: statePath,
12639
- ...commit ? { commit } : {}
12870
+ ...commit ? { commit } : {},
12871
+ ...p.driver !== "appium" && !f["driver-version"] && f["maestro-version"] ? { "driver-version": f["maestro-version"] } : {}
12640
12872
  }, rt);
12641
12873
  if (openCode !== EXIT.ok)
12642
12874
  return openCode;
@@ -12679,15 +12911,15 @@ async function farmRun(deps, f, rt, ports) {
12679
12911
  }
12680
12912
 
12681
12913
  // apps/agent/src/sourcemaps.ts
12682
- import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
12914
+ import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
12683
12915
  import { access, readFile as readFile4, readdir as readdir2, rename, rm, writeFile as writeFile2 } from "node:fs/promises";
12684
- import { basename as basename3, join as join17, resolve as resolve7 } from "node:path";
12916
+ import { basename as basename3, join as join17, resolve as resolve8 } from "node:path";
12685
12917
  import { parseArgs as parseArgs2 } from "node:util";
12686
12918
  import { gzipSync } from "node:zlib";
12687
12919
  var MAX_COMPLETE_ROUNDS = 20;
12688
12920
  var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
12689
12921
  async function prepareComponents(projectRoot, options = {}) {
12690
- const root = resolve7(projectRoot);
12922
+ const root = resolve8(projectRoot);
12691
12923
  const directory = join17(root, ".keepsnappy", "components");
12692
12924
  const configurations = await readdir2(directory, { withFileTypes: true }).catch((error) => {
12693
12925
  if (error.code === "ENOENT")
@@ -12728,7 +12960,7 @@ async function prepareComponents(projectRoot, options = {}) {
12728
12960
  throw new ConfigError([`invalid KeepSnappy component fragment ${file}`]);
12729
12961
  if (!hashes.has(fragment.source)) {
12730
12962
  try {
12731
- hashes.set(fragment.source, createHash2("sha256").update(await readFile4(resolve7(root, fragment.source))).digest("hex"));
12963
+ hashes.set(fragment.source, createHash3("sha256").update(await readFile4(resolve8(root, fragment.source))).digest("hex"));
12732
12964
  } catch (error) {
12733
12965
  if (error.code !== "ENOENT")
12734
12966
  throw error;
@@ -12753,7 +12985,7 @@ async function prepareComponents(projectRoot, options = {}) {
12753
12985
  await rm(temporary, { force: true });
12754
12986
  }
12755
12987
  const gzip = gzipSync(text);
12756
- return { file, count: components.length, bytes: Buffer.byteLength(text), gzip, sha256: createHash2("sha256").update(gzip).digest("hex") };
12988
+ return { file, count: components.length, bytes: Buffer.byteLength(text), gzip, sha256: createHash3("sha256").update(gzip).digest("hex") };
12757
12989
  }
12758
12990
  function stripSourcesContent(map) {
12759
12991
  if (!isObject(map))
@@ -12807,7 +13039,7 @@ async function uploadSourcemaps(opts) {
12807
13039
  const { map, stripped } = stripSourcesContent(parsed);
12808
13040
  const text = Buffer.from(JSON.stringify(map));
12809
13041
  const gz = gzipSync(text);
12810
- const sha256 = createHash2("sha256").update(gz).digest("hex");
13042
+ const sha256 = createHash3("sha256").update(gz).digest("hex");
12811
13043
  const name = basename3(file);
12812
13044
  const announced = await call(f, opts.url, `/v1/builds/${encodeURIComponent(opts.build)}/sourcemaps`, headers, {
12813
13045
  platform: opts.platform,
@@ -13060,7 +13292,7 @@ function measuredLines(l, r, opts) {
13060
13292
  }
13061
13293
  const lane = (x) => {
13062
13294
  const runs = x.runs.after !== null && x.runs.before !== null ? x.branch === l.baseline_branch ? `, ${x.runs.after} latest runs vs ${x.runs.before} earlier` : `, ${x.runs.after} branch runs vs ${x.runs.before} on ${l.baseline_branch}` : "";
13063
- return `${x.commit?.sha ? `commit ${x.commit.sha.slice(0, 7)}` : "commit not recorded"} (${when(x.at)}): ${x.word}${runs}${x.waiting_for_reprocess ? `; ${waitingWords(x.waiting_for_reprocess)}` : ""}`;
13295
+ return `${x.commit?.sha ? `commit ${x.commit.sha.slice(0, 7)}` : "commit not recorded"} (${when(x.at)}): ${x.word}${feltWords(x)}${runs}${x.waiting_for_reprocess ? `; ${waitingWords(x.waiting_for_reprocess)}` : ""}`;
13064
13296
  };
13065
13297
  if (m.lanes.length === 1) {
13066
13298
  r.both(`${scope}: newest measured ${f.branch ? "" : `on ${n.branch}, `}${lane(n)}; ${plural2(m.run_sets, "run set")} measured.`);
@@ -13086,6 +13318,7 @@ function measuredLines(l, r, opts) {
13086
13318
  ...m.lanes.length > shown.length ? [plural2(m.lanes.length - shown.length, "lane") + " beyond the eighth"] : []
13087
13319
  ];
13088
13320
  }
13321
+ var feltWords = (x) => `${x.felt_cost ? ` · ${x.felt_cost}` : ""}${x.to_check ? `; ${SUMMARY_WORDS.toCheck(x.to_check)}` : ""}`;
13089
13322
  function countsLine2(l) {
13090
13323
  const c = l.counts;
13091
13324
  const parts = [
@@ -13141,7 +13374,7 @@ function formatBranchStatus(s, opts = {}) {
13141
13374
  for (const l of s.lanes) {
13142
13375
  const shown = `${l.result === "blocks" ? `${l.word} (blocks)` : badgedWord(l.word, l.blocking)}${l.stale ? ` (${STALE_FLAG})` : ""}`;
13143
13376
  const sha = l.commit?.sha?.slice(0, 7);
13144
- r.both(` ${l.journey_title ?? l.journey} on ${l.phone_name}: ${shown} · ${sha ? `commit ${sha}` : "commit not recorded"} (${when(l.at)})${l.in_newest_run_set ? "" : " · older run set"}${l.waiting_for_reprocess ? `; ${waitingWords(l.waiting_for_reprocess)}` : ""}`);
13377
+ r.both(` ${l.journey_title ?? l.journey} on ${l.phone_name}: ${shown}${feltWords(l)} · ${sha ? `commit ${sha}` : "commit not recorded"} (${when(l.at)})${l.in_newest_run_set ? "" : " · older run set"}${l.waiting_for_reprocess ? `; ${waitingWords(l.waiting_for_reprocess)}` : ""}`);
13145
13378
  r.both(...resultContextLines(l.context, opts.verbose, opts.form === "short"));
13146
13379
  r.both(...(l.comparison_notes ?? []).map((line) => ` ${line}`));
13147
13380
  r.full(...(l.session_speed ?? []).map((line) => ` ${line}`));
@@ -13211,11 +13444,11 @@ function formatProblemList(l, opts = {}) {
13211
13444
  r.both(`${staleWords(c.stale_lanes)}: ${c.stale_lanes === 1 ? "its" : "their"} problems are listed, each marked “${STALE_MARK}”.`);
13212
13445
  l.problems.forEach((p, i) => {
13213
13446
  const keep = i < SHORT_PLACES;
13214
- r.add(keep, "", `${l.offset + i + 1}. ${p.text}: ${p.place_name}${opts.verbose ? ` (${p.place})` : ""}${p.intermittent ? ` — ${p.note}, not counted` : ""}`);
13447
+ r.add(keep, "", `${l.offset + i + 1}. ${p.place_name}${opts.verbose ? ` (${p.place})` : ""}: ${p.text}${p.intermittent ? ` — ${p.note}, not counted` : ""}`);
13215
13448
  r.add(keep, ` ${p.journey_title ?? p.journey} on ${p.phone_name}, ${p.runs_text}${p.tag_word ? `, ${p.tag_word}` : ""}${p.stale ? `, ${STALE_MARK}` : ""}`);
13216
13449
  const top = p.top_components[0];
13217
13450
  const topName = componentReadNames(p.top_components)[0];
13218
- const detail = [p.work, top ? `heaviest ${topName}${opts.verbose && top.key ? ` (${top.key})` : ""} ${top.self_text}` : null, p.new_component ? NEW_IN_PR_CHIP : null, p.range_text ? `${p.range_text} between runs` : null].filter(Boolean);
13451
+ const detail = [p.work, top ? `${namePartsText(heaviestWords(`${topName}${opts.verbose && top.key ? ` (${top.key})` : ""}`, top.self_ms === null ? null : top.self_text))}` : null, p.new_component ? NEW_IN_PR_CHIP : null, p.range_text ? `${p.range_text} between runs` : null].filter(Boolean);
13219
13452
  if (detail.length)
13220
13453
  r.full(` ${detail.join("; ")}`);
13221
13454
  if (p.url)
@@ -13958,6 +14191,7 @@ var USAGE = `keepsnappy <command>
13958
14191
  [--config keepsnappy.yml] [--flows <files|dirs|globs,...>] (journey titles: keepsnappy.yml flows.paths, else the flow's name: / top comment)
13959
14192
  [--completion-marker] (the flow posts step run:complete at the end of each iteration; runs without it are invalid)
13960
14193
  [--driver appium --journey-module FILE] [--driver-version V] [--app-build-id ID] [--harness-version V] [--journey-version V] [--flag-set LABEL] (setup fingerprint, plan 51)
14194
+ [--plant JSON] (what the build plants against main, [{ place, kind, factor, unit: budget|MB }]: the run set's meta.plant)
13961
14195
  [--sdk-args k=v&...] (over the run defaults profile=1 diag=1; tokens.js carries them to the flows' activation link)
13962
14196
  farm close --state <file> [--wait [--fail-on-worse]] [--bs-build browserstack.ios.json] [--settle-seconds 60] [--max-wait-seconds 300] [--out DIR]
13963
14197
  (--fail-on-worse as for run: with no verdict computed on the run set it checks nothing)
@@ -14053,6 +14287,7 @@ async function main2(argv) {
14053
14287
  "harness-version": { type: "string" },
14054
14288
  "journey-version": { type: "string" },
14055
14289
  "flag-set": { type: "string" },
14290
+ plant: { type: "string" },
14056
14291
  app: { type: "string" },
14057
14292
  driver: { type: "string" },
14058
14293
  "tunnel-for": { type: "string", multiple: true },
@@ -14103,9 +14338,9 @@ async function main2(argv) {
14103
14338
  if (platform === "ios-device" && !o.device)
14104
14339
  throw new ConfigError(["--device <udid> is required for ios-device"]);
14105
14340
  const arms = parseArmEnv(o["arm-env"], parseArms(o.arms));
14106
- const out = resolve8(o.out ?? `keepsnappy-${new Date().toISOString().replace(/[:.]/g, "-")}`);
14341
+ const out = resolve9(o.out ?? `keepsnappy-${new Date().toISOString().replace(/[:.]/g, "-")}`);
14107
14342
  const udid = o.device ?? "booted";
14108
- const source = platform === "ios-sim" ? new IosSimulatorDataSource(deps, udid) : new IosDeviceDataSource(deps, udid, resolve8(out, ".scratch"));
14343
+ const source = platform === "ios-sim" ? new IosSimulatorDataSource(deps, udid) : new IosDeviceDataSource(deps, udid, resolve9(out, ".scratch"));
14109
14344
  const installer = platform === "ios-sim" ? new IosSimulatorInstaller(deps, udid) : new IosDeviceInstaller(deps, udid);
14110
14345
  const bundleId = await bundleIdOf(deps, arms[0]?.target, o["bundle-id"]);
14111
14346
  let markers;
@@ -14124,7 +14359,7 @@ async function main2(argv) {
14124
14359
  outDir: out,
14125
14360
  testCommand: positionals,
14126
14361
  bundleId,
14127
- ...o["ri-capture"] ? { ri: new FileRiSource(deps, resolve8(o["ri-capture"])) } : {},
14362
+ ...o["ri-capture"] ? { ri: new FileRiSource(deps, resolve9(o["ri-capture"])) } : {},
14128
14363
  ...o["mem-window"] ? { memWindow: o["mem-window"] } : {},
14129
14364
  ...o.timeout ? { testTimeoutMs: Number(o.timeout) * 1000 } : {}
14130
14365
  });
@@ -14136,9 +14371,9 @@ async function main2(argv) {
14136
14371
  const dir = positionals[0];
14137
14372
  if (!dir)
14138
14373
  throw new ConfigError(["extract needs <out-dir>"]);
14139
- const ri = o["ri-capture"] ? new FileRiSource(deps, resolve8(o["ri-capture"])) : undefined;
14140
- const rows = await extract(deps, resolve8(dir), ri, o["mem-window"], o["clock-offset-ms"] ? Number(o["clock-offset-ms"]) : undefined);
14141
- console.log(`${rows.length} rows -> ${resolve8(dir, "metrics.csv")}`);
14374
+ const ri = o["ri-capture"] ? new FileRiSource(deps, resolve9(o["ri-capture"])) : undefined;
14375
+ const rows = await extract(deps, resolve9(dir), ri, o["mem-window"], o["clock-offset-ms"] ? Number(o["clock-offset-ms"]) : undefined);
14376
+ console.log(`${rows.length} rows -> ${resolve9(dir, "metrics.csv")}`);
14142
14377
  return 0;
14143
14378
  }
14144
14379
  if (cmd === "aa") {
@@ -14146,7 +14381,7 @@ async function main2(argv) {
14146
14381
  if (!dir)
14147
14382
  throw new ConfigError(["aa needs <out-dir>"]);
14148
14383
  const config = { ...o.reps ? { reps: Number(o.reps) } : {}, ...o.boot ? { nBoot: Number(o.boot) } : {} };
14149
- console.log((await aa(deps, resolve8(dir), { config })).table);
14384
+ console.log((await aa(deps, resolve9(dir), { config })).table);
14150
14385
  return 0;
14151
14386
  }
14152
14387
  console.error(USAGE);